Merge pull request #159 from modelcontextprotocol/martinalong/mcp-apps/custom-fonts
[MCP Apps] Add way to pass custom fonts
This commit is contained in:
commit
242ec23856
12 changed files with 323 additions and 4 deletions
|
|
@ -2,6 +2,9 @@
|
|||
:root {
|
||||
color-scheme: light dark;
|
||||
|
||||
/* Font families */
|
||||
--font-sans: system-ui, -apple-system, sans-serif;
|
||||
|
||||
/* Background colors */
|
||||
--color-background-primary: light-dark(#ffffff, #111827);
|
||||
--color-background-secondary: light-dark(#f9fafb, #1f2937);
|
||||
|
|
@ -33,6 +36,7 @@
|
|||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: var(--font-sans);
|
||||
color: var(--color-text-primary);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
App,
|
||||
PostMessageTransport,
|
||||
applyHostStyleVariables,
|
||||
applyHostFonts,
|
||||
applyDocumentTheme,
|
||||
} from "@modelcontextprotocol/ext-apps";
|
||||
import { Chart, registerables } from "chart.js";
|
||||
|
|
@ -448,7 +449,7 @@ applyDocumentTheme(systemDark ? "dark" : "light");
|
|||
// Register handlers and connect
|
||||
app.onerror = log.error;
|
||||
|
||||
// Handle host context changes (theme and styles from host)
|
||||
// Handle host context changes (theme, styles, and fonts from host)
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.theme) {
|
||||
applyDocumentTheme(params.theme);
|
||||
|
|
@ -456,6 +457,9 @@ app.onhostcontextchanged = (params) => {
|
|||
if (params.styles?.variables) {
|
||||
applyHostStyleVariables(params.styles.variables);
|
||||
}
|
||||
if (params.styles?.css?.fonts) {
|
||||
applyHostFonts(params.styles.css.fonts);
|
||||
}
|
||||
// Recreate chart to pick up new colors
|
||||
if (state.chart && (params.theme || params.styles?.variables)) {
|
||||
state.chart.destroy();
|
||||
|
|
@ -472,6 +476,9 @@ app.connect().then(() => {
|
|||
if (ctx?.styles?.variables) {
|
||||
applyHostStyleVariables(ctx.styles.variables);
|
||||
}
|
||||
if (ctx?.styles?.css?.fonts) {
|
||||
applyHostFonts(ctx.styles.css.fonts);
|
||||
}
|
||||
});
|
||||
|
||||
// Fetch data after connection
|
||||
|
|
|
|||
|
|
@ -451,6 +451,11 @@ interface HostContext {
|
|||
styles?: {
|
||||
/** CSS variables for theming */
|
||||
variables?: Record<McpUiStyleVariableKey, string | undefined>;
|
||||
/** CSS blocks that apps can inject */
|
||||
css?: {
|
||||
/** CSS for font loading (@font-face rules or @import statements) */
|
||||
fonts?: string;
|
||||
};
|
||||
};
|
||||
/** How the UI is currently displayed */
|
||||
displayMode?: "inline" | "fullscreen" | "pip";
|
||||
|
|
@ -503,8 +508,11 @@ Example:
|
|||
"variables": {
|
||||
"--color-background-primary": "light-dark(#ffffff, #171717)",
|
||||
"--color-text-primary": "light-dark(#171717, #fafafa)",
|
||||
"--font-family-sans": "system-ui, sans-serif",
|
||||
"--font-sans": "Anthropic Sans, sans-serif",
|
||||
...
|
||||
},
|
||||
"css": {
|
||||
"fonts": "@font-face { font-family: \"Custom Font Name\"; src: url(\"https://...\"); }"
|
||||
}
|
||||
},
|
||||
"displayMode": "inline",
|
||||
|
|
@ -638,7 +646,46 @@ Example usage of standardized CSS variables:
|
|||
.container {
|
||||
background: var(--color-background-primary);
|
||||
color: var(--color-text-primary);
|
||||
font: var(--font-style-body);
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom Fonts
|
||||
|
||||
Hosts can provide custom fonts via `styles.css.fonts`, which can contain `@font-face` rules for self-hosted fonts, `@import` statements for font services like Google Fonts, or both:
|
||||
|
||||
```typescript
|
||||
hostContext.styles.variables["--font-sans"] = '"Font Name", sans-serif';
|
||||
|
||||
// Self-hosted fonts
|
||||
hostContext.styles.css.fonts = `
|
||||
@font-face {
|
||||
font-family: "Font Name";
|
||||
src: url("https://url-where-font-is-hosted.com/.../Regular.otf") format("opentype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "Font Name";
|
||||
src: url("https://url-where-font-is-hosted.com/.../Medium.otf") format("opentype");
|
||||
font-weight: 500;
|
||||
font-style: medium;
|
||||
font-display: swap;
|
||||
}
|
||||
`;
|
||||
|
||||
// Google Fonts
|
||||
hostContext.styles.css.fonts = `
|
||||
@import url('https://fonts.googleapis.com/css2?family=Font+Name&display=swap');
|
||||
`;
|
||||
```
|
||||
|
||||
Apps can use the `applyHostFonts` utility to inject the font CSS into the document:
|
||||
|
||||
```typescript
|
||||
if (hostContext.styles?.css?.fonts) {
|
||||
applyHostFonts(hostContext.styles.css.fonts);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export { PostMessageTransport } from "./message-transport";
|
|||
export * from "./types";
|
||||
export {
|
||||
applyHostStyleVariables,
|
||||
applyHostFonts,
|
||||
getDocumentTheme,
|
||||
applyDocumentTheme,
|
||||
} from "./styles";
|
||||
|
|
|
|||
55
src/generated/schema.json
generated
55
src/generated/schema.json
generated
|
|
@ -585,6 +585,17 @@
|
|||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"description": "CSS blocks that apps can inject.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fonts": {
|
||||
"description": "CSS for font loading (@font-face rules or",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -1196,6 +1207,17 @@
|
|||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"description": "CSS blocks that apps can inject.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fonts": {
|
||||
"description": "CSS for font loading (@font-face rules or",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -1319,6 +1341,17 @@
|
|||
},
|
||||
"additionalProperties": {}
|
||||
},
|
||||
"McpUiHostCss": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fonts": {
|
||||
"description": "CSS for font loading (@font-face rules or",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"McpUiHostStyles": {
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
|
|
@ -1640,6 +1673,17 @@
|
|||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"description": "CSS blocks that apps can inject.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fonts": {
|
||||
"description": "CSS for font loading (@font-face rules or",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -2314,6 +2358,17 @@
|
|||
{}
|
||||
]
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"description": "CSS blocks that apps can inject.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fonts": {
|
||||
"description": "CSS for font loading (@font-face rules or",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
6
src/generated/schema.test.ts
generated
6
src/generated/schema.test.ts
generated
|
|
@ -63,6 +63,10 @@ export type McpUiToolCancelledNotificationSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiToolCancelledNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiHostCssSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiHostCssSchema
|
||||
>;
|
||||
|
||||
export type McpUiHostStylesSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiHostStylesSchema
|
||||
>;
|
||||
|
|
@ -197,6 +201,8 @@ expectType<spec.McpUiToolCancelledNotification>(
|
|||
expectType<McpUiToolCancelledNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiToolCancelledNotification,
|
||||
);
|
||||
expectType<spec.McpUiHostCss>({} as McpUiHostCssSchemaInferredType);
|
||||
expectType<McpUiHostCssSchemaInferredType>({} as spec.McpUiHostCss);
|
||||
expectType<spec.McpUiHostStyles>({} as McpUiHostStylesSchemaInferredType);
|
||||
expectType<McpUiHostStylesSchemaInferredType>({} as spec.McpUiHostStyles);
|
||||
expectType<spec.McpUiResourceTeardownRequest>(
|
||||
|
|
|
|||
15
src/generated/schema.ts
generated
15
src/generated/schema.ts
generated
|
|
@ -285,6 +285,17 @@ export const McpUiToolCancelledNotificationSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description CSS blocks that can be injected by apps.
|
||||
*/
|
||||
export const McpUiHostCssSchema = z.object({
|
||||
/** @description CSS for font loading (@font-face rules or @import statements). Apps must apply using applyHostFonts(). */
|
||||
fonts: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("CSS for font loading (@font-face rules or"),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Style configuration for theming MCP apps.
|
||||
*/
|
||||
|
|
@ -293,6 +304,10 @@ export const McpUiHostStylesSchema = z.object({
|
|||
variables: McpUiStylesSchema.optional().describe(
|
||||
"CSS variables for theming the app.",
|
||||
),
|
||||
/** @description CSS blocks that apps can inject. */
|
||||
css: McpUiHostCssSchema.optional().describe(
|
||||
"CSS blocks that apps can inject.",
|
||||
),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
*
|
||||
* - {@link useApp} - React hook to create and connect an MCP App
|
||||
* - {@link useHostStyleVariables} - React hook to apply host style variables and theme
|
||||
* - {@link useHostFonts} - React hook to apply host fonts
|
||||
* - {@link useDocumentTheme} - React hook for reactive document theme
|
||||
* - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed)
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { App } from "../app";
|
||||
import { applyDocumentTheme, applyHostStyleVariables } from "../styles";
|
||||
import {
|
||||
applyDocumentTheme,
|
||||
applyHostFonts,
|
||||
applyHostStyleVariables,
|
||||
} from "../styles";
|
||||
import { McpUiHostContext } from "../types";
|
||||
|
||||
/**
|
||||
|
|
@ -54,6 +58,7 @@ import { McpUiHostContext } from "../types";
|
|||
*
|
||||
* @see {@link applyHostStyleVariables} for the underlying styles function
|
||||
* @see {@link applyDocumentTheme} for the underlying theme function
|
||||
* @see {@link useHostFonts} for applying host fonts
|
||||
* @see {@link McpUiStyles} for available CSS variables
|
||||
*/
|
||||
export function useHostStyleVariables(
|
||||
|
|
@ -94,3 +99,104 @@ export function useHostStyleVariables(
|
|||
};
|
||||
}, [app]);
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that applies host fonts from CSS.
|
||||
*
|
||||
* This hook listens to host context changes and automatically applies:
|
||||
* - `styles.css.fonts` as a `<style>` tag for custom fonts
|
||||
*
|
||||
* The CSS can contain `@font-face` rules for self-hosted fonts,
|
||||
* `@import` statements for Google Fonts or other font services, or both.
|
||||
*
|
||||
* The hook also applies fonts from the initial host context when
|
||||
* the app first connects.
|
||||
*
|
||||
* @param app - The connected App instance, or null during initialization
|
||||
* @param initialContext - Initial host context from the connection (optional).
|
||||
* If provided, fonts will be applied immediately on mount.
|
||||
*
|
||||
* @example Basic usage with useApp
|
||||
* ```tsx
|
||||
* import { useApp } from '@modelcontextprotocol/ext-apps/react';
|
||||
* import { useHostFonts } from '@modelcontextprotocol/ext-apps/react';
|
||||
*
|
||||
* function MyApp() {
|
||||
* const { app, isConnected } = useApp({
|
||||
* appInfo: { name: "MyApp", version: "1.0.0" },
|
||||
* capabilities: {},
|
||||
* });
|
||||
*
|
||||
* // Automatically apply host fonts
|
||||
* useHostFonts(app);
|
||||
*
|
||||
* return (
|
||||
* <div style={{ fontFamily: 'var(--font-sans)' }}>
|
||||
* Hello!
|
||||
* </div>
|
||||
* );
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @example With initial context
|
||||
* ```tsx
|
||||
* const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null);
|
||||
*
|
||||
* // ... get initial context from app.connect() result
|
||||
*
|
||||
* useHostFonts(app, hostContext);
|
||||
* ```
|
||||
*
|
||||
* @see {@link applyHostFonts} for the underlying fonts function
|
||||
* @see {@link useHostStyleVariables} for applying style variables and theme
|
||||
*/
|
||||
export function useHostFonts(
|
||||
app: App | null,
|
||||
initialContext?: McpUiHostContext | null,
|
||||
): void {
|
||||
const initialApplied = useRef(false);
|
||||
|
||||
// Apply initial fonts once on mount
|
||||
useEffect(() => {
|
||||
if (initialApplied.current) {
|
||||
return;
|
||||
}
|
||||
if (initialContext?.styles?.css?.fonts) {
|
||||
applyHostFonts(initialContext.styles.css.fonts);
|
||||
initialApplied.current = true;
|
||||
}
|
||||
}, [initialContext]);
|
||||
|
||||
// Listen for host context changes and apply updated fonts
|
||||
useEffect(() => {
|
||||
if (!app) {
|
||||
return;
|
||||
}
|
||||
|
||||
app.onhostcontextchanged = (params) => {
|
||||
if (params.styles?.css?.fonts) {
|
||||
applyHostFonts(params.styles.css.fonts);
|
||||
}
|
||||
};
|
||||
}, [app]);
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook that applies host styles, fonts, and theme.
|
||||
*
|
||||
* This is a convenience hook that combines {@link useHostStyleVariables} and
|
||||
* {@link useHostFonts}. Use the individual hooks if you need more control.
|
||||
*
|
||||
* @param app - The connected App instance, or null during initialization
|
||||
* @param initialContext - Initial host context from the connection (optional).
|
||||
*
|
||||
* @see {@link useHostStyleVariables} for style variables and theme only
|
||||
* @see {@link useHostFonts} for fonts only
|
||||
*/
|
||||
export function useHostStyles(
|
||||
app: App | null,
|
||||
initialContext?: McpUiHostContext | null,
|
||||
): void {
|
||||
useHostStyleVariables(app, initialContext);
|
||||
useHostFonts(app, initialContext);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -284,12 +284,22 @@ export interface McpUiToolCancelledNotification {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @description CSS blocks that can be injected by apps.
|
||||
*/
|
||||
export interface McpUiHostCss {
|
||||
/** @description CSS for font loading (@font-face rules or @import statements). Apps must apply using applyHostFonts(). */
|
||||
fonts?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Style configuration for theming MCP apps.
|
||||
*/
|
||||
export interface McpUiHostStyles {
|
||||
/** @description CSS variables for theming the app. */
|
||||
variables?: McpUiStyles;
|
||||
/** @description CSS blocks that apps can inject. */
|
||||
css?: McpUiHostCss;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -114,3 +114,68 @@ export function applyHostStyleVariables(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply host font CSS to the document.
|
||||
*
|
||||
* This function takes the `css.fonts` string from `McpUiHostContext.styles` and
|
||||
* injects it as a `<style>` tag. The CSS can contain `@font-face` rules for
|
||||
* self-hosted fonts, `@import` statements for Google Fonts or other font services,
|
||||
* or a combination of both.
|
||||
*
|
||||
* The styles are only injected once. Subsequent calls will not create duplicate
|
||||
* style tags.
|
||||
*
|
||||
* @param fontCss - CSS string containing @font-face rules and/or @import statements
|
||||
*
|
||||
* @example Apply fonts from host context
|
||||
* ```typescript
|
||||
* import { applyHostFonts } from '@modelcontextprotocol/ext-apps';
|
||||
*
|
||||
* app.onhostcontextchanged = (params) => {
|
||||
* if (params.styles?.css?.fonts) {
|
||||
* applyHostFonts(params.styles.css.fonts);
|
||||
* }
|
||||
* };
|
||||
* ```
|
||||
*
|
||||
* @example Host providing self-hosted fonts
|
||||
* ```typescript
|
||||
* hostContext.styles.css.fonts = `
|
||||
* @font-face {
|
||||
* font-family: "Anthropic Sans";
|
||||
* src: url("https://assets.anthropic.com/.../Regular.otf") format("opentype");
|
||||
* font-weight: 400;
|
||||
* }
|
||||
* `;
|
||||
* ```
|
||||
*
|
||||
* @example Host providing Google Fonts
|
||||
* ```typescript
|
||||
* hostContext.styles.css.fonts = `
|
||||
* @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
|
||||
* `;
|
||||
* ```
|
||||
*
|
||||
* @example Use host fonts in CSS
|
||||
* ```css
|
||||
* body {
|
||||
* font-family: "Anthropic Sans", sans-serif;
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link McpUiHostContext} for the full host context structure
|
||||
*/
|
||||
export function applyHostFonts(fontCss: string): void {
|
||||
const styleId = "__mcp-host-fonts";
|
||||
|
||||
// Check if already loaded
|
||||
if (document.getElementById(styleId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = styleId;
|
||||
style.textContent = fontCss;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export {
|
|||
type McpUiDisplayMode,
|
||||
type McpUiStyleVariableKey,
|
||||
type McpUiStyles,
|
||||
type McpUiHostCss,
|
||||
type McpUiHostStyles,
|
||||
type McpUiOpenLinkRequest,
|
||||
type McpUiOpenLinkResult,
|
||||
|
|
@ -72,6 +73,7 @@ import type {
|
|||
export {
|
||||
McpUiThemeSchema,
|
||||
McpUiDisplayModeSchema,
|
||||
McpUiHostCssSchema,
|
||||
McpUiHostStylesSchema,
|
||||
McpUiOpenLinkRequestSchema,
|
||||
McpUiOpenLinkResultSchema,
|
||||
|
|
|
|||
Loading…
Reference in a new issue