diff --git a/AGENTS.md b/AGENTS.md index a7ad2f2..623d561 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,7 +82,7 @@ Guest UI (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP JSDoc `@example` tags use `{@includeCode ./file.examples.ts#regionName}` to pull in type-checked code from companion `.examples.ts`/`.examples.tsx` files. Regions are marked with `//#region name` and `//#endregion name`, wrapped in functions (whose parameters provide types for external values). Region names follow `exportedName_variant` or `ClassName_methodName_variant` pattern (e.g., `useApp_basicUsage`, `App_hostCapabilities_checkAfterConnection`). -Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts` files using the same `@includeCode` pattern. +Standalone docs in `docs/` (listed in `typedoc.config.mjs` `projectDocuments`) can also have type-checked companion `.ts`/`.tsx` files using the same `@includeCode` pattern. ## Full Examples diff --git a/docs/patterns.md b/docs/patterns.md index 5c6b10d..e10210f 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -18,49 +18,41 @@ Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"] - See [PDF example](https://github.com/modelcontextprotocol/ext-apps/blob/main/examples/pdf-viewer) to read binaries by chunks to avoid call tool size limitations on platforms like claude.ai - Pass auth token in `_meta` (will be loaded again in the future) + refresh token + store in local storage (see Persist data section below) -{@includeCode ./patterns.ts#authenticatedCalls} +{@includeCode ./patterns.tsx#authenticatedCalls} ## [TODO] Giving errors back to model - Before app runs: validate inputs in tool call - After it runs: use `updateModelContext` -{@includeCode ./patterns.ts#errorsToModel} +{@includeCode ./patterns.tsx#errorsToModel} -## Support native host styling using CSS variables +## Matching host styling (CSS variables, theme, and fonts) -Hosts provide CSS variables (e.g., `--color-background-primary`) that match their theme. Apply these to the document, then use them in your styles via `var()`. +Use the SDK's style helpers to apply host styling, then reference them in your CSS: + +- **CSS variables** — Use `var(--color-background-primary)`, etc. in your CSS +- **Theme** — Use `[data-theme="dark"]` selectors or `light-dark()` function for theme-aware styles +- **Fonts** — Use `var(--font-sans)` or `var(--font-mono)` with fallbacks (e.g., `font-family: var(--font-sans, system-ui, sans-serif)`) **Vanilla JS:** -{@includeCode ../src/styles.examples.ts#applyHostStyleVariables_fromHostContext} +{@includeCode ./patterns.tsx#hostStylingVanillaJs} **React:** -{@includeCode ../src/react/useHostStyles.examples.tsx#useHostStyles_basicUsage} - -## Reacting to light/dark theme changes - -The host provides a `theme` value (`"light"` or `"dark"`) in the context. Apply it to `` so you can use CSS selectors like `[data-theme="dark"]` for styling. - -**Vanilla JS:** - -{@includeCode ../src/styles.examples.ts#applyDocumentTheme_fromHostContext} - -**React:** `useHostStyles()` (shown above) applies theme automatically. To read the theme reactively for conditional rendering: - -{@includeCode ../src/react/useDocumentTheme.examples.tsx#useDocumentTheme_conditionalRender} +{@includeCode ./patterns.tsx#hostStylingReact} ## [TODO] Support fullscreen / exit fullscreen -{@includeCode ./patterns.ts#fullscreen} +{@includeCode ./patterns.tsx#fullscreen} ## [TODO] Persist data (incl. widget state) - Note: OAI's `window.openai.setWidgetState({modelContent, privateContent, imageIds})` has only a partial equivalent in the MCP Apps spec (for now!): `App.updateModelContext({content, structuredContent})` - For data persistence / to reload when conversation is reloaded, you must use localStorage / IndexedDb with `hostInfo.toolInfo.id` as key returned `CallToolResult._meta.widgetUUID = randomUUID()` -{@includeCode ./patterns.ts#persistData} +{@includeCode ./patterns.tsx#persistData} ## [TODO] Lower perceived latency / manage loading time @@ -68,13 +60,13 @@ Leverage partial inputs to show widgets as possible. Beware of partial JSON being partial (but healed), so some of your objects may not be complete (e.g. in a list you may need to skip the last item if your code validates input schemas strictly). -{@includeCode ./patterns.ts#lowerPerceivedLatency} +{@includeCode ./patterns.tsx#lowerPerceivedLatency} ## [TODO] Supporting both iframe & MCP Apps in same binary See recipe: https://github.com/modelcontextprotocol/ext-apps/issues/34 -{@includeCode ./patterns.ts#iframeAndMcpApps} +{@includeCode ./patterns.tsx#iframeAndMcpApps} ## [TODO] Migrating from OpenAI to MCP Apps @@ -82,4 +74,4 @@ See [OpenAI -> MCP Apps](https://docs.google.com/document/d/13ROImOR9B8xc32yhqsF Also: [Managing State](https://platform.openai.com/docs/actions/managing-state) (OpenAI) -{@includeCode ./patterns.ts#migrateFromOpenai} +{@includeCode ./patterns.tsx#migrateFromOpenai} diff --git a/docs/patterns.ts b/docs/patterns.tsx similarity index 60% rename from docs/patterns.ts rename to docs/patterns.tsx index 757d654..6d6bfcc 100644 --- a/docs/patterns.ts +++ b/docs/patterns.tsx @@ -8,6 +8,13 @@ */ import { App } from "../src/app.js"; +import { + applyDocumentTheme, + applyHostFonts, + applyHostStyleVariables, +} from "../src/styles.js"; +import type { McpUiHostContext } from "../src/types.js"; +import { useApp, useHostStyles } from "../src/react/index.js"; /** * Example: Authenticated calls from App @@ -31,6 +38,65 @@ function errorsToModel(app: App) { //#endregion errorsToModel } +/** + * Example: Unified host styling (theme, CSS variables, fonts) + */ +function hostStylingVanillaJs(app: App) { + //#region hostStylingVanillaJs + function applyHostContext(ctx: McpUiHostContext) { + if (ctx.theme) { + applyDocumentTheme(ctx.theme); + } + if (ctx.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + if (ctx.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + } + + // Apply when host context changes + app.onhostcontextchanged = applyHostContext; + + // Apply initial styles after connecting + app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx) { + applyHostContext(ctx); + } + }); + //#endregion hostStylingVanillaJs +} + +/** + * Example: Host styling with React (CSS variables, theme, fonts) + */ +function hostStylingReact() { + //#region hostStylingReact + function MyApp() { + const { app } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + }); + + // Apply all host styles (variables, theme, fonts) + useHostStyles(app, app?.getHostContext()); + + return ( +
+

Styled with host CSS variables and fonts

+

Uses [data-theme] selectors

+
+ ); + } + //#endregion hostStylingReact +} + /** * Example: Support fullscreen / exit fullscreen */ @@ -87,6 +153,8 @@ function migrateFromOpenai() { // Suppress unused variable warnings void authenticatedCalls; void errorsToModel; +void hostStylingVanillaJs; +void hostStylingReact; void fullscreen; void persistData; void lowerPerceivedLatency; diff --git a/src/styles.examples.ts b/src/styles.examples.ts index 17aa959..8a92281 100644 --- a/src/styles.examples.ts +++ b/src/styles.examples.ts @@ -91,11 +91,20 @@ function applyHostStyleVariables_toElement(app: App) { */ function applyHostFonts_fromHostContext(app: App) { //#region applyHostFonts_fromHostContext + // Apply when host context changes app.onhostcontextchanged = (params) => { if (params.styles?.css?.fonts) { applyHostFonts(params.styles.css.fonts); } }; + + // Apply initial fonts after connecting + app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx?.styles?.css?.fonts) { + applyHostFonts(ctx.styles.css.fonts); + } + }); //#endregion applyHostFonts_fromHostContext } diff --git a/src/styles.ts b/src/styles.ts index 0727ced..884564b 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -77,6 +77,19 @@ export function applyDocumentTheme(theme: McpUiTheme): void { * @example Apply to a specific element * {@includeCode ./styles.examples.ts#applyHostStyleVariables_toElement} * + * @example Use host style variables in CSS + * ```css + * body { + * background-color: var(--color-background-primary); + * color: var(--color-text-primary); + * } + * + * .card { + * background-color: var(--color-background-secondary); + * border: 1px solid var(--color-border-primary); + * } + * ``` + * * @see {@link McpUiStyles} for the available CSS variables * @see {@link McpUiHostContext} for the full host context structure */ @@ -116,7 +129,7 @@ export function applyHostStyleVariables( * @example Use host fonts in CSS * ```css * body { - * font-family: "Anthropic Sans", sans-serif; + * font-family: var(--font-sans, system-ui, sans-serif); * } * ``` * diff --git a/tsconfig.json b/tsconfig.json index f9cbb41..08ff110 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,6 @@ "skipLibCheck": true, "resolveJsonModule": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.ts"], + "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.ts", "docs/**/*.tsx"], "exclude": ["node_modules", "dist", "examples/**/*.ts"] }