diff --git a/examples/customer-segmentation-server/src/mcp-app.css b/examples/customer-segmentation-server/src/mcp-app.css index 6bb5df4f..aacaef42 100644 --- a/examples/customer-segmentation-server/src/mcp-app.css +++ b/examples/customer-segmentation-server/src/mcp-app.css @@ -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; } diff --git a/examples/customer-segmentation-server/src/mcp-app.ts b/examples/customer-segmentation-server/src/mcp-app.ts index 77e348de..2feaa2b6 100644 --- a/examples/customer-segmentation-server/src/mcp-app.ts +++ b/examples/customer-segmentation-server/src/mcp-app.ts @@ -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(new PostMessageTransport(window.parent)).then(() => { if (ctx?.styles?.variables) { applyHostStyleVariables(ctx.styles.variables); } + if (ctx?.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } }); // Fetch data after connection diff --git a/specification/draft/apps.mdx b/specification/draft/apps.mdx index 2619964c..9b820437 100644 --- a/specification/draft/apps.mdx +++ b/specification/draft/apps.mdx @@ -409,6 +409,11 @@ interface HostContext { styles?: { /** CSS variables for theming */ variables?: Record; + /** 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"; @@ -461,8 +466,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", @@ -596,7 +604,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); } ``` diff --git a/src/app.ts b/src/app.ts index b8a92388..429137ef 100644 --- a/src/app.ts +++ b/src/app.ts @@ -51,6 +51,7 @@ export { PostMessageTransport } from "./message-transport"; export * from "./types"; export { applyHostStyleVariables, + applyHostFonts, getDocumentTheme, applyDocumentTheme, } from "./styles"; diff --git a/src/generated/schema.json b/src/generated/schema.json index 5867796b..a23be2a8 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -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 diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 702d441e..e87a8ec8 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -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 >; @@ -189,6 +193,8 @@ expectType( expectType( {} as spec.McpUiToolCancelledNotification, ); +expectType({} as McpUiHostCssSchemaInferredType); +expectType({} as spec.McpUiHostCss); expectType({} as McpUiHostStylesSchemaInferredType); expectType({} as spec.McpUiHostStyles); expectType( diff --git a/src/generated/schema.ts b/src/generated/schema.ts index e5e04d6c..d0fcac69 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -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.", + ), }); /** diff --git a/src/react/index.tsx b/src/react/index.tsx index 7759493e..4307dc5c 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -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) * diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index 90c246ad..0e7fbf30 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -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 `