Skip to content

Commit 3be22a5

Browse files
committed
Add way to pass fonts
1 parent c516cf0 commit 3be22a5

File tree

12 files changed

+314
-4
lines changed

12 files changed

+314
-4
lines changed

examples/customer-segmentation-server/src/mcp-app.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
:root {
33
color-scheme: light dark;
44

5+
/* Font families */
6+
--font-sans: system-ui, -apple-system, sans-serif;
7+
58
/* Background colors */
69
--color-background-primary: light-dark(#ffffff, #111827);
710
--color-background-secondary: light-dark(#f9fafb, #1f2937);
@@ -33,6 +36,7 @@
3336
html, body {
3437
margin: 0;
3538
padding: 0;
39+
font-family: var(--font-sans);
3640
color: var(--color-text-primary);
3741
overflow: hidden;
3842
}

examples/customer-segmentation-server/src/mcp-app.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
App,
66
PostMessageTransport,
77
applyHostStyleVariables,
8+
applyHostFonts,
89
applyDocumentTheme,
910
} from "@modelcontextprotocol/ext-apps";
1011
import { Chart, registerables } from "chart.js";
@@ -448,14 +449,17 @@ applyDocumentTheme(systemDark ? "dark" : "light");
448449
// Register handlers and connect
449450
app.onerror = log.error;
450451

451-
// Handle host context changes (theme and styles from host)
452+
// Handle host context changes (theme, styles, and fonts from host)
452453
app.onhostcontextchanged = (params) => {
453454
if (params.theme) {
454455
applyDocumentTheme(params.theme);
455456
}
456457
if (params.styles?.variables) {
457458
applyHostStyleVariables(params.styles.variables);
458459
}
460+
if (params.styles?.css?.fonts) {
461+
applyHostFonts(params.styles.css.fonts);
462+
}
459463
// Recreate chart to pick up new colors
460464
if (state.chart && (params.theme || params.styles?.variables)) {
461465
state.chart.destroy();
@@ -472,6 +476,9 @@ app.connect(new PostMessageTransport(window.parent)).then(() => {
472476
if (ctx?.styles?.variables) {
473477
applyHostStyleVariables(ctx.styles.variables);
474478
}
479+
if (ctx?.styles?.css?.fonts) {
480+
applyHostFonts(ctx.styles.css.fonts);
481+
}
475482
});
476483

477484
// Fetch data after connection

specification/draft/apps.mdx

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,11 @@ interface HostContext {
409409
styles?: {
410410
/** CSS variables for theming */
411411
variables?: Record<McpUiStyleVariableKey, string | undefined>;
412+
/** CSS blocks that apps can inject */
413+
css?: {
414+
/** CSS for font loading (@font-face rules or @import statements) */
415+
fonts?: string;
416+
};
412417
};
413418
/** How the UI is currently displayed */
414419
displayMode?: "inline" | "fullscreen" | "pip";
@@ -461,8 +466,11 @@ Example:
461466
"variables": {
462467
"--color-background-primary": "light-dark(#ffffff, #171717)",
463468
"--color-text-primary": "light-dark(#171717, #fafafa)",
464-
"--font-family-sans": "system-ui, sans-serif",
469+
"--font-sans": "Anthropic Sans, sans-serif",
465470
...
471+
},
472+
"css": {
473+
"fonts": "@font-face { font-family: \"Custom Font Name\"; src: url(\"https://...\"); }"
466474
}
467475
},
468476
"displayMode": "inline",
@@ -596,7 +604,37 @@ Example usage of standardized CSS variables:
596604
.container {
597605
background: var(--color-background-primary);
598606
color: var(--color-text-primary);
599-
font: var(--font-style-body);
607+
font-family: var(--font-sans);
608+
}
609+
```
610+
611+
#### Custom Fonts
612+
613+
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:
614+
615+
```typescript
616+
// Self-hosted fonts
617+
hostContext.styles.css.fonts = `
618+
@font-face {
619+
font-family: "Custom Font Name"; // Name should match the font name used in one of their font variables (`--font-sans`, `--font-mono`)
620+
src: url("https://url-where-font-is-hosted.com/.../Regular.otf") format("opentype");
621+
font-weight: 400;
622+
font-style: normal;
623+
font-display: swap;
624+
}
625+
`;
626+
627+
// Google Fonts
628+
hostContext.styles.css.fonts = `
629+
@import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap');
630+
`;
631+
```
632+
633+
Apps can use the `applyHostFonts` utility to inject the font CSS into the document:
634+
635+
```typescript
636+
if (hostContext.styles?.css?.fonts) {
637+
applyHostFonts(hostContext.styles.css.fonts);
600638
}
601639
```
602640

src/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export { PostMessageTransport } from "./message-transport";
5151
export * from "./types";
5252
export {
5353
applyHostStyleVariables,
54+
applyHostFonts,
5455
getDocumentTheme,
5556
applyDocumentTheme,
5657
} from "./styles";

src/generated/schema.json

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,17 @@
585585
{}
586586
]
587587
}
588+
},
589+
"css": {
590+
"description": "CSS blocks that apps can inject.",
591+
"type": "object",
592+
"properties": {
593+
"fonts": {
594+
"description": "CSS for font loading (@font-face rules or",
595+
"type": "string"
596+
}
597+
},
598+
"additionalProperties": false
588599
}
589600
},
590601
"additionalProperties": false
@@ -1196,6 +1207,17 @@
11961207
{}
11971208
]
11981209
}
1210+
},
1211+
"css": {
1212+
"description": "CSS blocks that apps can inject.",
1213+
"type": "object",
1214+
"properties": {
1215+
"fonts": {
1216+
"description": "CSS for font loading (@font-face rules or",
1217+
"type": "string"
1218+
}
1219+
},
1220+
"additionalProperties": false
11991221
}
12001222
},
12011223
"additionalProperties": false
@@ -1319,6 +1341,17 @@
13191341
},
13201342
"additionalProperties": {}
13211343
},
1344+
"McpUiHostCss": {
1345+
"$schema": "https://json-schema.org/draft/2020-12/schema",
1346+
"type": "object",
1347+
"properties": {
1348+
"fonts": {
1349+
"description": "CSS for font loading (@font-face rules or",
1350+
"type": "string"
1351+
}
1352+
},
1353+
"additionalProperties": false
1354+
},
13221355
"McpUiHostStyles": {
13231356
"$schema": "https://json-schema.org/draft/2020-12/schema",
13241357
"type": "object",
@@ -1640,6 +1673,17 @@
16401673
{}
16411674
]
16421675
}
1676+
},
1677+
"css": {
1678+
"description": "CSS blocks that apps can inject.",
1679+
"type": "object",
1680+
"properties": {
1681+
"fonts": {
1682+
"description": "CSS for font loading (@font-face rules or",
1683+
"type": "string"
1684+
}
1685+
},
1686+
"additionalProperties": false
16431687
}
16441688
},
16451689
"additionalProperties": false
@@ -2314,6 +2358,17 @@
23142358
{}
23152359
]
23162360
}
2361+
},
2362+
"css": {
2363+
"description": "CSS blocks that apps can inject.",
2364+
"type": "object",
2365+
"properties": {
2366+
"fonts": {
2367+
"description": "CSS for font loading (@font-face rules or",
2368+
"type": "string"
2369+
}
2370+
},
2371+
"additionalProperties": false
23172372
}
23182373
},
23192374
"additionalProperties": false

src/generated/schema.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export type McpUiToolCancelledNotificationSchemaInferredType = z.infer<
6363
typeof generated.McpUiToolCancelledNotificationSchema
6464
>;
6565

66+
export type McpUiHostCssSchemaInferredType = z.infer<
67+
typeof generated.McpUiHostCssSchema
68+
>;
69+
6670
export type McpUiHostStylesSchemaInferredType = z.infer<
6771
typeof generated.McpUiHostStylesSchema
6872
>;
@@ -189,6 +193,8 @@ expectType<spec.McpUiToolCancelledNotification>(
189193
expectType<McpUiToolCancelledNotificationSchemaInferredType>(
190194
{} as spec.McpUiToolCancelledNotification,
191195
);
196+
expectType<spec.McpUiHostCss>({} as McpUiHostCssSchemaInferredType);
197+
expectType<McpUiHostCssSchemaInferredType>({} as spec.McpUiHostCss);
192198
expectType<spec.McpUiHostStyles>({} as McpUiHostStylesSchemaInferredType);
193199
expectType<McpUiHostStylesSchemaInferredType>({} as spec.McpUiHostStyles);
194200
expectType<spec.McpUiResourceTeardownRequest>(

src/generated/schema.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,17 @@ export const McpUiToolCancelledNotificationSchema = z.object({
285285
}),
286286
});
287287

288+
/**
289+
* @description CSS blocks that can be injected by apps.
290+
*/
291+
export const McpUiHostCssSchema = z.object({
292+
/** @description CSS for font loading (@font-face rules or @import statements). Apps must apply using applyHostFonts(). */
293+
fonts: z
294+
.string()
295+
.optional()
296+
.describe("CSS for font loading (@font-face rules or"),
297+
});
298+
288299
/**
289300
* @description Style configuration for theming MCP apps.
290301
*/
@@ -293,6 +304,10 @@ export const McpUiHostStylesSchema = z.object({
293304
variables: McpUiStylesSchema.optional().describe(
294305
"CSS variables for theming the app.",
295306
),
307+
/** @description CSS blocks that apps can inject. */
308+
css: McpUiHostCssSchema.optional().describe(
309+
"CSS blocks that apps can inject.",
310+
),
296311
});
297312

298313
/**

src/react/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
*
1111
* - {@link useApp} - React hook to create and connect an MCP App
1212
* - {@link useHostStyleVariables} - React hook to apply host style variables and theme
13+
* - {@link useHostFonts} - React hook to apply host fonts
1314
* - {@link useDocumentTheme} - React hook for reactive document theme
1415
* - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed)
1516
*

src/react/useHostStyles.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { useEffect, useRef } from "react";
22
import { App } from "../app";
3-
import { applyDocumentTheme, applyHostStyleVariables } from "../styles";
3+
import {
4+
applyDocumentTheme,
5+
applyHostFonts,
6+
applyHostStyleVariables,
7+
} from "../styles";
48
import { McpUiHostContext } from "../types";
59

610
/**
@@ -54,6 +58,7 @@ import { McpUiHostContext } from "../types";
5458
*
5559
* @see {@link applyHostStyleVariables} for the underlying styles function
5660
* @see {@link applyDocumentTheme} for the underlying theme function
61+
* @see {@link useHostFonts} for applying host fonts
5762
* @see {@link McpUiStyles} for available CSS variables
5863
*/
5964
export function useHostStyleVariables(
@@ -94,3 +99,104 @@ export function useHostStyleVariables(
9499
};
95100
}, [app]);
96101
}
102+
103+
/**
104+
* React hook that applies host fonts from CSS.
105+
*
106+
* This hook listens to host context changes and automatically applies:
107+
* - `styles.css.fonts` as a `<style>` tag for custom fonts
108+
*
109+
* The CSS can contain `@font-face` rules for self-hosted fonts,
110+
* `@import` statements for Google Fonts or other font services, or both.
111+
*
112+
* The hook also applies fonts from the initial host context when
113+
* the app first connects.
114+
*
115+
* @param app - The connected App instance, or null during initialization
116+
* @param initialContext - Initial host context from the connection (optional).
117+
* If provided, fonts will be applied immediately on mount.
118+
*
119+
* @example Basic usage with useApp
120+
* ```tsx
121+
* import { useApp } from '@modelcontextprotocol/ext-apps/react';
122+
* import { useHostFonts } from '@modelcontextprotocol/ext-apps/react';
123+
*
124+
* function MyApp() {
125+
* const { app, isConnected } = useApp({
126+
* appInfo: { name: "MyApp", version: "1.0.0" },
127+
* capabilities: {},
128+
* });
129+
*
130+
* // Automatically apply host fonts
131+
* useHostFonts(app);
132+
*
133+
* return (
134+
* <div style={{ fontFamily: 'var(--font-sans)' }}>
135+
* Hello!
136+
* </div>
137+
* );
138+
* }
139+
* ```
140+
*
141+
* @example With initial context
142+
* ```tsx
143+
* const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null);
144+
*
145+
* // ... get initial context from app.connect() result
146+
*
147+
* useHostFonts(app, hostContext);
148+
* ```
149+
*
150+
* @see {@link applyHostFonts} for the underlying fonts function
151+
* @see {@link useHostStyleVariables} for applying style variables and theme
152+
*/
153+
export function useHostFonts(
154+
app: App | null,
155+
initialContext?: McpUiHostContext | null,
156+
): void {
157+
const initialApplied = useRef(false);
158+
159+
// Apply initial fonts once on mount
160+
useEffect(() => {
161+
if (initialApplied.current) {
162+
return;
163+
}
164+
if (initialContext?.styles?.css?.fonts) {
165+
applyHostFonts(initialContext.styles.css.fonts);
166+
initialApplied.current = true;
167+
}
168+
}, [initialContext]);
169+
170+
// Listen for host context changes and apply updated fonts
171+
useEffect(() => {
172+
if (!app) {
173+
return;
174+
}
175+
176+
app.onhostcontextchanged = (params) => {
177+
if (params.styles?.css?.fonts) {
178+
applyHostFonts(params.styles.css.fonts);
179+
}
180+
};
181+
}, [app]);
182+
}
183+
184+
/**
185+
* React hook that applies host styles, fonts, and theme.
186+
*
187+
* This is a convenience hook that combines {@link useHostStyleVariables} and
188+
* {@link useHostFonts}. Use the individual hooks if you need more control.
189+
*
190+
* @param app - The connected App instance, or null during initialization
191+
* @param initialContext - Initial host context from the connection (optional).
192+
*
193+
* @see {@link useHostStyleVariables} for style variables and theme only
194+
* @see {@link useHostFonts} for fonts only
195+
*/
196+
export function useHostStyles(
197+
app: App | null,
198+
initialContext?: McpUiHostContext | null,
199+
): void {
200+
useHostStyleVariables(app, initialContext);
201+
useHostFonts(app, initialContext);
202+
}

0 commit comments

Comments
 (0)