From e5c90c738588c57655061fc3d3b594312f358af9 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Sun, 18 Jan 2026 14:42:00 -0600 Subject: [PATCH 1/3] Use absolute GitHub URLs for example links in README Relative directory links like `](examples/map-server)` caused TypeDoc warnings ("relative path is not a file") and resulted in broken links in the generated API documentation. Converted all 20 example directory links to full GitHub URLs while keeping image paths relative. Co-Authored-By: Claude Opus 4.5 --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ece4234a..c2ebe358 100644 --- a/README.md +++ b/README.md @@ -59,22 +59,22 @@ Or edit your `package.json` manually: | | | | |:---:|:---:|:---:| -| [![Map](examples/map-server/grid-cell.png "Interactive 3D globe viewer using CesiumJS")](examples/map-server) | [![Three.js](examples/threejs-server/grid-cell.png "Interactive 3D scene renderer")](examples/threejs-server) | [![ShaderToy](examples/shadertoy-server/grid-cell.png "Real-time GLSL shader renderer")](examples/shadertoy-server) | -| [**Map**](examples/map-server) | [**Three.js**](examples/threejs-server) | [**ShaderToy**](examples/shadertoy-server) | -| [![Sheet Music](examples/sheet-music-server/grid-cell.png "ABC notation to sheet music")](examples/sheet-music-server) | [![Wiki Explorer](examples/wiki-explorer-server/grid-cell.png "Wikipedia link graph visualization")](examples/wiki-explorer-server) | [![Cohort Heatmap](examples/cohort-heatmap-server/grid-cell.png "Customer retention heatmap")](examples/cohort-heatmap-server) | -| [**Sheet Music**](examples/sheet-music-server) | [**Wiki Explorer**](examples/wiki-explorer-server) | [**Cohort Heatmap**](examples/cohort-heatmap-server) | -| [![Scenario Modeler](examples/scenario-modeler-server/grid-cell.png "SaaS business projections")](examples/scenario-modeler-server) | [![Budget Allocator](examples/budget-allocator-server/grid-cell.png "Interactive budget allocation")](examples/budget-allocator-server) | [![Customer Segmentation](examples/customer-segmentation-server/grid-cell.png "Scatter chart with clustering")](examples/customer-segmentation-server) | -| [**Scenario Modeler**](examples/scenario-modeler-server) | [**Budget Allocator**](examples/budget-allocator-server) | [**Customer Segmentation**](examples/customer-segmentation-server) | -| [![System Monitor](examples/system-monitor-server/grid-cell.png "Real-time OS metrics")](examples/system-monitor-server) | [![Transcript](examples/transcript-server/grid-cell.png "Live speech transcription")](examples/transcript-server) | [![Video Resource](examples/video-resource-server/grid-cell.png "Binary video via MCP resources")](examples/video-resource-server) | -| [**System Monitor**](examples/system-monitor-server) | [**Transcript**](examples/transcript-server) | [**Video Resource**](examples/video-resource-server) | -| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](examples/qr-server) | [![Say Demo](examples/say-server/grid-cell.png "Text-to-speech demo")](examples/say-server) | -| [**PDF Server**](examples/pdf-server) | [**QR Code (Python)**](examples/qr-server) | [**Say Demo**](examples/say-server) | +| [![Map](examples/map-server/grid-cell.png "Interactive 3D globe viewer using CesiumJS")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) | [![Three.js](examples/threejs-server/grid-cell.png "Interactive 3D scene renderer")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) | [![ShaderToy](examples/shadertoy-server/grid-cell.png "Real-time GLSL shader renderer")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) | +| [**Map**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/map-server) | [**Three.js**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/threejs-server) | [**ShaderToy**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/shadertoy-server) | +| [![Sheet Music](examples/sheet-music-server/grid-cell.png "ABC notation to sheet music")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) | [![Wiki Explorer](examples/wiki-explorer-server/grid-cell.png "Wikipedia link graph visualization")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/wiki-explorer-server) | [![Cohort Heatmap](examples/cohort-heatmap-server/grid-cell.png "Customer retention heatmap")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/cohort-heatmap-server) | +| [**Sheet Music**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/sheet-music-server) | [**Wiki Explorer**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/wiki-explorer-server) | [**Cohort Heatmap**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/cohort-heatmap-server) | +| [![Scenario Modeler](examples/scenario-modeler-server/grid-cell.png "SaaS business projections")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/scenario-modeler-server) | [![Budget Allocator](examples/budget-allocator-server/grid-cell.png "Interactive budget allocation")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/budget-allocator-server) | [![Customer Segmentation](examples/customer-segmentation-server/grid-cell.png "Scatter chart with clustering")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/customer-segmentation-server) | +| [**Scenario Modeler**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/scenario-modeler-server) | [**Budget Allocator**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/budget-allocator-server) | [**Customer Segmentation**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/customer-segmentation-server) | +| [![System Monitor](examples/system-monitor-server/grid-cell.png "Real-time OS metrics")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) | [![Transcript](examples/transcript-server/grid-cell.png "Live speech transcription")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) | [![Video Resource](examples/video-resource-server/grid-cell.png "Binary video via MCP resources")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/video-resource-server) | +| [**System Monitor**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/system-monitor-server) | [**Transcript**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/transcript-server) | [**Video Resource**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/video-resource-server) | +| [![PDF Server](examples/pdf-server/grid-cell.png "Interactive PDF viewer with chunked loading")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) | [![QR Code](examples/qr-server/grid-cell.png "QR code generator")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) | [![Say Demo](examples/say-server/grid-cell.png "Text-to-speech demo")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/say-server) | +| [**PDF Server**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/pdf-server) | [**QR Code (Python)**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/qr-server) | [**Say Demo**](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/say-server) | ### Starter Templates | | | |:---:|:---| -| [![Basic](examples/basic-server-react/grid-cell.png "Starter template")](examples/basic-server-react) | The same app built with different frameworks — pick your favorite!

[React](examples/basic-server-react) · [Vue](examples/basic-server-vue) · [Svelte](examples/basic-server-svelte) · [Preact](examples/basic-server-preact) · [Solid](examples/basic-server-solid) · [Vanilla JS](examples/basic-server-vanillajs) | +| [![Basic](examples/basic-server-react/grid-cell.png "Starter template")](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) | The same app built with different frameworks — pick your favorite!

[React](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-react) · [Vue](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vue) · [Svelte](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-svelte) · [Preact](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-preact) · [Solid](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-solid) · [Vanilla JS](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-server-vanillajs) | The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases. From ad04e897454ab9cc7bba54d2db8f7ba574627909 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Sun, 18 Jan 2026 15:11:00 -0600 Subject: [PATCH 2/3] Move JSDoc examples to type-checked `.examples.ts` files Extract inline code examples from JSDoc comments into companion `.examples.ts`/`.examples.tsx` files. Examples are now: - Type-checked by wrapping them in functions - Referenced via `{@includeCode ./file.examples.ts#regionName}` tags - Organized using `//#region` and `//#endregion` markers This keeps the main source files cleaner while ensuring examples compile correctly. Updated TypeDoc config with `exampleTag: false` to support title text on `@example` tags. Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 8 +- src/app-bridge.examples.ts | 509 ++++++++++++++++++++++++ src/app-bridge.ts | 291 ++------------ src/app.examples.ts | 446 +++++++++++++++++++++ src/app.ts | 275 ++----------- src/message-transport.examples.ts | 58 +++ src/message-transport.ts | 26 +- src/react/index.examples.tsx | 26 ++ src/react/index.tsx | 16 +- src/react/useApp.examples.tsx | 54 +++ src/react/useApp.tsx | 37 +- src/react/useAutoResize.examples.tsx | 42 ++ src/react/useAutoResize.ts | 27 +- src/react/useDocumentTheme.examples.tsx | 46 +++ src/react/useDocumentTheme.ts | 29 +- src/react/useHostStyles.examples.tsx | 87 ++++ src/react/useHostStyles.ts | 58 +-- src/server/index.examples.ts | 192 +++++++++ src/server/index.ts | 99 +---- src/styles.examples.ts | 112 ++++++ src/styles.ts | 58 +-- typedoc.config.mjs | 3 + 22 files changed, 1661 insertions(+), 838 deletions(-) create mode 100644 src/app-bridge.examples.ts create mode 100644 src/app.examples.ts create mode 100644 src/message-transport.examples.ts create mode 100644 src/react/index.examples.tsx create mode 100644 src/react/useApp.examples.tsx create mode 100644 src/react/useAutoResize.examples.tsx create mode 100644 src/react/useDocumentTheme.examples.tsx create mode 100644 src/react/useHostStyles.examples.tsx create mode 100644 src/server/index.examples.ts create mode 100644 src/styles.examples.ts diff --git a/AGENTS.md b/AGENTS.md index 3b549777..77018851 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -78,9 +78,13 @@ Guest UI (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP 6. Host sends `sendToolResult()` when tool execution completes 7. Host calls `teardownResource()` before unmounting iframe -## Examples +## Documentation -Uses npm workspaces. Examples in `examples/` are separate packages: +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`). + +## Full Examples + +Uses npm workspaces. Full examples in `examples/` are separate packages: - `basic-server-*` - Starter templates (vanillajs, react, vue, svelte, preact, solid). Use these as the basis for new examples. - `basic-host` - Reference host implementation diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts new file mode 100644 index 00000000..4c1f7610 --- /dev/null +++ b/src/app-bridge.examples.ts @@ -0,0 +1,509 @@ +/** + * Type-checked examples for {@link AppBridge}. + * + * These examples are included in the API documentation via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import { + CallToolResult, + CallToolResultSchema, + ListResourcesResultSchema, + ReadResourceResultSchema, + ListPromptsResultSchema, +} from "@modelcontextprotocol/sdk/types.js"; +import { AppBridge, PostMessageTransport } from "./app-bridge.js"; + +/** + * Example: Basic usage of the AppBridge class with PostMessageTransport. + */ +async function AppBridge_basicUsage(serverTransport: Transport) { + //#region AppBridge_basicUsage + // Create MCP client for the server + const client = new Client({ + name: "MyHost", + version: "1.0.0", + }); + await client.connect(serverTransport); + + // Create bridge for the Guest UI + const bridge = new AppBridge( + client, + { name: "MyHost", version: "1.0.0" }, + { openLinks: {}, serverTools: {}, logging: {} }, + ); + + // Set up iframe and connect + const iframe = document.getElementById("app") as HTMLIFrameElement; + const transport = new PostMessageTransport( + iframe.contentWindow!, + iframe.contentWindow!, + ); + + bridge.oninitialized = () => { + console.log("Guest UI initialized"); + // Now safe to send tool input + bridge.sendToolInput({ arguments: { location: "NYC" } }); + }; + + await bridge.connect(transport); + //#endregion AppBridge_basicUsage +} + +/** + * Example: Creating an AppBridge with an MCP client for automatic forwarding. + */ +function AppBridge_constructor_withMcpClient(mcpClient: Client) { + //#region AppBridge_constructor_withMcpClient + const bridge = new AppBridge( + mcpClient, + { name: "MyHost", version: "1.0.0" }, + { openLinks: {}, serverTools: {}, logging: {} }, + ); + //#endregion AppBridge_constructor_withMcpClient + return bridge; +} + +/** + * Example: Creating an AppBridge without an MCP client, using manual handlers. + */ +function AppBridge_constructor_withoutMcpClient() { + //#region AppBridge_constructor_withoutMcpClient + const bridge = new AppBridge( + null, + { name: "MyHost", version: "1.0.0" }, + { openLinks: {}, serverTools: {}, logging: {} }, + ); + bridge.oncalltool = async (params, extra) => { + // Handle tool calls manually + return { content: [] }; + }; + //#endregion AppBridge_constructor_withoutMcpClient + return bridge; +} + +/** + * Example: Check Guest UI capabilities after initialization. + */ +function AppBridge_guestCapabilities_checkAfterInit(bridge: AppBridge) { + //#region AppBridge_guestCapabilities_checkAfterInit + bridge.oninitialized = () => { + const caps = bridge.getAppCapabilities(); + if (caps?.tools) { + console.log("Guest UI provides tools"); + } + }; + //#endregion AppBridge_guestCapabilities_checkAfterInit +} + +/** + * Example: Log Guest UI information after initialization. + */ +function AppBridge_guestInfo_logAfterInit(bridge: AppBridge) { + //#region AppBridge_guestInfo_logAfterInit + bridge.oninitialized = () => { + const appInfo = bridge.getAppVersion(); + if (appInfo) { + console.log(`Guest UI: ${appInfo.name} v${appInfo.version}`); + } + }; + //#endregion AppBridge_guestInfo_logAfterInit +} + +/** + * Example: Handle Guest UI initialization and send tool input. + */ +function AppBridge_oninitialized_sendToolInput(bridge: AppBridge) { + const toolArgs = { location: "NYC" }; + //#region AppBridge_oninitialized_sendToolInput + bridge.oninitialized = () => { + console.log("Guest UI ready"); + bridge.sendToolInput({ arguments: toolArgs }); + }; + //#endregion AppBridge_oninitialized_sendToolInput +} + +/** + * Example: Handle message requests from the Guest UI. + */ +function AppBridge_onmessage_logMessage(bridge: AppBridge) { + //#region AppBridge_onmessage_logMessage + bridge.onmessage = async ({ role, content }, extra) => { + try { + await chatManager.addMessage({ role, content, source: "app" }); + return {}; // Success + } catch (error) { + console.error("Failed to add message:", error); + return { isError: true }; + } + }; + //#endregion AppBridge_onmessage_logMessage +} + +// Stub for example code - represents a hypothetical chat manager +declare const chatManager: { + addMessage(message: { + role: string; + content: unknown; + source: string; + }): Promise; +}; + +// Stub for example code - represents a hypothetical URL validator +declare function isAllowedDomain(url: string): boolean; + +// Stub for example code - represents a hypothetical dialog API +declare function showDialog(options: { + message: string; + buttons: string[]; +}): Promise; + +// Stub for example code - represents a hypothetical model context storage +declare let modelContext: { + type: string; + content: unknown; + structuredContent: unknown; + timestamp: number; +}; + +// Stub for example code - represents a hypothetical MCP client that can be passed to AppBridge +// Using Client type directly since AppBridge expects Client | null +declare const mcpClient: Client; + +/** + * Example: Handle external link requests from the Guest UI. + */ +function AppBridge_onopenlink_handleRequest(bridge: AppBridge) { + //#region AppBridge_onopenlink_handleRequest + bridge.onopenlink = async ({ url }, extra) => { + if (!isAllowedDomain(url)) { + console.warn("Blocked external link:", url); + return { isError: true }; + } + + const confirmed = await showDialog({ + message: `Open external link?\n${url}`, + buttons: ["Open", "Cancel"], + }); + + if (confirmed) { + window.open(url, "_blank", "noopener,noreferrer"); + return {}; + } + + return { isError: true }; + }; + //#endregion AppBridge_onopenlink_handleRequest +} + +/** + * Example: Store model context updates from the Guest UI. + */ +function AppBridge_onupdatemodelcontext_storeContext(bridge: AppBridge) { + //#region AppBridge_onupdatemodelcontext_storeContext + bridge.onupdatemodelcontext = async ( + { content, structuredContent }, + extra, + ) => { + // Update the model context with the new snapshot + modelContext = { + type: "app_context", + content, + structuredContent, + timestamp: Date.now(), + }; + return {}; + }; + //#endregion AppBridge_onupdatemodelcontext_storeContext +} + +/** + * Example: Forward tool calls to the MCP server. + */ +function AppBridge_oncalltool_forwardToServer(bridge: AppBridge) { + //#region AppBridge_oncalltool_forwardToServer + bridge.oncalltool = async ({ name, arguments: args }, extra) => { + return mcpClient.request( + { method: "tools/call", params: { name, arguments: args } }, + CallToolResultSchema, + { signal: extra.signal }, + ); + }; + //#endregion AppBridge_oncalltool_forwardToServer +} + +/** + * Example: Forward list resources requests to the MCP server. + */ +function AppBridge_onlistresources_returnResources(bridge: AppBridge) { + //#region AppBridge_onlistresources_returnResources + bridge.onlistresources = async (params, extra) => { + return mcpClient.request( + { method: "resources/list", params }, + ListResourcesResultSchema, + { signal: extra.signal }, + ); + }; + //#endregion AppBridge_onlistresources_returnResources +} + +/** + * Example: Forward read resource requests to the MCP server. + */ +function AppBridge_onreadresource_returnResource(bridge: AppBridge) { + //#region AppBridge_onreadresource_returnResource + bridge.onreadresource = async ({ uri }, extra) => { + return mcpClient.request( + { method: "resources/read", params: { uri } }, + ReadResourceResultSchema, + { signal: extra.signal }, + ); + }; + //#endregion AppBridge_onreadresource_returnResource +} + +/** + * Example: Forward list prompts requests to the MCP server. + */ +function AppBridge_onlistprompts_returnPrompts(bridge: AppBridge) { + //#region AppBridge_onlistprompts_returnPrompts + bridge.onlistprompts = async (params, extra) => { + return mcpClient.request( + { method: "prompts/list", params }, + ListPromptsResultSchema, + { signal: extra.signal }, + ); + }; + //#endregion AppBridge_onlistprompts_returnPrompts +} + +// Stub for example code - represents a hypothetical iframe element +declare const iframe: HTMLIFrameElement; + +// Stub for example code - represents a hypothetical host context +declare const hostContext: { + availableDisplayModes?: Array<"inline" | "fullscreen" | "pip">; +}; + +/** + * Example: Handle ping requests from the Guest UI. + */ +function AppBridge_onping_handleRequest(bridge: AppBridge) { + //#region AppBridge_onping_handleRequest + bridge.onping = (params, extra) => { + console.log("Received ping from Guest UI"); + }; + //#endregion AppBridge_onping_handleRequest +} + +/** + * Example: Handle size change notifications from the Guest UI. + */ +function AppBridge_onsizechange_handleResize(bridge: AppBridge) { + //#region AppBridge_onsizechange_handleResize + bridge.onsizechange = ({ width, height }) => { + if (width != null) { + iframe.style.width = `${width}px`; + } + if (height != null) { + iframe.style.height = `${height}px`; + } + }; + //#endregion AppBridge_onsizechange_handleResize +} + +/** + * Example: Handle display mode requests from the Guest UI. + */ +function AppBridge_onrequestdisplaymode_handleRequest(bridge: AppBridge) { + //#region AppBridge_onrequestdisplaymode_handleRequest + type McpUiDisplayMode = "inline" | "fullscreen" | "pip"; + let currentDisplayMode: McpUiDisplayMode = "inline"; + + bridge.onrequestdisplaymode = async ({ mode }, extra) => { + const availableModes = hostContext.availableDisplayModes ?? ["inline"]; + if (availableModes.includes(mode)) { + currentDisplayMode = mode; + return { mode }; + } + // Return current mode if requested mode not available + return { mode: currentDisplayMode }; + }; + //#endregion AppBridge_onrequestdisplaymode_handleRequest +} + +/** + * Example: Handle logging messages from the Guest UI. + */ +function AppBridge_onloggingmessage_handleLog(bridge: AppBridge) { + //#region AppBridge_onloggingmessage_handleLog + bridge.onloggingmessage = ({ level, logger, data }) => { + const prefix = logger ? `[${logger}]` : "[Guest UI]"; + console[level === "error" ? "error" : "log"]( + `${prefix} ${level.toUpperCase()}:`, + data, + ); + }; + //#endregion AppBridge_onloggingmessage_handleLog +} + +/** + * Example: Gracefully tear down the Guest UI before unmounting. + */ +async function AppBridge_teardownResource_gracefulShutdown(bridge: AppBridge) { + //#region AppBridge_teardownResource_gracefulShutdown + try { + await bridge.teardownResource({}); + // Guest UI is ready, safe to unmount iframe + iframe.remove(); + } catch (error) { + console.error("Teardown failed:", error); + } + //#endregion AppBridge_teardownResource_gracefulShutdown +} + +/** + * Example: Update theme when user toggles dark mode. + */ +function AppBridge_setHostContext_updateTheme(bridge: AppBridge) { + //#region AppBridge_setHostContext_updateTheme + bridge.setHostContext({ theme: "dark" }); + //#endregion AppBridge_setHostContext_updateTheme +} + +/** + * Example: Update multiple context fields at once. + */ +function AppBridge_setHostContext_updateMultiple(bridge: AppBridge) { + //#region AppBridge_setHostContext_updateMultiple + bridge.setHostContext({ + theme: "dark", + containerDimensions: { maxHeight: 600, width: 800 }, + }); + //#endregion AppBridge_setHostContext_updateMultiple +} + +/** + * Example: Send tool input after initialization. + */ +function AppBridge_sendToolInput_afterInit(bridge: AppBridge) { + //#region AppBridge_sendToolInput_afterInit + bridge.oninitialized = () => { + bridge.sendToolInput({ + arguments: { location: "New York", units: "metric" }, + }); + }; + //#endregion AppBridge_sendToolInput_afterInit +} + +/** + * Example: Stream partial arguments as they arrive. + */ +function AppBridge_sendToolInputPartial_streaming(bridge: AppBridge) { + //#region AppBridge_sendToolInputPartial_streaming + // As streaming progresses... + bridge.sendToolInputPartial({ arguments: { loc: "N" } }); + bridge.sendToolInputPartial({ arguments: { location: "New" } }); + bridge.sendToolInputPartial({ arguments: { location: "New York" } }); + + // When complete, send final input + bridge.sendToolInput({ + arguments: { location: "New York", units: "metric" }, + }); + //#endregion AppBridge_sendToolInputPartial_streaming +} + +/** + * Example: Send tool result after execution. + */ +async function AppBridge_sendToolResult_afterExecution( + bridge: AppBridge, + mcpClientParam: Client, + args: Record, +) { + //#region AppBridge_sendToolResult_afterExecution + // import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; + + const result = await mcpClient.request( + { method: "tools/call", params: { name: "get_weather", arguments: args } }, + CallToolResultSchema, + ); + bridge.sendToolResult(result); + //#endregion AppBridge_sendToolResult_afterExecution + + // Make the parameter used to satisfy linting + void mcpClientParam; +} + +/** + * Example: User-initiated cancellation. + */ +function AppBridge_sendToolCancelled_userInitiated(bridge: AppBridge) { + //#region AppBridge_sendToolCancelled_userInitiated + // User clicked "Cancel" button + bridge.sendToolCancelled({ reason: "User cancelled the operation" }); + //#endregion AppBridge_sendToolCancelled_userInitiated +} + +/** + * Example: System-level cancellation. + */ +function AppBridge_sendToolCancelled_systemLevel(bridge: AppBridge) { + //#region AppBridge_sendToolCancelled_systemLevel + // Sampling error or timeout + bridge.sendToolCancelled({ reason: "Request timeout after 30 seconds" }); + + // Classifier intervention + bridge.sendToolCancelled({ reason: "Content policy violation detected" }); + //#endregion AppBridge_sendToolCancelled_systemLevel +} + +/** + * Example: Connect with MCP client for automatic forwarding. + */ +async function AppBridge_connect_withMcpClient( + mcpClientParam: Client, + hostInfo: { name: string; version: string }, + capabilities: { openLinks: {}; serverTools: {}; logging: {} }, + toolArgs: Record, +) { + //#region AppBridge_connect_withMcpClient + const bridge = new AppBridge(mcpClient, hostInfo, capabilities); + const transport = new PostMessageTransport( + iframe.contentWindow!, + iframe.contentWindow!, + ); + + bridge.oninitialized = () => { + console.log("Guest UI ready"); + bridge.sendToolInput({ arguments: toolArgs }); + }; + + await bridge.connect(transport); + //#endregion AppBridge_connect_withMcpClient +} + +/** + * Example: Connect without MCP client using manual handlers. + */ +async function AppBridge_connect_withoutMcpClient( + hostInfo: { name: string; version: string }, + capabilities: { openLinks: {}; serverTools: {}; logging: {} }, + transport: Transport, +) { + //#region AppBridge_connect_withoutMcpClient + const bridge = new AppBridge(null, hostInfo, capabilities); + + // Register handlers manually + bridge.oncalltool = async (params, extra) => { + // Custom tool call handling + return { content: [] }; + }; + + await bridge.connect(transport); + //#endregion AppBridge_connect_withoutMcpClient +} diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 2e969585..c59cd1da 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -215,39 +215,7 @@ type RequestHandlerExtra = Parameters< * 5. **Teardown**: Call {@link teardownResource} before unmounting iframe * * @example Basic usage - * ```typescript - * import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge'; - * import { Client } from '@modelcontextprotocol/sdk/client/index.js'; - * - * // Create MCP client for the server - * const client = new Client({ - * name: "MyHost", - * version: "1.0.0", - * }); - * await client.connect(serverTransport); - * - * // Create bridge for the Guest UI - * const bridge = new AppBridge( - * client, - * { name: "MyHost", version: "1.0.0" }, - * { openLinks: {}, serverTools: {}, logging: {} } - * ); - * - * // Set up iframe and connect - * const iframe = document.getElementById('app') as HTMLIFrameElement; - * const transport = new PostMessageTransport( - * iframe.contentWindow!, - * iframe.contentWindow!, - * ); - * - * bridge.oninitialized = () => { - * console.log("Guest UI initialized"); - * // Now safe to send tool input - * bridge.sendToolInput({ arguments: { location: "NYC" } }); - * }; - * - * await bridge.connect(transport); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_basicUsage} */ export class AppBridge extends Protocol< AppRequest, @@ -270,23 +238,10 @@ export class AppBridge extends Protocol< * @param options - Configuration options (inherited from Protocol) * * @example With MCP client (automatic forwarding) - * ```typescript - * const bridge = new AppBridge( - * mcpClient, - * { name: "MyHost", version: "1.0.0" }, - * { openLinks: {}, serverTools: {}, logging: {} } - * ); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_constructor_withMcpClient} * * @example Without MCP client (manual handlers) - * ```typescript - * const bridge = new AppBridge( - * null, - * { name: "MyHost", version: "1.0.0" }, - * { openLinks: {}, serverTools: {}, logging: {} } - * ); - * bridge.oncalltool = async (params, extra) => { ... }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_constructor_withoutMcpClient} */ constructor( private _client: Client | null, @@ -325,14 +280,7 @@ export class AppBridge extends Protocol< * @returns Guest UI capabilities, or `undefined` if not yet initialized * * @example Check Guest UI capabilities after initialization - * ```typescript - * bridge.oninitialized = () => { - * const caps = bridge.getAppCapabilities(); - * if (caps?.tools) { - * console.log("Guest UI provides tools"); - * } - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_guestCapabilities_checkAfterInit} * * @see {@link McpUiAppCapabilities} for the capabilities structure */ @@ -349,14 +297,7 @@ export class AppBridge extends Protocol< * @returns Guest UI implementation info, or `undefined` if not yet initialized * * @example Log Guest UI information after initialization - * ```typescript - * bridge.oninitialized = () => { - * const appInfo = bridge.getAppVersion(); - * if (appInfo) { - * console.log(`Guest UI: ${appInfo.name} v${appInfo.version}`); - * } - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_guestInfo_logAfterInit} */ getAppVersion(): Implementation | undefined { return this._appInfo; @@ -376,11 +317,7 @@ export class AppBridge extends Protocol< * @param extra - Request metadata (abort signal, session info) * * @example - * ```typescript - * bridge.onping = (params, extra) => { - * console.log("Received ping from Guest UI"); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onping_handleRequest} */ onping?: (params: PingRequest["params"], extra: RequestHandlerExtra) => void; @@ -395,16 +332,7 @@ export class AppBridge extends Protocol< * host container dimension changes, use {@link setHostContext}. * * @example - * ```typescript - * bridge.onsizechange = ({ width, height }) => { - * if (width != null) { - * iframe.style.width = `${width}px`; - * } - * if (height != null) { - * iframe.style.height = `${height}px`; - * } - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onsizechange_handleResize} * * @see {@link McpUiSizeChangedNotification} for the notification type * @see {@link app!App.sendSizeChanged} - the Guest UI method that sends these notifications @@ -462,12 +390,7 @@ export class AppBridge extends Protocol< * initialization handshake and is ready to receive tool input and other data. * * @example - * ```typescript - * bridge.oninitialized = () => { - * console.log("Guest UI ready"); - * bridge.sendToolInput({ arguments: toolArgs }); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_oninitialized_sendToolInput} * * @see {@link McpUiInitializedNotification} for the notification type * @see {@link sendToolInput} for sending tool arguments to the Guest UI @@ -499,17 +422,7 @@ export class AppBridge extends Protocol< * - Returns: `Promise` with optional `isError` flag * * @example - * ```typescript - * bridge.onmessage = async ({ role, content }, extra) => { - * try { - * await chatManager.addMessage({ role, content, source: "app" }); - * return {}; // Success - * } catch (error) { - * console.error("Failed to add message:", error); - * return { isError: true }; - * } - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onmessage_logMessage} * * @see {@link McpUiMessageRequest} for the request type * @see {@link McpUiMessageResult} for the result type @@ -547,26 +460,7 @@ export class AppBridge extends Protocol< * - Returns: `Promise` with optional `isError` flag * * @example - * ```typescript - * bridge.onopenlink = async ({ url }, extra) => { - * if (!isAllowedDomain(url)) { - * console.warn("Blocked external link:", url); - * return { isError: true }; - * } - * - * const confirmed = await showDialog({ - * message: `Open external link?\n${url}`, - * buttons: ["Open", "Cancel"] - * }); - * - * if (confirmed) { - * window.open(url, "_blank", "noopener,noreferrer"); - * return {}; - * } - * - * return { isError: true }; - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onopenlink_handleRequest} * * @see {@link McpUiOpenLinkRequest} for the request type * @see {@link McpUiOpenLinkResult} for the result type @@ -605,19 +499,7 @@ export class AppBridge extends Protocol< * - Returns: `Promise` with the actual mode set * * @example - * ```typescript - * let currentDisplayMode: McpUiDisplayMode = "inline"; - * - * bridge.onrequestdisplaymode = async ({ mode }, extra) => { - * const availableModes = hostContext.availableDisplayModes ?? ["inline"]; - * if (availableModes.includes(mode)) { - * currentDisplayMode = mode; - * return { mode }; - * } - * // Return current mode if requested mode not available - * return { mode: currentDisplayMode }; - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onrequestdisplaymode_handleRequest} * * @see {@link McpUiRequestDisplayModeRequest} for the request type * @see {@link McpUiRequestDisplayModeResult} for the result type @@ -653,15 +535,7 @@ export class AppBridge extends Protocol< * - `params.data` - Log message and optional structured data * * @example - * ```typescript - * bridge.onloggingmessage = ({ level, logger, data }) => { - * const prefix = logger ? `[${logger}]` : "[Guest UI]"; - * console[level === "error" ? "error" : "log"]( - * `${prefix} ${level.toUpperCase()}:`, - * data - * ); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onloggingmessage_handleLog} */ set onloggingmessage( callback: (params: LoggingMessageNotification["params"]) => void, @@ -687,18 +561,7 @@ export class AppBridge extends Protocol< * update received. * * @example - * ```typescript - * bridge.onupdatemodelcontext = async ({ content, structuredContent }, extra) => { - * // Update the model context with the new snapshot - * modelContext = { - * type: "app_context", - * content, - * structuredContent, - * timestamp: Date.now() - * }; - * return {}; - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onupdatemodelcontext_storeContext} * * @see {@link McpUiUpdateModelContextRequest} for the request type */ @@ -729,15 +592,7 @@ export class AppBridge extends Protocol< * - `extra` - Request metadata (abort signal, session info) * * @example - * ```typescript - * bridge.oncalltool = async ({ name, arguments: args }, extra) => { - * return mcpClient.request( - * { method: "tools/call", params: { name, arguments: args } }, - * CallToolResultSchema, - * { signal: extra.signal } - * ); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_oncalltool_forwardToServer} * * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type @@ -792,15 +647,7 @@ export class AppBridge extends Protocol< * - `extra` - Request metadata (abort signal, session info) * * @example - * ```typescript - * bridge.onlistresources = async (params, extra) => { - * return mcpClient.request( - * { method: "resources/list", params }, - * ListResourcesResultSchema, - * { signal: extra.signal } - * ); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onlistresources_returnResources} * * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type @@ -872,15 +719,7 @@ export class AppBridge extends Protocol< * - `extra` - Request metadata (abort signal, session info) * * @example - * ```typescript - * bridge.onreadresource = async ({ uri }, extra) => { - * return mcpClient.request( - * { method: "resources/read", params: { uri } }, - * ReadResourceResultSchema, - * { signal: extra.signal } - * ); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onreadresource_returnResource} * * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type @@ -940,15 +779,7 @@ export class AppBridge extends Protocol< * - `extra` - Request metadata (abort signal, session info) * * @example - * ```typescript - * bridge.onlistprompts = async (params, extra) => { - * return mcpClient.request( - * { method: "prompts/list", params }, - * ListPromptsResultSchema, - * { signal: extra.signal } - * ); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_onlistprompts_returnPrompts} * * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type @@ -1084,17 +915,10 @@ export class AppBridge extends Protocol< * @param hostContext - The complete new host context state * * @example Update theme when user toggles dark mode - * ```typescript - * bridge.setHostContext({ theme: "dark" }); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_setHostContext_updateTheme} * * @example Update multiple context fields - * ```typescript - * bridge.setHostContext({ - * theme: "dark", - * containerDimensions: { maxHeight: 600, width: 800 } - * }); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_setHostContext_updateMultiple} * * @see {@link McpUiHostContext} for the context structure * @see {@link McpUiHostContextChangedNotification} for the notification type @@ -1147,13 +971,7 @@ export class AppBridge extends Protocol< * @param params - Complete tool call arguments * * @example - * ```typescript - * bridge.oninitialized = () => { - * bridge.sendToolInput({ - * arguments: { location: "New York", units: "metric" } - * }); - * }; - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolInput_afterInit} * * @see {@link McpUiToolInputNotification} for the notification type * @see {@link oninitialized} for the initialization callback @@ -1180,15 +998,7 @@ export class AppBridge extends Protocol< * @param params - Partial tool call arguments (may be incomplete) * * @example Stream partial arguments as they arrive - * ```typescript - * // As streaming progresses... - * bridge.sendToolInputPartial({ arguments: { loc: "N" } }); - * bridge.sendToolInputPartial({ arguments: { location: "New" } }); - * bridge.sendToolInputPartial({ arguments: { location: "New York" } }); - * - * // When complete, send final input - * bridge.sendToolInput({ arguments: { location: "New York", units: "metric" } }); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolInputPartial_streaming} * * @see {@link McpUiToolInputPartialNotification} for the notification type * @see {@link sendToolInput} for sending complete arguments @@ -1211,15 +1021,7 @@ export class AppBridge extends Protocol< * @param params - Standard MCP tool execution result * * @example - * ```typescript - * import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; - * - * const result = await mcpClient.request( - * { method: "tools/call", params: { name: "get_weather", arguments: args } }, - * CallToolResultSchema - * ); - * bridge.sendToolResult(result); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution} * * @see {@link McpUiToolResultNotification} for the notification type * @see {@link sendToolInput} for sending tool arguments before results @@ -1243,19 +1045,10 @@ export class AppBridge extends Protocol< * - `reason`: Human-readable explanation for why the tool was cancelled * * @example User-initiated cancellation - * ```typescript - * // User clicked "Cancel" button - * bridge.sendToolCancelled({ reason: "User cancelled the operation" }); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolCancelled_userInitiated} * * @example System-level cancellation - * ```typescript - * // Sampling error or timeout - * bridge.sendToolCancelled({ reason: "Request timeout after 30 seconds" }); - * - * // Classifier intervention - * bridge.sendToolCancelled({ reason: "Content policy violation detected" }); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_sendToolCancelled_systemLevel} * * @see {@link McpUiToolCancelledNotification} for the notification type * @see {@link sendToolResult} for sending successful results @@ -1306,15 +1099,7 @@ export class AppBridge extends Protocol< * @returns Promise resolving when Guest UI confirms readiness for teardown * * @example - * ```typescript - * try { - * await bridge.teardownResource({}); - * // Guest UI is ready, safe to unmount iframe - * iframe.remove(); - * } catch (error) { - * console.error("Teardown failed:", error); - * } - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_teardownResource_gracefulShutdown} */ teardownResource( params: McpUiResourceTeardownRequest["params"], @@ -1359,32 +1144,10 @@ export class AppBridge extends Protocol< * before calling `bridge.connect()`. * * @example With MCP client (automatic forwarding) - * ```typescript - * const bridge = new AppBridge(mcpClient, hostInfo, capabilities); - * const transport = new PostMessageTransport( - * iframe.contentWindow!, - * iframe.contentWindow!, - * ); - * - * bridge.oninitialized = () => { - * console.log("Guest UI ready"); - * bridge.sendToolInput({ arguments: toolArgs }); - * }; - * - * await bridge.connect(transport); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_connect_withMcpClient} * * @example Without MCP client (manual handlers) - * ```typescript - * const bridge = new AppBridge(null, hostInfo, capabilities); - * - * // Register handlers manually - * bridge.oncalltool = async (params, extra) => { - * // Custom tool call handling - * }; - * - * await bridge.connect(transport); - * ``` + * {@includeCode ./app-bridge.examples.ts#AppBridge_connect_withoutMcpClient} */ async connect(transport: Transport) { if (this._client) { diff --git a/src/app.examples.ts b/src/app.examples.ts new file mode 100644 index 00000000..6bbe42dd --- /dev/null +++ b/src/app.examples.ts @@ -0,0 +1,446 @@ +/** + * Type-checked examples for {@link App} and constants in {@link ./app.ts}. + * + * These examples are included in the API documentation via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import { + App, + PostMessageTransport, + McpUiToolInputNotificationSchema, + RESOURCE_URI_META_KEY, +} from "./app.js"; + +/** + * Example: How MCP servers use RESOURCE_URI_META_KEY (server-side, not in Apps). + */ +function RESOURCE_URI_META_KEY_serverSide(): CallToolResult { + //#region RESOURCE_URI_META_KEY_serverSide + // In an MCP server's tool handler: + return { + content: [{ type: "text", text: "Result" }], + _meta: { + [RESOURCE_URI_META_KEY]: "ui://weather/forecast", + }, + }; + //#endregion RESOURCE_URI_META_KEY_serverSide +} + +/** + * Example: How hosts check for RESOURCE_URI_META_KEY metadata (host-side). + */ +async function RESOURCE_URI_META_KEY_hostSide(mcpClient: { + callTool: (args: { + name: string; + arguments: object; + }) => Promise; +}) { + //#region RESOURCE_URI_META_KEY_hostSide + const result = await mcpClient.callTool({ name: "weather", arguments: {} }); + const uiUri = result._meta?.[RESOURCE_URI_META_KEY]; + if (uiUri) { + // Load and display the UI resource + } + //#endregion RESOURCE_URI_META_KEY_hostSide +} + +/** + * Example: App constructor with appInfo, capabilities, and options. + */ +function App_constructor_basic() { + //#region App_constructor_basic + const app = new App( + { name: "MyApp", version: "1.0.0" }, + { tools: { listChanged: true } }, // capabilities + { autoResize: true }, // options + ); + //#endregion App_constructor_basic + return app; +} + +/** + * Example: Basic usage of the App class with PostMessageTransport. + */ +async function App_basicUsage() { + //#region App_basicUsage + const app = new App( + { name: "WeatherApp", version: "1.0.0" }, + {}, // capabilities + ); + + // Register notification handler using setter (simpler) + app.ontoolinput = (params) => { + console.log("Tool arguments:", params.arguments); + }; + + // OR using inherited setNotificationHandler (more explicit) + app.setNotificationHandler( + McpUiToolInputNotificationSchema, + (notification) => { + console.log("Tool arguments:", notification.params.arguments); + }, + ); + + await app.connect(new PostMessageTransport(window.parent, window.parent)); + //#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. + */ +async function App_hostCapabilities_checkAfterConnection( + app: App, + transport: PostMessageTransport, +) { + //#region App_hostCapabilities_checkAfterConnection + await app.connect(transport); + const caps = app.getHostCapabilities(); + if (caps === undefined) { + console.error("Not connected"); + return; + } + if (caps.serverTools) { + console.log("Host supports server tool calls"); + } + //#endregion App_hostCapabilities_checkAfterConnection +} + +/** + * Example: Log host information after connection. + */ +async function App_hostInfo_logAfterConnection( + app: App, + transport: PostMessageTransport, +) { + //#region App_hostInfo_logAfterConnection + await app.connect(transport); + const host = app.getHostVersion(); + if (host === undefined) { + console.error("Not connected"); + return; + } + console.log(`Connected to ${host.name} v${host.version}`); + //#endregion App_hostInfo_logAfterConnection +} + +/** + * Example: Access host context after connection. + */ +async function App_hostContext_accessAfterConnection( + app: App, + transport: PostMessageTransport, +) { + //#region App_hostContext_accessAfterConnection + await app.connect(transport); + const context = app.getHostContext(); + if (context === undefined) { + console.error("Not connected"); + return; + } + if (context.theme === "dark") { + document.body.classList.add("dark-theme"); + } + if (context.toolInfo) { + console.log("Tool:", context.toolInfo.tool.name); + } + //#endregion App_hostContext_accessAfterConnection +} + +/** + * Example: Using the ontoolinput setter (simpler approach). + */ +async function App_ontoolinput_setter( + app: App, + transport: PostMessageTransport, +) { + //#region App_ontoolinput_setter + // Register before connecting to ensure no notifications are missed + app.ontoolinput = (params) => { + console.log("Tool:", params.arguments); + // Update your UI with the tool arguments + }; + await app.connect(transport); + //#endregion App_ontoolinput_setter +} + +/** + * Example: Using setNotificationHandler for tool input (more explicit approach). + */ +function App_ontoolinput_setNotificationHandler(app: App) { + //#region App_ontoolinput_setNotificationHandler + app.setNotificationHandler( + McpUiToolInputNotificationSchema, + (notification) => { + console.log("Tool:", notification.params.arguments); + }, + ); + //#endregion App_ontoolinput_setNotificationHandler +} + +/** + * Example: Progressive rendering of tool arguments using ontoolinputpartial. + */ +function App_ontoolinputpartial_progressiveRendering(app: App) { + //#region App_ontoolinputpartial_progressiveRendering + app.ontoolinputpartial = (params) => { + console.log("Partial args:", params.arguments); + // Update your UI progressively as arguments stream in + }; + //#endregion App_ontoolinputpartial_progressiveRendering +} + +/** + * Example: Display tool execution results using ontoolresult. + */ +function App_ontoolresult_displayResults(app: App) { + //#region App_ontoolresult_displayResults + app.ontoolresult = (params) => { + if (params.content) { + console.log("Tool output:", params.content); + } + if (params.isError) { + console.error("Tool execution failed"); + } + }; + //#endregion App_ontoolresult_displayResults +} + +/** + * Example: Handle tool cancellation notifications. + */ +function App_ontoolcancelled_handleCancellation(app: App) { + //#region App_ontoolcancelled_handleCancellation + app.ontoolcancelled = (params) => { + console.log("Tool cancelled:", params.reason); + showCancelledMessage(params.reason ?? "Operation was cancelled"); + }; + //#endregion App_ontoolcancelled_handleCancellation +} + +// Stub for example +declare function showCancelledMessage(message: string): void; + +/** + * Example: Respond to theme changes using onhostcontextchanged. + */ +function App_onhostcontextchanged_respondToTheme(app: App) { + //#region App_onhostcontextchanged_respondToTheme + app.onhostcontextchanged = (params) => { + if (params.theme === "dark") { + document.body.classList.add("dark-theme"); + } else { + document.body.classList.remove("dark-theme"); + } + }; + //#endregion App_onhostcontextchanged_respondToTheme +} + +/** + * Example: Perform cleanup before teardown. + */ +function App_onteardown_performCleanup(app: App) { + //#region App_onteardown_performCleanup + app.onteardown = async () => { + await saveState(); + closeConnections(); + console.log("App ready for teardown"); + return {}; + }; + //#endregion App_onteardown_performCleanup +} + +// Stubs for example +declare function saveState(): Promise; +declare function closeConnections(): void; + +/** + * Example: Handle tool calls from the host. + */ +function App_oncalltool_handleFromHost(app: App) { + //#region App_oncalltool_handleFromHost + app.oncalltool = async (params, extra) => { + if (params.name === "greet") { + const name = params.arguments?.name ?? "World"; + return { content: [{ type: "text", text: `Hello, ${name}!` }] }; + } + throw new Error(`Unknown tool: ${params.name}`); + }; + //#endregion App_oncalltool_handleFromHost +} + +/** + * Example: Return available tools from the onlisttools handler. + */ +function App_onlisttools_returnTools(app: App) { + //#region App_onlisttools_returnTools + app.onlisttools = async (params, extra) => { + return { + tools: ["calculate", "convert", "format"], + }; + }; + //#endregion App_onlisttools_returnTools +} + +/** + * Example: Fetch updated weather data using callServerTool. + */ +async function App_callServerTool_fetchWeather(app: App) { + //#region App_callServerTool_fetchWeather + try { + const result = await app.callServerTool({ + name: "get_weather", + arguments: { location: "Tokyo" }, + }); + if (result.isError) { + console.error("Tool returned error:", result.content); + } else { + console.log(result.content); + } + } catch (error) { + console.error("Tool call failed:", error); + } + //#endregion App_callServerTool_fetchWeather +} + +/** + * Example: Send a text message from user interaction. + */ +async function App_sendMessage_textFromInteraction(app: App) { + //#region App_sendMessage_textFromInteraction + try { + await app.sendMessage({ + role: "user", + content: [{ type: "text", text: "Show me details for item #42" }], + }); + } catch (error) { + console.error("Failed to send message:", error); + // Handle error appropriately for your app + } + //#endregion App_sendMessage_textFromInteraction +} + +/** + * Example: Log app state for debugging. + */ +function App_sendLog_debugState(app: App) { + //#region App_sendLog_debugState + app.sendLog({ + level: "info", + data: "Weather data refreshed", + logger: "WeatherApp", + }); + //#endregion App_sendLog_debugState +} + +/** + * Example: Update model context with current app state. + */ +async function App_updateModelContext_appState(app: App) { + //#region App_updateModelContext_appState + await app.updateModelContext({ + content: [{ type: "text", text: "User selected 3 items totaling $150.00" }], + }); + //#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: Open documentation link. + */ +async function App_openLink_documentation(app: App) { + //#region App_openLink_documentation + try { + await app.openLink({ url: "https://docs.example.com" }); + } catch (error) { + console.error("Failed to open link:", error); + // Optionally show fallback: display URL for manual copy + } + //#endregion App_openLink_documentation +} + +/** + * Example: Request fullscreen mode. + */ +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); + } + //#endregion App_requestDisplayMode_fullscreen +} + +/** + * Example: Manually notify host of size change. + */ +function App_sendSizeChanged_manual(app: App) { + //#region App_sendSizeChanged_manual + app.sendSizeChanged({ + width: 400, + height: 600, + }); + //#endregion App_sendSizeChanged_manual +} + +/** + * Example: Manual setup for custom scenarios (setupSizeChangedNotifications). + */ +async function App_setupAutoResize_manual(transport: PostMessageTransport) { + //#region App_setupAutoResize_manual + const app = new App( + { name: "MyApp", version: "1.0.0" }, + {}, + { autoResize: false }, + ); + await app.connect(transport); + + // Later, enable auto-resize manually + const cleanup = app.setupSizeChangedNotifications(); + + // Clean up when done + cleanup(); + //#endregion App_setupAutoResize_manual +} + +/** + * Example: Connect with PostMessageTransport. + */ +async function App_connect_withPostMessageTransport() { + //#region App_connect_withPostMessageTransport + const app = new App({ name: "MyApp", version: "1.0.0" }, {}); + + try { + await app.connect(new PostMessageTransport(window.parent, window.parent)); + console.log("Connected successfully!"); + } catch (error) { + console.error("Failed to connect:", error); + } + //#endregion App_connect_withPostMessageTransport +} diff --git a/src/app.ts b/src/app.ts index e24913e3..b16fe6fd 100644 --- a/src/app.ts +++ b/src/app.ts @@ -70,24 +70,10 @@ export { * in their tool handlers; App developers typically don't need to use it directly. * * @example How MCP servers use this key (server-side, not in Apps) - * ```typescript - * // In an MCP server's tool handler: - * return { - * content: [{ type: "text", text: "Result" }], - * _meta: { - * [RESOURCE_URI_META_KEY]: "ui://weather/forecast" - * } - * }; - * ``` + * {@includeCode ./app.examples.ts#RESOURCE_URI_META_KEY_serverSide} * * @example How hosts check for this metadata (host-side) - * ```typescript - * const result = await mcpClient.callTool({ name: "weather", arguments: {} }); - * const uiUri = result._meta?.[RESOURCE_URI_META_KEY]; - * if (uiUri) { - * // Load and display the UI resource - * } - * ``` + * {@includeCode ./app.examples.ts#RESOURCE_URI_META_KEY_hostSide} */ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; @@ -167,41 +153,10 @@ type RequestHandlerExtra = Parameters< * Both patterns work; use whichever fits your coding style better. * * @example Basic usage with PostMessageTransport - * ```typescript - * import { - * App, - * PostMessageTransport, - * McpUiToolInputNotificationSchema - * } from '@modelcontextprotocol/ext-apps'; - * - * const app = new App( - * { name: "WeatherApp", version: "1.0.0" }, - * {} // capabilities - * ); - * - * // Register notification handler using setter (simpler) - * app.ontoolinput = (params) => { - * console.log("Tool arguments:", params.arguments); - * }; - * - * // OR using inherited setNotificationHandler (more explicit) - * app.setNotificationHandler( - * McpUiToolInputNotificationSchema, - * (notification) => { - * console.log("Tool arguments:", notification.params.arguments); - * } - * ); - * - * await app.connect(new PostMessageTransport(window.parent, window.parent)); - * ``` + * {@includeCode ./app.examples.ts#App_basicUsage} * * @example Sending a message to the host's chat - * ```typescript - * await app.sendMessage({ - * role: "user", - * content: [{ type: "text", text: "Weather updated!" }] - * }); - * ``` + * {@includeCode ./app.examples.ts#App_sendMessage} */ export class App extends Protocol { private _hostCapabilities?: McpUiHostCapabilities; @@ -216,13 +171,7 @@ export class App extends Protocol { * @param options - Configuration options including `autoResize` behavior * * @example - * ```typescript - * const app = new App( - * { name: "MyApp", version: "1.0.0" }, - * { tools: { listChanged: true } }, // capabilities - * { autoResize: true } // options - * ); - * ``` + * {@includeCode ./app.examples.ts#App_constructor_basic} */ constructor( private _appInfo: Implementation, @@ -251,17 +200,7 @@ export class App extends Protocol { * @returns Host capabilities, or `undefined` if not yet connected * * @example Check host capabilities after connection - * ```typescript - * await app.connect(transport); - * const caps = app.getHostCapabilities(); - * if (caps === undefined) { - * console.error("Not connected"); - * return; - * } - * if (caps.serverTools) { - * console.log("Host supports server tool calls"); - * } - * ``` + * {@includeCode ./app.examples.ts#App_hostCapabilities_checkAfterConnection} * * @see {@link connect} for the initialization handshake * @see {@link McpUiHostCapabilities} for the capabilities structure @@ -280,15 +219,7 @@ export class App extends Protocol { * @returns Host implementation info, or `undefined` if not yet connected * * @example Log host information after connection - * ```typescript - * await app.connect(transport); - * const host = app.getHostVersion(); - * if (host === undefined) { - * console.error("Not connected"); - * return; - * } - * console.log(`Connected to ${host.name} v${host.version}`); - * ``` + * {@includeCode ./app.examples.ts#App_hostInfo_logAfterConnection} * * @see {@link connect} for the initialization handshake */ @@ -309,20 +240,7 @@ export class App extends Protocol { * @returns Host context, or `undefined` if not yet connected * * @example Access host context after connection - * ```typescript - * await app.connect(transport); - * const context = app.getHostContext(); - * if (context === undefined) { - * console.error("Not connected"); - * return; - * } - * if (context.theme === "dark") { - * document.body.classList.add("dark-theme"); - * } - * if (context.toolInfo) { - * console.log("Tool:", context.toolInfo.tool.name); - * } - * ``` + * {@includeCode ./app.examples.ts#App_hostContext_accessAfterConnection} * * @see {@link connect} for the initialization handshake * @see {@link onhostcontextchanged} for context change notifications @@ -347,24 +265,10 @@ export class App extends Protocol { * @param callback - Function called with the tool input params ({@link McpUiToolInputNotification.params}) * * @example Using the setter (simpler) - * ```typescript - * // Register before connecting to ensure no notifications are missed - * app.ontoolinput = (params) => { - * console.log("Tool:", params.arguments); - * // Update your UI with the tool arguments - * }; - * await app.connect(transport); - * ``` + * {@includeCode ./app.examples.ts#App_ontoolinput_setter} * * @example Using setNotificationHandler (more explicit) - * ```typescript - * app.setNotificationHandler( - * McpUiToolInputNotificationSchema, - * (notification) => { - * console.log("Tool:", notification.params.arguments); - * } - * ); - * ``` + * {@includeCode ./app.examples.ts#App_ontoolinput_setNotificationHandler} * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiToolInputNotification} for the notification structure @@ -392,12 +296,7 @@ export class App extends Protocol { * @param callback - Function called with each partial tool input update ({@link McpUiToolInputPartialNotification.params}) * * @example Progressive rendering of tool arguments - * ```typescript - * app.ontoolinputpartial = (params) => { - * console.log("Partial args:", params.arguments); - * // Update your UI progressively as arguments stream in - * }; - * ``` + * {@includeCode ./app.examples.ts#App_ontoolinputpartial_progressiveRendering} * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiToolInputPartialNotification} for the notification structure @@ -426,16 +325,7 @@ export class App extends Protocol { * @param callback - Function called with the tool result ({@link McpUiToolResultNotification.params}) * * @example Display tool execution results - * ```typescript - * app.ontoolresult = (params) => { - * if (params.content) { - * console.log("Tool output:", params.content); - * } - * if (params.isError) { - * console.error("Tool execution failed"); - * } - * }; - * ``` + * {@includeCode ./app.examples.ts#App_ontoolresult_displayResults} * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiToolResultNotification} for the notification structure @@ -465,12 +355,7 @@ export class App extends Protocol { * @param callback - Function called when tool execution is cancelled. Receives optional cancellation reason — see {@link McpUiToolCancelledNotification.params}. * * @example Handle tool cancellation - * ```typescript - * app.ontoolcancelled = (params) => { - * console.log("Tool cancelled:", params.reason); - * showCancelledMessage(params.reason ?? "Operation was cancelled"); - * }; - * ``` + * {@includeCode ./app.examples.ts#App_ontoolcancelled_handleCancellation} * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiToolCancelledNotification} for the notification structure @@ -504,15 +389,7 @@ export class App extends Protocol { * @param callback - Function called with the updated host context * * @example Respond to theme changes - * ```typescript - * app.onhostcontextchanged = (params) => { - * if (params.theme === "dark") { - * document.body.classList.add("dark-theme"); - * } else { - * document.body.classList.remove("dark-theme"); - * } - * }; - * ``` + * {@includeCode ./app.examples.ts#App_onhostcontextchanged_respondToTheme} * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiHostContextChangedNotification} for the notification structure @@ -550,14 +427,7 @@ export class App extends Protocol { * Must return `McpUiResourceTeardownResult` (can be an empty object `{}`) or a Promise resolving to it. * * @example Perform cleanup before teardown - * ```typescript - * app.onteardown = async () => { - * await saveState(); - * closeConnections(); - * console.log("App ready for teardown"); - * return {}; - * }; - * ``` + * {@includeCode ./app.examples.ts#App_onteardown_performCleanup} * * @see {@link setRequestHandler} for the underlying method * @see {@link McpUiResourceTeardownRequest} for the request structure @@ -593,15 +463,7 @@ export class App extends Protocol { * in the constructor. * * @example Handle tool calls from the host - * ```typescript - * app.oncalltool = async (params, extra) => { - * if (params.name === "greet") { - * const name = params.arguments?.name ?? "World"; - * return { content: [{ type: "text", text: `Hello, ${name}!` }] }; - * } - * throw new Error(`Unknown tool: ${params.name}`); - * }; - * ``` + * {@includeCode ./app.examples.ts#App_oncalltool_handleFromHost} * * @see {@link setRequestHandler} for the underlying method */ @@ -635,13 +497,7 @@ export class App extends Protocol { * allowed; capability validation occurs when handlers are invoked. * * @example Return available tools - * ```typescript - * app.onlisttools = async (params, extra) => { - * return { - * tools: ["calculate", "convert", "format"] - * }; - * }; - * ``` + * {@includeCode ./app.examples.ts#App_onlisttools_returnTools} * * @see {@link setRequestHandler} for the underlying method * @see {@link oncalltool} for handling tool execution @@ -730,21 +586,7 @@ export class App extends Protocol { * between transport failures (thrown) and tool execution failures (returned). * * @example Fetch updated weather data - * ```typescript - * try { - * const result = await app.callServerTool({ - * name: "get_weather", - * arguments: { location: "Tokyo" } - * }); - * if (result.isError) { - * console.error("Tool returned error:", result.content); - * } else { - * console.log(result.content); - * } - * } catch (error) { - * console.error("Tool call failed:", error); - * } - * ``` + * {@includeCode ./app.examples.ts#App_callServerTool_fetchWeather} */ async callServerTool( params: CallToolRequest["params"], @@ -770,17 +612,7 @@ export class App extends Protocol { * @throws {Error} If the host rejects the message * * @example Send a text message from user interaction - * ```typescript - * try { - * await app.sendMessage({ - * role: "user", - * content: [{ type: "text", text: "Show me details for item #42" }] - * }); - * } catch (error) { - * console.error("Failed to send message:", error); - * // Handle error appropriately for your app - * } - * ``` + * {@includeCode ./app.examples.ts#App_sendMessage_textFromInteraction} * * @see {@link McpUiMessageRequest} for request structure */ @@ -804,13 +636,7 @@ export class App extends Protocol { * @param params - Log level and message * * @example Log app state for debugging - * ```typescript - * app.sendLog({ - * level: "info", - * data: "Weather data refreshed", - * logger: "WeatherApp" - * }); - * ``` + * {@includeCode ./app.examples.ts#App_sendLog_debugState} * * @returns Promise that resolves when the log notification is sent */ @@ -838,18 +664,10 @@ export class App extends Protocol { * @throws {Error} If the host rejects the context update (e.g., unsupported content type) * * @example Update model context with current app state - * ```typescript - * await app.updateModelContext({ - * content: [{ type: "text", text: "User selected 3 items totaling $150.00" }] - * }); - * ``` + * {@includeCode ./app.examples.ts#App_updateModelContext_appState} * * @example Update with structured content - * ```typescript - * await app.updateModelContext({ - * structuredContent: { selectedItems: 3, total: 150.00, currency: "USD" } - * }); - * ``` + * {@includeCode ./app.examples.ts#App_updateModelContext_structuredContent} * * @returns Promise that resolves when the context update is acknowledged */ @@ -881,14 +699,7 @@ export class App extends Protocol { * @throws {Error} If the request times out or the connection is lost * * @example Open documentation link - * ```typescript - * try { - * await app.openLink({ url: "https://docs.example.com" }); - * } catch (error) { - * console.error("Failed to open link:", error); - * // Optionally show fallback: display URL for manual copy - * } - * ``` + * {@includeCode ./app.examples.ts#App_openLink_documentation} * * @see {@link McpUiOpenLinkRequest} for request structure */ @@ -919,13 +730,7 @@ export class App extends Protocol { * @returns Result containing the actual display mode that was set * * @example Request fullscreen mode - * ```typescript - * const context = app.getHostContext(); - * if (context?.availableDisplayModes?.includes("fullscreen")) { - * const result = await app.requestDisplayMode({ mode: "fullscreen" }); - * console.log("Display mode set to:", result.mode); - * } - * ``` + * {@includeCode ./app.examples.ts#App_requestDisplayMode_fullscreen} * * @see {@link McpUiRequestDisplayModeRequest} for request structure * @see {@link McpUiHostContext} for checking availableDisplayModes @@ -953,12 +758,7 @@ export class App extends Protocol { * @param params - New width and height in pixels * * @example Manually notify host of size change - * ```typescript - * app.sendSizeChanged({ - * width: 400, - * height: 600 - * }); - * ``` + * {@includeCode ./app.examples.ts#App_sendSizeChanged_manual} * * @returns Promise that resolves when the notification is sent * @@ -985,16 +785,7 @@ export class App extends Protocol { * @returns Cleanup function to disconnect the observer * * @example Manual setup for custom scenarios - * ```typescript - * const app = new App(appInfo, capabilities, { autoResize: false }); - * await app.connect(transport); - * - * // Later, enable auto-resize manually - * const cleanup = app.setupSizeChangedNotifications(); - * - * // Clean up when done - * cleanup(); - * ``` + * {@includeCode ./app.examples.ts#App_setupAutoResize_manual} */ setupSizeChangedNotifications() { let scheduled = false; @@ -1066,19 +857,7 @@ export class App extends Protocol { * @throws {Error} If initialization fails or connection is lost * * @example Connect with PostMessageTransport - * ```typescript - * const app = new App( - * { name: "MyApp", version: "1.0.0" }, - * {} - * ); - * - * try { - * await app.connect(new PostMessageTransport(window.parent, window.parent)); - * console.log("Connected successfully!"); - * } catch (error) { - * console.error("Failed to connect:", error); - * } - * ``` + * {@includeCode ./app.examples.ts#App_connect_withPostMessageTransport} * * @see {@link McpUiInitializeRequest} for the initialization request structure * @see {@link McpUiInitializedNotification} for the initialized notification diff --git a/src/message-transport.examples.ts b/src/message-transport.examples.ts new file mode 100644 index 00000000..2597a4d3 --- /dev/null +++ b/src/message-transport.examples.ts @@ -0,0 +1,58 @@ +/** + * Type-checked examples for {@link PostMessageTransport}. + * + * These examples are included in the API documentation via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import { PostMessageTransport } from "./message-transport.js"; +import type { App } from "./app.js"; +import type { AppBridge } from "./app-bridge.js"; + +/** + * Example: Guest UI connecting to parent window. + */ +async function PostMessageTransport_guestUI(app: App) { + //#region PostMessageTransport_guestUI + const transport = new PostMessageTransport(window.parent, window.parent); + await app.connect(transport); + //#endregion PostMessageTransport_guestUI +} + +/** + * Example: Host connecting to an iframe. + */ +async function PostMessageTransport_host(bridge: AppBridge) { + //#region PostMessageTransport_host + const iframe = document.getElementById("app-iframe") as HTMLIFrameElement; + const transport = new PostMessageTransport( + iframe.contentWindow!, + iframe.contentWindow!, + ); + await bridge.connect(transport); + //#endregion PostMessageTransport_host +} + +/** + * Example: Creating transport for guest UI (constructor only). + */ +function PostMessageTransport_constructor_guestUI() { + //#region PostMessageTransport_constructor_guestUI + const transport = new PostMessageTransport(window.parent, window.parent); + //#endregion PostMessageTransport_constructor_guestUI +} + +/** + * Example: Creating transport for host (constructor only). + */ +function PostMessageTransport_constructor_host() { + //#region PostMessageTransport_constructor_host + const iframe = document.getElementById("app") as HTMLIFrameElement; + const transport = new PostMessageTransport( + iframe.contentWindow!, + iframe.contentWindow!, + ); + //#endregion PostMessageTransport_constructor_host +} diff --git a/src/message-transport.ts b/src/message-transport.ts index 0b53d9a1..c7353cff 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -24,20 +24,10 @@ import { * ## Usage * * **Guest UI**: - * ```typescript - * const transport = new PostMessageTransport(window.parent, window.parent); - * await app.connect(transport); - * ``` + * {@includeCode ./message-transport.examples.ts#PostMessageTransport_guestUI} * * **Host**: - * ```typescript - * const iframe = document.getElementById('app-iframe') as HTMLIFrameElement; - * const transport = new PostMessageTransport( - * iframe.contentWindow!, - * iframe.contentWindow! - * ); - * await bridge.connect(transport); - * ``` + * {@includeCode ./message-transport.examples.ts#PostMessageTransport_host} * * @see {@link app!App.connect} for Guest UI usage * @see {@link app-bridge!AppBridge.connect} for Host usage @@ -56,18 +46,10 @@ export class PostMessageTransport implements Transport { * `window.parent`. For hosts, pass `iframe.contentWindow`. * * @example Guest UI connecting to parent - * ```typescript - * const transport = new PostMessageTransport(window.parent, window.parent); - * ``` + * {@includeCode ./message-transport.examples.ts#PostMessageTransport_constructor_guestUI} * * @example Host connecting to iframe - * ```typescript - * const iframe = document.getElementById('app') as HTMLIFrameElement; - * const transport = new PostMessageTransport( - * iframe.contentWindow!, - * iframe.contentWindow! - * ); - * ``` + * {@includeCode ./message-transport.examples.ts#PostMessageTransport_constructor_host} */ constructor( private eventTarget: Window = window.parent, diff --git a/src/react/index.examples.tsx b/src/react/index.examples.tsx new file mode 100644 index 00000000..3497a85d --- /dev/null +++ b/src/react/index.examples.tsx @@ -0,0 +1,26 @@ +/** + * Type-checked examples for the React module overview. + * + * @module + */ + +import { useApp } from "./index.js"; + +/** + * Example: Basic React App from module overview. + */ +function index_basicReactApp() { + //#region index_basicReactApp + function MyApp() { + const { app, isConnected, error } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + }); + + if (error) return
Error: {error.message}
; + if (!isConnected) return
Connecting...
; + + return
Connected!
; + } + //#endregion index_basicReactApp +} diff --git a/src/react/index.tsx b/src/react/index.tsx index 4307dc5c..53190849 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -17,21 +17,7 @@ * @module @modelcontextprotocol/ext-apps/react * * @example Basic React App - * ```tsx - * import { useApp } from '@modelcontextprotocol/ext-apps/react'; - * - * function MyApp() { - * const { app, isConnected, error } = useApp({ - * appInfo: { name: "MyApp", version: "1.0.0" }, - * capabilities: {} - * }); - * - * if (error) return
Error: {error.message}
; - * if (!isConnected) return
Connecting...
; - * - * return
Connected!
; - * } - * ``` + * {@includeCode ./index.examples.tsx#index_basicReactApp} */ export * from "./useApp"; export * from "./useAutoResize"; diff --git a/src/react/useApp.examples.tsx b/src/react/useApp.examples.tsx new file mode 100644 index 00000000..69a1603d --- /dev/null +++ b/src/react/useApp.examples.tsx @@ -0,0 +1,54 @@ +/** + * Type-checked examples for the useApp hook. + * + * @module + */ + +import { useApp, McpUiToolInputNotificationSchema } from "./index.js"; + +/** + * Example: Register a notification handler in onAppCreated. + */ +function useApp_registerHandler() { + //#region useApp_registerHandler + useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + onAppCreated: (app) => { + app.setNotificationHandler( + McpUiToolInputNotificationSchema, + (notification) => { + console.log("Tool input:", notification.params.arguments); + }, + ); + }, + }); + //#endregion useApp_registerHandler +} + +/** + * Example: Basic usage of useApp hook. + */ +function useApp_basicUsage() { + //#region useApp_basicUsage + function MyApp() { + const { app, isConnected, error } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + onAppCreated: (app) => { + // Register handlers before connection + app.setNotificationHandler( + McpUiToolInputNotificationSchema, + (notification) => { + console.log("Tool input:", notification.params.arguments); + }, + ); + }, + }); + + if (error) return
Error: {error.message}
; + if (!isConnected) return
Connecting...
; + return
Connected!
; + } + //#endregion useApp_basicUsage +} diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 65b93daf..cc8426ba 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -30,18 +30,7 @@ export interface UseAppOptions { * @param app - The newly created `App` instance * * @example Register a notification handler - * ```typescript - * import { McpUiToolInputNotificationSchema } from '@modelcontextprotocol/ext-apps/react'; - * - * onAppCreated: (app) => { - * app.setNotificationHandler( - * McpUiToolInputNotificationSchema, - * (notification) => { - * console.log("Tool input:", notification.params.arguments); - * } - * ); - * } - * ``` + * {@includeCode ./useApp.examples.tsx#useApp_registerHandler} */ onAppCreated?: (app: App) => void; } @@ -81,29 +70,7 @@ export interface AppState { * timeouts, initialization handshake failures, or transport errors). * * @example Basic usage - * ```typescript - * import { useApp, McpUiToolInputNotificationSchema } from '@modelcontextprotocol/ext-apps/react'; - * - * function MyApp() { - * const { app, isConnected, error } = useApp({ - * appInfo: { name: "MyApp", version: "1.0.0" }, - * capabilities: {}, - * onAppCreated: (app) => { - * // Register handlers before connection - * app.setNotificationHandler( - * McpUiToolInputNotificationSchema, - * (notification) => { - * console.log("Tool input:", notification.params.arguments); - * } - * ); - * }, - * }); - * - * if (error) return
Error: {error.message}
; - * if (!isConnected) return
Connecting...
; - * return
Connected!
; - * } - * ``` + * {@includeCode ./useApp.examples.tsx#useApp_basicUsage} * * @see {@link App.connect} for the underlying connection method * @see {@link useAutoResize} for manual auto-resize control when using custom App options diff --git a/src/react/useAutoResize.examples.tsx b/src/react/useAutoResize.examples.tsx new file mode 100644 index 00000000..e132d776 --- /dev/null +++ b/src/react/useAutoResize.examples.tsx @@ -0,0 +1,42 @@ +/** + * Type-checked examples for the useAutoResize hook. + * + * @module + */ + +import { useState, useEffect } from "react"; +import { App, PostMessageTransport } from "./index.js"; +import { useAutoResize } from "./useAutoResize.js"; + +/** + * Example: Manual App creation with custom auto-resize control. + */ +function useAutoResize_manualApp() { + //#region useAutoResize_manualApp + function MyComponent() { + // For custom App options, create App manually instead of using useApp + const [app, setApp] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const myApp = new App( + { name: "MyApp", version: "1.0.0" }, + {}, // capabilities + { autoResize: false }, // Disable default auto-resize + ); + + const transport = new PostMessageTransport(window.parent, window.parent); + myApp + .connect(transport) + .then(() => setApp(myApp)) + .catch((err) => setError(err)); + }, []); + + // Add manual auto-resize control + useAutoResize(app); + + if (error) return
Connection failed: {error.message}
; + return
My content
; + } + //#endregion useAutoResize_manualApp +} diff --git a/src/react/useAutoResize.ts b/src/react/useAutoResize.ts index 4c83eff6..d7e3de73 100644 --- a/src/react/useAutoResize.ts +++ b/src/react/useAutoResize.ts @@ -20,32 +20,7 @@ import { App } from "../app"; * cause unnecessary effect re-runs; omit this parameter. * * @example Manual App creation with custom auto-resize control - * ```tsx - * function MyComponent() { - * // For custom App options, create App manually instead of using useApp - * const [app, setApp] = useState(null); - * const [error, setError] = useState(null); - * - * useEffect(() => { - * const myApp = new App( - * { name: "MyApp", version: "1.0.0" }, - * {}, // capabilities - * { autoResize: false } // Disable default auto-resize - * ); - * - * const transport = new PostMessageTransport(window.parent, window.parent); - * myApp.connect(transport) - * .then(() => setApp(myApp)) - * .catch((err) => setError(err)); - * }, []); - * - * // Add manual auto-resize control - * useAutoResize(app); - * - * if (error) return
Connection failed: {error.message}
; - * return
My content
; - * } - * ``` + * {@includeCode ./useAutoResize.examples.tsx#useAutoResize_manualApp} * * @see {@link App.setupSizeChangedNotifications} for the underlying implementation * @see {@link useApp} which enables auto-resize by default diff --git a/src/react/useDocumentTheme.examples.tsx b/src/react/useDocumentTheme.examples.tsx new file mode 100644 index 00000000..a0474cea --- /dev/null +++ b/src/react/useDocumentTheme.examples.tsx @@ -0,0 +1,46 @@ +/** + * Type-checked examples for the useDocumentTheme hook. + * + * @module + */ + +import { useDocumentTheme } from "./useDocumentTheme.js"; + +// Stub components for examples +declare function DarkIcon(): React.JSX.Element; +declare function LightIcon(): React.JSX.Element; + +/** + * Example: Conditionally render based on theme. + */ +function useDocumentTheme_conditionalRender() { + //#region useDocumentTheme_conditionalRender + function MyApp() { + const theme = useDocumentTheme(); + + return
{theme === "dark" ? : }
; + } + //#endregion useDocumentTheme_conditionalRender +} + +/** + * Example: Use with theme-aware styling. + */ +function useDocumentTheme_themedButton() { + //#region useDocumentTheme_themedButton + function ThemedButton() { + const theme = useDocumentTheme(); + + return ( + + ); + } + //#endregion useDocumentTheme_themedButton +} diff --git a/src/react/useDocumentTheme.ts b/src/react/useDocumentTheme.ts index a04c6e4b..81a7b162 100644 --- a/src/react/useDocumentTheme.ts +++ b/src/react/useDocumentTheme.ts @@ -15,35 +15,10 @@ import { McpUiTheme } from "../types"; * @returns The current theme ("light" or "dark") * * @example Conditionally render based on theme - * ```tsx - * import { useDocumentTheme } from '@modelcontextprotocol/ext-apps/react'; - * - * function MyApp() { - * const theme = useDocumentTheme(); - * - * return ( - *
- * {theme === 'dark' ? : } - *
- * ); - * } - * ``` + * {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_conditionalRender} * * @example Use with theme-aware styling - * ```tsx - * function ThemedButton() { - * const theme = useDocumentTheme(); - * - * return ( - * - * ); - * } - * ``` + * {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_themedButton} * * @see {@link getDocumentTheme} for the underlying function * @see {@link applyDocumentTheme} to set the theme diff --git a/src/react/useHostStyles.examples.tsx b/src/react/useHostStyles.examples.tsx new file mode 100644 index 00000000..7ab541b7 --- /dev/null +++ b/src/react/useHostStyles.examples.tsx @@ -0,0 +1,87 @@ +/** + * Type-checked examples for the useHostStyles hooks. + * + * @module + */ + +import { useState } from "react"; +import { + useApp, + useHostStyleVariables, + useHostFonts, + useHostStyles, + App, +} from "./index.js"; +import type { McpUiHostContext } from "../types.js"; + +/** + * Example: Basic usage of useHostStyleVariables. + */ +function useHostStyleVariables_basicUsage() { + //#region useHostStyleVariables_basicUsage + function MyApp() { + const { app, isConnected } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + }); + + // Apply host styles - pass initial context to apply styles from connect() immediately + useHostStyleVariables(app, app?.getHostContext()); + + return ( +
+ Hello! +
+ ); + } + //#endregion useHostStyleVariables_basicUsage +} + +/** + * Example: Basic usage of useHostFonts with useApp. + */ +function useHostFonts_basicUsage() { + //#region useHostFonts_basicUsage + function MyApp() { + const { app, isConnected } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + }); + + // Automatically apply host fonts + useHostFonts(app); + + return
Hello!
; + } + //#endregion useHostFonts_basicUsage +} + +/** + * Example: useHostFonts with initial context. + */ +function useHostFonts_withInitialContext(app: App) { + //#region useHostFonts_withInitialContext + const [hostContext, setHostContext] = useState(null); + + // ... get initial context from app.connect() result + + useHostFonts(app, hostContext); + //#endregion useHostFonts_withInitialContext +} + +/** + * Example: Basic usage of useHostStyles. + */ +function useHostStyles_basicUsage() { + const appInfo = { name: "MyApp", version: "1.0.0" }; + //#region useHostStyles_basicUsage + function MyApp() { + const { app } = useApp({ appInfo, capabilities: {} }); + useHostStyles(app, app?.getHostContext()); + + return ( +
...
+ ); + } + //#endregion useHostStyles_basicUsage +} diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index 26006bc3..e0686e50 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -26,25 +26,7 @@ import { McpUiHostContext } from "../types"; * If provided, styles and theme will be applied immediately on mount. * * @example - * ```tsx - * import { useApp, useHostStyleVariables } from '@modelcontextprotocol/ext-apps/react'; - * - * function MyApp() { - * const { app, isConnected } = useApp({ - * appInfo: { name: "MyApp", version: "1.0.0" }, - * capabilities: {}, - * }); - * - * // Apply host styles - pass initial context to apply styles from connect() immediately - * useHostStyleVariables(app, app?.getHostContext()); - * - * return ( - *
- * Hello! - *
- * ); - * } - * ``` + * {@includeCode ./useHostStyles.examples.tsx#useHostStyleVariables_basicUsage} * * @see {@link applyHostStyleVariables} for the underlying styles function * @see {@link applyDocumentTheme} for the underlying theme function @@ -107,35 +89,10 @@ export function useHostStyleVariables( * 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 ( - *
- * Hello! - *
- * ); - * } - * ``` + * {@includeCode ./useHostStyles.examples.tsx#useHostFonts_basicUsage} * * @example With initial context - * ```tsx - * const [hostContext, setHostContext] = useState(null); - * - * // ... get initial context from app.connect() result - * - * useHostFonts(app, hostContext); - * ``` + * {@includeCode ./useHostStyles.examples.tsx#useHostFonts_withInitialContext} * * @see {@link applyHostFonts} for the underlying fonts function * @see {@link useHostStyleVariables} for applying style variables and theme @@ -182,14 +139,7 @@ export function useHostFonts( * Pass `app?.getHostContext()` to apply styles immediately on mount. * * @example - * ```tsx - * function MyApp() { - * const { app } = useApp({ appInfo, capabilities: {} }); - * useHostStyles(app, app?.getHostContext()); - * - * return
...
; - * } - * ``` + * {@includeCode ./useHostStyles.examples.tsx#useHostStyles_basicUsage} * * @see {@link useHostStyleVariables} for style variables and theme only * @see {@link useHostFonts} for fonts only diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts new file mode 100644 index 00000000..5045eee6 --- /dev/null +++ b/src/server/index.examples.ts @@ -0,0 +1,192 @@ +/** + * Type-checked examples for {@link registerAppTool} and {@link registerAppResource}. + * + * These examples are included in the API documentation via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { z } from "zod"; +import { + registerAppTool, + registerAppResource, + RESOURCE_MIME_TYPE, +} from "./index.js"; + +// Stubs for external functions used in examples +declare function fetchWeather( + location: string, +): Promise<{ temp: number; conditions: string }>; +declare function getCart(): Promise<{ items: unknown[]; total: number }>; +declare function updateCartItem( + itemId: string, + quantity: number, +): Promise<{ items: unknown[]; total: number }>; +declare const fs: { readFile(path: string, encoding: string): Promise }; +declare function readCallback(): Promise<{ + contents: { uri: string; mimeType: string; text: string }[]; +}>; + +/** + * Example: Module overview showing basic registration of tools and resources. + */ +function index_overview( + server: McpServer, + handler: () => Promise<{ content: { type: "text"; text: string }[] }>, +) { + //#region index_overview + // Register a tool that displays a widget + registerAppTool( + server, + "weather", + { + description: "Get weather forecast", + _meta: { ui: { resourceUri: "ui://weather/widget.html" } }, + }, + handler, + ); + + // Register the HTML resource the tool references + registerAppResource( + server, + "Weather Widget", + "ui://weather/widget.html", + {}, + readCallback, + ); + //#endregion index_overview +} + +/** + * Example: Basic usage of registerAppTool with input schema and handler. + */ +function registerAppTool_basicUsage(server: McpServer) { + //#region registerAppTool_basicUsage + registerAppTool( + server, + "get-weather", + { + title: "Get Weather", + description: "Get current weather for a location", + inputSchema: { location: z.string() }, + _meta: { + ui: { resourceUri: "ui://weather/widget.html" }, + }, + }, + async (args) => { + const weather = await fetchWeather(args.location); + return { content: [{ type: "text", text: JSON.stringify(weather) }] }; + }, + ); + //#endregion registerAppTool_basicUsage +} + +/** + * Example: Tool visibility - create app-only tools for UI actions. + */ +function registerAppTool_toolVisibility(server: McpServer) { + //#region registerAppTool_toolVisibility + // Main tool - visible to both model and app (default) + registerAppTool( + server, + "show-cart", + { + description: "Display the user's shopping cart", + _meta: { + ui: { + resourceUri: "ui://shop/cart.html", + visibility: ["model", "app"], + }, + }, + }, + async () => { + const cart = await getCart(); + return { content: [{ type: "text", text: JSON.stringify(cart) }] }; + }, + ); + + // App-only tool - hidden from the model, only callable by the UI + registerAppTool( + server, + "update-quantity", + { + description: "Update item quantity in cart", + inputSchema: { itemId: z.string(), quantity: z.number() }, + _meta: { + ui: { + resourceUri: "ui://shop/cart.html", + visibility: ["app"], + }, + }, + }, + async ({ itemId, quantity }) => { + const cart = await updateCartItem(itemId, quantity); + return { content: [{ type: "text", text: JSON.stringify(cart) }] }; + }, + ); + //#endregion registerAppTool_toolVisibility +} + +/** + * Example: Basic usage of registerAppResource. + */ +function registerAppResource_basicUsage(server: McpServer) { + //#region registerAppResource_basicUsage + registerAppResource( + server, + "Weather Widget", + "ui://weather/widget.html", + { + description: "Interactive weather display", + }, + async () => ({ + contents: [ + { + uri: "ui://weather/widget.html", + mimeType: RESOURCE_MIME_TYPE, + text: await fs.readFile("dist/widget.html", "utf-8"), + }, + ], + }), + ); + //#endregion registerAppResource_basicUsage +} + +/** + * Example: registerAppResource with CSP configuration for external domains. + */ +function registerAppResource_withCsp( + server: McpServer, + musicPlayerHtml: string, +) { + //#region registerAppResource_withCsp + registerAppResource( + server, + "Music Player", + "ui://music/player.html", + { + description: "Audio player with external soundfonts", + }, + async () => ({ + contents: [ + { + uri: "ui://music/player.html", + mimeType: RESOURCE_MIME_TYPE, + text: musicPlayerHtml, + // CSP must be on the content item, not the resource config + _meta: { + ui: { + csp: { + connectDomains: ["https://api.example.com"], // For fetch/WebSocket + resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images + }, + }, + }, + }, + ], + }), + ); + //#endregion registerAppResource_withCsp +} diff --git a/src/server/index.ts b/src/server/index.ts index 6ef07669..cbd9f1f1 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -8,18 +8,7 @@ * @module server-helpers * * @example - * ```typescript - * import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from '@modelcontextprotocol/ext-apps/server'; - * - * // Register a tool that displays a widget - * registerAppTool(server, "weather", { - * description: "Get weather forecast", - * _meta: { ui: { resourceUri: "ui://weather/widget.html" } }, - * }, handler); - * - * // Register the HTML resource the tool references - * registerAppResource(server, "Weather Widget", "ui://weather/widget.html", {}, readCallback); - * ``` + * {@includeCode ./index.examples.ts#index_overview} */ import { @@ -126,57 +115,10 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @param cb - Tool handler function * * @example Basic usage - * ```typescript - * import { registerAppTool } from '@modelcontextprotocol/ext-apps/server'; - * import { z } from 'zod'; - * - * registerAppTool(server, "get-weather", { - * title: "Get Weather", - * description: "Get current weather for a location", - * inputSchema: { location: z.string() }, - * _meta: { - * ui: { resourceUri: "ui://weather/widget.html" }, - * }, - * }, async (args) => { - * const weather = await fetchWeather(args.location); - * return { content: [{ type: "text", text: JSON.stringify(weather) }] }; - * }); - * ``` + * {@includeCode ./index.examples.ts#registerAppTool_basicUsage} * * @example Tool visibility - create app-only tools for UI actions - * ```typescript - * import { registerAppTool } from '@modelcontextprotocol/ext-apps/server'; - * import { z } from 'zod'; - * - * // Main tool - visible to both model and app (default) - * registerAppTool(server, "show-cart", { - * description: "Display the user's shopping cart", - * _meta: { - * ui: { - * resourceUri: "ui://shop/cart.html", - * visibility: ["model", "app"], - * }, - * }, - * }, async () => { - * const cart = await getCart(); - * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; - * }); - * - * // App-only tool - hidden from the model, only callable by the UI - * registerAppTool(server, "update-quantity", { - * description: "Update item quantity in cart", - * inputSchema: { itemId: z.string(), quantity: z.number() }, - * _meta: { - * ui: { - * resourceUri: "ui://shop/cart.html", - * visibility: ["app"], - * }, - * }, - * }, async ({ itemId, quantity }) => { - * const cart = await updateCartItem(itemId, quantity); - * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; - * }); - * ``` + * {@includeCode ./index.examples.ts#registerAppTool_toolVisibility} * * @see {@link registerAppResource} to register the HTML resource referenced by the tool */ @@ -225,41 +167,10 @@ export function registerAppTool< * @param readCallback - Callback that returns the resource contents * * @example Basic usage - * ```typescript - * import { registerAppResource } from '@modelcontextprotocol/ext-apps/server'; - * - * registerAppResource(server, "Weather Widget", "ui://weather/widget.html", { - * description: "Interactive weather display", - * }, async () => ({ - * contents: [{ - * uri: "ui://weather/widget.html", - * mimeType: RESOURCE_MIME_TYPE, - * text: await fs.readFile("dist/widget.html", "utf-8"), - * }], - * })); - * ``` + * {@includeCode ./index.examples.ts#registerAppResource_basicUsage} * * @example With CSP configuration for external domains - * ```typescript - * registerAppResource(server, "Music Player", "ui://music/player.html", { - * description: "Audio player with external soundfonts", - * }, async () => ({ - * contents: [{ - * uri: "ui://music/player.html", - * mimeType: RESOURCE_MIME_TYPE, - * text: PLAYER_HTML, - * // CSP must be on the content item, not the resource config - * _meta: { - * ui: { - * csp: { - * connectDomains: ["https://api.example.com"], // For fetch/WebSocket - * resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images - * }, - * }, - * }, - * }], - * })); - * ``` + * {@includeCode ./index.examples.ts#registerAppResource_withCsp} * * @see {@link registerAppTool} to register tools that reference this resource */ diff --git a/src/styles.examples.ts b/src/styles.examples.ts new file mode 100644 index 00000000..43f352e1 --- /dev/null +++ b/src/styles.examples.ts @@ -0,0 +1,112 @@ +/** + * Type-checked examples for style utilities in {@link ./styles.ts}. + * + * These examples are included in the API documentation via `@includeCode` tags. + * Each function's region markers define the code snippet that appears in the docs. + * + * @module + */ + +import type { McpUiHostContext } from "./types.js"; +import { App } from "./app.js"; +import { + getDocumentTheme, + applyDocumentTheme, + applyHostStyleVariables, + applyHostFonts, +} from "./styles.js"; + +// Stub declarations for examples +declare const app: App; +declare const hostContext: McpUiHostContext; + +/** + * Example: Check current theme. + */ +function getDocumentTheme_checkCurrent() { + //#region getDocumentTheme_checkCurrent + const theme = getDocumentTheme(); + console.log(`Current theme: ${theme}`); + //#endregion getDocumentTheme_checkCurrent +} + +/** + * Example: Apply theme from host context. + */ +function applyDocumentTheme_fromHostContext() { + //#region applyDocumentTheme_fromHostContext + app.onhostcontextchanged = (params) => { + if (params.theme) { + applyDocumentTheme(params.theme); + } + }; + //#endregion applyDocumentTheme_fromHostContext +} + +/** + * Example: Apply style variables from host context. + */ +function applyHostStyleVariables_fromHostContext() { + //#region applyHostStyleVariables_fromHostContext + app.onhostcontextchanged = (params) => { + if (params.styles?.variables) { + applyHostStyleVariables(params.styles.variables); + } + }; + //#endregion applyHostStyleVariables_fromHostContext +} + +/** + * Example: Apply to a specific element. + */ +function applyHostStyleVariables_toElement() { + //#region applyHostStyleVariables_toElement + const container = document.getElementById("app-root"); + if (container && hostContext.styles?.variables) { + applyHostStyleVariables(hostContext.styles.variables, container); + } + //#endregion applyHostStyleVariables_toElement +} + +/** + * Example: Apply fonts from host context. + */ +function applyHostFonts_fromHostContext() { + //#region applyHostFonts_fromHostContext + app.onhostcontextchanged = (params) => { + if (params.styles?.css?.fonts) { + applyHostFonts(params.styles.css.fonts); + } + }; + //#endregion applyHostFonts_fromHostContext +} + +/** + * Example: Host providing self-hosted fonts. + */ +function applyHostFonts_selfHosted() { + //#region applyHostFonts_selfHosted + // Example of what a host might provide: + const fontCss = ` + @font-face { + font-family: "Anthropic Sans"; + src: url("https://assets.anthropic.com/.../Regular.otf") format("opentype"); + font-weight: 400; + } + `; + applyHostFonts(fontCss); + //#endregion applyHostFonts_selfHosted +} + +/** + * Example: Host providing Google Fonts. + */ +function applyHostFonts_googleFonts() { + //#region applyHostFonts_googleFonts + // Example of what a host might provide: + const fontCss = ` + @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); + `; + applyHostFonts(fontCss); + //#endregion applyHostFonts_googleFonts +} diff --git a/src/styles.ts b/src/styles.ts index 322c5e9f..0727ced6 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -10,12 +10,7 @@ import { McpUiStyles, McpUiTheme } from "./types"; * @returns The current theme ("light" or "dark") * * @example Check current theme - * ```typescript - * import { getDocumentTheme } from '@modelcontextprotocol/ext-apps'; - * - * const theme = getDocumentTheme(); - * console.log(`Current theme: ${theme}`); - * ``` + * {@includeCode ./styles.examples.ts#getDocumentTheme_checkCurrent} * * @see {@link applyDocumentTheme} to set the theme * @see {@link McpUiTheme} for the theme type @@ -44,15 +39,7 @@ export function getDocumentTheme(): McpUiTheme { * @param theme - The theme to apply ("light" or "dark") * * @example Apply theme from host context - * ```typescript - * import { applyDocumentTheme } from '@modelcontextprotocol/ext-apps'; - * - * app.onhostcontextchanged = (params) => { - * if (params.theme) { - * applyDocumentTheme(params.theme); - * } - * }; - * ``` + * {@includeCode ./styles.examples.ts#applyDocumentTheme_fromHostContext} * * @example Use with CSS selectors * ```css @@ -85,21 +72,10 @@ export function applyDocumentTheme(theme: McpUiTheme): void { * @param root - The element to apply styles to (defaults to `document.documentElement`) * * @example Apply style variables from host context - * ```typescript - * import { applyHostStyleVariables } from '@modelcontextprotocol/ext-apps'; - * - * app.onhostcontextchanged = (params) => { - * if (params.styles?.variables) { - * applyHostStyleVariables(params.styles.variables); - * } - * }; - * ``` + * {@includeCode ./styles.examples.ts#applyHostStyleVariables_fromHostContext} * * @example Apply to a specific element - * ```typescript - * const container = document.getElementById('app-root'); - * applyHostStyleVariables(hostContext.styles?.variables, container); - * ``` + * {@includeCode ./styles.examples.ts#applyHostStyleVariables_toElement} * * @see {@link McpUiStyles} for the available CSS variables * @see {@link McpUiHostContext} for the full host context structure @@ -129,33 +105,13 @@ export function applyHostStyleVariables( * @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); - * } - * }; - * ``` + * {@includeCode ./styles.examples.ts#applyHostFonts_fromHostContext} * * @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; - * } - * `; - * ``` + * {@includeCode ./styles.examples.ts#applyHostFonts_selfHosted} * * @example Host providing Google Fonts - * ```typescript - * hostContext.styles.css.fonts = ` - * @import url('https://fonts.googleapis.com/css2?family=Roboto&display=swap'); - * `; - * ``` + * {@includeCode ./styles.examples.ts#applyHostFonts_googleFonts} * * @example Use host fonts in CSS * ```css diff --git a/typedoc.config.mjs b/typedoc.config.mjs index bda329ee..20163619 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -17,6 +17,9 @@ const config = { gitRevision: "main", excludePrivate: true, excludeInternal: false, + jsDocCompatibility: { + exampleTag: false, + }, categorizeByGroup: true, navigationLinks: { GitHub: "https://github.com/modelcontextprotocol/ext-apps", From f0c7cc1b14bcd148405c43447aa25750c3e1ad35 Mon Sep 17 00:00:00 2001 From: Jonathan Hefner Date: Sun, 18 Jan 2026 18:24:44 -0600 Subject: [PATCH 3/3] Refine JSDoc examples for consistency and type safety MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the examples extraction: pass dependencies as function parameters instead of using global stub declarations, rename region names to match method names (e.g., `AppBridge_guestCapabilities_*` → `AppBridge_getAppCapabilities_*`), and simplify examples using optional chaining. Also clarifies `RESOURCE_URI_META_KEY` documentation to indicate it's for tool definition metadata (from `tools/list`), not tool result metadata, and removes the redundant `setNotificationHandler` example in favor of the simpler event setter approach. Co-Authored-By: Claude Opus 4.5 --- src/app-bridge.examples.ts | 116 +++++++++----------- src/app-bridge.ts | 4 +- src/app.examples.ts | 157 ++++++++++----------------- src/app.ts | 36 +++--- src/message-transport.examples.ts | 2 +- src/react/useApp.examples.tsx | 45 +++++--- src/react/useApp.tsx | 4 +- src/react/useHostStyles.examples.tsx | 36 +++--- src/react/useHostStyles.ts | 3 - src/server/index.examples.ts | 19 ++-- src/styles.examples.ts | 25 ++--- 11 files changed, 197 insertions(+), 250 deletions(-) diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index 4c1f7610..8f36cb61 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -17,6 +17,7 @@ import { ListPromptsResultSchema, } from "@modelcontextprotocol/sdk/types.js"; import { AppBridge, PostMessageTransport } from "./app-bridge.js"; +import type { McpUiDisplayMode } from "./types.js"; /** * Example: Basic usage of the AppBridge class with PostMessageTransport. @@ -65,7 +66,6 @@ function AppBridge_constructor_withMcpClient(mcpClient: Client) { { openLinks: {}, serverTools: {}, logging: {} }, ); //#endregion AppBridge_constructor_withMcpClient - return bridge; } /** @@ -83,42 +83,43 @@ function AppBridge_constructor_withoutMcpClient() { return { content: [] }; }; //#endregion AppBridge_constructor_withoutMcpClient - return bridge; } /** * Example: Check Guest UI capabilities after initialization. */ -function AppBridge_guestCapabilities_checkAfterInit(bridge: AppBridge) { - //#region AppBridge_guestCapabilities_checkAfterInit +function AppBridge_getAppCapabilities_checkAfterInit(bridge: AppBridge) { + //#region AppBridge_getAppCapabilities_checkAfterInit bridge.oninitialized = () => { const caps = bridge.getAppCapabilities(); if (caps?.tools) { console.log("Guest UI provides tools"); } }; - //#endregion AppBridge_guestCapabilities_checkAfterInit + //#endregion AppBridge_getAppCapabilities_checkAfterInit } /** * Example: Log Guest UI information after initialization. */ -function AppBridge_guestInfo_logAfterInit(bridge: AppBridge) { - //#region AppBridge_guestInfo_logAfterInit +function AppBridge_getAppVersion_logAfterInit(bridge: AppBridge) { + //#region AppBridge_getAppVersion_logAfterInit bridge.oninitialized = () => { const appInfo = bridge.getAppVersion(); if (appInfo) { console.log(`Guest UI: ${appInfo.name} v${appInfo.version}`); } }; - //#endregion AppBridge_guestInfo_logAfterInit + //#endregion AppBridge_getAppVersion_logAfterInit } /** * Example: Handle Guest UI initialization and send tool input. */ -function AppBridge_oninitialized_sendToolInput(bridge: AppBridge) { - const toolArgs = { location: "NYC" }; +function AppBridge_oninitialized_sendToolInput( + bridge: AppBridge, + toolArgs: Record, +) { //#region AppBridge_oninitialized_sendToolInput bridge.oninitialized = () => { console.log("Guest UI ready"); @@ -162,18 +163,11 @@ declare function showDialog(options: { buttons: string[]; }): Promise; -// Stub for example code - represents a hypothetical model context storage -declare let modelContext: { - type: string; - content: unknown; - structuredContent: unknown; - timestamp: number; +// Stub for example code - represents a hypothetical model context manager +declare const modelContextManager: { + update(context: { content?: unknown; structuredContent?: unknown }): void; }; -// Stub for example code - represents a hypothetical MCP client that can be passed to AppBridge -// Using Client type directly since AppBridge expects Client | null -declare const mcpClient: Client; - /** * Example: Handle external link requests from the Guest UI. */ @@ -209,13 +203,8 @@ function AppBridge_onupdatemodelcontext_storeContext(bridge: AppBridge) { { content, structuredContent }, extra, ) => { - // Update the model context with the new snapshot - modelContext = { - type: "app_context", - content, - structuredContent, - timestamp: Date.now(), - }; + // Store the context snapshot for inclusion in the next model request + modelContextManager.update({ content, structuredContent }); return {}; }; //#endregion AppBridge_onupdatemodelcontext_storeContext @@ -224,11 +213,14 @@ function AppBridge_onupdatemodelcontext_storeContext(bridge: AppBridge) { /** * Example: Forward tool calls to the MCP server. */ -function AppBridge_oncalltool_forwardToServer(bridge: AppBridge) { +function AppBridge_oncalltool_forwardToServer( + bridge: AppBridge, + mcpClient: Client, +) { //#region AppBridge_oncalltool_forwardToServer - bridge.oncalltool = async ({ name, arguments: args }, extra) => { + bridge.oncalltool = async (params, extra) => { return mcpClient.request( - { method: "tools/call", params: { name, arguments: args } }, + { method: "tools/call", params }, CallToolResultSchema, { signal: extra.signal }, ); @@ -239,7 +231,10 @@ function AppBridge_oncalltool_forwardToServer(bridge: AppBridge) { /** * Example: Forward list resources requests to the MCP server. */ -function AppBridge_onlistresources_returnResources(bridge: AppBridge) { +function AppBridge_onlistresources_returnResources( + bridge: AppBridge, + mcpClient: Client, +) { //#region AppBridge_onlistresources_returnResources bridge.onlistresources = async (params, extra) => { return mcpClient.request( @@ -254,11 +249,14 @@ function AppBridge_onlistresources_returnResources(bridge: AppBridge) { /** * Example: Forward read resource requests to the MCP server. */ -function AppBridge_onreadresource_returnResource(bridge: AppBridge) { +function AppBridge_onreadresource_returnResource( + bridge: AppBridge, + mcpClient: Client, +) { //#region AppBridge_onreadresource_returnResource - bridge.onreadresource = async ({ uri }, extra) => { + bridge.onreadresource = async (params, extra) => { return mcpClient.request( - { method: "resources/read", params: { uri } }, + { method: "resources/read", params }, ReadResourceResultSchema, { signal: extra.signal }, ); @@ -269,7 +267,10 @@ function AppBridge_onreadresource_returnResource(bridge: AppBridge) { /** * Example: Forward list prompts requests to the MCP server. */ -function AppBridge_onlistprompts_returnPrompts(bridge: AppBridge) { +function AppBridge_onlistprompts_returnPrompts( + bridge: AppBridge, + mcpClient: Client, +) { //#region AppBridge_onlistprompts_returnPrompts bridge.onlistprompts = async (params, extra) => { return mcpClient.request( @@ -281,14 +282,6 @@ function AppBridge_onlistprompts_returnPrompts(bridge: AppBridge) { //#endregion AppBridge_onlistprompts_returnPrompts } -// Stub for example code - represents a hypothetical iframe element -declare const iframe: HTMLIFrameElement; - -// Stub for example code - represents a hypothetical host context -declare const hostContext: { - availableDisplayModes?: Array<"inline" | "fullscreen" | "pip">; -}; - /** * Example: Handle ping requests from the Guest UI. */ @@ -303,7 +296,10 @@ function AppBridge_onping_handleRequest(bridge: AppBridge) { /** * Example: Handle size change notifications from the Guest UI. */ -function AppBridge_onsizechange_handleResize(bridge: AppBridge) { +function AppBridge_onsizechange_handleResize( + bridge: AppBridge, + iframe: HTMLIFrameElement, +) { //#region AppBridge_onsizechange_handleResize bridge.onsizechange = ({ width, height }) => { if (width != null) { @@ -319,18 +315,16 @@ function AppBridge_onsizechange_handleResize(bridge: AppBridge) { /** * Example: Handle display mode requests from the Guest UI. */ -function AppBridge_onrequestdisplaymode_handleRequest(bridge: AppBridge) { +function AppBridge_onrequestdisplaymode_handleRequest( + bridge: AppBridge, + currentDisplayMode: McpUiDisplayMode, + availableDisplayModes: McpUiDisplayMode[], +) { //#region AppBridge_onrequestdisplaymode_handleRequest - type McpUiDisplayMode = "inline" | "fullscreen" | "pip"; - let currentDisplayMode: McpUiDisplayMode = "inline"; - bridge.onrequestdisplaymode = async ({ mode }, extra) => { - const availableModes = hostContext.availableDisplayModes ?? ["inline"]; - if (availableModes.includes(mode)) { + if (availableDisplayModes.includes(mode)) { currentDisplayMode = mode; - return { mode }; } - // Return current mode if requested mode not available return { mode: currentDisplayMode }; }; //#endregion AppBridge_onrequestdisplaymode_handleRequest @@ -342,9 +336,8 @@ function AppBridge_onrequestdisplaymode_handleRequest(bridge: AppBridge) { function AppBridge_onloggingmessage_handleLog(bridge: AppBridge) { //#region AppBridge_onloggingmessage_handleLog bridge.onloggingmessage = ({ level, logger, data }) => { - const prefix = logger ? `[${logger}]` : "[Guest UI]"; console[level === "error" ? "error" : "log"]( - `${prefix} ${level.toUpperCase()}:`, + `[${logger ?? "Guest UI"}] ${level.toUpperCase()}:`, data, ); }; @@ -354,7 +347,10 @@ function AppBridge_onloggingmessage_handleLog(bridge: AppBridge) { /** * Example: Gracefully tear down the Guest UI before unmounting. */ -async function AppBridge_teardownResource_gracefulShutdown(bridge: AppBridge) { +async function AppBridge_teardownResource_gracefulShutdown( + bridge: AppBridge, + iframe: HTMLIFrameElement, +) { //#region AppBridge_teardownResource_gracefulShutdown try { await bridge.teardownResource({}); @@ -422,21 +418,16 @@ function AppBridge_sendToolInputPartial_streaming(bridge: AppBridge) { */ async function AppBridge_sendToolResult_afterExecution( bridge: AppBridge, - mcpClientParam: Client, + mcpClient: Client, args: Record, ) { //#region AppBridge_sendToolResult_afterExecution - // import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js'; - const result = await mcpClient.request( { method: "tools/call", params: { name: "get_weather", arguments: args } }, CallToolResultSchema, ); bridge.sendToolResult(result); //#endregion AppBridge_sendToolResult_afterExecution - - // Make the parameter used to satisfy linting - void mcpClientParam; } /** @@ -466,7 +457,8 @@ function AppBridge_sendToolCancelled_systemLevel(bridge: AppBridge) { * Example: Connect with MCP client for automatic forwarding. */ async function AppBridge_connect_withMcpClient( - mcpClientParam: Client, + iframe: HTMLIFrameElement, + mcpClient: Client, hostInfo: { name: string; version: string }, capabilities: { openLinks: {}; serverTools: {}; logging: {} }, toolArgs: Record, diff --git a/src/app-bridge.ts b/src/app-bridge.ts index c59cd1da..3083b575 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -280,7 +280,7 @@ export class AppBridge extends Protocol< * @returns Guest UI capabilities, or `undefined` if not yet initialized * * @example Check Guest UI capabilities after initialization - * {@includeCode ./app-bridge.examples.ts#AppBridge_guestCapabilities_checkAfterInit} + * {@includeCode ./app-bridge.examples.ts#AppBridge_getAppCapabilities_checkAfterInit} * * @see {@link McpUiAppCapabilities} for the capabilities structure */ @@ -297,7 +297,7 @@ export class AppBridge extends Protocol< * @returns Guest UI implementation info, or `undefined` if not yet initialized * * @example Log Guest UI information after initialization - * {@includeCode ./app-bridge.examples.ts#AppBridge_guestInfo_logAfterInit} + * {@includeCode ./app-bridge.examples.ts#AppBridge_getAppVersion_logAfterInit} */ getAppVersion(): Implementation | undefined { return this._appInfo; diff --git a/src/app.examples.ts b/src/app.examples.ts index 6bbe42dd..7ff766d1 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -7,43 +7,43 @@ * @module */ -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import { - App, - PostMessageTransport, - McpUiToolInputNotificationSchema, - RESOURCE_URI_META_KEY, -} from "./app.js"; +import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { + McpServer, + ToolCallback, +} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { App, PostMessageTransport, RESOURCE_URI_META_KEY } from "./app.js"; /** * Example: How MCP servers use RESOURCE_URI_META_KEY (server-side, not in Apps). */ -function RESOURCE_URI_META_KEY_serverSide(): CallToolResult { +function RESOURCE_URI_META_KEY_serverSide( + server: McpServer, + handler: ToolCallback, +) { //#region RESOURCE_URI_META_KEY_serverSide - // In an MCP server's tool handler: - return { - content: [{ type: "text", text: "Result" }], - _meta: { - [RESOURCE_URI_META_KEY]: "ui://weather/forecast", + server.registerTool( + "weather", + { + description: "Get weather forecast", + _meta: { + [RESOURCE_URI_META_KEY]: "ui://weather/forecast", + }, }, - }; + handler, + ); //#endregion RESOURCE_URI_META_KEY_serverSide } /** * Example: How hosts check for RESOURCE_URI_META_KEY metadata (host-side). */ -async function RESOURCE_URI_META_KEY_hostSide(mcpClient: { - callTool: (args: { - name: string; - arguments: object; - }) => Promise; -}) { +function RESOURCE_URI_META_KEY_hostSide(tool: Tool) { //#region RESOURCE_URI_META_KEY_hostSide - const result = await mcpClient.callTool({ name: "weather", arguments: {} }); - const uiUri = result._meta?.[RESOURCE_URI_META_KEY]; - if (uiUri) { - // Load and display the UI resource + // Check tool definition metadata (from tools/list response): + const uiUri = tool._meta?.[RESOURCE_URI_META_KEY]; + if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { + // Fetch the resource and display the UI } //#endregion RESOURCE_URI_META_KEY_hostSide } @@ -72,20 +72,12 @@ async function App_basicUsage() { {}, // capabilities ); - // Register notification handler using setter (simpler) + // Register handlers before connecting to ensure no notifications are missed app.ontoolinput = (params) => { console.log("Tool arguments:", params.arguments); }; - // OR using inherited setNotificationHandler (more explicit) - app.setNotificationHandler( - McpUiToolInputNotificationSchema, - (notification) => { - console.log("Tool arguments:", notification.params.arguments); - }, - ); - - await app.connect(new PostMessageTransport(window.parent, window.parent)); + await app.connect(); //#endregion App_basicUsage } @@ -104,95 +96,62 @@ async function App_sendMessage(app: App) { /** * Example: Check host capabilities after connection. */ -async function App_hostCapabilities_checkAfterConnection( - app: App, - transport: PostMessageTransport, -) { - //#region App_hostCapabilities_checkAfterConnection - await app.connect(transport); - const caps = app.getHostCapabilities(); - if (caps === undefined) { - console.error("Not connected"); - return; - } - if (caps.serverTools) { +async function App_getHostCapabilities_checkAfterConnection(app: App) { + //#region App_getHostCapabilities_checkAfterConnection + await app.connect(); + if (app.getHostCapabilities()?.serverTools) { console.log("Host supports server tool calls"); } - //#endregion App_hostCapabilities_checkAfterConnection + //#endregion App_getHostCapabilities_checkAfterConnection } /** * Example: Log host information after connection. */ -async function App_hostInfo_logAfterConnection( +async function App_getHostVersion_logAfterConnection( app: App, transport: PostMessageTransport, ) { - //#region App_hostInfo_logAfterConnection + //#region App_getHostVersion_logAfterConnection await app.connect(transport); - const host = app.getHostVersion(); - if (host === undefined) { - console.error("Not connected"); - return; - } - console.log(`Connected to ${host.name} v${host.version}`); - //#endregion App_hostInfo_logAfterConnection + const { name, version } = app.getHostVersion() ?? {}; + console.log(`Connected to ${name} v${version}`); + //#endregion App_getHostVersion_logAfterConnection } /** * Example: Access host context after connection. */ -async function App_hostContext_accessAfterConnection( +async function App_getHostContext_accessAfterConnection( app: App, transport: PostMessageTransport, ) { - //#region App_hostContext_accessAfterConnection + //#region App_getHostContext_accessAfterConnection await app.connect(transport); const context = app.getHostContext(); - if (context === undefined) { - console.error("Not connected"); - return; - } - if (context.theme === "dark") { + if (context?.theme === "dark") { document.body.classList.add("dark-theme"); } - if (context.toolInfo) { + if (context?.toolInfo) { console.log("Tool:", context.toolInfo.tool.name); } - //#endregion App_hostContext_accessAfterConnection + //#endregion App_getHostContext_accessAfterConnection } /** * Example: Using the ontoolinput setter (simpler approach). */ -async function App_ontoolinput_setter( - app: App, - transport: PostMessageTransport, -) { +async function App_ontoolinput_setter(app: App) { //#region App_ontoolinput_setter // Register before connecting to ensure no notifications are missed app.ontoolinput = (params) => { console.log("Tool:", params.arguments); // Update your UI with the tool arguments }; - await app.connect(transport); + await app.connect(); //#endregion App_ontoolinput_setter } -/** - * Example: Using setNotificationHandler for tool input (more explicit approach). - */ -function App_ontoolinput_setNotificationHandler(app: App) { - //#region App_ontoolinput_setNotificationHandler - app.setNotificationHandler( - McpUiToolInputNotificationSchema, - (notification) => { - console.log("Tool:", notification.params.arguments); - }, - ); - //#endregion App_ontoolinput_setNotificationHandler -} - /** * Example: Progressive rendering of tool arguments using ontoolinputpartial. */ @@ -211,11 +170,10 @@ function App_ontoolinputpartial_progressiveRendering(app: App) { function App_ontoolresult_displayResults(app: App) { //#region App_ontoolresult_displayResults app.ontoolresult = (params) => { - if (params.content) { - console.log("Tool output:", params.content); - } if (params.isError) { - console.error("Tool execution failed"); + console.error("Tool execution failed:", params.content); + } else if (params.content) { + console.log("Tool output:", params.content); } }; //#endregion App_ontoolresult_displayResults @@ -228,14 +186,11 @@ function App_ontoolcancelled_handleCancellation(app: App) { //#region App_ontoolcancelled_handleCancellation app.ontoolcancelled = (params) => { console.log("Tool cancelled:", params.reason); - showCancelledMessage(params.reason ?? "Operation was cancelled"); + // Update your UI to show cancellation state }; //#endregion App_ontoolcancelled_handleCancellation } -// Stub for example -declare function showCancelledMessage(message: string): void; - /** * Example: Respond to theme changes using onhostcontextchanged. */ @@ -291,7 +246,7 @@ function App_onlisttools_returnTools(app: App) { //#region App_onlisttools_returnTools app.onlisttools = async (params, extra) => { return { - tools: ["calculate", "convert", "format"], + tools: ["greet", "calculate", "format"], }; }; //#endregion App_onlisttools_returnTools @@ -324,13 +279,17 @@ async function App_callServerTool_fetchWeather(app: App) { async function App_sendMessage_textFromInteraction(app: App) { //#region App_sendMessage_textFromInteraction try { - await app.sendMessage({ + const result = await app.sendMessage({ role: "user", content: [{ type: "text", text: "Show me details for item #42" }], }); + if (result.isError) { + console.error("Host rejected the message"); + // Handle rejection appropriately for your app + } } catch (error) { console.error("Failed to send message:", error); - // Handle error appropriately for your app + // Handle transport/protocol error } //#endregion App_sendMessage_textFromInteraction } @@ -375,11 +334,11 @@ async function App_updateModelContext_structuredContent(app: App) { */ async function App_openLink_documentation(app: App) { //#region App_openLink_documentation - try { - await app.openLink({ url: "https://docs.example.com" }); - } catch (error) { - console.error("Failed to open link:", error); + const { isError } = await app.openLink({ url: "https://docs.example.com" }); + if (isError) { + // Host denied the request (e.g., blocked domain, user cancelled) // Optionally show fallback: display URL for manual copy + console.warn("Link request denied"); } //#endregion App_openLink_documentation } diff --git a/src/app.ts b/src/app.ts index b16fe6fd..dee12b86 100644 --- a/src/app.ts +++ b/src/app.ts @@ -60,14 +60,16 @@ export { } from "./styles"; /** - * Metadata key for associating a resource URI with a tool call. + * Metadata key for associating a UI resource URI with a tool. * - * MCP servers include this key in tool call result metadata to indicate which - * UI resource should be displayed for the tool. When hosts receive a tool result - * containing this metadata, they resolve and render the corresponding {@link App}. + * MCP servers include this key in tool definition metadata (via `tools/list`) + * to indicate which UI resource should be displayed when the tool is called. + * When hosts see a tool with this metadata, they fetch and render the + * corresponding {@link App}. * - * **Note**: This constant is provided for reference. MCP servers set this metadata - * in their tool handlers; App developers typically don't need to use it directly. + * **Note**: This constant is provided for reference. App developers typically + * don't need to use it directly. Prefer using {@link server-helpers!registerAppTool} + * with the `_meta.ui.resourceUri` format instead. * * @example How MCP servers use this key (server-side, not in Apps) * {@includeCode ./app.examples.ts#RESOURCE_URI_META_KEY_serverSide} @@ -200,7 +202,7 @@ export class App extends Protocol { * @returns Host capabilities, or `undefined` if not yet connected * * @example Check host capabilities after connection - * {@includeCode ./app.examples.ts#App_hostCapabilities_checkAfterConnection} + * {@includeCode ./app.examples.ts#App_getHostCapabilities_checkAfterConnection} * * @see {@link connect} for the initialization handshake * @see {@link McpUiHostCapabilities} for the capabilities structure @@ -219,7 +221,7 @@ export class App extends Protocol { * @returns Host implementation info, or `undefined` if not yet connected * * @example Log host information after connection - * {@includeCode ./app.examples.ts#App_hostInfo_logAfterConnection} + * {@includeCode ./app.examples.ts#App_getHostVersion_logAfterConnection} * * @see {@link connect} for the initialization handshake */ @@ -240,7 +242,7 @@ export class App extends Protocol { * @returns Host context, or `undefined` if not yet connected * * @example Access host context after connection - * {@includeCode ./app.examples.ts#App_hostContext_accessAfterConnection} + * {@includeCode ./app.examples.ts#App_getHostContext_accessAfterConnection} * * @see {@link connect} for the initialization handshake * @see {@link onhostcontextchanged} for context change notifications @@ -264,12 +266,9 @@ export class App extends Protocol { * * @param callback - Function called with the tool input params ({@link McpUiToolInputNotification.params}) * - * @example Using the setter (simpler) + * @example * {@includeCode ./app.examples.ts#App_ontoolinput_setter} * - * @example Using setNotificationHandler (more explicit) - * {@includeCode ./app.examples.ts#App_ontoolinput_setNotificationHandler} - * * @see {@link setNotificationHandler} for the underlying method * @see {@link McpUiToolInputNotification} for the notification structure */ @@ -607,9 +606,9 @@ export class App extends Protocol { * * @param params - Message role and content * @param options - Request options (timeout, etc.) - * @returns Result indicating success or error (no message content returned) + * @returns Result with optional `isError` flag indicating host rejection * - * @throws {Error} If the host rejects the message + * @throws {Error} If the request times out or the connection is lost * * @example Send a text message from user interaction * {@includeCode ./app.examples.ts#App_sendMessage_textFromInteraction} @@ -662,6 +661,7 @@ export class App extends Protocol { * @param options - Request options (timeout, etc.) * * @throws {Error} If the host rejects the context update (e.g., unsupported content type) + * @throws {Error} If the request times out or the connection is lost * * @example Update model context with current app state * {@includeCode ./app.examples.ts#App_updateModelContext_appState} @@ -689,19 +689,19 @@ export class App extends Protocol { * Request the host to open an external URL in the default browser. * * The host may deny this request based on user preferences or security policy. - * Apps should handle rejection gracefully. + * Apps should handle rejection gracefully by checking `result.isError`. * * @param params - URL to open * @param options - Request options (timeout, etc.) - * @returns Result indicating success or error + * @returns Result with `isError: true` if the host denied the request (e.g., blocked domain, user cancelled) * - * @throws {Error} If the host denies the request (e.g., blocked domain, user cancelled) * @throws {Error} If the request times out or the connection is lost * * @example Open documentation link * {@includeCode ./app.examples.ts#App_openLink_documentation} * * @see {@link McpUiOpenLinkRequest} for request structure + * @see {@link McpUiOpenLinkResult} for result structure */ openLink(params: McpUiOpenLinkRequest["params"], options?: RequestOptions) { return this.request( diff --git a/src/message-transport.examples.ts b/src/message-transport.examples.ts index 2597a4d3..38abf46a 100644 --- a/src/message-transport.examples.ts +++ b/src/message-transport.examples.ts @@ -49,7 +49,7 @@ function PostMessageTransport_constructor_guestUI() { */ function PostMessageTransport_constructor_host() { //#region PostMessageTransport_constructor_host - const iframe = document.getElementById("app") as HTMLIFrameElement; + const iframe = document.getElementById("app-iframe") as HTMLIFrameElement; const transport = new PostMessageTransport( iframe.contentWindow!, iframe.contentWindow!, diff --git a/src/react/useApp.examples.tsx b/src/react/useApp.examples.tsx index 69a1603d..eb25fe72 100644 --- a/src/react/useApp.examples.tsx +++ b/src/react/useApp.examples.tsx @@ -4,10 +4,12 @@ * @module */ -import { useApp, McpUiToolInputNotificationSchema } from "./index.js"; +import { useState } from "react"; +import type { McpUiHostContext } from "../types.js"; +import { useApp } from "./index.js"; /** - * Example: Register a notification handler in onAppCreated. + * Example: Register an event handler in onAppCreated. */ function useApp_registerHandler() { //#region useApp_registerHandler @@ -15,40 +17,49 @@ function useApp_registerHandler() { appInfo: { name: "MyApp", version: "1.0.0" }, capabilities: {}, onAppCreated: (app) => { - app.setNotificationHandler( - McpUiToolInputNotificationSchema, - (notification) => { - console.log("Tool input:", notification.params.arguments); - }, - ); + app.ontoolresult = (result) => { + console.log("Tool result:", result); + }; }, }); //#endregion useApp_registerHandler } /** - * Example: Basic usage of useApp hook. + * Example: Basic usage of useApp hook with common event handlers. */ function useApp_basicUsage() { //#region useApp_basicUsage function MyApp() { + const [hostContext, setHostContext] = useState< + McpUiHostContext | undefined + >(undefined); + const { app, isConnected, error } = useApp({ appInfo: { name: "MyApp", version: "1.0.0" }, capabilities: {}, onAppCreated: (app) => { - // Register handlers before connection - app.setNotificationHandler( - McpUiToolInputNotificationSchema, - (notification) => { - console.log("Tool input:", notification.params.arguments); - }, - ); + app.ontoolinput = (input) => { + console.log("Tool input:", input); + }; + app.ontoolresult = (result) => { + console.log("Tool result:", result); + }; + app.ontoolcancelled = (params) => { + console.log("Tool cancelled:", params.reason); + }; + app.onerror = (error) => { + console.log("Error:", error); + }; + app.onhostcontextchanged = (params) => { + setHostContext((prev) => ({ ...prev, ...params })); + }; }, }); if (error) return
Error: {error.message}
; if (!isConnected) return
Connecting...
; - return
Connected!
; + return
Theme: {hostContext?.theme}
; } //#endregion useApp_basicUsage } diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index cc8426ba..248f78f1 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -29,7 +29,7 @@ export interface UseAppOptions { * * @param app - The newly created `App` instance * - * @example Register a notification handler + * @example Register an event handler * {@includeCode ./useApp.examples.tsx#useApp_registerHandler} */ onAppCreated?: (app: App) => void; @@ -69,7 +69,7 @@ export interface AppState { * initialization, the `error` field will contain the error (typically connection * timeouts, initialization handshake failures, or transport errors). * - * @example Basic usage + * @example Basic usage of useApp hook with common event handlers * {@includeCode ./useApp.examples.tsx#useApp_basicUsage} * * @see {@link App.connect} for the underlying connection method diff --git a/src/react/useHostStyles.examples.tsx b/src/react/useHostStyles.examples.tsx index 7ab541b7..48d83647 100644 --- a/src/react/useHostStyles.examples.tsx +++ b/src/react/useHostStyles.examples.tsx @@ -4,15 +4,12 @@ * @module */ -import { useState } from "react"; import { useApp, useHostStyleVariables, useHostFonts, useHostStyles, - App, } from "./index.js"; -import type { McpUiHostContext } from "../types.js"; /** * Example: Basic usage of useHostStyleVariables. @@ -20,7 +17,7 @@ import type { McpUiHostContext } from "../types.js"; function useHostStyleVariables_basicUsage() { //#region useHostStyleVariables_basicUsage function MyApp() { - const { app, isConnected } = useApp({ + const { app } = useApp({ appInfo: { name: "MyApp", version: "1.0.0" }, capabilities: {}, }); @@ -43,44 +40,37 @@ function useHostStyleVariables_basicUsage() { function useHostFonts_basicUsage() { //#region useHostFonts_basicUsage function MyApp() { - const { app, isConnected } = useApp({ + const { app } = useApp({ appInfo: { name: "MyApp", version: "1.0.0" }, capabilities: {}, }); - // Automatically apply host fonts - useHostFonts(app); + // Apply host fonts - pass initial context to apply fonts from connect() immediately + useHostFonts(app, app?.getHostContext()); return
Hello!
; } //#endregion useHostFonts_basicUsage } -/** - * Example: useHostFonts with initial context. - */ -function useHostFonts_withInitialContext(app: App) { - //#region useHostFonts_withInitialContext - const [hostContext, setHostContext] = useState(null); - - // ... get initial context from app.connect() result - - useHostFonts(app, hostContext); - //#endregion useHostFonts_withInitialContext -} - /** * Example: Basic usage of useHostStyles. */ function useHostStyles_basicUsage() { - const appInfo = { name: "MyApp", version: "1.0.0" }; //#region useHostStyles_basicUsage function MyApp() { - const { app } = useApp({ appInfo, capabilities: {} }); + const { app } = useApp({ + appInfo: { name: "MyApp", version: "1.0.0" }, + capabilities: {}, + }); + + // Apply all host styles - pass initial context to apply styles from connect() immediately useHostStyles(app, app?.getHostContext()); return ( -
...
+
+ Hello! +
); } //#endregion useHostStyles_basicUsage diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts index e0686e50..6c88b710 100644 --- a/src/react/useHostStyles.ts +++ b/src/react/useHostStyles.ts @@ -91,9 +91,6 @@ export function useHostStyleVariables( * @example Basic usage with useApp * {@includeCode ./useHostStyles.examples.tsx#useHostFonts_basicUsage} * - * @example With initial context - * {@includeCode ./useHostStyles.examples.tsx#useHostFonts_withInitialContext} - * * @see {@link applyHostFonts} for the underlying fonts function * @see {@link useHostStyleVariables} for applying style variables and theme */ diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index 5045eee6..9c5ae660 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -7,7 +7,12 @@ * @module */ -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import * as fs from "node:fs/promises"; +import type { + McpServer, + ToolCallback, + ReadResourceCallback, +} from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import { registerAppTool, @@ -24,17 +29,14 @@ declare function updateCartItem( itemId: string, quantity: number, ): Promise<{ items: unknown[]; total: number }>; -declare const fs: { readFile(path: string, encoding: string): Promise }; -declare function readCallback(): Promise<{ - contents: { uri: string; mimeType: string; text: string }[]; -}>; /** * Example: Module overview showing basic registration of tools and resources. */ function index_overview( server: McpServer, - handler: () => Promise<{ content: { type: "text"; text: string }[] }>, + toolCallback: ToolCallback, + readCallback: ReadResourceCallback, ) { //#region index_overview // Register a tool that displays a widget @@ -45,7 +47,7 @@ function index_overview( description: "Get weather forecast", _meta: { ui: { resourceUri: "ui://weather/widget.html" } }, }, - handler, + toolCallback, ); // Register the HTML resource the tool references @@ -175,12 +177,11 @@ function registerAppResource_withCsp( uri: "ui://music/player.html", mimeType: RESOURCE_MIME_TYPE, text: musicPlayerHtml, - // CSP must be on the content item, not the resource config _meta: { ui: { csp: { - connectDomains: ["https://api.example.com"], // For fetch/WebSocket resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images + connectDomains: ["https://api.example.com"], // For fetch/WebSocket }, }, }, diff --git a/src/styles.examples.ts b/src/styles.examples.ts index 43f352e1..bf403d0e 100644 --- a/src/styles.examples.ts +++ b/src/styles.examples.ts @@ -7,7 +7,6 @@ * @module */ -import type { McpUiHostContext } from "./types.js"; import { App } from "./app.js"; import { getDocumentTheme, @@ -16,24 +15,20 @@ import { applyHostFonts, } from "./styles.js"; -// Stub declarations for examples -declare const app: App; -declare const hostContext: McpUiHostContext; - /** * Example: Check current theme. */ function getDocumentTheme_checkCurrent() { //#region getDocumentTheme_checkCurrent const theme = getDocumentTheme(); - console.log(`Current theme: ${theme}`); + const isDark = theme === "dark"; //#endregion getDocumentTheme_checkCurrent } /** * Example: Apply theme from host context. */ -function applyDocumentTheme_fromHostContext() { +function applyDocumentTheme_fromHostContext(app: App) { //#region applyDocumentTheme_fromHostContext app.onhostcontextchanged = (params) => { if (params.theme) { @@ -46,7 +41,7 @@ function applyDocumentTheme_fromHostContext() { /** * Example: Apply style variables from host context. */ -function applyHostStyleVariables_fromHostContext() { +function applyHostStyleVariables_fromHostContext(app: App) { //#region applyHostStyleVariables_fromHostContext app.onhostcontextchanged = (params) => { if (params.styles?.variables) { @@ -59,19 +54,21 @@ function applyHostStyleVariables_fromHostContext() { /** * Example: Apply to a specific element. */ -function applyHostStyleVariables_toElement() { +function applyHostStyleVariables_toElement(app: App) { //#region applyHostStyleVariables_toElement - const container = document.getElementById("app-root"); - if (container && hostContext.styles?.variables) { - applyHostStyleVariables(hostContext.styles.variables, container); - } + app.onhostcontextchanged = (params) => { + const container = document.getElementById("app-root"); + if (container && params.styles?.variables) { + applyHostStyleVariables(params.styles.variables, container); + } + }; //#endregion applyHostStyleVariables_toElement } /** * Example: Apply fonts from host context. */ -function applyHostFonts_fromHostContext() { +function applyHostFonts_fromHostContext(app: App) { //#region applyHostFonts_fromHostContext app.onhostcontextchanged = (params) => { if (params.styles?.css?.fonts) {