diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000..d7ad47b7
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,29 @@
+name: Deploy Documentation
+
+on:
+ workflow_dispatch:
+ # Future: Uncomment to auto-deploy on release
+ # release:
+ # types: [published]
+
+permissions:
+ contents: write
+
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: oven-sh/setup-bun@v2
+ - uses: actions/setup-node@v4
+ with:
+ node-version: "20"
+ - run: npm install
+ - run: npm run build
+ - run: npm run docs
+ - uses: peaceiris/actions-gh-pages@v4
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ publish_dir: ./docs
+ publish_branch: gh-pages
+ force_orphan: true
diff --git a/.gitignore b/.gitignore
index 32da72cf..3a7c2891 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@ node_modules/
package-lock.json
yarn.lock
.vscode/
+docs/api/
+tmp/
diff --git a/README.md b/README.md
index 67160dbf..bcc3252a 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,37 @@
# @modelcontextprotocol/ext-apps
-This repo contains the SDK and [specification](./specification/draft/apps.mdx) for MCP Apps Extension ([SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)).
+[](https://modelcontextprotocol.github.io/ext-apps/)
-MCP Apps are proposed standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients / chatbots.
+This repo contains the SDK and [specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx) for MCP Apps Extension ([SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)).
-This repo provides:
+MCP Apps are a proposed standard inspired by [MCP-UI](https://mcpui.dev/) and [OpenAI's Apps SDK](https://developers.openai.com/apps-sdk/) to allow MCP Servers to display interactive UI elements in conversational MCP clients / chatbots.
-- [specification/draft/apps.mdx](./specification/draft/apps.mdx): The Draft Extension Specification. It's still... in flux! Feedback welcome! (also see discussions in [SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)).
+## Overview
-- [types.ts](./src/types.ts): Types of JSON-RPC messages used to communicate between Apps & their host
- - Note that MCP Apps also use some standard MCP messages (e.g. `tools/call` for the App to trigger actions on its originating Server - these calls are proxied through the Host), but these types are the additional messages defined by the extension
-- [examples/simple-example](./examples/simple-server): Example Server + Apps
- - [server.ts](./examples/simple-server/server.ts): MCP server with three tools that declare UI resources of Apps to be show in the chat when called
- - [ui-react.tsx](./examples/simple-server/src/ui-react.tsx): React App returned by the `create-ui-react` tool shows how to use the `useApp` hook to register MCP callbacks
- - [ui-vanilla.tsx](./examples/simple-server/src/ui-vanilla.ts): vanilla App returned by the `create-ui-vanilla`
- - [ui-raw.tsx](./examples/simple-server/src/ui-raw.ts): same as vanilla App but doesn't use the SDK runtime (just its types)
+This SDK serves two audiences:
-- [examples/simple-host](./examples/simple-host): bare-bone examples on how to host MCP Apps (both use the [AppBridge](./src/app-bridge.ts) class to talk to a hosted App)
- - [example-host-react.tsx](./examples/simple-host/src/example-host-react.tsx) uses React (esp. [AppRenderer.tsx](./examples/simple-host/src/AppRenderer.tsx))
- - [example-host-vanilla.tsx](./examples/simple-host/src/example-host-vanilla.tsx) doesn't use React
+### App Developers
-- [message-transport](./src/message-transport.ts): `PostMessageTransport` class that uses `postMessage` to exchange JSON-RPC messages between windows / iframes
+Build interactive UIs that run inside MCP-enabled chat clients.
-- [app.ts](./src/app.ts): `App` class used by an App to talk to its host
+- **SDK for Apps**: `@modelcontextprotocol/ext-apps` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/modules/_modelcontextprotocol_ext_apps.html)
+- **React hooks**: `@modelcontextprotocol/ext-apps/react` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/modules/_modelcontextprotocol_ext_apps_react.html)
-- [app-bridge.ts](./src/app-bridge.ts): `AppBridge` class used by the host to talk to a single App
+### Host Developers
-- _Soon_: more examples!
+Embed and communicate with MCP Apps in your chat application.
-What this repo does NOT provide:
+- **SDK for Hosts**: `@modelcontextprotocol/ext-apps/app-bridge` — [API Docs](https://modelcontextprotocol.github.io/ext-apps/modules/_modelcontextprotocol_ext_apps_app_bridge.html)
-- There's no _supported_ host implementation in this repo (beyond the [examples/simple-host](./examples/simple-host) example)
- - We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic.
+There's no _supported_ host implementation in this repo (beyond the [examples/simple-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host) example).
-## Using the SDK
+We have [contributed a tentative implementation](https://github.com/MCP-UI-Org/mcp-ui/pull/147) of hosting / iframing / sandboxing logic to the [MCP-UI](https://github.com/idosal/mcp-ui) repository, and expect OSS clients may use it, while other clients might roll their own hosting logic.
-### Run examples
+## Installation
-Run the examples in this repo end-to-end:
+This repo is in flux and isn't published to npm yet. When it is, it will use the `@modelcontextprotocol/ext-apps` package.
-```
-npm i
-npm start
-```
-
-open http://localhost:8080/
-
-> [!NOTE]
-> Please bear with us while we add more examples!
-
-### Using the SDK in your project
-
-This repo is in flux and isn't published to npm yet: when it is, it will use the `@modelcontextprotocol/ext-apps` package.
-
-In the meantime you can depend on the SDK library in a Node.js project by installing it w/ its git URL:
+In the meantime you can depend on the SDK library in a Node.js project by installing it with its git URL:
```bash
npm install -S git+https://github.com/modelcontextprotocol/ext-apps.git
@@ -63,15 +41,33 @@ Your `package.json` will then look like:
```json
{
- ...
"dependencies": {
- ...
"@modelcontextprotocol/ext-apps": "git+https://github.com/modelcontextprotocol/ext-apps.git"
}
}
```
-> [!NOTE]
+> [!NOTE]
> The build tools (`esbuild`, `tsx`, `typescript`) are in `dependencies` rather than `devDependencies`. This is intentional: it allows the `prepare` script to run when the package is installed from git, since npm doesn't install devDependencies for git dependencies.
>
> Once the package is published to npm with pre-built `dist/`, these can be moved back to `devDependencies`.
+
+## Examples
+
+- [examples/simple-server](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-server) — Example MCP server with tools that return UI Apps
+- [examples/simple-host](https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/simple-host) — Bare-bones example of hosting MCP Apps
+
+To run the examples end-to-end:
+
+```
+npm i
+npm start
+```
+
+Then open http://localhost:8080/
+
+## Resources
+
+- [API Documentation](https://modelcontextprotocol.github.io/ext-apps/)
+- [Draft Specification](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx)
+- [SEP-1865 Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 00000000..609d10e8
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Redirecting...
+
+
+ Redirecting to API Documentation...
+
+
+
diff --git a/package.json b/package.json
index 36873758..b24b6a34 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,8 @@
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:example-host' 'npm run start:example-mcp-server'",
"build": "bun build.bun.ts",
"prepare": "npm run build && husky",
+ "docs": "typedoc",
+ "docs:watch": "typedoc --watch",
"prettier:base-cmd": "prettier -u --ignore-path ./.gitignore --ignore-path ./.prettierignore",
"prettier": "yarn prettier:base-cmd \"$(pwd)/**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --check",
"prettier:fix": "yarn prettier:base-cmd \"$(pwd)/**/*.{js,jsx,ts,tsx,mjs,json,md,yml,yaml}\" --write --list-different"
@@ -45,10 +47,11 @@
"express": "^5.1.0",
"husky": "^9.1.7",
"prettier": "^3.6.2",
+ "typedoc": "^0.28.14",
"typescript": "^5.9.3"
},
"dependencies": {
- "@modelcontextprotocol/sdk": "^1.22.0",
+ "@modelcontextprotocol/sdk": "^1.23.0",
"bun": "^1.3.2",
"react": "^19.2.0",
"react-dom": "^19.2.0",
diff --git a/src/app-bridge.ts b/src/app-bridge.ts
index 90756a7a..8e682f3c 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -1,5 +1,5 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { ZodLiteral, ZodObject } from "zod";
+import { ZodLiteral, ZodObject } from "zod/v4";
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
import {
@@ -59,18 +59,114 @@ import {
export * from "./types";
export { PostMessageTransport } from "./message-transport";
-type HostOptions = ProtocolOptions;
+/**
+ * Options for configuring AppBridge behavior.
+ *
+ * @see ProtocolOptions from @modelcontextprotocol/sdk for available options
+ */
+export type HostOptions = ProtocolOptions;
+/**
+ * Protocol versions supported by this AppBridge implementation.
+ *
+ * The SDK automatically handles version negotiation during initialization.
+ * Hosts don't need to manage protocol versions manually.
+ */
export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION];
+/**
+ * Extra metadata passed to request handlers.
+ *
+ * This type represents the additional context provided by the Protocol class
+ * when handling requests, including abort signals and session information.
+ * It is extracted from the MCP SDK's request handler signature.
+ *
+ * @internal
+ */
type RequestHandlerExtra = Parameters<
Parameters[1]
>[1];
+/**
+ * Host-side bridge for communicating with a single Guest UI (App).
+ *
+ * AppBridge extends the MCP SDK's Protocol class and acts as a proxy between
+ * the host application and a Guest UI running in an iframe. It automatically
+ * forwards MCP server capabilities (tools, resources, prompts) to the Guest UI
+ * and handles the initialization handshake.
+ *
+ * ## Architecture
+ *
+ * **Guest UI ↔ AppBridge ↔ Host ↔ MCP Server**
+ *
+ * The bridge proxies requests from the Guest UI to the MCP server and forwards
+ * responses back. It also sends host-initiated notifications like tool input
+ * and results to the Guest UI.
+ *
+ * ## Lifecycle
+ *
+ * 1. **Create**: Instantiate AppBridge with MCP client and capabilities
+ * 2. **Connect**: Call `connect()` with transport to establish communication
+ * 3. **Wait for init**: Guest UI sends initialize request, bridge responds
+ * 4. **Send data**: Call `sendToolInput()`, `sendToolResult()`, etc.
+ * 5. **Teardown**: Call `sendResourceTeardown()` 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);
+ * ```
+ */
export class AppBridge extends Protocol {
private _appCapabilities?: McpUiAppCapabilities;
private _appInfo?: Implementation;
+ /**
+ * Create a new AppBridge instance.
+ *
+ * @param _client - MCP client connected to the server (for proxying requests)
+ * @param _hostInfo - Host application identification (name and version)
+ * @param _capabilities - Features and capabilities the host supports
+ * @param options - Configuration options (inherited from Protocol)
+ *
+ * @example
+ * ```typescript
+ * const bridge = new AppBridge(
+ * mcpClient,
+ * { name: "MyHost", version: "1.0.0" },
+ * { openLinks: {}, serverTools: {}, logging: {} }
+ * );
+ * ```
+ */
constructor(
private _client: Client,
private _hostInfo: Implementation,
@@ -89,15 +185,100 @@ export class AppBridge extends Protocol {
});
}
+ /**
+ * Get the Guest UI's capabilities discovered during initialization.
+ *
+ * Returns the capabilities that the Guest UI advertised during its
+ * initialization request. Returns `undefined` if called before
+ * initialization completes.
+ *
+ * @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");
+ * }
+ * };
+ * ```
+ *
+ * @see {@link McpUiAppCapabilities} for the capabilities structure
+ */
getAppCapabilities(): McpUiAppCapabilities | undefined {
return this._appCapabilities;
}
+
+ /**
+ * Get the Guest UI's implementation info discovered during initialization.
+ *
+ * Returns the Guest UI's name and version as provided in its initialization
+ * request. Returns `undefined` if called before initialization completes.
+ *
+ * @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}`);
+ * }
+ * };
+ * ```
+ */
getAppVersion(): Implementation | undefined {
return this._appInfo;
}
+ /**
+ * Optional handler for ping requests from the Guest UI.
+ *
+ * The Guest UI can send standard MCP `ping` requests to verify the connection
+ * is alive. The AppBridge automatically responds with an empty object, but this
+ * handler allows the host to observe or log ping activity.
+ *
+ * Unlike the other handlers which use setters, this is a direct property
+ * assignment. It is optional; if not set, pings are still handled automatically.
+ *
+ * @param params - Empty params object from the ping request
+ * @param extra - Request metadata (abort signal, session info)
+ *
+ * @example
+ * ```typescript
+ * bridge.onping = (params, extra) => {
+ * console.log("Received ping from Guest UI");
+ * };
+ * ```
+ */
onping?: (params: PingRequest["params"], extra: RequestHandlerExtra) => void;
+ /**
+ * Register a handler for size change notifications from the Guest UI.
+ *
+ * The Guest UI sends `ui/notifications/size-change` when its rendered content
+ * size changes, typically via ResizeObserver. Set this callback to dynamically
+ * adjust the iframe container dimensions based on the Guest UI's content.
+ *
+ * Note: This is for Guest UI → Host communication. To notify the Guest UI of
+ * host viewport changes, use {@link app.App.sendSizeChange}.
+ *
+ * @example
+ * ```typescript
+ * bridge.onsizechange = ({ width, height }) => {
+ * if (width != null) {
+ * iframe.style.width = `${width}px`;
+ * }
+ * if (height != null) {
+ * iframe.style.height = `${height}px`;
+ * }
+ * };
+ * ```
+ *
+ * @see {@link McpUiSizeChangeNotification} for the notification type
+ * @see {@link app.App.sendSizeChange} for Host → Guest UI size notifications
+ */
set onsizechange(
callback: (params: McpUiSizeChangeNotification["params"]) => void,
) {
@@ -105,6 +286,37 @@ export class AppBridge extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Register a handler for sandbox proxy ready notifications.
+ *
+ * This is an internal callback used by web-based hosts implementing the
+ * double-iframe sandbox architecture. The sandbox proxy sends
+ * `ui/notifications/sandbox-proxy-ready` after it loads and is ready to receive
+ * HTML content.
+ *
+ * When this fires, the host should call {@link sendSandboxResourceReady} with
+ * the HTML content to load into the inner sandboxed iframe.
+ *
+ * @example
+ * ```typescript
+ * bridge.onsandboxready = async () => {
+ * const resource = await mcpClient.request(
+ * { method: "resources/read", params: { uri: "ui://my-app" } },
+ * ReadResourceResultSchema
+ * );
+ *
+ * bridge.sendSandboxResourceReady({
+ * html: resource.contents[0].text,
+ * sandbox: "allow-scripts"
+ * });
+ * };
+ * ```
+ *
+ * @internal
+ * @see {@link McpUiSandboxProxyReadyNotification} for the notification type
+ * @see {@link sendSandboxResourceReady} for sending content to the sandbox
+ */
set onsandboxready(
callback: (params: McpUiSandboxProxyReadyNotification["params"]) => void,
) {
@@ -112,6 +324,24 @@ export class AppBridge extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Called when the Guest UI completes initialization.
+ *
+ * Set this callback to be notified when the Guest UI has finished its
+ * 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 });
+ * };
+ * ```
+ *
+ * @see {@link McpUiInitializedNotification} for the notification type
+ * @see {@link sendToolInput} for sending tool arguments to the Guest UI
+ */
set oninitialized(
callback: (params: McpUiInitializedNotification["params"]) => void,
) {
@@ -119,6 +349,41 @@ export class AppBridge extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Register a handler for message requests from the Guest UI.
+ *
+ * The Guest UI sends `ui/message` requests when it wants to add a message to
+ * the host's chat interface. This enables interactive apps to communicate with
+ * the user through the conversation thread.
+ *
+ * The handler should process the message (add it to the chat) and return a
+ * result indicating success or failure. For security, the host should NOT
+ * return conversation content or follow-up results to prevent information
+ * leakage.
+ *
+ * @param callback - Handler that receives message params and returns a result
+ * - params.role - Message role (currently only "user" is supported)
+ * - params.content - Message content blocks (text, image, etc.)
+ * - extra - Request metadata (abort signal, session info)
+ * - Returns: Promise with optional isError flag
+ *
+ * @example
+ * ```typescript
+ * bridge.onmessage = async ({ role, content }, extra) => {
+ * try {
+ * await chatManager.addMessage({ role, content, source: "app" });
+ * return {}; // Success
+ * } catch (error) {
+ * console.error("Failed to add message:", error);
+ * return { isError: true };
+ * }
+ * };
+ * ```
+ *
+ * @see {@link McpUiMessageRequest} for the request type
+ * @see {@link McpUiMessageResult} for the result type
+ */
set onmessage(
callback: (
params: McpUiMessageRequest["params"],
@@ -132,6 +397,50 @@ export class AppBridge extends Protocol {
},
);
}
+
+ /**
+ * Register a handler for external link requests from the Guest UI.
+ *
+ * The Guest UI sends `ui/open-link` requests when it wants to open an external
+ * URL in the host's default browser. The handler should validate the URL and
+ * open it according to the host's security policy and user preferences.
+ *
+ * The host MAY:
+ * - Show a confirmation dialog before opening
+ * - Block URLs based on a security policy or allowlist
+ * - Log the request for audit purposes
+ * - Reject the request entirely
+ *
+ * @param callback - Handler that receives URL params and returns a result
+ * - params.url - URL to open in the host's browser
+ * - extra - Request metadata (abort signal, session info)
+ * - Returns: Promise with optional isError flag
+ *
+ * @example
+ * ```typescript
+ * bridge.onopenlink = async ({ url }, extra) => {
+ * if (!isAllowedDomain(url)) {
+ * console.warn("Blocked external link:", url);
+ * return { isError: true };
+ * }
+ *
+ * const confirmed = await showDialog({
+ * message: `Open external link?\n${url}`,
+ * buttons: ["Open", "Cancel"]
+ * });
+ *
+ * if (confirmed) {
+ * window.open(url, "_blank", "noopener,noreferrer");
+ * return {};
+ * }
+ *
+ * return { isError: true };
+ * };
+ * ```
+ *
+ * @see {@link McpUiOpenLinkRequest} for the request type
+ * @see {@link McpUiOpenLinkResult} for the result type
+ */
set onopenlink(
callback: (
params: McpUiOpenLinkRequest["params"],
@@ -145,6 +454,34 @@ export class AppBridge extends Protocol {
},
);
}
+
+ /**
+ * Register a handler for logging messages from the Guest UI.
+ *
+ * The Guest UI sends standard MCP `notifications/message` (logging) notifications
+ * to report debugging information, errors, warnings, and other telemetry to the
+ * host. The host can display these in a console, log them to a file, or send
+ * them to a monitoring service.
+ *
+ * This uses the standard MCP logging notification format, not a UI-specific
+ * message type.
+ *
+ * @param callback - Handler that receives logging params
+ * - params.level - Log level: "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency"
+ * - params.logger - Optional logger name/identifier
+ * - 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
+ * );
+ * };
+ * ```
+ */
set onloggingmessage(
callback: (params: LoggingMessageNotification["params"]) => void,
) {
@@ -156,20 +493,45 @@ export class AppBridge extends Protocol {
);
}
+ /**
+ * Verify that the guest supports the capability required for the given request method.
+ * @internal
+ */
assertCapabilityForMethod(method: Request["method"]): void {
// TODO
}
+
+ /**
+ * Verify that a request handler is registered and supported for the given method.
+ * @internal
+ */
assertRequestHandlerCapability(method: Request["method"]): void {
// TODO
}
+
+ /**
+ * Verify that the host supports the capability required for the given notification method.
+ * @internal
+ */
assertNotificationCapability(method: Notification["method"]): void {
// TODO
}
+ /**
+ * Get the host capabilities passed to the constructor.
+ *
+ * @returns Host capabilities object
+ *
+ * @see {@link McpUiHostCapabilities} for the capabilities structure
+ */
getCapabilities(): McpUiHostCapabilities {
return this._capabilities;
}
+ /**
+ * Handle the ui/initialize request from the guest.
+ * @internal
+ */
private async _oninitialize(
request: McpUiInitializeRequest,
): Promise {
@@ -194,12 +556,59 @@ export class AppBridge extends Protocol {
};
}
+ /**
+ * Send complete tool arguments to the Guest UI.
+ *
+ * The host MUST send this notification after the Guest UI completes initialization
+ * (after {@link oninitialized} callback fires) and complete tool arguments become available.
+ * This notification is sent exactly once and is required before {@link sendToolResult}.
+ *
+ * @param params - Complete tool call arguments
+ *
+ * @example
+ * ```typescript
+ * bridge.oninitialized = () => {
+ * bridge.sendToolInput({
+ * arguments: { location: "New York", units: "metric" }
+ * });
+ * };
+ * ```
+ *
+ * @see {@link McpUiToolInputNotification} for the notification type
+ * @see {@link oninitialized} for the initialization callback
+ * @see {@link sendToolResult} for sending results after execution
+ */
sendToolInput(params: McpUiToolInputNotification["params"]) {
return this.notification({
method: "ui/notifications/tool-input",
params,
});
}
+
+ /**
+ * Send tool execution result to the Guest UI.
+ *
+ * The host MUST send this notification when tool execution completes successfully,
+ * provided the UI is still displayed. If the UI was closed before execution
+ * completes, the host MAY skip this notification. This must be sent after
+ * {@link sendToolInput}.
+ *
+ * @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);
+ * ```
+ *
+ * @see {@link McpUiToolResultNotification} for the notification type
+ * @see {@link sendToolInput} for sending tool arguments before results
+ */
sendToolResult(params: McpUiToolResultNotification["params"]) {
return this.notification({
method: "ui/notifications/tool-result",
@@ -207,6 +616,21 @@ export class AppBridge extends Protocol {
});
}
+ /**
+ * Send HTML resource to the sandbox proxy for secure loading.
+ *
+ * This is an internal method used by web-based hosts implementing the
+ * double-iframe sandbox architecture. After the sandbox proxy signals readiness
+ * via `ui/notifications/sandbox-proxy-ready`, the host sends this notification
+ * with the HTML content to load.
+ *
+ * @param params - HTML content and sandbox configuration:
+ * - `html`: The HTML content to load into the sandboxed iframe
+ * - `sandbox`: Optional sandbox attribute value (e.g., "allow-scripts")
+ *
+ * @internal
+ * @see {@link onsandboxready} for handling the sandbox proxy ready notification
+ */
sendSandboxResourceReady(
params: McpUiSandboxResourceReadyNotification["params"],
) {
@@ -216,6 +640,30 @@ export class AppBridge extends Protocol {
});
}
+ /**
+ * Request graceful shutdown of the Guest UI.
+ *
+ * The host MUST send this request before tearing down the UI resource (before
+ * unmounting the iframe). This gives the Guest UI an opportunity to save state,
+ * cancel pending operations, or show confirmation dialogs.
+ *
+ * The host SHOULD wait for the response before unmounting to prevent data loss.
+ *
+ * @param params - Empty params object
+ * @param options - Request options (timeout, etc.)
+ * @returns Promise resolving when Guest UI confirms readiness for teardown
+ *
+ * @example
+ * ```typescript
+ * try {
+ * await bridge.sendResourceTeardown({});
+ * // Guest UI is ready, safe to unmount iframe
+ * iframe.remove();
+ * } catch (error) {
+ * console.error("Teardown failed:", error);
+ * }
+ * ```
+ */
sendResourceTeardown(
params: McpUiResourceTeardownRequest["params"],
options?: RequestOptions,
@@ -253,6 +701,44 @@ export class AppBridge extends Protocol {
await this._client.notification(notification);
});
}
+
+ /**
+ * Connect to the Guest UI via transport and set up message forwarding.
+ *
+ * This method establishes the transport connection and automatically sets up
+ * request/notification forwarding based on the MCP server's capabilities.
+ * It proxies the following server capabilities to the Guest UI:
+ * - Tools (tools/call, tools/list_changed)
+ * - Resources (resources/list, resources/read, resources/templates/list, resources/list_changed)
+ * - Prompts (prompts/list, prompts/list_changed)
+ *
+ * After calling connect, wait for the `oninitialized` callback before sending
+ * tool input and other data to the Guest UI.
+ *
+ * @param transport - Transport layer (typically PostMessageTransport)
+ * @returns Promise resolving when connection is established
+ *
+ * @throws {Error} If server capabilities are not available. This occurs when
+ * connect() is called before the MCP client has completed its initialization
+ * with the server. Ensure `await client.connect()` completes before calling
+ * `bridge.connect()`.
+ *
+ * @example
+ * ```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);
+ * ```
+ */
async connect(transport: Transport) {
// Forward core available MCP features
const serverCapabilities = this._client.getServerCapabilities();
diff --git a/src/app.ts b/src/app.ts
index a57bc590..137c31fe 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -44,9 +44,55 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
export { PostMessageTransport } from "./message-transport.js";
export * from "./types";
+/**
+ * Metadata key for associating a resource URI with a tool call.
+ *
+ * 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 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.
+ *
+ * @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"
+ * }
+ * };
+ * ```
+ *
+ * @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
+ * }
+ * ```
+ */
export const RESOURCE_URI_META_KEY = "ui/resourceUri";
+/**
+ * Options for configuring App behavior.
+ *
+ * Extends ProtocolOptions from the MCP SDK with App-specific configuration.
+ *
+ * @see ProtocolOptions from @modelcontextprotocol/sdk for inherited options
+ */
type AppOptions = ProtocolOptions & {
+ /**
+ * Automatically report size changes to the host using ResizeObserver.
+ *
+ * When enabled, the App monitors `document.body` and `document.documentElement`
+ * for size changes and automatically sends `ui/notifications/size-change`
+ * notifications to the host.
+ *
+ * @default true
+ */
autoResize?: boolean;
};
@@ -54,10 +100,104 @@ type RequestHandlerExtra = Parameters<
Parameters[1]
>[1];
+/**
+ * Main class for MCP Apps to communicate with their host.
+ *
+ * The App class provides a framework-agnostic way to build interactive MCP Apps
+ * that run inside host applications. It extends the MCP SDK's Protocol class and
+ * handles the connection lifecycle, initialization handshake, and bidirectional
+ * communication with the host.
+ *
+ * ## Architecture
+ *
+ * Guest UIs (Apps) act as MCP clients connecting to the host via {@link PostMessageTransport}.
+ * The host proxies requests to the actual MCP server and forwards
+ * responses back to the App.
+ *
+ * ## Lifecycle
+ *
+ * 1. **Create**: Instantiate App with info and capabilities
+ * 2. **Connect**: Call `connect()` to establish transport and perform handshake
+ * 3. **Interactive**: Send requests, receive notifications, call tools
+ * 4. **Cleanup**: Host sends teardown request before unmounting
+ *
+ * ## Inherited Methods
+ *
+ * As a subclass of Protocol, App inherits key methods for handling communication:
+ * - `setRequestHandler()` - Register handlers for requests from host
+ * - `setNotificationHandler()` - Register handlers for notifications from host
+ *
+ * @see Protocol from @modelcontextprotocol/sdk for all inherited methods
+ *
+ * ## Notification Setters
+ *
+ * For common notifications, the App class provides convenient setter properties
+ * that simplify handler registration:
+ * - `ontoolinput` - Complete tool arguments from host
+ * - `ontoolinputpartial` - Streaming partial tool arguments
+ * - `ontoolresult` - Tool execution results
+ * - `onhostcontextchanged` - Host context changes (theme, viewport, etc.)
+ *
+ * These setters are convenience wrappers around `setNotificationHandler()`.
+ * 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));
+ * ```
+ *
+ * @example Sending a message to the host's chat
+ * ```typescript
+ * await app.sendMessage({
+ * role: "user",
+ * content: [{ type: "text", text: "Weather updated!" }]
+ * });
+ * ```
+ */
export class App extends Protocol {
private _hostCapabilities?: McpUiHostCapabilities;
private _hostInfo?: Implementation;
+ /**
+ * Create a new MCP App instance.
+ *
+ * @param _appInfo - App identification (name and version)
+ * @param _capabilities - Features and capabilities this app provides
+ * @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
+ * );
+ * ```
+ */
constructor(
private _appInfo: Implementation,
private _capabilities: McpUiAppCapabilities = {},
@@ -71,14 +211,98 @@ export class App extends Protocol {
});
}
+ /**
+ * Get the host's capabilities discovered during initialization.
+ *
+ * Returns the capabilities that the host advertised during the
+ * {@link connect} handshake. Returns `undefined` if called before
+ * connection is established.
+ *
+ * @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");
+ * }
+ * ```
+ *
+ * @see {@link connect} for the initialization handshake
+ * @see {@link McpUiHostCapabilities} for the capabilities structure
+ */
getHostCapabilities(): McpUiHostCapabilities | undefined {
return this._hostCapabilities;
}
+ /**
+ * Get the host's implementation info discovered during initialization.
+ *
+ * Returns the host's name and version as advertised during the
+ * {@link connect} handshake. Returns `undefined` if called before
+ * connection is established.
+ *
+ * @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}`);
+ * ```
+ *
+ * @see {@link connect} for the initialization handshake
+ */
getHostVersion(): Implementation | undefined {
return this._hostInfo;
}
+ /**
+ * Convenience handler for receiving complete tool input from the host.
+ *
+ * Set this property to register a handler that will be called when the host
+ * sends a tool's complete arguments. This is sent after a tool call begins
+ * and before the tool result is available.
+ *
+ * This setter is a convenience wrapper around `setNotificationHandler()` that
+ * automatically handles the notification schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing notifications.
+ *
+ * @param callback - Function called with the tool input 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);
+ * }
+ * );
+ * ```
+ *
+ * @see {@link setNotificationHandler} for the underlying method
+ * @see {@link McpUiToolInputNotification} for the notification structure
+ */
set ontoolinput(
callback: (params: McpUiToolInputNotification["params"]) => void,
) {
@@ -86,6 +310,33 @@ export class App extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Convenience handler for receiving streaming partial tool input from the host.
+ *
+ * Set this property to register a handler that will be called as the host
+ * streams partial tool arguments during tool call initialization. This enables
+ * progressive rendering of tool arguments before they're complete.
+ *
+ * This setter is a convenience wrapper around `setNotificationHandler()` that
+ * automatically handles the notification schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing notifications.
+ *
+ * @param callback - Function called with each partial tool input update
+ *
+ * @example Progressive rendering of tool arguments
+ * ```typescript
+ * app.ontoolinputpartial = (params) => {
+ * console.log("Partial args:", params.arguments);
+ * // Update your UI progressively as arguments stream in
+ * };
+ * ```
+ *
+ * @see {@link setNotificationHandler} for the underlying method
+ * @see {@link McpUiToolInputPartialNotification} for the notification structure
+ * @see {@link ontoolinput} for the complete tool input handler
+ */
set ontoolinputpartial(
callback: (params: McpUiToolInputPartialNotification["params"]) => void,
) {
@@ -93,6 +344,37 @@ export class App extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Convenience handler for receiving tool execution results from the host.
+ *
+ * Set this property to register a handler that will be called when the host
+ * sends the result of a tool execution. This is sent after the tool completes
+ * on the MCP server, allowing your app to display the results or update its state.
+ *
+ * This setter is a convenience wrapper around `setNotificationHandler()` that
+ * automatically handles the notification schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing notifications.
+ *
+ * @param callback - Function called with the tool result
+ *
+ * @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");
+ * }
+ * };
+ * ```
+ *
+ * @see {@link setNotificationHandler} for the underlying method
+ * @see {@link McpUiToolResultNotification} for the notification structure
+ * @see {@link ontoolinput} for the initial tool input handler
+ */
set ontoolresult(
callback: (params: McpUiToolResultNotification["params"]) => void,
) {
@@ -100,6 +382,37 @@ export class App extends Protocol {
callback(n.params),
);
}
+
+ /**
+ * Convenience handler for host context changes (theme, viewport, locale, etc.).
+ *
+ * Set this property to register a handler that will be called when the host's
+ * context changes, such as theme switching (light/dark), viewport size changes,
+ * locale changes, or other environmental updates. Apps should respond by
+ * updating their UI accordingly.
+ *
+ * This setter is a convenience wrapper around `setNotificationHandler()` that
+ * automatically handles the notification schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing notifications.
+ *
+ * @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");
+ * }
+ * };
+ * ```
+ *
+ * @see {@link setNotificationHandler} for the underlying method
+ * @see {@link McpUiHostContextChangedNotification} for the notification structure
+ * @see {@link McpUiHostContext} for the full context structure
+ */
set onhostcontextchanged(
callback: (params: McpUiHostContextChangedNotification["params"]) => void,
) {
@@ -108,6 +421,38 @@ export class App extends Protocol {
(n) => callback(n.params),
);
}
+
+ /**
+ * Convenience handler for tool call requests from the host.
+ *
+ * Set this property to register a handler that will be called when the host
+ * requests this app to execute a tool. This enables apps to provide their own
+ * tools that can be called by the host or LLM.
+ *
+ * The app must declare tool capabilities in the constructor to use this handler.
+ *
+ * This setter is a convenience wrapper around `setRequestHandler()` that
+ * automatically handles the request schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing requests.
+ *
+ * @param callback - Async function that executes the tool and returns the result.
+ * The callback will only be invoked if the app declared tool capabilities
+ * 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}`);
+ * };
+ * ```
+ *
+ * @see {@link setRequestHandler} for the underlying method
+ */
set oncalltool(
callback: (
params: CallToolRequest["params"],
@@ -118,6 +463,37 @@ export class App extends Protocol {
callback(request.params, extra),
);
}
+
+ /**
+ * Convenience handler for listing available tools.
+ *
+ * Set this property to register a handler that will be called when the host
+ * requests a list of tools this app provides. This enables dynamic tool
+ * discovery by the host or LLM.
+ *
+ * The app must declare tool capabilities in the constructor to use this handler.
+ *
+ * This setter is a convenience wrapper around `setRequestHandler()` that
+ * automatically handles the request schema and extracts the params for you.
+ *
+ * Register handlers before calling {@link connect} to avoid missing requests.
+ *
+ * @param callback - Async function that returns the list of available tools.
+ * The callback will only be invoked if the app declared tool capabilities
+ * in the constructor.
+ *
+ * @example Return available tools
+ * ```typescript
+ * app.onlisttools = async (params, extra) => {
+ * return {
+ * tools: ["calculate", "convert", "format"]
+ * };
+ * };
+ * ```
+ *
+ * @see {@link setRequestHandler} for the underlying method
+ * @see {@link oncalltool} for handling tool execution
+ */
set onlisttools(
callback: (
params: ListToolsRequest["params"],
@@ -129,9 +505,18 @@ export class App extends Protocol {
);
}
+ /**
+ * Verify that the host supports the capability required for the given request method.
+ * @internal
+ */
assertCapabilityForMethod(method: Request["method"]): void {
// TODO
}
+
+ /**
+ * Verify that the app declared the capability required for the given request method.
+ * @internal
+ */
assertRequestHandlerCapability(method: Request["method"]): void {
switch (method) {
case "tools/call":
@@ -148,10 +533,50 @@ export class App extends Protocol {
throw new Error(`No handler for method ${method} registered`);
}
}
+
+ /**
+ * Verify that the app supports the capability required for the given notification method.
+ * @internal
+ */
assertNotificationCapability(method: Notification["method"]): void {
// TODO
}
+ /**
+ * Call a tool on the originating MCP server (proxied through the host).
+ *
+ * Apps can call tools to fetch fresh data or trigger server-side actions.
+ * The host proxies the request to the actual MCP server and returns the result.
+ *
+ * @param params - Tool name and arguments
+ * @param options - Request options (timeout, etc.)
+ * @returns Tool execution result
+ *
+ * @throws {Error} If the tool does not exist on the server
+ * @throws {Error} If the request times out or the connection is lost
+ * @throws {Error} If the host rejects the request
+ *
+ * Note: Tool-level execution errors are returned in the result with `isError: true`
+ * rather than throwing exceptions. Always check `result.isError` to distinguish
+ * 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);
+ * }
+ * ```
+ */
async callServerTool(
params: CallToolRequest["params"],
options?: RequestOptions,
@@ -163,6 +588,33 @@ export class App extends Protocol {
);
}
+ /**
+ * Send a message to the host's chat interface.
+ *
+ * Enables the app to add messages to the conversation thread. Useful for
+ * user-initiated messages or app-to-conversation communication.
+ *
+ * @param params - Message role and content
+ * @param options - Request options (timeout, etc.)
+ * @returns Result indicating success or error (no message content returned)
+ *
+ * @throws {Error} If the host rejects the message
+ *
+ * @example Send a text message from user interaction
+ * ```typescript
+ * try {
+ * await app.sendMessage({
+ * role: "user",
+ * content: [{ type: "text", text: "Show me details for item #42" }]
+ * });
+ * } catch (error) {
+ * console.error("Failed to send message:", error);
+ * // Handle error appropriately for your app
+ * }
+ * ```
+ *
+ * @see {@link McpUiMessageRequest} for request structure
+ */
sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) {
return this.request(
{
@@ -174,6 +626,25 @@ export class App extends Protocol {
);
}
+ /**
+ * Send log messages to the host for debugging and telemetry.
+ *
+ * Logs are not added to the conversation but may be recorded by the host
+ * for debugging purposes.
+ *
+ * @param params - Log level and message
+ *
+ * @example Log app state for debugging
+ * ```typescript
+ * app.sendLog({
+ * level: "info",
+ * data: "Weather data refreshed",
+ * logger: "WeatherApp"
+ * });
+ * ```
+ *
+ * @returns Promise that resolves when the log notification is sent
+ */
sendLog(params: LoggingMessageNotification["params"]) {
return this.notification({
method: "notifications/message",
@@ -181,6 +652,31 @@ 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.
+ *
+ * @param params - URL to open
+ * @param options - Request options (timeout, etc.)
+ * @returns Result indicating success or error
+ *
+ * @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.sendOpenLink({ url: "https://docs.example.com" });
+ * } catch (error) {
+ * console.error("Failed to open link:", error);
+ * // Optionally show fallback: display URL for manual copy
+ * }
+ * ```
+ *
+ * @see {@link McpUiOpenLinkRequest} for request structure
+ */
sendOpenLink(
params: McpUiOpenLinkRequest["params"],
options?: RequestOptions,
@@ -195,6 +691,26 @@ export class App extends Protocol {
);
}
+ /**
+ * Notify the host of UI size changes.
+ *
+ * Apps can manually report size changes to help the host adjust the container.
+ * If `autoResize` is enabled (default), this is called automatically.
+ *
+ * @param params - New width and height in pixels
+ *
+ * @example Manually notify host of size change
+ * ```typescript
+ * app.sendSizeChange({
+ * width: 400,
+ * height: 600
+ * });
+ * ```
+ *
+ * @returns Promise that resolves when the notification is sent
+ *
+ * @see {@link McpUiSizeChangeNotification} for notification structure
+ */
sendSizeChange(params: McpUiSizeChangeNotification["params"]) {
return this.notification({
method: "ui/notifications/size-change",
@@ -202,6 +718,31 @@ export class App extends Protocol {
});
}
+ /**
+ * Set up automatic size change notifications using ResizeObserver.
+ *
+ * Observes both `document.documentElement` and `document.body` for size changes
+ * and automatically sends `ui/notifications/size-change` notifications to the host.
+ * The notifications are debounced using requestAnimationFrame to avoid duplicates.
+ *
+ * Note: This method is automatically called by `connect()` if the `autoResize`
+ * option is true (default). You typically don't need to call this manually unless
+ * you disabled autoResize and want to enable it later.
+ *
+ * @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.setupSizeChangeNotifications();
+ *
+ * // Clean up when done
+ * cleanup();
+ * ```
+ */
setupSizeChangeNotifications() {
let scheduled = false;
const sendBodySizeChange = () => {
@@ -230,6 +771,43 @@ export class App extends Protocol {
return () => resizeObserver.disconnect();
}
+ /**
+ * Establish connection with the host and perform initialization handshake.
+ *
+ * This method performs the following steps:
+ * 1. Connects the transport layer
+ * 2. Sends `ui/initialize` request with app info and capabilities
+ * 3. Receives host capabilities and context in response
+ * 4. Sends `ui/notifications/initialized` notification
+ * 5. Sets up auto-resize using {@link setupSizeChangeNotifications} if enabled (default)
+ *
+ * If initialization fails, the connection is automatically closed and an error
+ * is thrown.
+ *
+ * @param transport - Transport layer (typically PostMessageTransport)
+ * @param options - Request options for the initialize request
+ *
+ * @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));
+ * console.log("Connected successfully!");
+ * } catch (error) {
+ * console.error("Failed to connect:", error);
+ * }
+ * ```
+ *
+ * @see {@link McpUiInitializeRequest} for the initialization request structure
+ * @see {@link McpUiInitializedNotification} for the initialized notification
+ * @see {@link PostMessageTransport} for the typical transport implementation
+ */
override async connect(
transport: Transport,
options?: RequestOptions,
diff --git a/src/message-transport.ts b/src/message-transport.ts
index 4210d4da..b997d7c6 100644
--- a/src/message-transport.ts
+++ b/src/message-transport.ts
@@ -8,12 +8,69 @@ import {
TransportSendOptions,
} from "@modelcontextprotocol/sdk/shared/transport.js";
+/**
+ * JSON-RPC transport using window.postMessage for iframe↔parent communication.
+ *
+ * This transport enables bidirectional communication between MCP Apps running in
+ * iframes and their host applications using the browser's postMessage API. It
+ * implements the MCP SDK's Transport interface.
+ *
+ * ## Security
+ *
+ * The `eventSource` parameter provides origin validation by filtering messages
+ * from specific sources. Guest UIs typically don't need to specify this (they only
+ * communicate with their parent), but hosts should validate the iframe source for
+ * security.
+ *
+ * ## Usage
+ *
+ * **Guest UI (default)**:
+ * ```typescript
+ * const transport = new PostMessageTransport(window.parent);
+ * await app.connect(transport);
+ * ```
+ *
+ * **Host (with source validation)**:
+ * ```typescript
+ * const iframe = document.getElementById('app-iframe') as HTMLIFrameElement;
+ * const transport = new PostMessageTransport(
+ * iframe.contentWindow!,
+ * iframe.contentWindow // Validate messages from this iframe only
+ * );
+ * await bridge.connect(transport);
+ * ```
+ *
+ * @see {@link app.App.connect} for Guest UI usage
+ * @see {@link app-bridge.AppBridge.connect} for Host usage
+ */
export class PostMessageTransport implements Transport {
private messageListener: (
this: Window,
ev: WindowEventMap["message"],
) => any | undefined;
+ /**
+ * Create a new PostMessageTransport.
+ *
+ * @param eventTarget - Target window to send messages to (default: window.parent)
+ * @param eventSource - Optional source validation. If specified, only messages from
+ * this source will be accepted. Guest UIs typically don't need this (they only
+ * receive from parent), but hosts should validate the iframe source.
+ *
+ * @example Guest UI connecting to parent
+ * ```typescript
+ * const transport = new PostMessageTransport(window.parent);
+ * ```
+ *
+ * @example Host connecting to iframe with validation
+ * ```typescript
+ * const iframe = document.getElementById('app') as HTMLIFrameElement;
+ * const transport = new PostMessageTransport(
+ * iframe.contentWindow!,
+ * iframe.contentWindow // Only accept messages from this iframe
+ * );
+ * ```
+ */
constructor(
private eventTarget: Window = window.parent,
private eventSource?: MessageEventSource,
@@ -37,20 +94,84 @@ export class PostMessageTransport implements Transport {
}
};
}
+
+ /**
+ * Begin listening for messages from the event source.
+ *
+ * Registers a message event listener on the window. Must be called before
+ * messages can be received.
+ */
async start() {
window.addEventListener("message", this.messageListener);
}
+
+ /**
+ * Send a JSON-RPC message to the target window.
+ *
+ * Messages are sent using postMessage with "*" origin, meaning they are visible
+ * to all frames. The receiver should validate the message source for security.
+ *
+ * @param message - JSON-RPC message to send
+ * @param options - Optional send options (currently unused)
+ */
async send(message: JSONRPCMessage, options?: TransportSendOptions) {
console.info("[host] Sending message", message);
this.eventTarget.postMessage(message, "*");
}
+
+ /**
+ * Stop listening for messages and cleanup.
+ *
+ * Removes the message event listener and calls the {@link onclose} callback if set.
+ */
async close() {
window.removeEventListener("message", this.messageListener);
this.onclose?.();
}
+
+ /**
+ * Called when the transport is closed.
+ *
+ * Set this handler to be notified when {@link close} is called.
+ */
onclose?: () => void;
+
+ /**
+ * Called when a message parsing error occurs.
+ *
+ * This handler is invoked when a received message fails JSON-RPC schema
+ * validation. The error parameter contains details about the validation failure.
+ *
+ * @param error - Error describing the validation failure
+ */
onerror?: (error: Error) => void;
+
+ /**
+ * Called when a valid JSON-RPC message is received.
+ *
+ * This handler is invoked after message validation succeeds. The {@link start}
+ * method must be called before messages will be received.
+ *
+ * @param message - The validated JSON-RPC message
+ * @param extra - Optional metadata about the message (unused in this transport)
+ */
onmessage?: (message: JSONRPCMessage, extra?: MessageExtraInfo) => void;
+
+ /**
+ * Optional session identifier for this transport connection.
+ *
+ * Set by the MCP SDK to track the connection session. Not required for
+ * PostMessageTransport functionality.
+ */
sessionId?: string;
+
+ /**
+ * Callback to set the negotiated protocol version.
+ *
+ * The MCP SDK calls this during initialization to communicate the protocol
+ * version negotiated with the peer.
+ *
+ * @param version - The negotiated protocol version string
+ */
setProtocolVersion?: (version: string) => void;
}
diff --git a/src/react/index.tsx b/src/react/index.tsx
index 47a326f2..d5ce3760 100644
--- a/src/react/index.tsx
+++ b/src/react/index.tsx
@@ -1,2 +1,34 @@
+/**
+ * React utilities for building MCP Apps.
+ *
+ * This module provides React hooks and utilities for easily building
+ * interactive MCP Apps using React. This is optional - the core SDK
+ * ({@link App}, {@link PostMessageTransport}) is framework-agnostic and can be
+ * used with any UI framework or vanilla JavaScript.
+ *
+ * ## Main Exports
+ *
+ * - {@link useApp} - React hook to create and connect an MCP App
+ * - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed)
+ *
+ * @module @modelcontextprotocol/ext-apps/react
+ *
+ * @example Basic React App
+ * ```tsx
+ * import { useApp } from '@modelcontextprotocol/ext-apps/react';
+ *
+ * function MyApp() {
+ * const { app, isConnected, error } = useApp({
+ * appInfo: { name: "MyApp", version: "1.0.0" },
+ * capabilities: {}
+ * });
+ *
+ * if (error) return Error: {error.message}
;
+ * if (!isConnected) return Connecting...
;
+ *
+ * return Connected!
;
+ * }
+ * ```
+ */
export * from "./useApp";
export * from "./useAutoResize";
diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx
index 1e6cfc3a..ccfce3eb 100644
--- a/src/react/useApp.tsx
+++ b/src/react/useApp.tsx
@@ -4,22 +4,105 @@ import { Client } from "@modelcontextprotocol/sdk/client";
import { App, McpUiAppCapabilities, PostMessageTransport } from "../app";
export * from "../app";
+/**
+ * Options for configuring the useApp hook.
+ *
+ * Note: This interface does NOT expose App options like `autoResize`.
+ * The hook creates the App with default options (autoResize: true). If you need
+ * custom App options, create the App manually instead of using this hook.
+ *
+ * @see {@link useApp} for the hook that uses these options
+ * @see {@link useAutoResize} for manual auto-resize control with custom App options
+ */
export interface UseAppOptions {
+ /** App identification (name and version) */
appInfo: Implementation;
+ /** Features and capabilities this app provides */
capabilities: McpUiAppCapabilities;
/**
- * Called after client is created but before connection.
- * Use this to register handlers via app.ontoolinput, app.toolresult, etc.
+ * Called after App is created but before connection.
+ *
+ * Use this to register request/notification handlers that need to be in place
+ * before the initialization handshake completes.
+ *
+ * @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);
+ * }
+ * );
+ * }
+ * ```
*/
onAppCreated?: (app: App) => void;
}
+/**
+ * State returned by the useApp hook.
+ */
export interface AppState {
+ /** The connected App instance, null during initialization */
app: App | null;
+ /** Whether initialization completed successfully */
isConnected: boolean;
+ /** Connection error if initialization failed, null otherwise */
error: Error | null;
}
+/**
+ * React hook to create and connect an MCP App.
+ *
+ * This hook manages the complete lifecycle of an {@link App}: creation, connection,
+ * and cleanup. It automatically creates a {@link PostMessageTransport} to window.parent
+ * and handles initialization.
+ *
+ * **Important**: The hook intentionally does NOT re-run when options change
+ * to avoid reconnection loops. Options are only used during the initial mount.
+ *
+ * **Note**: This is part of the optional React integration. The core SDK
+ * (App, PostMessageTransport) is framework-agnostic and can be
+ * used with any UI framework or vanilla JavaScript.
+ *
+ * @param options - Configuration for the app
+ * @returns Current connection state and app instance. If connection fails during
+ * 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 Error: {error.message}
;
+ * if (!isConnected) return Connecting...
;
+ * return Connected!
;
+ * }
+ * ```
+ *
+ * @see {@link App.connect} for the underlying connection method
+ * @see {@link useAutoResize} for manual auto-resize control when using custom App options
+ */
export function useApp({
appInfo,
capabilities,
diff --git a/src/react/useAutoResize.ts b/src/react/useAutoResize.ts
index ce39c75a..44f32e82 100644
--- a/src/react/useAutoResize.ts
+++ b/src/react/useAutoResize.ts
@@ -2,9 +2,52 @@ import { useEffect, RefObject } from "react";
import { App } from "../app";
/**
- * Custom hook that automatically reports size changes to the parent window.
+ * React hook that automatically reports UI size changes to the host.
*
- * @param client - MCP UI client for sending size notifications
+ * Uses ResizeObserver to watch `document.body` and `document.documentElement` for
+ * size changes and sends `ui/notifications/size-change` notifications.
+ *
+ * **Note**: This hook is rarely needed since the {@link useApp} hook automatically enables
+ * auto-resize by default. This hook is provided for advanced cases where you
+ * create the {@link App} manually with `autoResize: false` and want to add auto-resize
+ * behavior later.
+ *
+ * @param app - The connected App instance, or null during initialization
+ * @param elementRef - Currently unused. The hook always observes `document.body`
+ * and `document.documentElement` regardless of this value. Passing a ref will
+ * cause unnecessary effect re-runs; omit this parameter.
+ *
+ * @example Manual App creation with custom auto-resize control
+ * ```tsx
+ * function MyComponent() {
+ * // For custom App options, create App manually instead of using useApp
+ * const [app, setApp] = useState(null);
+ * const [error, setError] = useState(null);
+ *
+ * useEffect(() => {
+ * const myApp = new App(
+ * { name: "MyApp", version: "1.0.0" },
+ * {}, // capabilities
+ * { autoResize: false } // Disable default auto-resize
+ * );
+ *
+ * const transport = new PostMessageTransport(window.parent);
+ * myApp.connect(transport)
+ * .then(() => setApp(myApp))
+ * .catch((err) => setError(err));
+ * }, []);
+ *
+ * // Add manual auto-resize control
+ * useAutoResize(app);
+ *
+ * if (error) return Connection failed: {error.message}
;
+ * return My content
;
+ * }
+ * ```
+ *
+ * @see {@link App.setupSizeChangeNotifications} for the underlying implementation
+ * @see {@link useApp} which enables auto-resize by default
+ * @see {@link App} constructor for configuring `autoResize` option
*/
export function useAutoResize(
app: App | null,
diff --git a/src/types.ts b/src/types.ts
index d58bfc5c..0ce8540d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1,29 +1,133 @@
import {
+ CallToolResult,
CallToolResultSchema,
+ ContentBlock,
ContentBlockSchema,
EmptyResultSchema,
+ Implementation,
ImplementationSchema,
+ RequestId,
RequestIdSchema,
RequestSchema,
+ Tool,
ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
-import { z } from "zod";
+import { z } from "zod/v4";
+/**
+ * Type-level assertion that validates a Zod schema produces the expected interface.
+ *
+ * This helper is used for request and notification schemas that cannot use
+ * `z.ZodType` type annotations. Adding `: z.ZodType` to
+ * schemas widens the type from the specific `ZodObject` to the generic `ZodType`,
+ * which breaks MCP SDK's `setRequestHandler()` and `setNotificationHandler()`
+ * methods that require the specific `ZodObject` type.
+ *
+ * By using this type-level assertion instead, we get compile-time validation that
+ * the schema matches the interface without affecting the runtime schema type.
+ *
+ * @internal
+ */
+type VerifySchemaMatches =
+ z.infer extends TInterface
+ ? TInterface extends z.infer
+ ? true
+ : never
+ : never;
+
+/**
+ * Current protocol version supported by this SDK.
+ *
+ * The SDK automatically handles version negotiation during initialization.
+ * Apps and hosts don't need to manage protocol versions manually.
+ */
export const LATEST_PROTOCOL_VERSION = "2025-11-21";
+/**
+ * Request to open an external URL in the host's default browser.
+ *
+ * Sent from the Guest UI to the Host when requesting to open an external link.
+ * The host may deny the request based on user preferences or security policy.
+ *
+ * @see {@link app.App.sendOpenLink} for the method that sends this request
+ */
+export interface McpUiOpenLinkRequest {
+ method: "ui/open-link";
+ params: {
+ /** URL to open in the host's browser */
+ url: string;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiOpenLinkRequest}.
+ * @internal
+ */
export const McpUiOpenLinkRequestSchema = RequestSchema.extend({
method: z.literal("ui/open-link"),
params: z.object({
url: z.string().url(),
}),
});
-export type McpUiOpenLinkRequest = z.infer;
-export const McpUiOpenLinkResultSchema = z.object({
- isError: z.boolean().optional(),
-});
-export type McpUiOpenLinkResult = z.infer;
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyOpenLinkRequest = VerifySchemaMatches<
+ typeof McpUiOpenLinkRequestSchema,
+ McpUiOpenLinkRequest
+>;
+
+/**
+ * Result from a {@link McpUiOpenLinkRequest}.
+ *
+ * The host returns this result after attempting to open the requested URL.
+ *
+ * @see {@link McpUiOpenLinkRequest}
+ */
+export interface McpUiOpenLinkResult {
+ /**
+ * True if the host failed to open the URL (e.g., due to security policy,
+ * user cancellation, or system error). False or undefined indicates success.
+ */
+ isError?: boolean;
+ /**
+ * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Note: The schema intentionally omits this to enforce strict validation.
+ */
+ [key: string]: unknown;
+}
+
+/**
+ * Runtime validation schema for {@link McpUiOpenLinkResult}.
+ * @internal
+ */
+export const McpUiOpenLinkResultSchema: z.ZodType =
+ z.object({
+ isError: z.boolean().optional(),
+ });
+/**
+ * Request to send a message to the host's chat interface.
+ *
+ * Sent from the Guest UI to the Host when the app wants to add a message to the
+ * conversation thread. This enables interactive apps to communicate with the user
+ * through the host's chat interface.
+ *
+ * @see {@link app.App.sendMessage} for the method that sends this request
+ */
+export interface McpUiMessageRequest {
+ method: "ui/message";
+ params: {
+ /** Message role, currently only "user" is supported */
+ role: "user";
+ /** Message content blocks (text, image, etc.) */
+ content: ContentBlock[];
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiMessageRequest}.
+ * @internal
+ */
export const McpUiMessageRequestSchema = RequestSchema.extend({
method: z.literal("ui/message"),
params: z.object({
@@ -31,38 +135,146 @@ export const McpUiMessageRequestSchema = RequestSchema.extend({
content: z.array(ContentBlockSchema),
}),
});
-export type McpUiMessageRequest = z.infer;
-export const McpUiMessageResultSchema = z.object({
- // Note: we don't return the result from follow up messages as they might leak info from the chat.
- // We do tell the caller if it errored, though.
- isError: z.boolean().optional(),
-});
-export type McpUiMessageResult = z.infer;
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyMessageRequest = VerifySchemaMatches<
+ typeof McpUiMessageRequestSchema,
+ McpUiMessageRequest
+>;
+
+/**
+ * Result from a {@link McpUiMessageRequest}.
+ *
+ * Note: The host does not return message content or follow-up results to prevent
+ * leaking information from the conversation. Only error status is provided.
+ *
+ * @see {@link McpUiMessageRequest}
+ */
+export interface McpUiMessageResult {
+ /**
+ * True if the host rejected or failed to deliver the message (e.g., due to
+ * rate limiting, content policy, or system error). False or undefined
+ * indicates the message was accepted.
+ */
+ isError?: boolean;
+ /**
+ * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Note: The schema intentionally omits this to enforce strict validation.
+ */
+ [key: string]: unknown;
+}
+
+/**
+ * Runtime validation schema for {@link McpUiMessageResult}.
+ * @internal
+ */
+export const McpUiMessageResultSchema: z.ZodType = z.object(
+ {
+ isError: z.boolean().optional(),
+ },
+);
// McpUiIframeReadyNotification removed - replaced by standard MCP initialization
// The SDK's oninitialized callback now handles the ready signal
+/**
+ * Notification that the sandbox proxy iframe is ready to receive content.
+ *
+ * This is an internal message used by web-based hosts implementing the
+ * double-iframe sandbox architecture. The sandbox proxy sends this to the host
+ * after it loads and is ready to receive HTML content via
+ * {@link McpUiSandboxResourceReadyNotification}.
+ *
+ * @internal
+ * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
+ */
+export interface McpUiSandboxProxyReadyNotification {
+ method: "ui/notifications/sandbox-proxy-ready";
+ params: {};
+}
+
+/**
+ * Runtime validation schema for {@link McpUiSandboxProxyReadyNotification}.
+ * @internal
+ */
export const McpUiSandboxProxyReadyNotificationSchema = z.object({
method: z.literal("ui/notifications/sandbox-proxy-ready"),
params: z.object({}),
});
-export type McpUiSandboxProxyReadyNotification = z.infer<
- typeof McpUiSandboxProxyReadyNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifySandboxProxyReadyNotification = VerifySchemaMatches<
+ typeof McpUiSandboxProxyReadyNotificationSchema,
+ McpUiSandboxProxyReadyNotification
>;
+/**
+ * Notification containing HTML resource for the sandbox proxy to load.
+ *
+ * This is an internal message used by web-based hosts implementing the
+ * double-iframe sandbox architecture. After the sandbox proxy signals readiness,
+ * the host sends this notification with the HTML content and optional sandbox
+ * attributes to load into the inner iframe.
+ *
+ * @internal
+ * @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
+ */
+export interface McpUiSandboxResourceReadyNotification {
+ method: "ui/notifications/sandbox-resource-ready";
+ params: {
+ /** HTML content to load into the inner iframe */
+ html: string;
+ /** Optional override for the inner iframe's sandbox attribute */
+ sandbox?: string;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiSandboxResourceReadyNotification}.
+ * @internal
+ */
export const McpUiSandboxResourceReadyNotificationSchema = z.object({
method: z.literal("ui/notifications/sandbox-resource-ready"),
params: z.object({
- html: z.string(), //ReadResourceResultSchema,
+ html: z.string(),
sandbox: z.string().optional(),
}),
});
-export type McpUiSandboxResourceReadyNotification = z.infer<
- typeof McpUiSandboxResourceReadyNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifySandboxResourceReadyNotification = VerifySchemaMatches<
+ typeof McpUiSandboxResourceReadyNotificationSchema,
+ McpUiSandboxResourceReadyNotification
>;
-// Fired by the iframe when its body changes, and by the host when the viewport size changes.
+/**
+ * Notification of UI size changes (bidirectional: Guest ↔ Host).
+ *
+ * **Guest UI → Host**: Sent by the Guest UI when its rendered content size changes,
+ * typically using ResizeObserver. This helps the host adjust the iframe container.
+ * If {@link app.App} is configured with `autoResize: true` (default), this is sent
+ * automatically.
+ *
+ * **Host → Guest UI**: Sent by the Host when the viewport size changes (e.g.,
+ * window resize, orientation change). This allows the Guest UI to adjust its layout.
+ *
+ * @see {@link app.App.sendSizeChange} for the method to send this from Guest UI
+ * @see {@link app.App.setupSizeChangeNotifications} for automatic size reporting
+ */
+export interface McpUiSizeChangeNotification {
+ method: "ui/notifications/size-change";
+ params: {
+ /** New width in pixels */
+ width?: number;
+ /** New height in pixels */
+ height?: number;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiSizeChangeNotification}.
+ * @internal
+ */
export const McpUiSizeChangeNotificationSchema = z.object({
method: z.literal("ui/notifications/size-change"),
params: z.object({
@@ -70,45 +282,223 @@ export const McpUiSizeChangeNotificationSchema = z.object({
height: z.number().optional(),
}),
});
-export type McpUiSizeChangeNotification = z.infer<
- typeof McpUiSizeChangeNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifySizeChangeNotification = VerifySchemaMatches<
+ typeof McpUiSizeChangeNotificationSchema,
+ McpUiSizeChangeNotification
>;
+/**
+ * Notification containing complete tool arguments (Host → Guest UI).
+ *
+ * The host MUST send this notification after the Guest UI's initialize request
+ * completes, when complete tool arguments become available. This notification is
+ * sent exactly once and is required before {@link McpUiToolResultNotification}.
+ *
+ * The arguments object contains the complete tool call parameters that triggered
+ * this App instance.
+ */
+export interface McpUiToolInputNotification {
+ method: "ui/notifications/tool-input";
+ params: {
+ /** Complete tool call arguments as key-value pairs */
+ arguments?: Record;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiToolInputNotification}.
+ * @internal
+ */
export const McpUiToolInputNotificationSchema = z.object({
method: z.literal("ui/notifications/tool-input"),
params: z.object({
- arguments: z.record(z.unknown()).optional(),
+ arguments: z.record(z.string(), z.unknown()).optional(),
}),
});
-export type McpUiToolInputNotification = z.infer<
- typeof McpUiToolInputNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyToolInputNotification = VerifySchemaMatches<
+ typeof McpUiToolInputNotificationSchema,
+ McpUiToolInputNotification
>;
+/**
+ * Notification containing partial/streaming tool arguments (Host → Guest UI).
+ *
+ * The host MAY send this notification zero or more times while the agent is
+ * streaming tool arguments, before {@link McpUiToolInputNotification} is sent
+ * with complete arguments.
+ *
+ * The arguments object represents best-effort recovery of incomplete JSON, with
+ * unclosed structures automatically closed to produce valid JSON. Guest UIs may
+ * ignore these notifications or use them to render progressive loading states.
+ *
+ * Guest UIs MUST NOT rely on partial arguments for critical operations and SHOULD
+ * gracefully handle missing or changing fields between notifications.
+ */
+export interface McpUiToolInputPartialNotification {
+ method: "ui/notifications/tool-input-partial";
+ params: {
+ /** Partial tool call arguments (incomplete, may change) */
+ arguments?: Record;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiToolInputPartialNotification}.
+ * @internal
+ */
export const McpUiToolInputPartialNotificationSchema = z.object({
method: z.literal("ui/notifications/tool-input-partial"),
params: z.object({
- arguments: z.record(z.unknown()).optional(),
+ arguments: z.record(z.string(), z.unknown()).optional(),
}),
});
-export type McpUiToolInputPartialNotification = z.infer<
- typeof McpUiToolInputPartialNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyToolInputPartialNotification = VerifySchemaMatches<
+ typeof McpUiToolInputPartialNotificationSchema,
+ McpUiToolInputPartialNotification
>;
-// Fired once both tool call returned *AND* host received ui/ui-lifecycle-iframe-ready.
+/**
+ * Notification containing tool execution result (Host → Guest UI).
+ *
+ * The host MUST send this notification when tool execution completes successfully,
+ * provided the UI is still displayed. If the UI was closed before execution
+ * completes, the host MAY skip this notification. This notification is sent after
+ * {@link McpUiToolInputNotification}.
+ *
+ * The result follows the standard MCP CallToolResult format, containing content
+ * for the model and optionally structuredContent optimized for UI rendering.
+ */
+export interface McpUiToolResultNotification {
+ method: "ui/notifications/tool-result";
+ /** Standard MCP tool execution result */
+ params: CallToolResult;
+}
+
+/**
+ * Runtime validation schema for {@link McpUiToolResultNotification}.
+ * @internal
+ */
export const McpUiToolResultNotificationSchema = z.object({
method: z.literal("ui/notifications/tool-result"),
params: CallToolResultSchema,
});
-export type McpUiToolResultNotification = z.infer<
- typeof McpUiToolResultNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyToolResultNotification = VerifySchemaMatches<
+ typeof McpUiToolResultNotificationSchema,
+ McpUiToolResultNotification
>;
-export const McpUiHostContextSchema = z.object({
+/**
+ * Rich context about the host environment provided to Guest UIs.
+ *
+ * Hosts provide this context in the {@link McpUiInitializeResult} response and send
+ * updates via {@link McpUiHostContextChangedNotification} when values change.
+ * All fields are optional and Guest UIs should handle missing fields gracefully.
+ *
+ * @example
+ * ```typescript
+ * // Received during initialization
+ * const result = await app.connect(transport);
+ * const context = result.hostContext;
+ *
+ * if (context.theme === "dark") {
+ * document.body.classList.add("dark-mode");
+ * }
+ * ```
+ */
+export interface McpUiHostContext {
+ /** Metadata of the tool call that instantiated this App */
+ toolInfo?: {
+ /** JSON-RPC id of the tools/call request */
+ id: RequestId;
+ /** Tool definition including name, inputSchema, etc. */
+ tool: Tool;
+ };
+ /**
+ * Current color theme preference.
+ * @example "dark"
+ */
+ theme?: "light" | "dark" | "system";
+ /**
+ * How the UI is currently displayed.
+ * @example "inline"
+ */
+ displayMode?: "inline" | "fullscreen" | "pip" | "carousel";
+ /**
+ * Display modes the host supports.
+ * Apps can use this to offer mode-switching UI if applicable.
+ */
+ availableDisplayModes?: string[];
+ /** Current and maximum dimensions available to the UI */
+ viewport?: {
+ /** Current viewport width in pixels */
+ width: number;
+ /** Current viewport height in pixels */
+ height: number;
+ /** Maximum available height in pixels (if constrained) */
+ maxHeight?: number;
+ /** Maximum available width in pixels (if constrained) */
+ maxWidth?: number;
+ };
+ /**
+ * User's language and region preference in BCP 47 format.
+ * @example "en-US", "fr-CA", "ja-JP"
+ */
+ locale?: string;
+ /**
+ * User's timezone in IANA format.
+ * @example "America/New_York", "Europe/London", "Asia/Tokyo"
+ */
+ timeZone?: string;
+ /**
+ * Host application identifier.
+ * @example "claude-desktop/1.0.0"
+ */
+ userAgent?: string;
+ /**
+ * Platform type for responsive design decisions.
+ * @example "desktop"
+ */
+ platform?: "web" | "desktop" | "mobile";
+ /** Device input capabilities */
+ deviceCapabilities?: {
+ /** Whether the device supports touch input */
+ touch?: boolean;
+ /** Whether the device supports hover interactions */
+ hover?: boolean;
+ };
+ /**
+ * Mobile safe area boundaries in pixels.
+ * Used to avoid notches, rounded corners, and system UI on mobile devices.
+ */
+ safeAreaInsets?: {
+ /** Top safe area inset in pixels */
+ top: number;
+ /** Right safe area inset in pixels */
+ right: number;
+ /** Bottom safe area inset in pixels */
+ bottom: number;
+ /** Left safe area inset in pixels */
+ left: number;
+ };
+}
+
+/**
+ * Runtime validation schema for {@link McpUiHostContext}.
+ * @internal
+ */
+export const McpUiHostContextSchema: z.ZodType = z.object({
toolInfo: z
.object({
- // Metadata of the tool call that instantiated the App
- id: RequestIdSchema, // JSON-RPC id of the tools/call request
- tool: ToolSchema, // contains name, inputSchema, etc…
+ id: RequestIdSchema,
+ tool: ToolSchema,
})
.optional(),
theme: z.enum(["light", "dark", "system"]).optional(),
@@ -122,8 +512,8 @@ export const McpUiHostContextSchema = z.object({
maxWidth: z.number().optional(),
})
.optional(),
- locale: z.string().optional(), // BCP 47, e.g., "en-US"
- timeZone: z.string().optional(), // IANA, e.g., "America/New_York"
+ locale: z.string().optional(),
+ timeZone: z.string().optional(),
userAgent: z.string().optional(),
platform: z.enum(["web", "desktop", "mobile"]).optional(),
deviceCapabilities: z
@@ -141,64 +531,219 @@ export const McpUiHostContextSchema = z.object({
})
.optional(),
});
-export type McpUiHostContext = z.infer;
+/**
+ * Notification that host context has changed (Host → Guest UI).
+ *
+ * The host MAY send this notification when any context field changes, such as:
+ * - Theme toggled (light/dark)
+ * - Display mode changed (inline/fullscreen)
+ * - Device orientation changed
+ * - Window/panel resized
+ *
+ * This notification contains partial updates. Guest UIs SHOULD merge received
+ * fields with their current context state rather than replacing it entirely.
+ *
+ * @see {@link McpUiHostContext} for the full context structure
+ */
+export interface McpUiHostContextChangedNotification {
+ method: "ui/notifications/host-context-changed";
+ /** Partial context update containing only changed fields */
+ params: McpUiHostContext;
+}
+
+/**
+ * Runtime validation schema for {@link McpUiHostContextChangedNotification}.
+ * @internal
+ */
export const McpUiHostContextChangedNotificationSchema = z.object({
method: z.literal("ui/notifications/host-context-changed"),
params: McpUiHostContextSchema,
});
-export type McpUiHostContextChangedNotification = z.infer<
- typeof McpUiHostContextChangedNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyHostContextChangedNotification = VerifySchemaMatches<
+ typeof McpUiHostContextChangedNotificationSchema,
+ McpUiHostContextChangedNotification
>;
+/**
+ * Request for graceful shutdown of the Guest UI (Host → Guest UI).
+ *
+ * The host MUST send this request before tearing down the UI resource, for any
+ * reason including user action, resource reallocation, or app closure. This gives
+ * the Guest UI an opportunity to save state, cancel pending operations, or show
+ * confirmation dialogs.
+ *
+ * The host SHOULD wait for the response before unmounting the iframe to prevent
+ * data loss.
+ *
+ * @see {@link app-bridge.AppBridge.sendResourceTeardown} for the host method that sends this
+ */
+export interface McpUiResourceTeardownRequest {
+ method: "ui/resource-teardown";
+ params: {};
+}
+
+/**
+ * Runtime validation schema for {@link McpUiResourceTeardownRequest}.
+ * @internal
+ */
export const McpUiResourceTeardownRequestSchema = RequestSchema.extend({
method: z.literal("ui/resource-teardown"),
params: z.object({}),
});
-export type McpUiResourceTeardownRequest = z.infer<
- typeof McpUiResourceTeardownRequestSchema
->;
-export const McpUiResourceTeardownResultSchema = EmptyResultSchema;
-export type McpUiResourceTeardownResult = z.infer<
- typeof McpUiResourceTeardownResultSchema
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyResourceTeardownRequest = VerifySchemaMatches<
+ typeof McpUiResourceTeardownRequestSchema,
+ McpUiResourceTeardownRequest
>;
-export const McpUiHostCapabilitiesSchema = z.object({
- experimental: z.object({}).optional(),
+/**
+ * Result from graceful shutdown request.
+ *
+ * Empty result indicates the Guest UI has completed cleanup and is ready to be
+ * torn down.
+ *
+ * @see {@link McpUiResourceTeardownRequest}
+ */
+export interface McpUiResourceTeardownResult {}
- openLinks: z.object({}).optional(),
+/**
+ * Runtime validation schema for {@link McpUiResourceTeardownResult}.
+ * @internal
+ */
+export const McpUiResourceTeardownResultSchema: z.ZodType =
+ EmptyResultSchema;
- serverTools: z
- .object({
- listChanged: z.boolean().optional(),
- })
- .optional(),
+/**
+ * Capabilities supported by the host application.
+ *
+ * Hosts declare these capabilities during the initialization handshake. Guest UIs
+ * can check capabilities before attempting to use specific features.
+ *
+ * @example Check if host supports opening links
+ * ```typescript
+ * const result = await app.connect(transport);
+ * if (result.hostCapabilities.openLinks) {
+ * await app.sendOpenLink({ url: "https://example.com" });
+ * }
+ * ```
+ *
+ * @see {@link McpUiInitializeResult} for the initialization result that includes these capabilities
+ */
+export interface McpUiHostCapabilities {
+ /** Experimental features (structure TBD) */
+ experimental?: {};
+ /** Host supports opening external URLs via {@link app.App.sendOpenLink} */
+ openLinks?: {};
+ /** Host can proxy tool calls to the MCP server */
+ serverTools?: {
+ /** Host supports tools/list_changed notifications */
+ listChanged?: boolean;
+ };
+ /** Host can proxy resource reads to the MCP server */
+ serverResources?: {
+ /** Host supports resources/list_changed notifications */
+ listChanged?: boolean;
+ };
+ /** Host accepts log messages via {@link app.App.sendLog} */
+ logging?: {};
+}
- serverResources: z
- .object({
- listChanged: z.boolean().optional(),
- })
- .optional(),
+/**
+ * Runtime validation schema for {@link McpUiHostCapabilities}.
+ * @internal
+ */
+export const McpUiHostCapabilitiesSchema: z.ZodType =
+ z.object({
+ experimental: z.object({}).optional(),
+ openLinks: z.object({}).optional(),
+ serverTools: z
+ .object({
+ listChanged: z.boolean().optional(),
+ })
+ .optional(),
+ serverResources: z
+ .object({
+ listChanged: z.boolean().optional(),
+ })
+ .optional(),
+ logging: z.object({}).optional(),
+ });
- logging: z.object({}).optional(),
+/**
+ * Capabilities provided by the Guest UI (App).
+ *
+ * Apps declare these capabilities during the initialization handshake to indicate
+ * what features they provide to the host.
+ *
+ * @example Declare tool capabilities
+ * ```typescript
+ * const app = new App(
+ * { name: "MyApp", version: "1.0.0" },
+ * { tools: { listChanged: true } }
+ * );
+ * ```
+ *
+ * @see {@link McpUiInitializeRequest} for the initialization request that includes these capabilities
+ */
+export interface McpUiAppCapabilities {
+ /** Experimental features (structure TBD) */
+ experimental?: {};
+ /**
+ * App exposes MCP-style tools that the host can call.
+ * These are app-specific tools, not proxied from the server.
+ */
+ tools?: {
+ /** App supports tools/list_changed notifications */
+ listChanged?: boolean;
+ };
+}
- // TODO: elicitation, sampling...
-});
-export type McpUiHostCapabilities = z.infer;
+/**
+ * Runtime validation schema for {@link McpUiAppCapabilities}.
+ * @internal
+ */
+export const McpUiAppCapabilitiesSchema: z.ZodType =
+ z.object({
+ experimental: z.object({}).optional(),
+ tools: z
+ .object({
+ listChanged: z.boolean().optional(),
+ })
+ .optional(),
+ });
-export const McpUiAppCapabilitiesSchema = z.object({
- experimental: z.object({}).optional(),
-
- // WebMCP-style tools exposed by the app to the host
- tools: z
- .object({
- listChanged: z.boolean().optional(),
- })
- .optional(),
-});
-export type McpUiAppCapabilities = z.infer;
+/**
+ * Initialization request sent from Guest UI to Host.
+ *
+ * This is the first message sent by the Guest UI after loading. The host responds
+ * with {@link McpUiInitializeResult} containing host capabilities and context.
+ * After receiving the response, the Guest UI MUST send
+ * {@link McpUiInitializedNotification}.
+ *
+ * This replaces the custom iframe-ready pattern used in pre-SEP MCP-UI.
+ *
+ * @see {@link app.App.connect} for the method that sends this request
+ */
+export interface McpUiInitializeRequest {
+ method: "ui/initialize";
+ params: {
+ /** App identification (name and version) */
+ appInfo: Implementation;
+ /** Features and capabilities this app provides */
+ appCapabilities: McpUiAppCapabilities;
+ /** Protocol version this app supports */
+ protocolVersion: string;
+ };
+}
+/**
+ * Runtime validation schema for {@link McpUiInitializeRequest}.
+ * @internal
+ */
export const McpUiInitializeRequestSchema = RequestSchema.extend({
method: z.literal("ui/initialize"),
params: z.object({
@@ -207,22 +752,74 @@ export const McpUiInitializeRequestSchema = RequestSchema.extend({
protocolVersion: z.string(),
}),
});
-export type McpUiInitializeRequest = z.infer<
- typeof McpUiInitializeRequestSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyInitializeRequest = VerifySchemaMatches<
+ typeof McpUiInitializeRequestSchema,
+ McpUiInitializeRequest
>;
-export const McpUiInitializeResultSchema = z.object({
- protocolVersion: z.string(),
- hostInfo: ImplementationSchema,
- hostCapabilities: McpUiHostCapabilitiesSchema,
- hostContext: McpUiHostContextSchema,
-});
-export type McpUiInitializeResult = z.infer;
+/**
+ * Initialization result returned from Host to Guest UI.
+ *
+ * Contains the negotiated protocol version, host information, capabilities,
+ * and rich context about the host environment.
+ *
+ * @see {@link McpUiInitializeRequest}
+ */
+export interface McpUiInitializeResult {
+ /** Negotiated protocol version string (e.g., "2025-11-21") */
+ protocolVersion: string;
+ /** Host application identification and version */
+ hostInfo: Implementation;
+ /** Features and capabilities provided by the host */
+ hostCapabilities: McpUiHostCapabilities;
+ /** Rich context about the host environment */
+ hostContext: McpUiHostContext;
+ /**
+ * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Note: The schema intentionally omits this to enforce strict validation.
+ */
+ [key: string]: unknown;
+}
+
+/**
+ * Runtime validation schema for {@link McpUiInitializeResult}.
+ * @internal
+ */
+export const McpUiInitializeResultSchema: z.ZodType =
+ z.object({
+ protocolVersion: z.string(),
+ hostInfo: ImplementationSchema,
+ hostCapabilities: McpUiHostCapabilitiesSchema,
+ hostContext: McpUiHostContextSchema,
+ });
+/**
+ * Notification that Guest UI has completed initialization (Guest UI → Host).
+ *
+ * The Guest UI MUST send this notification after receiving
+ * {@link McpUiInitializeResult} and completing any setup. The host waits for this
+ * notification before sending tool input and other data to the Guest UI.
+ *
+ * @see {@link app.App.connect} for the method that sends this notification
+ */
+export interface McpUiInitializedNotification {
+ method: "ui/notifications/initialized";
+ params?: {};
+}
+
+/**
+ * Runtime validation schema for {@link McpUiInitializedNotification}.
+ * @internal
+ */
export const McpUiInitializedNotificationSchema = z.object({
method: z.literal("ui/notifications/initialized"),
params: z.object({}).optional(),
});
-export type McpUiInitializedNotification = z.infer<
- typeof McpUiInitializedNotificationSchema
+
+/** @internal - Compile-time verification that schema matches interface */
+type _VerifyInitializedNotification = VerifySchemaMatches<
+ typeof McpUiInitializedNotificationSchema,
+ McpUiInitializedNotification
>;
diff --git a/typedoc.json b/typedoc.json
new file mode 100644
index 00000000..cb7d7790
--- /dev/null
+++ b/typedoc.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://typedoc.org/schema.json",
+ "entryPoints": [
+ "src/app.ts",
+ "src/app-bridge.ts",
+ "src/types.ts",
+ "src/message-transport.ts",
+ "src/react/index.tsx"
+ ],
+ "out": "docs/api",
+ "gitRevision": "main",
+ "excludePrivate": true,
+ "excludeInternal": false,
+ "categorizeByGroup": true,
+ "navigationLinks": {
+ "GitHub": "https://github.com/modelcontextprotocol/ext-apps",
+ "Specification": "https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx"
+ },
+ "readme": "README.md",
+ "includeVersion": true
+}