From 78b24ae2ac7fa8a030934bc9b60929257fc1709c Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 09:43:57 -0600 Subject: [PATCH 01/15] Add skeleton `docs/patterns.md` with type-checked companion file Introduces a patterns documentation page for common MCP Apps recipes. The skeleton includes TODO sections for: perceived latency, iframe/MCP Apps coexistence, authenticated calls, private tools, theme changes, fullscreen, host styling, data persistence, OpenAI migration, and error handling. - Add `docs/patterns.md` to `projectDocuments` in `typedoc.config.mjs` - Add `docs/patterns.ts` companion file with region-delimited code stubs - Include `docs/**/*.ts` in `tsconfig.json` for type-checking - Document the standalone docs pattern in `CLAUDE.md` Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 2 + docs/patterns.md | 69 +++++++++++++++++++++++ docs/patterns.ts | 133 +++++++++++++++++++++++++++++++++++++++++++++ tsconfig.json | 2 +- typedoc.config.mjs | 2 +- 5 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 docs/patterns.md create mode 100644 docs/patterns.ts diff --git a/AGENTS.md b/AGENTS.md index 7701885..a7ad2f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,8 @@ 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. + ## Full Examples Uses npm workspaces. Full examples in `examples/` are separate packages: diff --git a/docs/patterns.md b/docs/patterns.md new file mode 100644 index 0000000..a3eeaf9 --- /dev/null +++ b/docs/patterns.md @@ -0,0 +1,69 @@ +--- +title: Patterns +--- + +# MCP Apps Patterns + +This document covers common patterns and recipes for building MCP Apps. + +## [TODO] Tools that are private to Apps + +Use `Tool._meta.ui.visibility: ["apps"]` to make tools only visible to Apps. + +{@includeCode ./patterns.ts#privateTools} + +## [TODO] Authenticated calls from App + +- Use tool calls / read resources + - 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} + +## [TODO] Giving errors back to model + +- Before app runs: validate inputs in tool call +- After it runs: use `updateModelContext` + +{@includeCode ./patterns.ts#errorsToModel} + +## [TODO] Support native host styling / use CSS variables + +{@includeCode ./patterns.ts#hostStyling} + +## [TODO] Reacting to light/dark theme changes + +{@includeCode ./patterns.ts#lightDarkTheme} + +## [TODO] Support fullscreen / exit fullscreen + +{@includeCode ./patterns.ts#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} + +## [TODO] Lower perceived latency / manage loading time + +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} + +## [TODO] Supporting both iframe & MCP Apps in same binary + +See recipe: https://github.com/modelcontextprotocol/ext-apps/issues/34 + +{@includeCode ./patterns.ts#iframeAndMcpApps} + +## [TODO] Migrating from OpenAI to MCP Apps + +See [OpenAI -> MCP Apps](https://docs.google.com/document/d/13ROImOR9B8xc32yhqsFyC9Hh3_H63JFORDIyjyIPcU4/edit) migration guide. + +Also: [Managing State](https://platform.openai.com/docs/actions/managing-state) (OpenAI) + +{@includeCode ./patterns.ts#migrateFromOpenai} diff --git a/docs/patterns.ts b/docs/patterns.ts new file mode 100644 index 0000000..1321123 --- /dev/null +++ b/docs/patterns.ts @@ -0,0 +1,133 @@ +/** + * Type-checked code examples for the patterns documentation. + * + * These examples are included in {@link ./patterns.md} via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { App } from "../src/app.js"; + +// TODO: Add imports as patterns are implemented + +/** + * Example: Tools that are private to Apps + */ +function privateTools() { + //#region privateTools + // TODO: Tool._meta.ui.visibility: ["apps"] + //#endregion privateTools +} + +/** + * Example: Authenticated calls from App + */ +function authenticatedCalls(app: App) { + //#region authenticatedCalls + // TODO: Use tool calls / read resources + // See PDF example to read binaries by chunks + // Pass auth token in _meta + refresh token + store in local storage + //#endregion authenticatedCalls +} + +/** + * Example: Giving errors back to model + */ +function errorsToModel(app: App) { + //#region errorsToModel + // Before app runs: validate inputs in tool call + // After it runs: updateModelContext + // TODO: Complete implementation + //#endregion errorsToModel +} + +/** + * Example: Support native host styling / use CSS variables + */ +function hostStyling() { + //#region hostStyling + // TODO: Implement host styling support via CSS variables + //#endregion hostStyling +} + +/** + * Example: Reacting to light/dark theme changes + */ +function lightDarkTheme(app: App) { + //#region lightDarkTheme + const applyTheme = (theme: string) => { + document.documentElement.dataset.theme = theme; + }; + + applyTheme(app.getHostContext()?.theme ?? "light"); + app.onhostcontextchanged = (params) => { + if (params.theme) applyTheme(params.theme); + }; + //#endregion lightDarkTheme +} + +/** + * Example: Support fullscreen / exit fullscreen + */ +function fullscreen() { + //#region fullscreen + // TODO: Implement fullscreen support + //#endregion fullscreen +} + +/** + * Example: Persist data (incl. widget state) + */ +function persistData(app: App) { + //#region persistData + // Note: OAI's window.openai.setWidgetState({modelContent, privateContent, imageIds}) + // has only a partial equivalent in MCP Apps: App.updateModelContext({content, structuredContent}) + // For data persistence / to reload when conversation is reloaded, + // use localStorage / IndexedDb with hostInfo.toolInfo.id as key + // returned CallToolResult._meta.widgetUUID = randomUUID() + // TODO: Complete implementation + //#endregion persistData +} + +/** + * Example: Lower perceived latency / manage loading time + */ +function lowerPerceivedLatency(app: App) { + //#region lowerPerceivedLatency + // TODO: Leverage partial inputs to show widgets as possible. + // Beware of partial JSON being partial (but healed), + // so some objects may not be complete. + //#endregion lowerPerceivedLatency +} + +/** + * Example: Supporting both iframe & MCP Apps in same binary + */ +function iframeAndMcpApps() { + //#region iframeAndMcpApps + // TODO: See recipe: https://github.com/modelcontextprotocol/ext-apps/issues/34 + //#endregion iframeAndMcpApps +} + +/** + * Example: Migrating from OpenAI to MCP Apps + */ +function migrateFromOpenai() { + //#region migrateFromOpenai + // TODO: See OpenAI -> MCP Apps migration guide + // https://docs.google.com/document/d/13ROImOR9B8xc32yhqsFyC9Hh3_H63JFORDIyjyIPcU4/edit + //#endregion migrateFromOpenai +} + +// Suppress unused variable warnings +void privateTools; +void authenticatedCalls; +void errorsToModel; +void hostStyling; +void lightDarkTheme; +void fullscreen; +void persistData; +void lowerPerceivedLatency; +void iframeAndMcpApps; +void migrateFromOpenai; diff --git a/tsconfig.json b/tsconfig.json index 85ec789..f9cbb41 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,6 +15,6 @@ "skipLibCheck": true, "resolveJsonModule": true }, - "include": ["src/**/*.ts", "src/**/*.tsx"], + "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.ts"], "exclude": ["node_modules", "dist", "examples/**/*.ts"] } diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 2016361..3761585 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -4,7 +4,7 @@ import { OptionDefaults } from "typedoc"; const config = { blockTags: [...OptionDefaults.blockTags, "@description"], intentionallyNotExported: ["AppOptions"], - projectDocuments: ["docs/quickstart.md"], + projectDocuments: ["docs/quickstart.md", "docs/patterns.md"], entryPoints: [ "src/server/index.ts", "src/app.ts", From 7c3d1e817f4e2242c2e36b074f1e9036c2961bef Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 10:08:37 -0600 Subject: [PATCH 02/15] Complete "Tools that are private to Apps" pattern documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document the `_meta.ui.visibility: ["app"]` feature in `docs/patterns.md` by referencing the existing type-checked example from `src/server/index.examples.ts#registerAppTool_toolVisibility`. - Fix typo: `["apps"]` → `["app"]` (singular) - Remove placeholder function from `docs/patterns.ts` - Expand description to explain use cases Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 6 +++--- docs/patterns.ts | 12 ------------ src/server/index.examples.ts | 19 ++++++++++++------- src/server/index.ts | 7 +++++-- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index a3eeaf9..c109cfd 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -6,11 +6,11 @@ title: Patterns This document covers common patterns and recipes for building MCP Apps. -## [TODO] Tools that are private to Apps +## Tools that are private to Apps -Use `Tool._meta.ui.visibility: ["apps"]` to make tools only visible to Apps. +Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"]` to make tools only callable by Apps (hidden from the model). This is useful for UI-driven actions like updating quantities, toggling settings, or other interactions that shouldn't appear in the model's tool list. -{@includeCode ./patterns.ts#privateTools} +{@includeCode ../src/server/index.examples.ts#registerAppTool_appOnlyVisibility} ## [TODO] Authenticated calls from App diff --git a/docs/patterns.ts b/docs/patterns.ts index 1321123..5ac8b91 100644 --- a/docs/patterns.ts +++ b/docs/patterns.ts @@ -9,17 +9,6 @@ import { App } from "../src/app.js"; -// TODO: Add imports as patterns are implemented - -/** - * Example: Tools that are private to Apps - */ -function privateTools() { - //#region privateTools - // TODO: Tool._meta.ui.visibility: ["apps"] - //#endregion privateTools -} - /** * Example: Authenticated calls from App */ @@ -121,7 +110,6 @@ function migrateFromOpenai() { } // Suppress unused variable warnings -void privateTools; void authenticatedCalls; void errorsToModel; void hostStyling; diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index 9c5ae66..128279c 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -86,11 +86,10 @@ function registerAppTool_basicUsage(server: McpServer) { } /** - * Example: Tool visibility - create app-only tools for UI actions. + * Example: Model-only visibility - tools visible to model but not callable by UI. */ -function registerAppTool_toolVisibility(server: McpServer) { - //#region registerAppTool_toolVisibility - // Main tool - visible to both model and app (default) +function registerAppTool_modelOnlyVisibility(server: McpServer) { + //#region registerAppTool_modelOnlyVisibility registerAppTool( server, "show-cart", @@ -99,7 +98,7 @@ function registerAppTool_toolVisibility(server: McpServer) { _meta: { ui: { resourceUri: "ui://shop/cart.html", - visibility: ["model", "app"], + visibility: ["model"], }, }, }, @@ -108,8 +107,14 @@ function registerAppTool_toolVisibility(server: McpServer) { return { content: [{ type: "text", text: JSON.stringify(cart) }] }; }, ); + //#endregion registerAppTool_modelOnlyVisibility +} - // App-only tool - hidden from the model, only callable by the UI +/** + * Example: App-only visibility - tools hidden from model, only callable by UI. + */ +function registerAppTool_appOnlyVisibility(server: McpServer) { + //#region registerAppTool_appOnlyVisibility registerAppTool( server, "update-quantity", @@ -128,7 +133,7 @@ function registerAppTool_toolVisibility(server: McpServer) { return { content: [{ type: "text", text: JSON.stringify(cart) }] }; }, ); - //#endregion registerAppTool_toolVisibility + //#endregion registerAppTool_appOnlyVisibility } /** diff --git a/src/server/index.ts b/src/server/index.ts index cbd9f1f..826c8b3 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -117,8 +117,11 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @example Basic usage * {@includeCode ./index.examples.ts#registerAppTool_basicUsage} * - * @example Tool visibility - create app-only tools for UI actions - * {@includeCode ./index.examples.ts#registerAppTool_toolVisibility} + * @example Tool visible to model but not callable by UI + * {@includeCode ./index.examples.ts#registerAppTool_modelOnlyVisibility} + * + * @example Tool hidden from model, only callable by UI + * {@includeCode ./index.examples.ts#registerAppTool_appOnlyVisibility} * * @see {@link registerAppResource} to register the HTML resource referenced by the tool */ From fb5e96a719a2865c8cb41165e9391e5ea185fd62 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 11:16:22 -0600 Subject: [PATCH 03/15] Complete "Support native host styling using CSS variables" pattern documentation References existing type-checked examples instead of a stub: - Vanilla JS: `applyHostStyleVariables_fromHostContext` from `src/styles.examples.ts` - React: `useHostStyles_basicUsage` from `src/react/useHostStyles.examples.tsx` Expanded `applyHostStyleVariables_fromHostContext` to show the full pattern: using CSS variables in styles, applying on `onhostcontextchanged`, and applying initial styles after `connect()`. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 12 ++++++++++-- docs/patterns.ts | 10 ---------- src/styles.examples.ts | 12 ++++++++++++ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index c109cfd..77ad7ab 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -27,9 +27,17 @@ Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"] {@includeCode ./patterns.ts#errorsToModel} -## [TODO] Support native host styling / use CSS variables +## Support native host styling using CSS variables -{@includeCode ./patterns.ts#hostStyling} +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()`. + +**Vanilla JS:** + +{@includeCode ../src/styles.examples.ts#applyHostStyleVariables_fromHostContext} + +**React:** + +{@includeCode ../src/react/useHostStyles.examples.tsx#useHostStyles_basicUsage} ## [TODO] Reacting to light/dark theme changes diff --git a/docs/patterns.ts b/docs/patterns.ts index 5ac8b91..4aad13b 100644 --- a/docs/patterns.ts +++ b/docs/patterns.ts @@ -31,15 +31,6 @@ function errorsToModel(app: App) { //#endregion errorsToModel } -/** - * Example: Support native host styling / use CSS variables - */ -function hostStyling() { - //#region hostStyling - // TODO: Implement host styling support via CSS variables - //#endregion hostStyling -} - /** * Example: Reacting to light/dark theme changes */ @@ -112,7 +103,6 @@ function migrateFromOpenai() { // Suppress unused variable warnings void authenticatedCalls; void errorsToModel; -void hostStyling; void lightDarkTheme; void fullscreen; void persistData; diff --git a/src/styles.examples.ts b/src/styles.examples.ts index bf403d0..2e19161 100644 --- a/src/styles.examples.ts +++ b/src/styles.examples.ts @@ -43,11 +43,23 @@ function applyDocumentTheme_fromHostContext(app: App) { */ function applyHostStyleVariables_fromHostContext(app: App) { //#region applyHostStyleVariables_fromHostContext + // Use CSS variables in your styles + document.body.style.background = "var(--color-background-primary)"; + + // Apply when host context changes app.onhostcontextchanged = (params) => { if (params.styles?.variables) { applyHostStyleVariables(params.styles.variables); } }; + + // Apply initial styles after connecting + app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx?.styles?.variables) { + applyHostStyleVariables(ctx.styles.variables); + } + }); //#endregion applyHostStyleVariables_fromHostContext } From 77306d1aaf4b1d1aa4033c4f61d94c07401b6a5a Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 11:58:47 -0600 Subject: [PATCH 04/15] Complete "Reacting to light/dark theme changes" pattern documentation Reference existing JSDoc examples instead of maintaining a separate one in `docs/patterns.ts`: - Vanilla JS: `applyDocumentTheme_fromHostContext` from `styles.examples.ts` - React: `useDocumentTheme_conditionalRender` for reactive theme reading Expand `applyDocumentTheme_fromHostContext` example to show both the `onhostcontextchanged` handler and initial theme application after `connect()`. Note in the docs that `useHostStyles()` applies theme automatically, so React apps don't need additional setup unless they want to read the theme reactively for conditional rendering. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 12 ++++++++++-- docs/patterns.ts | 17 ----------------- src/styles.examples.ts | 11 ++++++++++- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index 77ad7ab..5c6b10d 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -39,9 +39,17 @@ Hosts provide CSS variables (e.g., `--color-background-primary`) that match thei {@includeCode ../src/react/useHostStyles.examples.tsx#useHostStyles_basicUsage} -## [TODO] Reacting to light/dark theme changes +## Reacting to light/dark theme changes -{@includeCode ./patterns.ts#lightDarkTheme} +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} ## [TODO] Support fullscreen / exit fullscreen diff --git a/docs/patterns.ts b/docs/patterns.ts index 4aad13b..757d654 100644 --- a/docs/patterns.ts +++ b/docs/patterns.ts @@ -31,22 +31,6 @@ function errorsToModel(app: App) { //#endregion errorsToModel } -/** - * Example: Reacting to light/dark theme changes - */ -function lightDarkTheme(app: App) { - //#region lightDarkTheme - const applyTheme = (theme: string) => { - document.documentElement.dataset.theme = theme; - }; - - applyTheme(app.getHostContext()?.theme ?? "light"); - app.onhostcontextchanged = (params) => { - if (params.theme) applyTheme(params.theme); - }; - //#endregion lightDarkTheme -} - /** * Example: Support fullscreen / exit fullscreen */ @@ -103,7 +87,6 @@ function migrateFromOpenai() { // Suppress unused variable warnings void authenticatedCalls; void errorsToModel; -void lightDarkTheme; void fullscreen; void persistData; void lowerPerceivedLatency; diff --git a/src/styles.examples.ts b/src/styles.examples.ts index 2e19161..17aa959 100644 --- a/src/styles.examples.ts +++ b/src/styles.examples.ts @@ -21,7 +21,7 @@ import { function getDocumentTheme_checkCurrent() { //#region getDocumentTheme_checkCurrent const theme = getDocumentTheme(); - const isDark = theme === "dark"; + document.body.classList.toggle("dark", theme === "dark"); //#endregion getDocumentTheme_checkCurrent } @@ -30,11 +30,20 @@ function getDocumentTheme_checkCurrent() { */ function applyDocumentTheme_fromHostContext(app: App) { //#region applyDocumentTheme_fromHostContext + // Apply when host context changes app.onhostcontextchanged = (params) => { if (params.theme) { applyDocumentTheme(params.theme); } }; + + // Apply initial theme after connecting + app.connect().then(() => { + const ctx = app.getHostContext(); + if (ctx?.theme) { + applyDocumentTheme(ctx.theme); + } + }); //#endregion applyDocumentTheme_fromHostContext } From 566ac58ab7f187424e6c0b24d1e9554912c646b8 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 13:19:36 -0600 Subject: [PATCH 05/15] Consolidate host styling patterns into unified documentation Merge the separate "Support native host styling using CSS variables" and "Reacting to light/dark theme changes" sections into a single "Matching host styling (CSS variables, theme, and fonts)" pattern. This provides clearer guidance by covering all three concerns together. - Rename `docs/patterns.ts` to `docs/patterns.tsx` for JSX support - Add `hostStylingVanillaJs` and `hostStylingReact` consolidated examples - Enhance `applyHostFonts_fromHostContext` to apply initial fonts after `connect()`, not just on context change - Add CSS usage examples to `applyHostStyleVariables()` JSDoc - Update font example to use `var(--font-sans, system-ui, sans-serif)` Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 2 +- docs/patterns.md | 38 +++++++---------- docs/{patterns.ts => patterns.tsx} | 68 ++++++++++++++++++++++++++++++ src/styles.examples.ts | 9 ++++ src/styles.ts | 15 ++++++- tsconfig.json | 2 +- 6 files changed, 108 insertions(+), 26 deletions(-) rename docs/{patterns.ts => patterns.tsx} (60%) 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"] } From dce143e92e1ef38b34afe54a57fe8da20c6eb5b8 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 14:22:00 -0600 Subject: [PATCH 06/15] Complete "Entering / exiting fullscreen" pattern documentation Add `App_onhostcontextchanged_respondToDisplayMode` example showing how to listen for display mode changes and update UI with `classList.toggle()`. Update `App_requestDisplayMode_fullscreen` to `App_requestDisplayMode_toggle` to show the common toggle pattern checking `availableDisplayModes` first. Refactor full examples (pdf-server, shadertoy-server) to use `classList.toggle(class, condition)` instead of add/remove branches. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 10 ++++++-- docs/patterns.tsx | 10 -------- examples/pdf-server/src/mcp-app.ts | 16 +++++------- examples/shadertoy-server/src/mcp-app.ts | 19 +++------------ src/app.examples.ts | 31 ++++++++++++++++++------ src/app.ts | 4 +-- 6 files changed, 43 insertions(+), 47 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index e10210f..4232554 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -43,9 +43,15 @@ Use the SDK's style helpers to apply host styling, then reference them in your C {@includeCode ./patterns.tsx#hostStylingReact} -## [TODO] Support fullscreen / exit fullscreen +## Entering / Exiting fullscreen -{@includeCode ./patterns.tsx#fullscreen} +Toggle fullscreen mode by calling {@link app!App.requestDisplayMode requestDisplayMode}: + +{@includeCode ../src/app.examples.ts#App_requestDisplayMode_toggle} + +Listen for display mode changes via {@link app!App.onhostcontextchanged onhostcontextchanged} to update your UI: + +{@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode} ## [TODO] Persist data (incl. widget state) diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 6d6bfcc..9a2f3dd 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -97,15 +97,6 @@ function hostStylingReact() { //#endregion hostStylingReact } -/** - * Example: Support fullscreen / exit fullscreen - */ -function fullscreen() { - //#region fullscreen - // TODO: Implement fullscreen support - //#endregion fullscreen -} - /** * Example: Persist data (incl. widget state) */ @@ -155,7 +146,6 @@ void authenticatedCalls; void errorsToModel; void hostStylingVanillaJs; void hostStylingReact; -void fullscreen; void persistData; void lowerPerceivedLatency; void iframeAndMcpApps; diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index b5b2419..28bf936 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -769,16 +769,12 @@ function handleHostContextChanged(ctx: McpUiHostContext) { if (ctx.displayMode) { const wasFullscreen = currentDisplayMode === "fullscreen"; currentDisplayMode = ctx.displayMode as "inline" | "fullscreen"; - if (ctx.displayMode === "fullscreen") { - mainEl.classList.add("fullscreen"); - log.info("Fullscreen mode enabled"); - } else { - mainEl.classList.remove("fullscreen"); - log.info("Inline mode"); - // When exiting fullscreen, request resize to fit content - if (wasFullscreen && pdfDocument) { - requestFitToContent(); - } + const isFullscreen = currentDisplayMode === "fullscreen"; + mainEl.classList.toggle("fullscreen", isFullscreen); + log.info(isFullscreen ? "Fullscreen mode enabled" : "Inline mode"); + // When exiting fullscreen, request resize to fit content + if (wasFullscreen && !isFullscreen && pdfDocument) { + requestFitToContent(); } updateFullscreenButton(); } diff --git a/examples/shadertoy-server/src/mcp-app.ts b/examples/shadertoy-server/src/mcp-app.ts index d19c8f1..546de5a 100644 --- a/examples/shadertoy-server/src/mcp-app.ts +++ b/examples/shadertoy-server/src/mcp-app.ts @@ -65,21 +65,14 @@ function handleHostContextChanged(ctx: McpUiHostContext) { // Show fullscreen button if available (only update if field is present) if (ctx.availableDisplayModes !== undefined) { - if (ctx.availableDisplayModes.includes("fullscreen")) { - fullscreenBtn.classList.add("available"); - } else { - fullscreenBtn.classList.remove("available"); - } + const canFullscreen = ctx.availableDisplayModes.includes("fullscreen"); + fullscreenBtn.classList.toggle("available", canFullscreen); } // Update display mode state and UI if (ctx.displayMode) { currentDisplayMode = ctx.displayMode as "inline" | "fullscreen"; - if (currentDisplayMode === "fullscreen") { - mainEl.classList.add("fullscreen"); - } else { - mainEl.classList.remove("fullscreen"); - } + mainEl.classList.toggle("fullscreen", currentDisplayMode === "fullscreen"); } } @@ -96,11 +89,7 @@ async function toggleFullscreen() { try { const result = await app.requestDisplayMode({ mode: newMode }); currentDisplayMode = result.mode as "inline" | "fullscreen"; - if (currentDisplayMode === "fullscreen") { - mainEl.classList.add("fullscreen"); - } else { - mainEl.classList.remove("fullscreen"); - } + mainEl.classList.toggle("fullscreen", currentDisplayMode === "fullscreen"); } catch (err) { log.error("Failed to change display mode:", err); } diff --git a/src/app.examples.ts b/src/app.examples.ts index 7ff766d..4c09796 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -206,6 +206,20 @@ function App_onhostcontextchanged_respondToTheme(app: App) { //#endregion App_onhostcontextchanged_respondToTheme } +/** + * Example: Respond to display mode changes using onhostcontextchanged. + */ +function App_onhostcontextchanged_respondToDisplayMode(app: App) { + //#region App_onhostcontextchanged_respondToDisplayMode + app.onhostcontextchanged = (params) => { + if (params.displayMode) { + const isFullscreen = params.displayMode === "fullscreen"; + document.body.classList.toggle("fullscreen", isFullscreen); + } + }; + //#endregion App_onhostcontextchanged_respondToDisplayMode +} + /** * Example: Perform cleanup before teardown. */ @@ -344,16 +358,17 @@ async function App_openLink_documentation(app: App) { } /** - * Example: Request fullscreen mode. + * Example: Toggle between inline and fullscreen display modes. */ -async function App_requestDisplayMode_fullscreen(app: App) { - //#region App_requestDisplayMode_fullscreen - const context = app.getHostContext(); - if (context?.availableDisplayModes?.includes("fullscreen")) { - const result = await app.requestDisplayMode({ mode: "fullscreen" }); - console.log("Display mode set to:", result.mode); +async function App_requestDisplayMode_toggle(app: App) { + //#region App_requestDisplayMode_toggle + const ctx = app.getHostContext(); + if (ctx?.availableDisplayModes?.includes("fullscreen")) { + const target = ctx.displayMode === "fullscreen" ? "inline" : "fullscreen"; + const result = await app.requestDisplayMode({ mode: target }); + console.log("Now in:", result.mode); } - //#endregion App_requestDisplayMode_fullscreen + //#endregion App_requestDisplayMode_toggle } /** diff --git a/src/app.ts b/src/app.ts index dee12b8..ab5fabd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -729,8 +729,8 @@ export class App extends Protocol { * @param options - Request options (timeout, etc.) * @returns Result containing the actual display mode that was set * - * @example Request fullscreen mode - * {@includeCode ./app.examples.ts#App_requestDisplayMode_fullscreen} + * @example Toggle display mode + * {@includeCode ./app.examples.ts#App_requestDisplayMode_toggle} * * @see {@link McpUiRequestDisplayModeRequest} for request structure * @see {@link McpUiHostContext} for checking availableDisplayModes From a1ae100ef08d32ffcf4ef3d564ef639bd5bd7b54 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 15:11:13 -0600 Subject: [PATCH 07/15] Complete "Lowering perceived latency" pattern documentation - Expand `App_ontoolinputpartial_progressiveRendering` example in `app.examples.ts` to show full state management pattern with `toolInputs` and `toolInputsPartial` variables - Add healed JSON caveat to `ontoolinputpartial` JSDoc in `app.ts` - Update `patterns.md` to reference the expanded example and document the streaming preview pattern - Remove empty `lowerPerceivedLatency` placeholder from `patterns.tsx` Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 9 +++++---- docs/patterns.tsx | 12 ------------ src/app.examples.ts | 25 +++++++++++++++++++++++-- src/app.ts | 5 +++++ 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index 4232554..dc29b8c 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -60,13 +60,14 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged onhostco {@includeCode ./patterns.tsx#persistData} -## [TODO] Lower perceived latency / manage loading time +## Lowering perceived latency -Leverage partial inputs to show widgets as possible. +Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available. -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 ../src/app.examples.ts#App_ontoolinputpartial_progressiveRendering} -{@includeCode ./patterns.tsx#lowerPerceivedLatency} +> [!IMPORTANT] +> Partial arguments are "healed" JSON — the host closes unclosed brackets/braces to produce valid JSON. This means objects may be incomplete (e.g., the last item in an array may be truncated). Don't rely on partial data for critical operations; use it only for preview UI. ## [TODO] Supporting both iframe & MCP Apps in same binary diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 9a2f3dd..c481ad3 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -111,17 +111,6 @@ function persistData(app: App) { //#endregion persistData } -/** - * Example: Lower perceived latency / manage loading time - */ -function lowerPerceivedLatency(app: App) { - //#region lowerPerceivedLatency - // TODO: Leverage partial inputs to show widgets as possible. - // Beware of partial JSON being partial (but healed), - // so some objects may not be complete. - //#endregion lowerPerceivedLatency -} - /** * Example: Supporting both iframe & MCP Apps in same binary */ @@ -147,6 +136,5 @@ void errorsToModel; void hostStylingVanillaJs; void hostStylingReact; void persistData; -void lowerPerceivedLatency; void iframeAndMcpApps; void migrateFromOpenai; diff --git a/src/app.examples.ts b/src/app.examples.ts index 4c09796..bf6f9dc 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -157,13 +157,34 @@ async function App_ontoolinput_setter(app: App) { */ function App_ontoolinputpartial_progressiveRendering(app: App) { //#region App_ontoolinputpartial_progressiveRendering + let toolInputs: Record | null = null; + let toolInputsPartial: Record | null = null; + app.ontoolinputpartial = (params) => { - console.log("Partial args:", params.arguments); - // Update your UI progressively as arguments stream in + toolInputsPartial = params.arguments as Record; + render(); }; + + app.ontoolinput = (params) => { + toolInputs = params.arguments as Record; + toolInputsPartial = null; + render(); + }; + + function render() { + if (toolInputs) { + renderFinalUI(toolInputs); + } else { + renderLoadingUI(toolInputsPartial); // e.g., shimmer with partial preview + } + } //#endregion App_ontoolinputpartial_progressiveRendering } +// Stubs for App_ontoolinputpartial_progressiveRendering example +declare function renderLoadingUI(data: Record | null): void; +declare function renderFinalUI(data: Record): void; + /** * Example: Display tool execution results using ontoolresult. */ diff --git a/src/app.ts b/src/app.ts index ab5fabd..0628029 100644 --- a/src/app.ts +++ b/src/app.ts @@ -287,6 +287,11 @@ export class App extends Protocol { * streams partial tool arguments during tool call initialization. This enables * progressive rendering of tool arguments before they're complete. * + * **Important:** Partial arguments are "healed" JSON — the host closes unclosed + * brackets/braces to produce valid JSON. This means objects may be incomplete + * (e.g., the last item in an array may be truncated). Use partial data only + * for preview UI, not for critical operations. + * * This setter is a convenience wrapper around `setNotificationHandler()` that * automatically handles the notification schema and extracts the params for you. * From a0265f92d29bd06e38d68dd7a09052ec4b928fc9 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 15:42:16 -0600 Subject: [PATCH 08/15] Complete "Giving errors back to model" pattern documentation Adds JSDoc example `App_updateModelContext_reportError` showing how to report runtime errors (e.g., microphone permission denied) back to the model using `updateModelContext`. Updates patterns.md with clearer framing: - Server-side (tool handler): return `{ isError: true, ... }` - Client-side (app): use `updateModelContext` for runtime errors Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 9 +++++---- docs/patterns.tsx | 12 ------------ src/app.examples.ts | 22 ++++++++++++++++++++++ src/app.ts | 3 +++ 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index dc29b8c..ab4a9f1 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -20,12 +20,13 @@ Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"] {@includeCode ./patterns.tsx#authenticatedCalls} -## [TODO] Giving errors back to model +## Giving errors back to model -- Before app runs: validate inputs in tool call -- After it runs: use `updateModelContext` +**Server-side**: Tool handler validates inputs and returns `{ isError: true, content: [...] }`. The model receives this error through the normal tool call response. -{@includeCode ./patterns.tsx#errorsToModel} +**Client-side**: If a runtime error occurs (e.g., API failure, permission denied, resource unavailable), use {@link app!App.updateModelContext updateModelContext} to inform the model: + +{@includeCode ../src/app.examples.ts#App_updateModelContext_reportError} ## Matching host styling (CSS variables, theme, and fonts) diff --git a/docs/patterns.tsx b/docs/patterns.tsx index c481ad3..a6fd259 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -27,17 +27,6 @@ function authenticatedCalls(app: App) { //#endregion authenticatedCalls } -/** - * Example: Giving errors back to model - */ -function errorsToModel(app: App) { - //#region errorsToModel - // Before app runs: validate inputs in tool call - // After it runs: updateModelContext - // TODO: Complete implementation - //#endregion errorsToModel -} - /** * Example: Unified host styling (theme, CSS variables, fonts) */ @@ -132,7 +121,6 @@ function migrateFromOpenai() { // Suppress unused variable warnings void authenticatedCalls; -void errorsToModel; void hostStylingVanillaJs; void hostStylingReact; void persistData; diff --git a/src/app.examples.ts b/src/app.examples.ts index bf6f9dc..c360837 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -364,6 +364,28 @@ async function App_updateModelContext_structuredContent(app: App) { //#endregion App_updateModelContext_structuredContent } +/** + * Example: Report runtime error to model. + */ +async function App_updateModelContext_reportError(app: App) { + //#region App_updateModelContext_reportError + try { + const _stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + // ... use _stream for transcription + } catch (err) { + // Inform the model that the app is in a degraded state + await app.updateModelContext({ + content: [ + { + type: "text", + text: "Error: transcription unavailable", + }, + ], + }); + } + //#endregion App_updateModelContext_reportError +} + /** * Example: Open documentation link. */ diff --git a/src/app.ts b/src/app.ts index 0628029..3408526 100644 --- a/src/app.ts +++ b/src/app.ts @@ -674,6 +674,9 @@ export class App extends Protocol { * @example Update with structured content * {@includeCode ./app.examples.ts#App_updateModelContext_structuredContent} * + * @example Report runtime error to model + * {@includeCode ./app.examples.ts#App_updateModelContext_reportError} + * * @returns Promise that resolves when the context update is acknowledged */ updateModelContext( From 05e07a76e7abd79154719f9ad6117295ea8e9c88 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 16:17:38 -0600 Subject: [PATCH 09/15] Complete "Persisting widget state" pattern documentation Replaces the TODO stub with complete server-side and client-side examples based on the `pdf-server` implementation pattern: - Server: shows returning `widgetUUID` via `CallToolResult._meta` - Client: shows receiving UUID via `ontoolresult`, using it as localStorage key with generic `saveState()` / `loadState()` helpers Also clarifies the distinction between widget state persistence (UI state surviving page reloads) and `updateModelContext()` (model-visible state). Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 13 ++++++-- docs/patterns.tsx | 77 +++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index ab4a9f1..aff095c 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -54,13 +54,20 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged onhostco {@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode} -## [TODO] Persist data (incl. widget state) +## Persisting 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()` +To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server. + +**Server-side**: Tool handler generates a unique `widgetUUID` and returns it in `CallToolResult._meta.widgetUUID`: + +{@includeCode ./patterns.tsx#persistDataServer} + +**Client-side**: Receive the UUID in {@link app!App.ontoolresult ontoolresult} and use it as the storage key: {@includeCode ./patterns.tsx#persistData} +> **Note:** For model-visible state (informing the LLM about what the user is viewing), use {@link app!App.updateModelContext updateModelContext} instead. Widget state persistence is for UI state that should survive page reloads but doesn't need to be seen by the model. + ## Lowering perceived latency Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available. diff --git a/docs/patterns.tsx b/docs/patterns.tsx index a6fd259..6002bfc 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -13,6 +13,8 @@ import { applyHostFonts, applyHostStyleVariables, } from "../src/styles.js"; +import { randomUUID } from "node:crypto"; +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { McpUiHostContext } from "../src/types.js"; import { useApp, useHostStyles } from "../src/react/index.js"; @@ -87,16 +89,72 @@ function hostStylingReact() { } /** - * Example: Persist data (incl. widget state) + * Example: Persisting widget state (server-side) */ -function persistData(app: App) { +function persistWidgetStateServer( + url: string, + title: string, + pageCount: number, +) { + function toolCallback(): CallToolResult { + //#region persistDataServer + // In your tool callback, include widgetUUID in the result metadata. + return { + content: [{ type: "text", text: `Displaying PDF viewer for "${title}"` }], + structuredContent: { url, title, pageCount, initialPage: 1 }, + _meta: { + widgetUUID: randomUUID(), + }, + }; + //#endregion persistDataServer + } +} + +/** + * Example: Persisting widget state (client-side) + */ +function persistWidgetState(app: App) { //#region persistData - // Note: OAI's window.openai.setWidgetState({modelContent, privateContent, imageIds}) - // has only a partial equivalent in MCP Apps: App.updateModelContext({content, structuredContent}) - // For data persistence / to reload when conversation is reloaded, - // use localStorage / IndexedDb with hostInfo.toolInfo.id as key - // returned CallToolResult._meta.widgetUUID = randomUUID() - // TODO: Complete implementation + // Store the widgetUUID received from the server + let widgetUUID: string | undefined; + + // Helper to save state to localStorage + function saveState(state: T): void { + if (!widgetUUID) return; + try { + localStorage.setItem(widgetUUID, JSON.stringify(state)); + } catch (err) { + console.error("Failed to save widget state:", err); + } + } + + // Helper to load state from localStorage + function loadState(): T | null { + if (!widgetUUID) return null; + try { + const saved = localStorage.getItem(widgetUUID); + return saved ? (JSON.parse(saved) as T) : null; + } catch (err) { + console.error("Failed to load widget state:", err); + return null; + } + } + + // Receive widgetUUID from the tool result + app.ontoolresult = (result) => { + widgetUUID = result._meta?.widgetUUID + ? String(result._meta.widgetUUID) + : undefined; + + // Restore any previously saved state + const savedState = loadState<{ currentPage: number }>(); + if (savedState) { + // Apply restored state to your UI... + } + }; + + // Call saveState() whenever your widget state changes + // e.g., saveState({ currentPage: 5 }); //#endregion persistData } @@ -123,6 +181,7 @@ function migrateFromOpenai() { void authenticatedCalls; void hostStylingVanillaJs; void hostStylingReact; -void persistData; +void persistWidgetStateServer; +void persistWidgetState; void iframeAndMcpApps; void migrateFromOpenai; From 3398ed49b401bc0bf059f3c2aaf6f6a54bcae23d Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 16:47:46 -0600 Subject: [PATCH 10/15] Complete "Pausing computation-heavy widgets" pattern documentation Add `IntersectionObserver` pattern for pausing expensive operations (animations, WebGL, polling) when widgets scroll out of view. - Add `visibilityBasedPause` example to `patterns.tsx` with play/pause callbacks and `onteardown` cleanup - Document pattern in `patterns.md` with link to MDN docs Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 6 ++++++ docs/patterns.tsx | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/docs/patterns.md b/docs/patterns.md index aff095c..ef072c4 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -68,6 +68,12 @@ To persist widget state across conversation reloads (e.g., current page in a PDF > **Note:** For model-visible state (informing the LLM about what the user is viewing), use {@link app!App.updateModelContext updateModelContext} instead. Widget state persistence is for UI state that should survive page reloads but doesn't need to be seen by the model. +## Pausing computation-heavy widgets when out of view + +Widgets with animations, WebGL rendering, or polling can consume significant CPU/GPU even when scrolled out of view. Use [`IntersectionObserver`](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to pause expensive operations when the widget isn't visible: + +{@includeCode ./patterns.tsx#visibilityBasedPause} + ## Lowering perceived latency Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available. diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 6002bfc..86afa7e 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -158,6 +158,36 @@ function persistWidgetState(app: App) { //#endregion persistData } +/** + * Example: Pausing computation-heavy widgets when out of view + */ +function visibilityBasedPause( + app: App, + container: HTMLElement, + animation: { play: () => void; pause: () => void }, +) { + //#region visibilityBasedPause + // Use IntersectionObserver to pause when widget scrolls out of view + const observer = new IntersectionObserver((entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + animation.play(); + } else { + animation.pause(); + } + }); + }); + observer.observe(container); + + // Clean up when the host tears down the widget + app.onteardown = async () => { + observer.disconnect(); + animation.pause(); + return {}; + }; + //#endregion visibilityBasedPause +} + /** * Example: Supporting both iframe & MCP Apps in same binary */ @@ -183,5 +213,6 @@ void hostStylingVanillaJs; void hostStylingReact; void persistWidgetStateServer; void persistWidgetState; +void visibilityBasedPause; void iframeAndMcpApps; void migrateFromOpenai; From e5b2a139961ec6ecbf0a215572b9dec8b413e38a Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 17:13:44 -0600 Subject: [PATCH 11/15] Complete "Reading large amounts of data" pattern documentation Adds type-checked code examples demonstrating how to bypass host platform size limits on tool call responses using app-only chunked tools: - Server-side: `registerAppTool` with `visibility: ["app"]`, chunked response schema with `hasMore`/`offset` pagination, base64 encoding - Client-side: `app.callServerTool()` loop with progress callback, base64 decoding, chunk reassembly Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 14 +++-- docs/patterns.tsx | 134 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index ef072c4..4958de0 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -12,13 +12,17 @@ Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"] {@includeCode ../src/server/index.examples.ts#registerAppTool_appOnlyVisibility} -## [TODO] Authenticated calls from App +## Reading large amounts of data via chunked tool calls -- Use tool calls / read resources - - 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) +Some host platforms have size limits on tool call responses, so large files (PDFs, images, etc.) cannot be sent in a single response. Use an app-only tool with chunked responses to bypass these limits while keeping the data out of model context. -{@includeCode ./patterns.tsx#authenticatedCalls} +**Server-side**: Register an app-only tool that returns data in chunks with pagination metadata: + +{@includeCode ./patterns.tsx#chunkedDataServer} + +**Client-side**: Loop calling the tool until all chunks are received: + +{@includeCode ./patterns.tsx#chunkedDataClient} ## Giving errors back to model diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 86afa7e..a0e1c2a 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -17,16 +17,133 @@ import { randomUUID } from "node:crypto"; import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; import type { McpUiHostContext } from "../src/types.js"; import { useApp, useHostStyles } from "../src/react/index.js"; +import { registerAppTool } from "../src/server/index.js"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; /** - * Example: Authenticated calls from App + * Example: Server-side chunked data tool (app-only) */ -function authenticatedCalls(app: App) { - //#region authenticatedCalls - // TODO: Use tool calls / read resources - // See PDF example to read binaries by chunks - // Pass auth token in _meta + refresh token + store in local storage - //#endregion authenticatedCalls +function chunkedDataServer(server: McpServer) { + //#region chunkedDataServer + // Define the chunk response schema + const DataChunkSchema = z.object({ + bytes: z.string(), // base64-encoded data + offset: z.number(), + byteCount: z.number(), + totalBytes: z.number(), + hasMore: z.boolean(), + }); + + const MAX_CHUNK_BYTES = 500 * 1024; // 500KB per chunk + + registerAppTool( + server, + "read_data_bytes", + { + title: "Read Data Bytes", + description: "Load binary data in chunks", + inputSchema: { + id: z.string().describe("Resource identifier"), + offset: z.number().min(0).default(0).describe("Byte offset"), + byteCount: z + .number() + .default(MAX_CHUNK_BYTES) + .describe("Bytes to read"), + }, + outputSchema: DataChunkSchema, + // Hidden from model - only callable by the App + _meta: { ui: { visibility: ["app"] } }, + }, + async ({ id, offset, byteCount }): Promise => { + const data = await loadData(id); // Your data loading logic + const chunk = data.slice(offset, offset + byteCount); + + return { + content: [{ type: "text", text: `${chunk.length} bytes at ${offset}` }], + structuredContent: { + bytes: Buffer.from(chunk).toString("base64"), + offset, + byteCount: chunk.length, + totalBytes: data.length, + hasMore: offset + chunk.length < data.length, + }, + }; + }, + ); + //#endregion chunkedDataServer +} + +// Stub for the example +declare function loadData(id: string): Promise; + +/** + * Example: Client-side chunked data loading + */ +function chunkedDataClient(app: App, resourceId: string) { + //#region chunkedDataClient + interface DataChunk { + bytes: string; // base64 + offset: number; + byteCount: number; + totalBytes: number; + hasMore: boolean; + } + + async function loadDataInChunks( + id: string, + onProgress?: (loaded: number, total: number) => void, + ): Promise { + const CHUNK_SIZE = 500 * 1024; // 500KB chunks + const chunks: Uint8Array[] = []; + let offset = 0; + let totalBytes = 0; + let hasMore = true; + + while (hasMore) { + const result = await app.callServerTool({ + name: "read_data_bytes", + arguments: { id, offset, byteCount: CHUNK_SIZE }, + }); + + if (result.isError || !result.structuredContent) { + throw new Error("Failed to load data chunk"); + } + + const chunk = result.structuredContent as unknown as DataChunk; + totalBytes = chunk.totalBytes; + hasMore = chunk.hasMore; + + // Decode base64 to bytes + const binaryString = atob(chunk.bytes); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + chunks.push(bytes); + + offset += chunk.byteCount; + onProgress?.(offset, totalBytes); + } + + // Combine all chunks into single array + const fullData = new Uint8Array(totalBytes); + let pos = 0; + for (const chunk of chunks) { + fullData.set(chunk, pos); + pos += chunk.length; + } + + return fullData; + } + + // Usage: load data with progress updates + loadDataInChunks(resourceId, (loaded, total) => { + console.log(`Loading: ${Math.round((loaded / total) * 100)}%`); + }).then((data) => { + console.log(`Loaded ${data.length} bytes`); + }); + //#endregion chunkedDataClient } /** @@ -208,7 +325,8 @@ function migrateFromOpenai() { } // Suppress unused variable warnings -void authenticatedCalls; +void chunkedDataServer; +void chunkedDataClient; void hostStylingVanillaJs; void hostStylingReact; void persistWidgetStateServer; From 89653cbb7af900574a5a0cb3c5e9983cd662c171 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 18:16:56 -0600 Subject: [PATCH 12/15] Complete "Passing contextual information" pattern documentation Documents `updateModelContext` for keeping the model informed about user interactions. The example shows a shopping cart use case with YAML frontmatter for structured, parseable context. Also refines the `updateModelContext` JSDoc to clarify the difference from `sendMessage` and removes the `structuredContent` example. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 6 ++++++ src/app.examples.ts | 30 +++++++++++++++++------------- src/app.ts | 12 ++++-------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index 4958de0..286b1f6 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -58,6 +58,12 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged onhostco {@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode} +## Passing contextual information from the App to the Model + +Use {@link app!App.updateModelContext updateModelContext} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing: + +{@includeCode ../src/app.examples.ts#App_updateModelContext_appState} + ## Persisting widget state To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server. diff --git a/src/app.examples.ts b/src/app.examples.ts index c360837..2c6c62c 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -345,25 +345,29 @@ function App_sendLog_debugState(app: App) { /** * Example: Update model context with current app state. */ -async function App_updateModelContext_appState(app: App) { +async function App_updateModelContext_appState( + app: App, + itemList: string[], + totalCost: string, + currency: string, +) { //#region App_updateModelContext_appState + const markdown = `--- +item-count: ${itemList.length} +total-cost: ${totalCost} +currency: ${currency} +--- + +User is viewing their shopping cart with ${itemList.length} items selected: + +${itemList.map((item) => `- ${item}`).join("\n")}`; + await app.updateModelContext({ - content: [{ type: "text", text: "User selected 3 items totaling $150.00" }], + content: [{ type: "text", text: markdown }], }); //#endregion App_updateModelContext_appState } -/** - * Example: Update with structured content. - */ -async function App_updateModelContext_structuredContent(app: App) { - //#region App_updateModelContext_structuredContent - await app.updateModelContext({ - structuredContent: { selectedItems: 3, total: 150.0, currency: "USD" }, - }); - //#endregion App_updateModelContext_structuredContent -} - /** * Example: Report runtime error to model. */ diff --git a/src/app.ts b/src/app.ts index 3408526..9e568be 100644 --- a/src/app.ts +++ b/src/app.ts @@ -654,13 +654,12 @@ export class App extends Protocol { /** * Update the host's model context with app state. * - * Unlike `sendLog`, which is for debugging/telemetry, context updates - * are intended to be available to the model in future reasoning, - * without requiring a follow-up action (like `sendMessage`). + * Context updates are intended to be available to the model in future + * turns, without triggering an immediate model response (unlike {@link sendMessage}). * * The host will typically defer sending the context to the model until the - * next user message (including `ui/message`), and will only send the last - * update received. Each call overwrites any previous context update. + * next user message — either from the actual user or via `sendMessage`. Only + * the last update is sent; each call overwrites any previous context. * * @param params - Context content and/or structured content * @param options - Request options (timeout, etc.) @@ -671,9 +670,6 @@ export class App extends Protocol { * @example Update model context with current app state * {@includeCode ./app.examples.ts#App_updateModelContext_appState} * - * @example Update with structured content - * {@includeCode ./app.examples.ts#App_updateModelContext_structuredContent} - * * @example Report runtime error to model * {@includeCode ./app.examples.ts#App_updateModelContext_reportError} * From e626b84d447e83fa0e1da8a990fa6abc4173c456 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Mon, 19 Jan 2026 20:11:43 -0600 Subject: [PATCH 13/15] Complete "Sending large follow-up messages" pattern documentation Add new pattern showing how to combine `updateModelContext()` with `sendMessage()` when data exceeds message size limits. The example demonstrates offloading a large transcript to model context first, then sending a brief trigger message. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 6 ++++++ src/app.examples.ts | 39 +++++++++++++++++++++++++++------------ src/app.ts | 6 +++--- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/docs/patterns.md b/docs/patterns.md index 286b1f6..9e7cc35 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -64,6 +64,12 @@ Use {@link app!App.updateModelContext updateModelContext} to keep the model info {@includeCode ../src/app.examples.ts#App_updateModelContext_appState} +## Sending large follow-up messages + +When you need to send more data than fits in a message, use {@link app!App.updateModelContext updateModelContext} to set the context first, then {@link app!App.sendMessage sendMessage} with a brief prompt to trigger a response: + +{@includeCode ../src/app.examples.ts#App_sendMessage_withLargeContext} + ## Persisting widget state To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server. diff --git a/src/app.examples.ts b/src/app.examples.ts index 2c6c62c..5c6a4ce 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -81,18 +81,6 @@ async function App_basicUsage() { //#endregion App_basicUsage } -/** - * Example: Sending a message to the host's chat. - */ -async function App_sendMessage(app: App) { - //#region App_sendMessage - await app.sendMessage({ - role: "user", - content: [{ type: "text", text: "Weather updated!" }], - }); - //#endregion App_sendMessage -} - /** * Example: Check host capabilities after connection. */ @@ -329,6 +317,33 @@ async function App_sendMessage_textFromInteraction(app: App) { //#endregion App_sendMessage_textFromInteraction } +/** + * Example: Send follow-up message after offloading large data to model context. + */ +async function App_sendMessage_withLargeContext( + app: App, + fullTranscript: string, + speakerNames: string[], +) { + //#region App_sendMessage_withLargeContext + const markdown = `--- +word-count: ${fullTranscript.split(/\s+/).length} +speaker-names: ${speakerNames.join(", ")} +--- + +${fullTranscript}`; + + // Offload long transcript to model context + await app.updateModelContext({ content: [{ type: "text", text: markdown }] }); + + // Send brief trigger message + await app.sendMessage({ + role: "user", + content: [{ type: "text", text: "Summarize the key points" }], + }); + //#endregion App_sendMessage_withLargeContext +} + /** * Example: Log app state for debugging. */ diff --git a/src/app.ts b/src/app.ts index 9e568be..4fa8776 100644 --- a/src/app.ts +++ b/src/app.ts @@ -156,9 +156,6 @@ type RequestHandlerExtra = Parameters< * * @example Basic usage with PostMessageTransport * {@includeCode ./app.examples.ts#App_basicUsage} - * - * @example Sending a message to the host's chat - * {@includeCode ./app.examples.ts#App_sendMessage} */ export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; @@ -618,6 +615,9 @@ export class App extends Protocol { * @example Send a text message from user interaction * {@includeCode ./app.examples.ts#App_sendMessage_textFromInteraction} * + * @example Send follow-up message after offloading large data to model context + * {@includeCode ./app.examples.ts#App_sendMessage_withLargeContext} + * * @see {@link McpUiMessageRequest} for request structure */ sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) { From 8cd96adc5a56b60739bce04d7b082dd1dda8fa6b Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Tue, 20 Jan 2026 07:44:24 -0600 Subject: [PATCH 14/15] Include OAI migration guide in TypeDoc documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add migration guide to `projectDocuments` in `typedoc.config.mjs` - Add YAML frontmatter with title to `migrate_from_openai_apps.md` - Fix heading hierarchy (### → ####) for proper nesting - Fix "Client-side" → "Client-Side" capitalization - Rename "Full Migration Example" → "Client-Side Migration Example" - Remove TODO placeholder sections from `patterns.md` Co-Authored-By: Claude Opus 4.5 --- docs/migrate_from_openai_apps.md | 12 ++++++++---- docs/patterns.md | 14 -------------- docs/patterns.tsx | 21 --------------------- typedoc.config.mjs | 6 +++++- 4 files changed, 13 insertions(+), 40 deletions(-) diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md index d681c34..c7b43a0 100644 --- a/docs/migrate_from_openai_apps.md +++ b/docs/migrate_from_openai_apps.md @@ -1,3 +1,7 @@ +--- +title: Migrate OpenAI App +--- + # Migrating from OpenAI Apps SDK to MCP Apps SDK This guide helps you migrate from the OpenAI Apps SDK to the MCP Apps SDK (`@modelcontextprotocol/ext-apps`). @@ -50,7 +54,7 @@ This guide helps you migrate from the OpenAI Apps SDK to the MCP Apps SDK (`@mod ### Server-Side Migration Example -### Before (OpenAI) +#### Before (OpenAI) ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -110,7 +114,7 @@ function createServer() { } ``` -### After (MCP Apps) +#### After (MCP Apps) ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -184,7 +188,7 @@ function createServer() { 6. **Helper Functions**: MCP provides `registerAppTool()` and `registerAppResource()` helpers 7. **Not Yet Implemented**: `_meta["openai/toolInvocation/invoking"]`, `_meta["openai/toolInvocation/invoked"]`, and `_meta["openai/widgetDescription"]` don't have MCP equivalents yet -## Client-side +## Client-Side ### Quick Start Comparison @@ -302,7 +306,7 @@ function createServer() { | — | `app.getHostVersion()` | Returns `{ name, version }` of host | | — | `app.getHostCapabilities()` | Check `serverTools`, `openLinks`, `logging`, etc. | -### Full Migration Example +### Client-Side Migration Example #### Before (OpenAI) diff --git a/docs/patterns.md b/docs/patterns.md index 9e7cc35..06db187 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -98,17 +98,3 @@ Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming t > [!IMPORTANT] > Partial arguments are "healed" JSON — the host closes unclosed brackets/braces to produce valid JSON. This means objects may be incomplete (e.g., the last item in an array may be truncated). Don't rely on partial data for critical operations; use it only for preview UI. - -## [TODO] Supporting both iframe & MCP Apps in same binary - -See recipe: https://github.com/modelcontextprotocol/ext-apps/issues/34 - -{@includeCode ./patterns.tsx#iframeAndMcpApps} - -## [TODO] Migrating from OpenAI to MCP Apps - -See [OpenAI -> MCP Apps](https://docs.google.com/document/d/13ROImOR9B8xc32yhqsFyC9Hh3_H63JFORDIyjyIPcU4/edit) migration guide. - -Also: [Managing State](https://platform.openai.com/docs/actions/managing-state) (OpenAI) - -{@includeCode ./patterns.tsx#migrateFromOpenai} diff --git a/docs/patterns.tsx b/docs/patterns.tsx index a0e1c2a..f21ff5c 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -305,25 +305,6 @@ function visibilityBasedPause( //#endregion visibilityBasedPause } -/** - * Example: Supporting both iframe & MCP Apps in same binary - */ -function iframeAndMcpApps() { - //#region iframeAndMcpApps - // TODO: See recipe: https://github.com/modelcontextprotocol/ext-apps/issues/34 - //#endregion iframeAndMcpApps -} - -/** - * Example: Migrating from OpenAI to MCP Apps - */ -function migrateFromOpenai() { - //#region migrateFromOpenai - // TODO: See OpenAI -> MCP Apps migration guide - // https://docs.google.com/document/d/13ROImOR9B8xc32yhqsFyC9Hh3_H63JFORDIyjyIPcU4/edit - //#endregion migrateFromOpenai -} - // Suppress unused variable warnings void chunkedDataServer; void chunkedDataClient; @@ -332,5 +313,3 @@ void hostStylingReact; void persistWidgetStateServer; void persistWidgetState; void visibilityBasedPause; -void iframeAndMcpApps; -void migrateFromOpenai; diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 3761585..a109cf3 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -4,7 +4,11 @@ import { OptionDefaults } from "typedoc"; const config = { blockTags: [...OptionDefaults.blockTags, "@description"], intentionallyNotExported: ["AppOptions"], - projectDocuments: ["docs/quickstart.md", "docs/patterns.md"], + projectDocuments: [ + "docs/quickstart.md", + "docs/patterns.md", + "docs/migrate_from_openai_apps.md", + ], entryPoints: [ "src/server/index.ts", "src/app.ts", From 36268be7efd5e60b621db5c3289ac2620be1490b Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Tue, 20 Jan 2026 09:43:06 -0600 Subject: [PATCH 15/15] Add links to example implementations in patterns documentation Each pattern section now includes a "See examples/..." link pointing to a working implementation in the repository. This helps readers quickly find real code demonstrating each pattern. Co-Authored-By: Claude Opus 4.5 --- docs/patterns.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/patterns.md b/docs/patterns.md index 06db187..abe8017 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -12,6 +12,8 @@ Set {@link types!McpUiToolMeta.visibility Tool.\_meta.ui.visibility} to `["app"] {@includeCode ../src/server/index.examples.ts#registerAppTool_appOnlyVisibility} +_See [`examples/system-monitor-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) for a full implementation of this pattern._ + ## Reading large amounts of data via chunked tool calls Some host platforms have size limits on tool call responses, so large files (PDFs, images, etc.) cannot be sent in a single response. Use an app-only tool with chunked responses to bypass these limits while keeping the data out of model context. @@ -24,6 +26,8 @@ Some host platforms have size limits on tool call responses, so large files (PDF {@includeCode ./patterns.tsx#chunkedDataClient} +_See [`examples/pdf-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) for a full implementation of this pattern._ + ## Giving errors back to model **Server-side**: Tool handler validates inputs and returns `{ isError: true, content: [...] }`. The model receives this error through the normal tool call response. @@ -48,6 +52,8 @@ Use the SDK's style helpers to apply host styling, then reference them in your C {@includeCode ./patterns.tsx#hostStylingReact} +_See [`examples/basic-server-vanillajs/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) and [`examples/basic-server-react/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) for full implementations of this pattern._ + ## Entering / Exiting fullscreen Toggle fullscreen mode by calling {@link app!App.requestDisplayMode requestDisplayMode}: @@ -58,18 +64,24 @@ Listen for display mode changes via {@link app!App.onhostcontextchanged onhostco {@includeCode ../src/app.examples.ts#App_onhostcontextchanged_respondToDisplayMode} +_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._ + ## Passing contextual information from the App to the Model Use {@link app!App.updateModelContext updateModelContext} to keep the model informed about what the user is viewing or interacting with. Structure the content with YAML frontmatter for easy parsing: {@includeCode ../src/app.examples.ts#App_updateModelContext_appState} +_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ + ## Sending large follow-up messages When you need to send more data than fits in a message, use {@link app!App.updateModelContext updateModelContext} to set the context first, then {@link app!App.sendMessage sendMessage} with a brief prompt to trigger a response: {@includeCode ../src/app.examples.ts#App_sendMessage_withLargeContext} +_See [`examples/transcript-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) for a full implementation of this pattern._ + ## Persisting widget state To persist widget state across conversation reloads (e.g., current page in a PDF viewer, camera position in a map), use [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) with a stable identifier provided by the server. @@ -82,7 +94,7 @@ To persist widget state across conversation reloads (e.g., current page in a PDF {@includeCode ./patterns.tsx#persistData} -> **Note:** For model-visible state (informing the LLM about what the user is viewing), use {@link app!App.updateModelContext updateModelContext} instead. Widget state persistence is for UI state that should survive page reloads but doesn't need to be seen by the model. +_See [`examples/map-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) for a full implementation of this pattern._ ## Pausing computation-heavy widgets when out of view @@ -90,6 +102,8 @@ Widgets with animations, WebGL rendering, or polling can consume significant CPU {@includeCode ./patterns.tsx#visibilityBasedPause} +_See [`examples/shadertoy-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) for a full implementation of this pattern._ + ## Lowering perceived latency Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming tool arguments as they arrive, allowing you to show a loading preview before the complete input is available. @@ -98,3 +112,5 @@ Use {@link app!App.ontoolinputpartial ontoolinputpartial} to receive streaming t > [!IMPORTANT] > Partial arguments are "healed" JSON — the host closes unclosed brackets/braces to produce valid JSON. This means objects may be incomplete (e.g., the last item in an array may be truncated). Don't rely on partial data for critical operations; use it only for preview UI. + +_See [`examples/threejs-server/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) for a full implementation of this pattern._