Merge pull request #302 from jonathanhefner/checked-examples

Move JSDoc examples to type-checked `.examples.ts` files
This commit is contained in:
Jonathan Hefner 2026-01-20 13:30:01 -06:00 • committed by GitHub
commit 26ae13df0f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1637 additions and 867 deletions

View file

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

View file

@ -58,22 +58,22 @@ A [Claude Code plugin](https://github.com/modelcontextprotocol/ext-apps/tree/mai
<!-- prettier-ignore-start -->
| | | |
|:---:|:---:|:---:|
| [![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!<br><br>[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!<br><br>[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) |
<!-- prettier-ignore-end -->
The [`examples/`](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples) directory contains additional demo apps showcasing real-world use cases.

501
src/app-bridge.examples.ts Normal file
View file

@ -0,0 +1,501 @@
/**
* 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";
import type { McpUiDisplayMode } from "./types.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
}
/**
* 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
}
/**
* Example: Check Guest UI capabilities after initialization.
*/
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_getAppCapabilities_checkAfterInit
}
/**
* Example: Log Guest UI information after initialization.
*/
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_getAppVersion_logAfterInit
}
/**
* Example: Handle Guest UI initialization and send tool input.
*/
function AppBridge_oninitialized_sendToolInput(
bridge: AppBridge,
toolArgs: Record<string, unknown>,
) {
//#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<void>;
};
// 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<boolean>;
// Stub for example code - represents a hypothetical model context manager
declare const modelContextManager: {
update(context: { content?: unknown; structuredContent?: unknown }): void;
};
/**
* 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,
) => {
// Store the context snapshot for inclusion in the next model request
modelContextManager.update({ content, structuredContent });
return {};
};
//#endregion AppBridge_onupdatemodelcontext_storeContext
}
/**
* Example: Forward tool calls to the MCP server.
*/
function AppBridge_oncalltool_forwardToServer(
bridge: AppBridge,
mcpClient: Client,
) {
//#region AppBridge_oncalltool_forwardToServer
bridge.oncalltool = async (params, extra) => {
return mcpClient.request(
{ method: "tools/call", params },
CallToolResultSchema,
{ signal: extra.signal },
);
};
//#endregion AppBridge_oncalltool_forwardToServer
}
/**
* Example: Forward list resources requests to the MCP server.
*/
function AppBridge_onlistresources_returnResources(
bridge: AppBridge,
mcpClient: Client,
) {
//#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,
mcpClient: Client,
) {
//#region AppBridge_onreadresource_returnResource
bridge.onreadresource = async (params, extra) => {
return mcpClient.request(
{ method: "resources/read", params },
ReadResourceResultSchema,
{ signal: extra.signal },
);
};
//#endregion AppBridge_onreadresource_returnResource
}
/**
* Example: Forward list prompts requests to the MCP server.
*/
function AppBridge_onlistprompts_returnPrompts(
bridge: AppBridge,
mcpClient: Client,
) {
//#region AppBridge_onlistprompts_returnPrompts
bridge.onlistprompts = async (params, extra) => {
return mcpClient.request(
{ method: "prompts/list", params },
ListPromptsResultSchema,
{ signal: extra.signal },
);
};
//#endregion AppBridge_onlistprompts_returnPrompts
}
/**
* 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,
iframe: HTMLIFrameElement,
) {
//#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,
currentDisplayMode: McpUiDisplayMode,
availableDisplayModes: McpUiDisplayMode[],
) {
//#region AppBridge_onrequestdisplaymode_handleRequest
bridge.onrequestdisplaymode = async ({ mode }, extra) => {
if (availableDisplayModes.includes(mode)) {
currentDisplayMode = mode;
}
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 }) => {
console[level === "error" ? "error" : "log"](
`[${logger ?? "Guest UI"}] ${level.toUpperCase()}:`,
data,
);
};
//#endregion AppBridge_onloggingmessage_handleLog
}
/**
* Example: Gracefully tear down the Guest UI before unmounting.
*/
async function AppBridge_teardownResource_gracefulShutdown(
bridge: AppBridge,
iframe: HTMLIFrameElement,
) {
//#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,
mcpClient: Client,
args: Record<string, unknown>,
) {
//#region AppBridge_sendToolResult_afterExecution
const result = await mcpClient.request(
{ method: "tools/call", params: { name: "get_weather", arguments: args } },
CallToolResultSchema,
);
bridge.sendToolResult(result);
//#endregion AppBridge_sendToolResult_afterExecution
}
/**
* 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(
iframe: HTMLIFrameElement,
mcpClient: Client,
hostInfo: { name: string; version: string },
capabilities: { openLinks: {}; serverTools: {}; logging: {} },
toolArgs: Record<string, unknown>,
) {
//#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
}

View file

@ -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_getAppCapabilities_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_getAppVersion_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<McpUiMessageResult>` 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<McpUiOpenLinkResult>` 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<McpUiRequestDisplayModeResult>` 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) {

405
src/app.examples.ts Normal file
View file

@ -0,0 +1,405 @@
/**
* 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 { 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(
server: McpServer,
handler: ToolCallback,
) {
//#region RESOURCE_URI_META_KEY_serverSide
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).
*/
function RESOURCE_URI_META_KEY_hostSide(tool: Tool) {
//#region RESOURCE_URI_META_KEY_hostSide
// 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
}
/**
* 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 handlers before connecting to ensure no notifications are missed
app.ontoolinput = (params) => {
console.log("Tool arguments:", params.arguments);
};
await app.connect();
//#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_getHostCapabilities_checkAfterConnection(app: App) {
//#region App_getHostCapabilities_checkAfterConnection
await app.connect();
if (app.getHostCapabilities()?.serverTools) {
console.log("Host supports server tool calls");
}
//#endregion App_getHostCapabilities_checkAfterConnection
}
/**
* Example: Log host information after connection.
*/
async function App_getHostVersion_logAfterConnection(
app: App,
transport: PostMessageTransport,
) {
//#region App_getHostVersion_logAfterConnection
await app.connect(transport);
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_getHostContext_accessAfterConnection(
app: App,
transport: PostMessageTransport,
) {
//#region App_getHostContext_accessAfterConnection
await app.connect(transport);
const context = app.getHostContext();
if (context?.theme === "dark") {
document.body.classList.add("dark-theme");
}
if (context?.toolInfo) {
console.log("Tool:", context.toolInfo.tool.name);
}
//#endregion App_getHostContext_accessAfterConnection
}
/**
* Example: Using the ontoolinput setter (simpler approach).
*/
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();
//#endregion App_ontoolinput_setter
}
/**
* 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.isError) {
console.error("Tool execution failed:", params.content);
} else if (params.content) {
console.log("Tool output:", params.content);
}
};
//#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);
// Update your UI to show cancellation state
};
//#endregion App_ontoolcancelled_handleCancellation
}
/**
* 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<void>;
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: ["greet", "calculate", "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 {
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 transport/protocol error
}
//#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
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
}
/**
* 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
}

View file

@ -60,34 +60,22 @@ 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)
* ```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 +155,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<AppRequest, AppNotification, AppResult> {
private _hostCapabilities?: McpUiHostCapabilities;
@ -216,13 +173,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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 +202,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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_getHostCapabilities_checkAfterConnection}
*
* @see {@link connect} for the initialization handshake
* @see {@link McpUiHostCapabilities} for the capabilities structure
@ -280,15 +221,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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_getHostVersion_logAfterConnection}
*
* @see {@link connect} for the initialization handshake
*/
@ -309,20 +242,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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_getHostContext_accessAfterConnection}
*
* @see {@link connect} for the initialization handshake
* @see {@link onhostcontextchanged} for context change notifications
@ -346,25 +266,8 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
*
* @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);
* ```
*
* @example Using setNotificationHandler (more explicit)
* ```typescript
* app.setNotificationHandler(
* McpUiToolInputNotificationSchema,
* (notification) => {
* console.log("Tool:", notification.params.arguments);
* }
* );
* ```
* @example
* {@includeCode ./app.examples.ts#App_ontoolinput_setter}
*
* @see {@link setNotificationHandler} for the underlying method
* @see {@link McpUiToolInputNotification} for the notification structure
@ -392,12 +295,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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 +324,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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 +354,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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 +388,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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 +426,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* 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 +462,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* 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 +496,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* 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 +585,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* 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"],
@ -765,22 +606,12 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
*
* @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
* ```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 +635,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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
*/
@ -836,20 +661,13 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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
* ```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
*/
@ -871,26 +689,19 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* 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
* ```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
* @see {@link McpUiOpenLinkResult} for result structure
*/
openLink(params: McpUiOpenLinkRequest["params"], options?: RequestOptions) {
return this.request(
@ -919,13 +730,7 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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<AppRequest, AppNotification, AppResult> {
* @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<AppRequest, AppNotification, AppResult> {
* @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<AppRequest, AppNotification, AppResult> {
* @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

View file

@ -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-iframe") as HTMLIFrameElement;
const transport = new PostMessageTransport(
iframe.contentWindow!,
iframe.contentWindow!,
);
//#endregion PostMessageTransport_constructor_host
}

View file

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

View file

@ -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 <div>Error: {error.message}</div>;
if (!isConnected) return <div>Connecting...</div>;
return <div>Connected!</div>;
}
//#endregion index_basicReactApp
}

View file

@ -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 <div>Error: {error.message}</div>;
* if (!isConnected) return <div>Connecting...</div>;
*
* return <div>Connected!</div>;
* }
* ```
* {@includeCode ./index.examples.tsx#index_basicReactApp}
*/
export * from "./useApp";
export * from "./useAutoResize";

View file

@ -0,0 +1,65 @@
/**
* Type-checked examples for the useApp hook.
*
* @module
*/
import { useState } from "react";
import type { McpUiHostContext } from "../types.js";
import { useApp } from "./index.js";
/**
* Example: Register an event handler in onAppCreated.
*/
function useApp_registerHandler() {
//#region useApp_registerHandler
useApp({
appInfo: { name: "MyApp", version: "1.0.0" },
capabilities: {},
onAppCreated: (app) => {
app.ontoolresult = (result) => {
console.log("Tool result:", result);
};
},
});
//#endregion useApp_registerHandler
}
/**
* 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) => {
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 <div>Error: {error.message}</div>;
if (!isConnected) return <div>Connecting...</div>;
return <div>Theme: {hostContext?.theme}</div>;
}
//#endregion useApp_basicUsage
}

View file

@ -29,19 +29,8 @@ 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);
* }
* );
* }
* ```
* @example Register an event handler
* {@includeCode ./useApp.examples.tsx#useApp_registerHandler}
*/
onAppCreated?: (app: App) => void;
}
@ -80,30 +69,8 @@ export interface AppState {
* initialization, the `error` field will contain the error (typically connection
* 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 <div>Error: {error.message}</div>;
* if (!isConnected) return <div>Connecting...</div>;
* return <div>Connected!</div>;
* }
* ```
* @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
* @see {@link useAutoResize} for manual auto-resize control when using custom App options

View file

@ -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<App | null>(null);
const [error, setError] = useState<Error | null>(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 <div>Connection failed: {error.message}</div>;
return <div>My content</div>;
}
//#endregion useAutoResize_manualApp
}

View file

@ -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<App | null>(null);
* const [error, setError] = useState<Error | null>(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 <div>Connection failed: {error.message}</div>;
* return <div>My content</div>;
* }
* ```
* {@includeCode ./useAutoResize.examples.tsx#useAutoResize_manualApp}
*
* @see {@link App.setupSizeChangedNotifications} for the underlying implementation
* @see {@link useApp} which enables auto-resize by default

View file

@ -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 <div>{theme === "dark" ? <DarkIcon /> : <LightIcon />}</div>;
}
//#endregion useDocumentTheme_conditionalRender
}
/**
* Example: Use with theme-aware styling.
*/
function useDocumentTheme_themedButton() {
//#region useDocumentTheme_themedButton
function ThemedButton() {
const theme = useDocumentTheme();
return (
<button
style={{
background: theme === "dark" ? "#333" : "#fff",
color: theme === "dark" ? "#fff" : "#333",
}}
>
Click me
</button>
);
}
//#endregion useDocumentTheme_themedButton
}

View file

@ -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 (
* <div>
* {theme === 'dark' ? <DarkIcon /> : <LightIcon />}
* </div>
* );
* }
* ```
* {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_conditionalRender}
*
* @example Use with theme-aware styling
* ```tsx
* function ThemedButton() {
* const theme = useDocumentTheme();
*
* return (
* <button style={{
* background: theme === 'dark' ? '#333' : '#fff',
* color: theme === 'dark' ? '#fff' : '#333',
* }}>
* Click me
* </button>
* );
* }
* ```
* {@includeCode ./useDocumentTheme.examples.tsx#useDocumentTheme_themedButton}
*
* @see {@link getDocumentTheme} for the underlying function
* @see {@link applyDocumentTheme} to set the theme

View file

@ -0,0 +1,77 @@
/**
* Type-checked examples for the useHostStyles hooks.
*
* @module
*/
import {
useApp,
useHostStyleVariables,
useHostFonts,
useHostStyles,
} from "./index.js";
/**
* Example: Basic usage of useHostStyleVariables.
*/
function useHostStyleVariables_basicUsage() {
//#region useHostStyleVariables_basicUsage
function MyApp() {
const { app } = 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 (
<div style={{ background: "var(--color-background-primary)" }}>
Hello!
</div>
);
}
//#endregion useHostStyleVariables_basicUsage
}
/**
* Example: Basic usage of useHostFonts with useApp.
*/
function useHostFonts_basicUsage() {
//#region useHostFonts_basicUsage
function MyApp() {
const { app } = useApp({
appInfo: { name: "MyApp", version: "1.0.0" },
capabilities: {},
});
// Apply host fonts - pass initial context to apply fonts from connect() immediately
useHostFonts(app, app?.getHostContext());
return <div style={{ fontFamily: "var(--font-sans)" }}>Hello!</div>;
}
//#endregion useHostFonts_basicUsage
}
/**
* Example: Basic usage of useHostStyles.
*/
function useHostStyles_basicUsage() {
//#region useHostStyles_basicUsage
function MyApp() {
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 (
<div style={{ background: "var(--color-background-primary)" }}>
Hello!
</div>
);
}
//#endregion useHostStyles_basicUsage
}

View file

@ -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 (
* <div style={{ background: 'var(--color-background-primary)' }}>
* Hello!
* </div>
* );
* }
* ```
* {@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,7 @@ 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 (
* <div style={{ fontFamily: 'var(--font-sans)' }}>
* Hello!
* </div>
* );
* }
* ```
*
* @example With initial context
* ```tsx
* const [hostContext, setHostContext] = useState<McpUiHostContext | null>(null);
*
* // ... get initial context from app.connect() result
*
* useHostFonts(app, hostContext);
* ```
* {@includeCode ./useHostStyles.examples.tsx#useHostFonts_basicUsage}
*
* @see {@link applyHostFonts} for the underlying fonts function
* @see {@link useHostStyleVariables} for applying style variables and theme
@ -182,14 +136,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 <div style={{ background: 'var(--color-background-primary)' }}>...</div>;
* }
* ```
* {@includeCode ./useHostStyles.examples.tsx#useHostStyles_basicUsage}
*
* @see {@link useHostStyleVariables} for style variables and theme only
* @see {@link useHostFonts} for fonts only

View file

@ -0,0 +1,193 @@
/**
* 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 * as fs from "node:fs/promises";
import type {
McpServer,
ToolCallback,
ReadResourceCallback,
} 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 }>;
/**
* Example: Module overview showing basic registration of tools and resources.
*/
function index_overview(
server: McpServer,
toolCallback: ToolCallback,
readCallback: ReadResourceCallback,
) {
//#region index_overview
// Register a tool that displays a widget
registerAppTool(
server,
"weather",
{
description: "Get weather forecast",
_meta: { ui: { resourceUri: "ui://weather/widget.html" } },
},
toolCallback,
);
// 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,
_meta: {
ui: {
csp: {
resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images
connectDomains: ["https://api.example.com"], // For fetch/WebSocket
},
},
},
},
],
}),
);
//#endregion registerAppResource_withCsp
}

View file

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

Some files were not shown because too many files have changed in this diff Show more