Merge pull request #159 from modelcontextprotocol/martinalong/mcp-apps/custom-fonts

[MCP Apps] Add way to pass custom fonts
This commit is contained in:
Anton Pidkuiko 2025-12-17 17:45:54 +07:00 • committed by GitHub
commit 242ec23856
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 323 additions and 4 deletions

View file

@ -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;
}

View file

@ -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

View file

@ -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);
}
```

View file

@ -52,6 +52,7 @@ export { PostMessageTransport } from "./message-transport";
export * from "./types";
export {
applyHostStyleVariables,
applyHostFonts,
getDocumentTheme,
applyDocumentTheme,
} from "./styles";

View file

@ -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

View file

@ -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>(

View file

@ -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.",
),
});
/**

View file

@ -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)
*

View file

@ -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);
}

View file

@ -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;
}
/**

View file

@ -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);
}

View file

@ -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,