diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index 4c1f761..8f36cb6 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 c59cd1d..3083b57 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 6bbe42d..7ff766d 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 b16fe6f..dee12b8 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 2597a4d..38abf46 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 69a1603..eb25fe7 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 cc8426b..248f78f 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 7ab541b..48d8364 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 e0686e5..6c88b71 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 5045eee..9c5ae66 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 43f352e..bf403d0 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) {