Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions examples/customer-segmentation-server/src/mcp-app.css
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -33,6 +36,7 @@
html, body {
margin: 0;
padding: 0;
font-family: var(--font-sans);
color: var(--color-text-primary);
overflow: hidden;
}
Expand Down
9 changes: 8 additions & 1 deletion examples/customer-segmentation-server/src/mcp-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
App,
PostMessageTransport,
applyHostStyleVariables,
applyHostFonts,
applyDocumentTheme,
} from "@modelcontextprotocol/ext-apps";
import { Chart, registerables } from "chart.js";
Expand Down Expand Up @@ -448,14 +449,17 @@ 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);
}
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();
Expand All @@ -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
Expand Down
51 changes: 49 additions & 2 deletions specification/draft/apps.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not conceptually different than having an arbitrary css chunk that can be injected, which we didn't want to introduce.
Perhaps we need to make it more restricted somehow?

Copy link
Collaborator Author

@martinalong martinalong Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this was considered, but I think it's worth it b/c this is the most ergonomic and flexible approach by far. Also, the host can already just inject arbitrary css directly into the app HTML anyways. So this doesn't change the "security" at all. We kind of have to trust hosts b/c they're already hosting the app and could do any number of things to it

};
};
/** How the UI is currently displayed */
displayMode?: "inline" | "fullscreen" | "pip";
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to refer to specific implementations in the abstract spec itself?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wdym refer to specific implementations? I have more specific examples of how hosts can specify font files elsewhere in this file already if that's what you're asking!

}
```

Expand Down
1 change: 1 addition & 0 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export { PostMessageTransport } from "./message-transport";
export * from "./types";
export {
applyHostStyleVariables,
applyHostFonts,
getDocumentTheme,
applyDocumentTheme,
} from "./styles";
Expand Down
55 changes: 55 additions & 0 deletions src/generated/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions src/generated/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
>;
Expand Down Expand Up @@ -189,6 +193,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>(
Expand Down
15 changes: 15 additions & 0 deletions src/generated/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.",
),
});

/**
Expand Down
1 change: 1 addition & 0 deletions src/react/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*
Expand Down
Loading
Loading