diff --git a/BREAKING.md b/BREAKING.md new file mode 100644 index 000000000..b4e8494a5 --- /dev/null +++ b/BREAKING.md @@ -0,0 +1,104 @@ +# ext-apps v2 — Breaking Changes + +This release moves ext-apps from `@modelcontextprotocol/sdk@^1` to +`@modelcontextprotocol/client@^2` + `@modelcontextprotocol/server@^2`. The +public method surface (`app.callServerTool()`, `app.openLink()`, +`bridge.sendToolInput()`, every `on*` handler, `addEventListener`) is preserved. +The breaks below are at the inheritance/import boundary. + +## Dependencies + +| v1 | v2 | +|---|---| +| `@modelcontextprotocol/sdk` (peer) | `@modelcontextprotocol/client` (peer) + `@modelcontextprotocol/server` (peer, optional unless you use `app-bridge` or `server`) | + +## `App` and `AppBridge` no longer extend `Protocol` + +v1 subclassed the SDK's `Protocol` class. v2 composes `Client`/`Server` +instances instead. If you were calling inherited `Protocol` members directly: + +| Removed inherited member | Replacement | +|---|---| +| `app.request(...)` / `app.notification(...)` | `app.ui.sendRequest(...)` / `app.ui.sendNotification(...)` for `ui/*`; `app.client.callTool(...)` etc. for standard MCP | +| `app.setRequestHandler(...)` | `app.ui.setRequestHandler(...)` (custom) or `app.client.setRequestHandler(...)` (standard) | +| `bridge.setRequestHandler(...)` | `bridge.ui.setRequestHandler(...)` or `bridge.server.setRequestHandler(...)` | +| `instanceof Protocol` | `app.client` is a `Client`, `bridge.server` is a `Server` | +| `assertCapabilityForMethod` etc. | Removed (were no-ops) | + +`app.transport` / `app.onerror` / `app.close()` are kept as forwarding shims. + +## `RequestHandlerExtra` slimmed + +The second argument to `on*` request handlers is now `{ signal: AbortSignal }` +only. v1 passed the full SDK `RequestHandlerExtra` (sessionId, sendNotification, +etc.). If you need the full SDK context, register directly via +`app.ui.setRequestHandler(method, schema, (params, ctx) => …)` — `ctx` is the +SDK's `BaseContext`. + +## Wire-protocol method renames + +These affect custom hosts/iframes that bypass the SDK and speak raw JSON-RPC: + +| v1 method | v2 method | Why | +|---|---|---| +| `notifications/message` (App→Host) | `ui/log` | v1 direction conflicted with core MCP semantics (SEP-1865 erratum candidate) | +| `tools/call` (Host→App) | `ui/call-view-tool` | Non-spec direction; renamed to a `ui/*` custom method | +| `tools/list` (Host→App) | `ui/list-view-tools` | Same | + +The TypeScript API names (`app.sendLog`, `bridge.callTool`, `app.oncalltool`) +are **unchanged** — only the wire-level method strings moved. + +### v1↔v2 interop + +Hosts and iframes upgrade independently. The wire renames are shimmed where the +v2 SDK's direction enforcement allows it; where it doesn't, you get a clear +error instead of silent failure. + +| Scenario | What works | What doesn't | +|---|---|---| +| **v2 host (`AppBridge`) + v1 iframe** | ✅ handshake (`ui/initialize`), all `ui/*` requests, **logging** (host dual-listens on both `ui/log` and legacy `notifications/message`), proxied `tools/call`/`resources/*` | ⚠️ `bridge.callTool()`/`bridge.listTools()` throw a descriptive error — host→iframe tool calls require the iframe on v2 | +| **v1 host + v2 iframe** | ✅ handshake (v2 `App` still sends `ui/initialize`), all `ui/*` requests, proxied standard MCP | ❌ `app.sendLog()` (sends `ui/log`; v1 host listens on `notifications/message` only — silently dropped). ❌ host-initiated `tools/call` (v2 iframe handles `ui/call-view-tool` only — MethodNotFound) | + +**Recommended upgrade order:** host first, then iframes. A v2 host accepts logs +from either generation; a v2 iframe's logs reach only a v2 host. + +## Capability negotiation via SEP-2133 + +`McpUiAppCapabilities` / `McpUiHostCapabilities` now travel in +`capabilities.extensions["io.modelcontextprotocol/ui"]` during the standard MCP +`initialize` exchange. `app.hostCapabilities` and `bridge.appCapabilities` read +from there. `ui/initialize` is kept (for `hostContext` delivery and v1 wire +compat) but no longer the sole capability source. + +`McpUiAppCapabilities` and `McpUiHostCapabilities` are now `type` aliases (were +`interface`) to satisfy `JSONObject`. + +## `ProtocolWithEvents` → `EventDispatcher` + +`src/events.ts` no longer extends `Protocol`. The class is renamed to +`EventDispatcher`; a `ProtocolWithEvents` alias is kept for one release. +`replaceRequestHandler` and the throw-on-double-set behavior are removed +(handlers are now eagerly registered with field-based delegation). + +## Import path changes + +| v1 import | v2 | +|---|---| +| `@modelcontextprotocol/sdk/types.js` | `@modelcontextprotocol/client` (types are re-exported there) | +| `@modelcontextprotocol/sdk/client/index.js` | `@modelcontextprotocol/client` | +| `@modelcontextprotocol/sdk/server/mcp.js` | `@modelcontextprotocol/server` | +| `@modelcontextprotocol/sdk/shared/transport.js` | `@modelcontextprotocol/client` | + +## `server` entry point + +`registerAppTool` / `registerAppResource` now take v2 `McpServer` and +`StandardSchemaWithJSON` (was v1 `ZodRawShapeCompat | AnySchema`). The call +shape is otherwise unchanged. `BaseToolCallback` re-export removed (use +`ToolCallback` from `@modelcontextprotocol/server`). + +## Validation depth + +Zod schemas for SDK-defined shapes (`CallToolResult`, `ContentBlock`, …) are now +type-preserving pass-throughs (`z.custom`) rather than deep validators (see +`src/sdk-compat.ts`). Validation of those shapes is the SDK's job at the actual +MCP boundary; ext-apps relays them unchanged. diff --git a/build.bun.ts b/build.bun.ts index a129d5ddd..d04d98e63 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -33,7 +33,7 @@ function buildJs( // zod is a peerDependency — keep it external so consumers share a single // zod instance (instanceof ZodError / schema.extend() break with duplicate copies). -const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"]; +const PEER_EXTERNALS = ["@modelcontextprotocol/client", "@modelcontextprotocol/server", "zod"]; await Promise.all([ buildJs("src/app.ts", { diff --git a/docs/patterns.md b/docs/patterns.md index 87a6d8232..6c70ff011 100644 --- a/docs/patterns.md +++ b/docs/patterns.md @@ -19,7 +19,7 @@ registerAppTool( "update-quantity", { description: "Update item quantity in cart", - inputSchema: { itemId: z.string(), quantity: z.number() }, + inputSchema: z.object({ itemId: z.string(), quantity: z.number() }), _meta: { ui: { resourceUri: "ui://shop/cart.html", @@ -127,14 +127,14 @@ registerAppTool( { title: "Read Data Bytes", description: "Load binary data in chunks", - inputSchema: { + inputSchema: z.object({ id: z.string().describe("Resource identifier"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z .number() .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: DataChunkSchema, // Hidden from model - only callable by the App _meta: { ui: { visibility: ["app"] } }, @@ -281,10 +281,9 @@ server.registerResource( ```ts source="./patterns.tsx#binaryBlobResourceClient" -const result = await app.request( - { method: "resources/read", params: { uri: `video://${videoId}` } }, - ReadResourceResultSchema, -); +const result = await app.readServerResource({ + uri: `video://${videoId}`, +}); const content = result.contents[0]; if (!content || !("blob" in content)) { diff --git a/docs/patterns.tsx b/docs/patterns.tsx index 6b876926b..afb0b4ce5 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -17,8 +17,8 @@ import { randomUUID } from "node:crypto"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; -import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; + import type { McpUiHostContext } from "../src/types.js"; import { useEffect, useState } from "react"; import { useApp } from "../src/react/index.js"; @@ -26,7 +26,7 @@ import { registerAppTool } from "../src/server/index.js"; import { McpServer, ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +} from "@modelcontextprotocol/server"; import { z } from "zod"; /** @@ -117,14 +117,14 @@ function chunkedDataServer(server: McpServer) { { title: "Read Data Bytes", description: "Load binary data in chunks", - inputSchema: { + inputSchema: z.object({ id: z.string().describe("Resource identifier"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z .number() .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: DataChunkSchema, // Hidden from model - only callable by the App _meta: { ui: { visibility: ["app"] } }, @@ -252,10 +252,9 @@ function binaryBlobResourceServer( */ async function binaryBlobResourceClient(app: App, videoId: string) { //#region binaryBlobResourceClient - const result = await app.request( - { method: "resources/read", params: { uri: `video://${videoId}` } }, - ReadResourceResultSchema, - ); + const result = await app.readServerResource({ + uri: `video://${videoId}`, + }); const content = result.contents[0]; if (!content || !("blob" in content)) { diff --git a/docs/quickstart.md b/docs/quickstart.md index 3e00796c5..697781391 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -178,7 +178,7 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -205,7 +205,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { @@ -240,10 +240,10 @@ export function createServer(): McpServer { ```ts source="../examples/quickstart/main.ts" -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index c2fc987d6..65fb9bc70 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,10 +12,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 31e36983e..b6fd1c404 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,8 +1,8 @@ import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; -import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; -import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js"; +import { Client } from "@modelcontextprotocol/client"; +import { SSEClientTransport } from "@modelcontextprotocol/client"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client"; +import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/server"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; @@ -303,7 +303,7 @@ export function newAppBridge( // Listen for theme changes (from toggle or system) and notify the app onThemeChange((newTheme) => { log.info("Theme changed:", newTheme); - appBridge.sendHostContextChange({ theme: newTheme }); + appBridge.sendHostContextChanged({ theme: newTheme }); }); // Per spec, the host SHOULD notify the view when container dimensions @@ -315,7 +315,7 @@ export function newAppBridge( const iframeResizeObserver = new ResizeObserver(([entry]) => { const width = Math.round(entry.contentRect.width); if (width > 0) { - appBridge.sendHostContextChange({ + appBridge.sendHostContextChanged({ containerDimensions: { width, maxHeight: 6000 }, }); } @@ -397,7 +397,7 @@ export function newAppBridge( log.info("Display mode request from MCP App:", params); const newMode = params.mode === "fullscreen" ? "fullscreen" : "inline"; // Update host context and notify the app - appBridge.sendHostContextChange({ + appBridge.sendHostContextChanged({ displayMode: newMode, }); // Notify the host UI (via callback) diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 3d488a792..470701ec5 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -1,5 +1,5 @@ import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge"; -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/server"; import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react"; import { createRoot } from "react-dom/client"; import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation"; diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts index 76426326d..f4b42c5d7 100644 --- a/examples/basic-server-preact/main.ts +++ b/examples/basic-server-preact/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 12e136501..822622e0c 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@preact/preset-vite": "^2.0.0", diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index bf14ea599..3a59d9b44 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; @@ -28,7 +28,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx index a8bdc68b8..76727b374 100644 --- a/examples/basic-server-preact/src/mcp-app.tsx +++ b/examples/basic-server-preact/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { useCallback, useEffect, useState } from "preact/hooks"; import { render } from "preact"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts index ec187b68a..01d4a0cdc 100644 --- a/examples/basic-server-react/main.ts +++ b/examples/basic-server-react/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index a7153e170..5ec38cc00 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -35,12 +35,14 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 23f6dda5a..b09e746a7 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -1,6 +1,6 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -29,7 +29,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx index 6bb554025..8c55064a5 100644 --- a/examples/basic-server-react/src/mcp-app.tsx +++ b/examples/basic-server-react/src/mcp-app.tsx @@ -3,7 +3,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts index c8d9de225..1e087eee2 100644 --- a/examples/basic-server-solid/main.ts +++ b/examples/basic-server-solid/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 0606408c8..bb5eb6e94 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 2ed2d9356..0ebd05d9d 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; @@ -28,7 +28,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx index 66d47f3f6..c8de38670 100644 --- a/examples/basic-server-solid/src/mcp-app.tsx +++ b/examples/basic-server-solid/src/mcp-app.tsx @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { createEffect, createSignal, onMount, Show } from "solid-js"; import { render } from "solid-js/web"; import styles from "./mcp-app.module.css"; diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts index 6c50a254f..5fe4e3cfb 100644 --- a/examples/basic-server-svelte/main.ts +++ b/examples/basic-server-svelte/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index 3b5430910..38a1ccbd9 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts index da603ca8d..4c2cee488 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; @@ -28,7 +28,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts index 286fa34f0..56225af67 100644 --- a/examples/basic-server-vanillajs/main.ts +++ b/examples/basic-server-vanillajs/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index dc7c333e0..ac75a4582 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -25,10 +25,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 5b2daf704..8d1c36280 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -1,6 +1,6 @@ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -29,7 +29,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + outputSchema: z.object({ time: z.string(), }), diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts index c3168c214..1ffe0aace 100644 --- a/examples/basic-server-vanillajs/src/mcp-app.ts +++ b/examples/basic-server-vanillajs/src/mcp-app.ts @@ -8,7 +8,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import "./global.css"; import "./mcp-app.css"; diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts index 669a07189..9db8b7b9e 100644 --- a/examples/basic-server-vue/main.ts +++ b/examples/basic-server-vue/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index c479adf2e..141d67178 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts index 5aa72de03..e7ff1934a 100644 --- a/examples/basic-server-vue/server.ts +++ b/examples/basic-server-vue/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; @@ -28,7 +28,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts index cc488f0b1..d0ce29a7a 100644 --- a/examples/budget-allocator-server/main.ts +++ b/examples/budget-allocator-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index bd8865f16..71120eb03 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,11 +27,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index e52291825..e2396f871 100755 --- a/examples/budget-allocator-server/server.ts +++ b/examples/budget-allocator-server/server.ts @@ -4,11 +4,11 @@ * Provides budget configuration, 24 months of historical allocation data, * and industry benchmarks by company stage. */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -270,7 +270,7 @@ export function createServer(): McpServer { title: "Get Budget Data", description: "Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage", - inputSchema: {}, + outputSchema: BudgetDataResponseSchema, _meta: { ui: { resourceUri } }, }, diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts index d59903b34..71d2bdca8 100644 --- a/examples/cohort-heatmap-server/main.ts +++ b/examples/cohort-heatmap-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index c92105a39..a5dd81fe4 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,12 +27,14 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 6d07144a2..fbe7f8bc0 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -1,5 +1,5 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -178,8 +178,8 @@ export function createServer(): McpServer { title: "Get Cohort Retention Data", description: "Returns cohort retention heatmap data showing customer retention over time by signup month", - inputSchema: GetCohortDataInputSchema.shape, - outputSchema: CohortDataSchema.shape, + inputSchema: GetCohortDataInputSchema, + outputSchema: CohortDataSchema, _meta: { ui: { resourceUri } }, }, async ({ metric, periodType, cohortCount, maxPeriods }) => { diff --git a/examples/customer-segmentation-server/main.ts b/examples/customer-segmentation-server/main.ts index 130076c58..1799612b5 100644 --- a/examples/customer-segmentation-server/main.ts +++ b/examples/customer-segmentation-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index b7532508f..2ed4a9b41 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,11 +27,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 244415d1f..bd8dbe2b6 100644 --- a/examples/customer-segmentation-server/server.ts +++ b/examples/customer-segmentation-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -100,8 +100,8 @@ export function createServer(): McpServer { title: "Get Customer Data", description: "Returns customer data with segment information for visualization. Optionally filter by segment.", - inputSchema: GetCustomerDataInputSchema.shape, - outputSchema: GetCustomerDataOutputSchema.shape, + inputSchema: GetCustomerDataInputSchema, + outputSchema: GetCustomerDataOutputSchema, _meta: { ui: { resourceUri } }, }, async ({ segment }): Promise => { diff --git a/examples/debug-server/main.ts b/examples/debug-server/main.ts index 3c89a975b..b9b3f3219 100644 --- a/examples/debug-server/main.ts +++ b/examples/debug-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 9f2ba836c..f8206490f 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,8 +35,10 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts index fd3452a19..716403cef 100644 --- a/examples/debug-server/server.ts +++ b/examples/debug-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import { appendFileSync } from "node:fs"; import path from "node:path"; diff --git a/examples/debug-server/src/mcp-app.ts b/examples/debug-server/src/mcp-app.ts index b1c862d36..53e7a0373 100644 --- a/examples/debug-server/src/mcp-app.ts +++ b/examples/debug-server/src/mcp-app.ts @@ -549,15 +549,9 @@ openLinkBtn.addEventListener("click", async () => { autoResizeToggleEl.addEventListener("change", () => { if (autoResizeToggleEl.checked) { - if (!state.autoResizeCleanup) { - state.autoResizeCleanup = app.setupSizeChangedNotifications(); - } - } else { - if (state.autoResizeCleanup) { - state.autoResizeCleanup(); - state.autoResizeCleanup = null; - } + app.setupSizeChangedNotifications(); } + // Note: v2 setupSizeChangedNotifications() has no cleanup return; toggle-off is a no-op. logEvent("auto-resize-toggle", { enabled: autoResizeToggleEl.checked }); }); @@ -635,7 +629,7 @@ app // Auto-resize is enabled by default in App, capture cleanup if we want to toggle // We'll set it up ourselves since we want toggle control - state.autoResizeCleanup = app.setupSizeChangedNotifications(); + app.setupSizeChangedNotifications(); }) .catch((e) => { logEvent("error", e); diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts index c45787e2b..c536b6787 100644 --- a/examples/integration-server/main.ts +++ b/examples/integration-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 807ce2d22..7427ae401 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -16,12 +16,14 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index e44689d24..945c7c5b6 100644 --- a/examples/integration-server/server.ts +++ b/examples/integration-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -33,7 +33,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + outputSchema: z.object({ time: z.string(), }), @@ -72,7 +72,7 @@ export function createServer(): McpServer { ); // Sample downloadable resource — used to demo ResourceLink in ui/download-file - server.resource( + server.registerResource( SAMPLE_DOWNLOAD_URI, SAMPLE_DOWNLOAD_URI, { diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx index 7f0b18c27..b8ae5a8f2 100644 --- a/examples/integration-server/src/mcp-app.tsx +++ b/examples/integration-server/src/mcp-app.tsx @@ -3,7 +3,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts index f91f57b6e..92025b839 100644 --- a/examples/map-server/main.ts +++ b/examples/map-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 52c6d8015..3cac9a304 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -27,10 +27,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts index 540d35c4d..b8d44afc6 100644 --- a/examples/map-server/server.ts +++ b/examples/map-server/server.ts @@ -5,11 +5,11 @@ * - geocode: Search for places using OpenStreetMap Nominatim * - show-map: Display an interactive 3D globe at a given location */ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -148,7 +148,7 @@ export function createServer(): McpServer { title: "Show Map", description: "Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location.", - inputSchema: { + inputSchema: z.object({ west: z .number() .optional() @@ -173,7 +173,7 @@ export function createServer(): McpServer { .string() .optional() .describe("Optional label to display on the map"), - }, + }), _meta: { ui: { resourceUri } }, }, async ({ west, south, east, north, label }): Promise => ({ @@ -196,13 +196,13 @@ export function createServer(): McpServer { title: "Geocode", description: "Search for places using OpenStreetMap. Returns coordinates and bounding boxes for up to 5 matches.", - inputSchema: { + inputSchema: z.object({ query: z .string() .describe( "Place name or address to search for (e.g., 'Paris', 'Golden Gate Bridge', '1600 Pennsylvania Ave')", ), - }, + }), }, async ({ query }): Promise => { try { diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts index 1ff848a3e..b9f228069 100644 --- a/examples/pdf-server/main.ts +++ b/examples/pdf-server/main.ts @@ -6,10 +6,10 @@ import fs from "node:fs"; import path from "node:path"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index fc08d5e40..801a6b84d 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,12 +26,14 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index a2f64ee49..afb0dbed4 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; +import { Client } from "@modelcontextprotocol/client"; +import { InMemoryTransport } from "@modelcontextprotocol/server"; import { createPdfCache, createServer, diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index d2a0b7d8b..bdfd0f0c6 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -13,18 +13,16 @@ import { randomUUID } from "crypto"; import fs from "node:fs"; import path from "node:path"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { McpServer, type Server } from "@modelcontextprotocol/server"; import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import { - RootsListChangedNotificationSchema, type CallToolResult, type ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; // Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy // build instantiates DOMMatrix at module scope and the @napi-rs/canvas // polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details. @@ -63,7 +61,7 @@ class FetchStandardFontDataFactory { import type { PrimitiveSchemaDefinition, ElicitResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import { z } from "zod"; // ============================================================================= @@ -1190,7 +1188,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { refreshRoots(server.server); }; server.server.setNotificationHandler( - RootsListChangedNotificationSchema, + "notifications/roots/list_changed", async () => { await refreshRoots(server.server); }, @@ -1201,10 +1199,9 @@ export function createServer(options: CreateServerOptions = {}): McpServer { const { readPdfRange } = createPdfCache(); // Tool: list_pdfs - List available PDFs - server.tool( + server.registerTool( "list_pdfs", - "List available PDFs that can be displayed", - {}, + { description: "List available PDFs that can be displayed" }, async (): Promise => { const seen = new Set(); const localFiles: string[] = []; @@ -1286,7 +1283,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { title: "Read PDF Bytes", description: "Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ url: z.string().describe("PDF URL or local file path"), offset: z.number().min(0).default(0).describe("Byte offset"), byteCount: z @@ -1295,7 +1292,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { .max(MAX_CHUNK_BYTES) .default(MAX_CHUNK_BYTES) .describe("Bytes to read"), - }, + }), outputSchema: z.object({ url: z.string(), bytes: z.string().describe("Base64 encoded bytes"), @@ -1382,7 +1379,7 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`: Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL. Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`, - inputSchema: { + inputSchema: z.object({ url: z .string() .default(DEFAULT_PDF) @@ -1398,7 +1395,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before "If true and the PDF has form fields, prompt the user to fill them before displaying", ), }), - }, + }), outputSchema: z.object({ viewUUID: z .string() @@ -2349,7 +2346,7 @@ Example — add a signature image and a stamp, then screenshot to verify: **FORMS** — fill_form: fill fields with \`fields\` array of {name, value}. **SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`, - inputSchema: { + inputSchema: z.object({ viewUUID: z .string() .describe( @@ -2446,7 +2443,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .describe( "Array of commands to execute sequentially. More efficient than separate calls. Tip: end with get_pages+getScreenshots to verify changes.", ), - }, + }), }, async ( { @@ -2523,7 +2520,7 @@ Example — add a signature image and a stamp, then screenshot to verify: const result = await processInteractCommand( uuid, commandList[i], - extra.signal, + extra.mcpReq.signal, ); if (result.isError) { const errText = result.content @@ -2580,7 +2577,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Page Data", description: "Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the get_pages command"), @@ -2593,7 +2590,7 @@ Example — add a signature image and a stamp, then screenshot to verify: }), ) .describe("Page data entries"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, pages }): Promise => { @@ -2623,7 +2620,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Save Data", description: "Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the save_as command"), @@ -2632,7 +2629,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .string() .optional() .describe("Error message if the viewer failed to build bytes"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, data, error }): Promise => { @@ -2662,7 +2659,7 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Submit Viewer State", description: "Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ requestId: z .string() .describe("The request ID from the get_viewer_state command"), @@ -2674,7 +2671,7 @@ Example — add a signature image and a stamp, then screenshot to verify: .string() .optional() .describe("Error message if the viewer failed to read state"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ requestId, state, error }): Promise => { @@ -2704,9 +2701,9 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Poll PDF Commands", description: "Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.", - inputSchema: { + inputSchema: z.object({ viewUUID: z.string().describe("The viewUUID of the PDF viewer"), - }, + }), _meta: { ui: { visibility: ["app"] } }, }, async ({ viewUUID: uuid }): Promise => { @@ -2751,10 +2748,10 @@ Example — add a signature image and a stamp, then screenshot to verify: title: "Save PDF", description: "Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.", - inputSchema: { + inputSchema: z.object({ url: z.string().describe("Original PDF URL or local file path"), data: z.string().describe("Base64-encoded PDF bytes"), - }, + }), outputSchema: z.object({ filePath: z.string(), mtimeMs: z.number(), diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts index f4a17901d..f28d0c100 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -12,8 +12,7 @@ import { applyDocumentTheme, applyHostStyleVariables, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import type { CallToolResult, ContentBlock } from "@modelcontextprotocol/client"; import * as pdfjsLib from "pdfjs-dist"; import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist"; import "pdfjs-dist/web/pdf_viewer.css"; diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a7813e3c5..647a21531 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -9,6 +9,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" } } diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts index 06895a49a..f954d0b0c 100644 --- a/examples/quickstart/main.ts +++ b/examples/quickstart/main.ts @@ -1,7 +1,7 @@ -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 8451e20eb..a84725e1a 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,9 +16,11 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts index eb3c3b1ab..78e5cac00 100644 --- a/examples/quickstart/server.ts +++ b/examples/quickstart/server.ts @@ -3,7 +3,7 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -30,7 +30,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 2e90491e7..6d20328fa 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -16,6 +16,8 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" } } diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts index dbd3ab6ae..01bf52cdf 100644 --- a/examples/scenario-modeler-server/main.ts +++ b/examples/scenario-modeler-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index e00afc09c..94695447d 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,13 +27,15 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index b59851c40..8a9b9990e 100644 --- a/examples/scenario-modeler-server/server.ts +++ b/examples/scenario-modeler-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -305,8 +305,8 @@ export function createServer(): McpServer { title: "Get Scenario Data", description: "Returns SaaS scenario templates and optionally computes custom projections for given inputs", - inputSchema: GetScenarioDataInputSchema.shape, - outputSchema: GetScenarioDataOutputSchema.shape, + inputSchema: GetScenarioDataInputSchema, + outputSchema: GetScenarioDataOutputSchema, _meta: { ui: { resourceUri } }, }, async (args: { diff --git a/examples/shadertoy-server/main.ts b/examples/shadertoy-server/main.ts index ac8f6ad8e..4c7db699f 100644 --- a/examples/shadertoy-server/main.ts +++ b/examples/shadertoy-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 03bcb96c5..7287ab97f 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,10 +25,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts index a4bfe9e00..9422b82a5 100644 --- a/examples/shadertoy-server/server.ts +++ b/examples/shadertoy-server/server.ts @@ -3,11 +3,11 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; diff --git a/examples/sheet-music-server/main.ts b/examples/sheet-music-server/main.ts index 8b143a7ae..71f3f0d16 100644 --- a/examples/sheet-music-server/main.ts +++ b/examples/sheet-music-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 3706c25a4..2b4f16a01 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,11 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts index cfd319b51..b831ae25c 100644 --- a/examples/sheet-music-server/server.ts +++ b/examples/sheet-music-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; diff --git a/examples/system-monitor-server/main.ts b/examples/system-monitor-server/main.ts index 365a56046..7e6a25392 100644 --- a/examples/system-monitor-server/main.ts +++ b/examples/system-monitor-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index c80b9ad7c..8d29dc58f 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,12 +27,14 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "systeminformation": "^5.31.5", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 8fac5f2f9..31f430944 100644 --- a/examples/system-monitor-server/server.ts +++ b/examples/system-monitor-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; @@ -135,8 +135,8 @@ export function createServer(): McpServer { title: "Get System Info", description: "Returns system information, including hostname, platform, CPU info, and memory.", - inputSchema: {}, - outputSchema: SystemInfoSchema.shape, + + outputSchema: SystemInfoSchema, _meta: { ui: { resourceUri } }, }, (): CallToolResult => { @@ -156,8 +156,8 @@ export function createServer(): McpServer { title: "Poll System Stats", description: "Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.", - inputSchema: {}, - outputSchema: PollStatsSchema.shape, + + outputSchema: PollStatsSchema, _meta: { ui: { visibility: ["app"] } }, }, async (): Promise => { diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts index 13a2084a8..7d1f6bf88 100644 --- a/examples/threejs-server/main.ts +++ b/examples/threejs-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index d4b8192f7..79a1c20fc 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -27,13 +27,15 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", "three": "^0.181.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index ae320df80..698758a1f 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -8,8 +8,8 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import type { ReadResourceResult } from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -161,7 +161,7 @@ export function createServer(): McpServer { title: "Show Three.js Scene", description: "Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.", - inputSchema: { + inputSchema: z.object({ code: z .string() .default(DEFAULT_THREEJS_CODE) @@ -172,7 +172,7 @@ export function createServer(): McpServer { .positive() .default(400) .describe("Height in pixels"), - }, + }), outputSchema: z.object({ success: z.boolean(), }), @@ -192,7 +192,7 @@ export function createServer(): McpServer { { title: "Learn Three.js", description: "Get documentation and examples for using the Three.js View", - inputSchema: {}, + }, async () => { return { diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 9ef24d263..a5a98edbb 100644 --- a/examples/threejs-server/src/mcp-app-wrapper.tsx +++ b/examples/threejs-server/src/mcp-app-wrapper.tsx @@ -6,7 +6,7 @@ */ import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { StrictMode, useState, useEffect } from "react"; import { createRoot } from "react-dom/client"; import ThreeJSApp from "./threejs-app.tsx"; diff --git a/examples/transcript-server/main.ts b/examples/transcript-server/main.ts index 68db8dc7d..c79cbf655 100644 --- a/examples/transcript-server/main.ts +++ b/examples/transcript-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index 341780127..5c0d9b74e 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,10 +25,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/transcript-server/server.ts b/examples/transcript-server/server.ts index 56bf88d99..f2ac58938 100644 --- a/examples/transcript-server/server.ts +++ b/examples/transcript-server/server.ts @@ -1,8 +1,8 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { @@ -33,7 +33,7 @@ export function createServer(): McpServer { title: "Transcribe Speech", description: "Opens a live speech transcription interface using the Web Speech API.", - inputSchema: {}, + _meta: { ui: { resourceUri } }, }, async (): Promise => { diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts index 082fa5abc..c1469f053 100644 --- a/examples/video-resource-server/main.ts +++ b/examples/video-resource-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index fb01e3136..e194096ba 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -25,10 +25,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 2cb317e41..3718e7d94 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -12,11 +12,11 @@ import { import { McpServer, ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +} from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -135,14 +135,14 @@ Available videos: ${Object.entries(VIDEO_LIBRARY) .map(([id, v]) => `- ${id}: ${v.description}`) .join("\n")}`, - inputSchema: { + inputSchema: z.object({ videoId: z .enum(Object.keys(VIDEO_LIBRARY) as [string, ...string[]]) .default("bunny-1mb") .describe( `Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`, ), - }, + }), outputSchema: z.object({ videoUri: z.string(), description: z.string(), diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts index 88ed913ee..31fe7b045 100644 --- a/examples/video-resource-server/src/mcp-app.ts +++ b/examples/video-resource-server/src/mcp-app.ts @@ -5,7 +5,7 @@ * The video is served as a base64 blob and converted to a data URI for playback. */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import "./global.css"; import "./mcp-app.css"; diff --git a/examples/wiki-explorer-server/main.ts b/examples/wiki-explorer-server/main.ts index cc7b3a221..3b6be02e4 100644 --- a/examples/wiki-explorer-server/main.ts +++ b/examples/wiki-explorer-server/main.ts @@ -4,10 +4,10 @@ * Or: node dist/index.js [--stdio] */ -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; +import { createMcpExpressApp } from "@modelcontextprotocol/express"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { NodeStreamableHTTPServerTransport as StreamableHTTPServerTransport } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 605c39ae3..aef7f8c2c 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -27,11 +27,13 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.1.13", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index fe2e89b53..395e9bd32 100644 --- a/examples/wiki-explorer-server/server.ts +++ b/examples/wiki-explorer-server/server.ts @@ -3,11 +3,11 @@ import { registerAppResource, registerAppTool, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import * as cheerio from "cheerio"; import fs from "node:fs/promises"; import path from "node:path"; diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts index 69bec05d4..11aeb158d 100644 --- a/examples/wiki-explorer-server/src/mcp-app.ts +++ b/examples/wiki-explorer-server/src/mcp-app.ts @@ -2,7 +2,7 @@ * Wiki Explorer - Force-directed graph visualization of Wikipedia link networks */ import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/server"; import { forceCenter, forceCollide, diff --git a/package-lock.json b/package-lock.json index b63a8e550..7c217feae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,10 @@ ], "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "file:../../../../../tmp/modelcontextprotocol-client-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:../../../../../tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:../../../../../tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:../../../../../tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -47,12 +50,16 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "file:../../../../../tmp/modelcontextprotocol-client-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:../../../../../tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" }, "peerDependenciesMeta": { + "@modelcontextprotocol/server": { + "optional": true + }, "react": { "optional": true }, @@ -65,8 +72,10 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.5.0", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" @@ -109,8 +118,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -153,8 +164,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -200,8 +213,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -244,8 +259,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -288,8 +305,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -330,8 +349,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -374,8 +395,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -417,8 +440,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -464,8 +489,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -507,8 +534,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "zod": "^4.1.13" }, "bin": { @@ -547,8 +576,10 @@ "examples/integration-server": { "version": "1.5.0", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -593,8 +624,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -635,8 +668,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "pdf-lib": "^1.17.1", @@ -678,8 +713,10 @@ "name": "@modelcontextprotocol/server-qr", "version": "1.5.0", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz" } }, "examples/quickstart": { @@ -687,8 +724,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0" }, @@ -719,8 +758,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz" } }, "examples/scenario-modeler-server": { @@ -728,8 +769,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -776,8 +819,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -818,8 +863,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -861,8 +908,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -931,8 +980,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -980,8 +1031,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1023,8 +1076,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1065,8 +1120,10 @@ "version": "1.5.0", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "file:/tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", "@modelcontextprotocol/ext-apps": "^1.0.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "file:/tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:/tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -2543,6 +2600,37 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-alpha.2", + "resolved": "file:../../../../../tmp/modelcontextprotocol-client-2.0.0-alpha.2.tgz", + "integrity": "sha512-6j6I1RvRA83hoKGIlSn+Qyq0AKcTOfj8xMFyL9OqRKEP4wfgiagD/ay4IV1PchtvD8FKVUwed0aFDICBOyhyhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/express": { + "version": "2.0.0-alpha.2", + "resolved": "file:../../../../../tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "integrity": "sha512-Yw34r4Ml9S99DY7G/ZW2r/Lg+GaGP6NklkGF9hccnm/aJHBIWVU5Jqt6jh8Xu9UMmWt47zZ0sTPVNr95DN+qAA==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-alpha.2", + "express": "^5.2.1" + } + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.0.0.tgz", @@ -2590,6 +2678,22 @@ "resolved": "examples/basic-host", "link": true }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0-alpha.2", + "resolved": "file:../../../../../tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "integrity": "sha512-mRX8GKImxxgMAFySSL2rhb9iD6aATIfV20J3ABTcxVEsYzJdKQv+biPVadBuUI+vACJJslrgrPmy7N2TAml+Sw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-alpha.2", + "hono": "^4.11.4" + } + }, "node_modules/@modelcontextprotocol/quickstart": { "resolved": "examples/quickstart", "link": true @@ -2599,6 +2703,7 @@ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", "license": "MIT", + "peer": true, "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", @@ -2634,6 +2739,18 @@ } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-alpha.2", + "resolved": "file:../../../../../tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", + "integrity": "sha512-itkhFVxm96ZI8HGMMRiNUVAb2Gvpli6JGYto+umFma9GR7O59Z4hrxrsJMpMsa/nBGKITCktKHQugbWtXTYgjA==", + "license": "MIT", + "dependencies": { + "zod": "^4.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/server-basic-preact": { "resolved": "examples/basic-server-preact", "link": true @@ -4374,6 +4491,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -4390,6 +4508,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "license": "MIT", + "peer": true, "dependencies": { "ajv": "^8.0.0" }, @@ -5965,6 +6084,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz", "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "10.1.0" }, @@ -5982,7 +6102,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fast-uri": { "version": "3.1.0", @@ -5998,7 +6119,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fflate": { "version": "0.8.2", @@ -6325,6 +6447,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.9.tgz", "integrity": "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6467,6 +6590,7 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -6656,13 +6780,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/json-schema-typed": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", - "license": "BSD-2-Clause" + "license": "BSD-2-Clause", + "peer": true }, "node_modules/json5": { "version": "2.2.3", @@ -7671,6 +7797,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -9974,6 +10101,7 @@ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", "license": "ISC", + "peer": true, "peerDependencies": { "zod": "^3.25 || ^4" } diff --git a/package.json b/package.json index 3e040d16f..f5c763268 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,8 @@ "build": "npm run generate:schemas && npm run sync:snippets && node scripts/run-bun.mjs build.bun.ts && node scripts/link-self.mjs", "prepack": "npm run build", "build:all": "npm run examples:build", - "test": "bun test src examples", + "test": "bun test src examples/pdf-server/src/pdf-annotations.test.ts", + "test:full": "bun test src examples", "test:e2e": "playwright test", "test:e2e:update": "playwright test --update-snapshots", "test:e2e:ui": "playwright test --ui", @@ -76,7 +77,10 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "file:../../../../../tmp/modelcontextprotocol-client-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/express": "file:../../../../../tmp/modelcontextprotocol-express-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/node": "file:../../../../../tmp/modelcontextprotocol-node-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:../../../../../tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -107,7 +111,8 @@ "zod": "^4.1.13" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "file:../../../../../tmp/modelcontextprotocol-client-2.0.0-alpha.2.tgz", + "@modelcontextprotocol/server": "file:../../../../../tmp/modelcontextprotocol-server-2.0.0-alpha.2.tgz", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^3.25.0 || ^4.0.0" @@ -118,6 +123,9 @@ }, "react-dom": { "optional": true + }, + "@modelcontextprotocol/server": { + "optional": true } }, "overrides": { diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 109f35247..89981dbc3 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -187,20 +187,22 @@ function postProcess(content: string): string { // 1. Rewrite to zod/v4 and add MCP SDK schema imports. // zod/v4 aligns with the SDK's own zod import — composing v3 and v4 // schema instances throws at parse time. See header comment for details. - const mcpImports = EXTERNAL_TYPE_SCHEMAS.join(",\n "); + const typeImports = EXTERNAL_TYPE_SCHEMAS.map((s) => + s.replace(/Schema$/, ""), + ).join(", "); content = content.replace( 'import { z } from "zod";', `import { z } from "zod/v4"; -import { - ${mcpImports}, -} from "@modelcontextprotocol/sdk/types.js";`, +import { isSpecType } from "@modelcontextprotocol/client"; +import type { ${typeImports} } from "@modelcontextprotocol/client";`, ); - // 2. Remove z.any() placeholders for external types (now imported from MCP SDK) + // 2. Replace z.any() placeholders for external SDK types with specTypeSchema() for (const schema of EXTERNAL_TYPE_SCHEMAS) { + const typeName = schema.replace(/Schema$/, ""); content = content.replace( - new RegExp(`(?:export )?const ${schema} = z\\.any\\(\\);\\n?`, "g"), - "", + new RegExp(`((?:export )?const ${schema}) = z\\.any\\(\\);`, "g"), + `$1 = z.custom<${typeName}>((v) => isSpecType("${typeName}", v));`, ); } diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index b613451c1..faba22a89 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -7,15 +7,11 @@ * @module */ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { - CallToolResult, - CallToolResultSchema, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; + Client, + type CallToolResult, + type Transport, +} from "@modelcontextprotocol/client"; import { AppBridge, PostMessageTransport } from "./app-bridge.js"; import type { McpUiDisplayMode } from "./types.js"; @@ -219,11 +215,9 @@ function AppBridge_oncalltool_forwardToServer( ) { //#region AppBridge_oncalltool_forwardToServer bridge.oncalltool = async (params, extra) => { - return mcpClient.request( - { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, - ); + return mcpClient.callTool(params, { + signal: extra.signal, + }) as Promise; }; //#endregion AppBridge_oncalltool_forwardToServer } @@ -237,11 +231,7 @@ function AppBridge_onlistresources_returnResources( ) { //#region AppBridge_onlistresources_returnResources bridge.onlistresources = async (params, extra) => { - return mcpClient.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, - ); + return mcpClient.listResources(params, { signal: extra.signal }); }; //#endregion AppBridge_onlistresources_returnResources } @@ -255,11 +245,7 @@ function AppBridge_onreadresource_returnResource( ) { //#region AppBridge_onreadresource_returnResource bridge.onreadresource = async (params, extra) => { - return mcpClient.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, - ); + return mcpClient.readResource(params, { signal: extra.signal }); }; //#endregion AppBridge_onreadresource_returnResource } @@ -273,11 +259,7 @@ function AppBridge_onlistprompts_returnPrompts( ) { //#region AppBridge_onlistprompts_returnPrompts bridge.onlistprompts = async (params, extra) => { - return mcpClient.request( - { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, - ); + return mcpClient.listPrompts(params, { signal: extra.signal }); }; //#endregion AppBridge_onlistprompts_returnPrompts } @@ -422,10 +404,10 @@ async function AppBridge_sendToolResult_afterExecution( args: Record, ) { //#region AppBridge_sendToolResult_afterExecution - const result = await mcpClient.request( - { method: "tools/call", params: { name: "get_weather", arguments: args } }, - CallToolResultSchema, - ); + const result = (await mcpClient.callTool({ + name: "get_weather", + arguments: args, + })) as CallToolResult; bridge.sendToolResult(result); //#endregion AppBridge_sendToolResult_afterExecution } diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 4761d766a..1473acfee 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,17 +1,6 @@ import { describe, it, expect, beforeEach, afterEach } from "bun:test"; -import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; -import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js"; -import { - EmptyResultSchema, - ListPromptsResultSchema, - ListResourcesResultSchema, - ListResourceTemplatesResultSchema, - PromptListChangedNotificationSchema, - ReadResourceResultSchema, - ResourceListChangedNotificationSchema, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { InMemoryTransport } from "@modelcontextprotocol/server"; +import type { Client, ServerCapabilities } from "@modelcontextprotocol/client"; import { App } from "./app"; import { @@ -27,15 +16,31 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); /** * Create a minimal mock MCP client for testing AppBridge. - * Only implements methods that AppBridge calls. + * With empty capabilities, AppBridge.connect() does not auto-wire any + * proxy handlers, so most fields are unused stubs. */ function createMockClient( serverCapabilities: ServerCapabilities = {}, -): Pick { +): Pick< + Client, + | "getServerCapabilities" + | "callTool" + | "listTools" + | "listResources" + | "listResourceTemplates" + | "readResource" + | "listPrompts" + | "setNotificationHandler" +> { return { getServerCapabilities: () => serverCapabilities, - request: async () => ({}) as never, - notification: async () => {}, + callTool: async () => ({ content: [] }), + listTools: async () => ({ tools: [] }), + listResources: async () => ({ resources: [] }), + listResourceTemplates: async () => ({ resourceTemplates: [] }), + readResource: async () => ({ contents: [] }), + listPrompts: async () => ({ prompts: [] }), + setNotificationHandler: () => {}, }; } @@ -353,11 +358,11 @@ describe("App <-> AppBridge integration", () => { await newApp.connect(newAppTransport); // Send partial update: only theme changes - newBridge.sendHostContextChange({ theme: "dark" }); + newBridge.sendHostContextChanged({ theme: "dark" }); await flush(); // Send another partial update: only containerDimensions change - newBridge.sendHostContextChange({ + newBridge.sendHostContextChanged({ containerDimensions: { width: 1024, maxHeight: 768 }, }); await flush(); @@ -675,17 +680,62 @@ describe("App <-> AppBridge integration", () => { }); describe("ping", () => { - it("App responds to ping from bridge", async () => { + it("App responds to ping from bridge via bridge.server.ping()", async () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); + await expect(bridge.server.ping()).resolves.toEqual({}); + }); + }); - // Bridge can send ping via the protocol's request method - const result = await bridge.request( - { method: "ping", params: {} }, - EmptyResultSchema, - ); + describe("v1↔v2 wire compat", () => { + it("AppBridge dual-listens: legacy notifications/message fires onloggingmessage", async () => { + let received: unknown; + bridge.onloggingmessage = (p) => { + received = p; + }; + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + // Simulate a v1 iframe by injecting the legacy wire method directly. + await appTransport.send({ + jsonrpc: "2.0", + method: "notifications/message", + params: { level: "info", data: "from v1 iframe" }, + }); + await flush(); + expect(received).toEqual({ level: "info", data: "from v1 iframe" }); + }); - expect(result).toEqual({}); + it("bridge.callTool() reaches a v2 iframe via ui/call-view-tool", async () => { + app.oncalltool = async (p) => ({ + content: [{ type: "text", text: `called ${p.name}` }], + }); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + const result = await bridge.callTool({ name: "view-tool", arguments: {} }); + expect(result.content?.[0]).toEqual({ + type: "text", + text: "called view-tool", + }); + }); + }); + + describe("on* setter replace semantics", () => { + it("reassigning a request-style on* setter replaces (warns, does not throw)", async () => { + const calls: string[] = []; + bridge.onmessage = async () => { + calls.push("first"); + return { ok: true }; + }; + expect(() => { + bridge.onmessage = async () => { + calls.push("second"); + return { ok: true }; + }; + }).not.toThrow(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + await app.sendMessage({ role: "user", content: [{ type: "text", text: "x" }] }); + expect(calls).toEqual(["second"]); }); }); @@ -794,13 +844,10 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); // App sends resources/list request via the protocol's request method - const result = await app.request( - { method: "resources/list", params: requestParams }, - ListResourcesResultSchema, - ); + const result = await app.listServerResources(); expect(receivedRequests).toHaveLength(1); - expect(receivedRequests[0]).toMatchObject(requestParams); + // v2: list* methods send undefined params when called with no args expect(result.resources).toEqual(resources); }); @@ -838,10 +885,7 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "resources/read", params: requestParams }, - ReadResourceResultSchema, - ); + const result = await app.readServerResource({ uri: "test://resource" }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -874,7 +918,7 @@ describe("App <-> AppBridge integration", () => { expect(result.contents).toEqual(contents); }); - it("onlistresourcetemplates setter registers handler for resources/templates/list requests", async () => { + it("onlistresourcetemplates setter registers handler", async () => { const requestParams = {}; const resourceTemplates = [ { uriTemplate: "test://{id}", name: "Test Template" }, @@ -889,17 +933,14 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "resources/templates/list", params: requestParams }, - ListResourceTemplatesResultSchema, - ); + const result = await app.client.listResourceTemplates(); expect(receivedRequests).toHaveLength(1); - expect(receivedRequests[0]).toMatchObject(requestParams); + // v2: list* methods send undefined params when called with no args expect(result.resourceTemplates).toEqual(resourceTemplates); }); - it("onlistprompts setter registers handler for prompts/list requests", async () => { + it("onlistprompts setter registers handler", async () => { const requestParams = {}; const prompts = [{ name: "test-prompt" }]; const receivedRequests: unknown[] = []; @@ -912,19 +953,16 @@ describe("App <-> AppBridge integration", () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - const result = await app.request( - { method: "prompts/list", params: requestParams }, - ListPromptsResultSchema, - ); + const result = await app.client.listPrompts(); expect(receivedRequests).toHaveLength(1); - expect(receivedRequests[0]).toMatchObject(requestParams); + // v2: list* methods send undefined params when called with no args expect(result.prompts).toEqual(prompts); }); it("sendToolListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => { + app.client.setNotificationHandler("notifications/tools/list_changed", (n) => { receivedNotifications.push(n.params); }); @@ -939,7 +977,7 @@ describe("App <-> AppBridge integration", () => { it("sendResourceListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => { + app.client.setNotificationHandler("notifications/resources/list_changed", (n) => { receivedNotifications.push(n.params); }); @@ -954,7 +992,7 @@ describe("App <-> AppBridge integration", () => { it("sendPromptListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => { + app.client.setNotificationHandler("notifications/prompts/list_changed", (n) => { receivedNotifications.push(n.params); }); @@ -1363,38 +1401,6 @@ describe("isToolVisibilityAppOnly", () => { expect(app.onteardown).toBe(handler); }); - it("direct setRequestHandler throws when called twice", () => { - const bridge2 = new AppBridge( - createMockClient() as Client, - testHostInfo, - testHostCapabilities, - ); - bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, - () => ({}), - ); - expect(() => { - bridge2.setRequestHandler( - // @ts-expect-error — exercising throw path with raw schema - { shape: { method: { value: "test/method" } } }, - () => ({}), - ); - }).toThrow(/already registered/); - }); - it("direct setNotificationHandler throws for event-mapped methods", () => { - const app2 = new App(testAppInfo, {}, { autoResize: false }); - app2.addEventListener("toolinput", () => {}); - expect(() => { - app2.setNotificationHandler( - // @ts-expect-error — exercising throw path with raw schema - { - shape: { method: { value: "ui/notifications/tool-input" } }, - }, - () => {}, - ); - }).toThrow(/already registered/); - }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 77125872c..20f485736 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,63 +1,39 @@ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; +import type { Client } from "@modelcontextprotocol/client"; import { - CallToolRequest, - CallToolRequestSchema, - CallToolResult, - CallToolResultSchema, - EmptyResult, - Implementation, - ListPromptsRequest, - ListPromptsRequestSchema, - ListPromptsResult, - ListPromptsResultSchema, - ListResourcesRequest, - ListResourcesRequestSchema, - ListResourcesResult, - ListResourcesResultSchema, - ListResourceTemplatesRequest, - ListResourceTemplatesRequestSchema, - ListResourceTemplatesResult, - ListResourceTemplatesResultSchema, - ListToolsRequest, - ListToolsRequestSchema, - ListToolsResultSchema, - LoggingMessageNotification, - LoggingMessageNotificationSchema, - PingRequest, - PingRequestSchema, - PromptListChangedNotification, - PromptListChangedNotificationSchema, - ReadResourceRequest, - ReadResourceRequestSchema, - ReadResourceResult, - ReadResourceResultSchema, - ResourceListChangedNotification, - ResourceListChangedNotificationSchema, - Tool, - ToolListChangedNotification, - ToolListChangedNotificationSchema, -} from "@modelcontextprotocol/sdk/types.js"; + Server, + type CallToolRequest, + type CallToolResult, + type EmptyResult, + type ExtensionHandle, + type Implementation, + type ListPromptsRequest, + type ListPromptsResult, + type ListResourcesRequest, + type ListResourcesResult, + type ListResourceTemplatesRequest, + type ListResourceTemplatesResult, + type ListToolsRequest, + type ListToolsResult, + type LoggingMessageNotification, + type PingRequest, + type ProtocolOptions, + type ReadResourceRequest, + type ReadResourceResult, + type RequestOptions, + type ServerContext, + type Tool, + type Transport, +} from "@modelcontextprotocol/server"; + +import { EventDispatcher } from "./events"; +import { specTypeSchema, type StandardSchemaV1 } from "@modelcontextprotocol/server"; import { - ProtocolOptions, - RequestOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { ProtocolWithEvents } from "./events"; - -import { - type AppNotification, - type AppRequest, - type AppResult, - type McpUiSandboxResourceReadyNotification, - type McpUiSizeChangedNotification, - type McpUiToolCancelledNotification, - type McpUiToolInputNotification, - type McpUiToolInputPartialNotification, - type McpUiToolResultNotification, LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, - McpUiUpdateModelContextRequest, - McpUiUpdateModelContextRequestSchema, + McpUiAppCapabilitiesSchema, + McpUiDownloadFileRequest, + McpUiDownloadFileRequestSchema, + McpUiDownloadFileResult, McpUiHostCapabilities, McpUiHostContext, McpUiHostContextChangedNotification, @@ -72,1769 +48,708 @@ import { McpUiOpenLinkRequest, McpUiOpenLinkRequestSchema, McpUiOpenLinkResult, - McpUiDownloadFileRequest, - McpUiDownloadFileRequestSchema, - McpUiDownloadFileResult, - McpUiResourceTeardownRequest, - McpUiResourceTeardownResultSchema, + McpUiRequestDisplayModeRequest, + McpUiRequestDisplayModeRequestSchema, + McpUiRequestDisplayModeResult, McpUiRequestTeardownNotification, McpUiRequestTeardownNotificationSchema, + McpUiResourceMeta, + McpUiResourcePermissions, + McpUiResourceTeardownRequest, + McpUiResourceTeardownResultSchema, McpUiSandboxProxyReadyNotification, McpUiSandboxProxyReadyNotificationSchema, + McpUiSandboxResourceReadyNotification, + McpUiSizeChangedNotification, McpUiSizeChangedNotificationSchema, - McpUiRequestDisplayModeRequest, - McpUiRequestDisplayModeRequestSchema, - McpUiRequestDisplayModeResult, - McpUiResourcePermissions, + McpUiToolCancelledNotification, + McpUiToolInputNotification, + McpUiToolInputPartialNotification, McpUiToolMeta, + McpUiToolResultNotification, + McpUiUpdateModelContextRequest, + McpUiUpdateModelContextRequestSchema, } from "./types"; +import { z } from "zod/v4"; + +import { + MCP_APPS_EXTENSION_ID, + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, + getToolUiResourceUri, + type RequestHandlerExtra, +} from "./app"; + export * from "./types"; -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; -import { RESOURCE_URI_META_KEY } from "./app"; +export { + MCP_APPS_EXTENSION_ID, + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, + getToolUiResourceUri, +}; export { PostMessageTransport } from "./message-transport"; +function toExtra(ctx: ServerContext): RequestHandlerExtra { + return { signal: ctx.mcpReq.signal }; +} + +const LogParamsSchema = specTypeSchema("LoggingMessageNotificationParams"); + +/** Options for constructing an {@link AppBridge `AppBridge`}. */ +export interface HostOptions extends ProtocolOptions { + /** + * Initial host context (theme, locale, displayMode, …) sent to the view in + * the `ui/initialize` result. + */ + hostContext?: McpUiHostContext; +} + /** - * Extract UI resource URI from tool metadata. - * - * Supports both the new nested format (`_meta.ui.resourceUri`) and the - * deprecated flat format (`_meta["ui/resourceUri"]`). The new nested format - * takes precedence if both are present. - * - * @param tool - A tool object with optional `_meta` property - * @returns The UI resource URI if valid, undefined if not present - * @throws Error if resourceUri is present but invalid (does not start with "ui://") - * - * @example - * ```typescript - * // New nested format (preferred) - * const uri = getToolUiResourceUri({ - * _meta: { ui: { resourceUri: "ui://server/app.html" } } - * }); - * - * // Deprecated flat format (still supported) - * const uri = getToolUiResourceUri({ - * _meta: { "ui/resourceUri": "ui://server/app.html" } - * }); - * ``` + * Event map for {@link AppBridge `AppBridge`} — notifications the view pushes + * to the host. */ -export function getToolUiResourceUri(tool: Partial): string | undefined { - // Try new nested format first: _meta.ui.resourceUri - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; - let uri: unknown = uiMeta?.resourceUri; - - // Fall back to deprecated flat format: _meta["ui/resourceUri"] - if (uri === undefined) { - uri = tool._meta?.[RESOURCE_URI_META_KEY]; - } +export type AppBridgeEventMap = { + sizechange: McpUiSizeChangedNotification["params"]; + sandboxready: McpUiSandboxProxyReadyNotification["params"]; + initialized: McpUiInitializedNotification["params"]; + requestteardown: McpUiRequestTeardownNotification["params"]; + /** + * Log message from the view. Wire method: `ui/log` (renamed from + * `notifications/message` in v2). + */ + loggingmessage: LoggingMessageNotification["params"]; +}; - if (typeof uri === "string" && uri.startsWith("ui://")) { - return uri; - } else if (uri !== undefined) { - throw new Error(`Invalid UI resource URI: ${JSON.stringify(uri)}`); - } - return undefined; -} +const BRIDGE_EVENT_NOTIFICATION_SCHEMAS: Record< + keyof AppBridgeEventMap, + { method: string; params: StandardSchemaV1 } +> = { + sizechange: { + method: McpUiSizeChangedNotificationSchema.shape.method.value, + params: McpUiSizeChangedNotificationSchema.shape.params, + }, + sandboxready: { + method: McpUiSandboxProxyReadyNotificationSchema.shape.method.value, + params: + McpUiSandboxProxyReadyNotificationSchema.shape.params ?? + z.object({}).optional(), + }, + initialized: { + method: McpUiInitializedNotificationSchema.shape.method.value, + params: + McpUiInitializedNotificationSchema.shape.params ?? + z.object({}).optional(), + }, + requestteardown: { + method: McpUiRequestTeardownNotificationSchema.shape.method.value, + params: + McpUiRequestTeardownNotificationSchema.shape.params ?? + z.object({}).optional(), + }, + loggingmessage: { method: "ui/log", params: LogParamsSchema }, +}; /** - * Check if a tool is visible to the model only. - * - * @param tool - Tool object with visibility metadata - * @returns True if the tool is visible to the model only, false otherwise + * Returns true if the tool's visibility excludes `"app"` (model-only). */ -export function isToolVisibilityModelOnly(tool: Partial): boolean { - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; - const visibility = uiMeta?.visibility; - if (!visibility) return false; - if (visibility.length === 1 && visibility[0] === "model") return true; - return false; +export function isToolVisibilityModelOnly(tool: { + _meta?: Record; + [key: string]: unknown; +}): boolean { + const v = (tool._meta?.ui as McpUiToolMeta | undefined)?.visibility; + return Array.isArray(v) && v.length > 0 && !v.includes("app"); } /** - * Check if a tool is visible to the app only. - * - * @param tool - Tool object with visibility metadata - * @returns True if the tool is visible to the app only, false otherwise + * Returns true if the tool's visibility excludes `"model"` (app-only). */ -export function isToolVisibilityAppOnly(tool: Partial): boolean { - const uiMeta = tool._meta?.ui as McpUiToolMeta | undefined; - const visibility = uiMeta?.visibility; - if (!visibility) return false; - if (visibility.length === 1 && visibility[0] === "app") return true; - return false; +export function isToolVisibilityAppOnly(tool: { + _meta?: Record; + [key: string]: unknown; +}): boolean { + const v = (tool._meta?.ui as McpUiToolMeta | undefined)?.visibility; + return Array.isArray(v) && v.length > 0 && !v.includes("model"); } /** - * Build iframe `allow` attribute string from permissions. - * - * Maps McpUiResourcePermissions to the Permission Policy allow attribute - * format used by iframes (e.g., "microphone; clipboard-write"). - * - * @param permissions - Permissions requested by the UI resource - * @returns Space-separated permission directives, or empty string if none - * - * @example - * ```typescript - * const allow = buildAllowAttribute({ microphone: {}, clipboardWrite: {} }); - * // Returns: "microphone; clipboard-write" - * iframe.setAttribute("allow", allow); - * ``` + * Build an iframe `allow` attribute string from + * {@link McpUiResourcePermissions `McpUiResourcePermissions`}. */ export function buildAllowAttribute( permissions: McpUiResourcePermissions | undefined, ): string { if (!permissions) return ""; - const allowList: string[] = []; if (permissions.camera) allowList.push("camera"); if (permissions.microphone) allowList.push("microphone"); if (permissions.geolocation) allowList.push("geolocation"); if (permissions.clipboardWrite) allowList.push("clipboard-write"); - return allowList.join("; "); } /** - * Options for configuring {@link AppBridge `AppBridge`} behavior. - * - * @property hostContext - Optional initial host context to provide to the view - * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options - * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure - */ -export type HostOptions = ProtocolOptions & { - hostContext?: McpUiHostContext; -}; - -/** - * 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. + * The Host side of the MCP Apps protocol — runs in the chat client embedding + * the iframe. * - * 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 + * `AppBridge` composes an MCP {@link Server `Server`} (the host is the MCP + * server on the postMessage wire, per SEP-1865) and an + * {@link ExtensionHandle `ExtensionHandle`} for the `ui/*` extension methods. + * Standard MCP requests from the iframe (`tools/call`, `resources/read`, …) are + * handled via the `Server`'s standard handlers and typically proxied to the + * real MCP server via the supplied `mcpClient`. */ -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +export class AppBridge extends EventDispatcher { + /** Underlying MCP Server (host ← iframe wire). */ + readonly server: Server; + /** SEP-2133 extension handle for `ui/*` methods. */ + readonly ui: ExtensionHandle; -/** - * Maps DOM-style event names to their notification `params` types. - * - * Used by {@link AppBridge `AppBridge`} to provide type-safe - * `addEventListener` / `removeEventListener` and singular `on*` handler - * support. - */ -export type AppBridgeEventMap = { - sizechange: McpUiSizeChangedNotification["params"]; - sandboxready: McpUiSandboxProxyReadyNotification["params"]; - initialized: McpUiInitializedNotification["params"]; - requestteardown: McpUiRequestTeardownNotification["params"]; - loggingmessage: LoggingMessageNotification["params"]; -}; - -/** - * Host-side bridge for communicating with a single View ({@link app!App `App`}). - * - * `AppBridge` extends the MCP SDK's `Protocol` class and acts as a proxy between - * the host application and a view running in an iframe. When an MCP client - * is provided to the constructor, it automatically forwards MCP server capabilities - * (tools, resources, prompts) to the view. It also handles the initialization - * handshake. - * - * ## Architecture - * - * **View ↔ AppBridge ↔ Host ↔ MCP Server** - * - * The bridge proxies requests from the view to the MCP server and forwards - * responses back. It also sends host-initiated notifications like tool input - * and results to the view. - * - * ## Lifecycle - * - * 1. **Create**: Instantiate `AppBridge` with MCP client and capabilities - * 2. **Connect**: Call `connect()` with transport to establish communication - * 3. **Wait for init**: View sends initialize request, bridge responds - * 4. **Send data**: Call {@link sendToolInput `sendToolInput`}, {@link sendToolResult `sendToolResult`}, etc. - * 5. **Teardown**: Call {@link teardownResource `teardownResource`} before unmounting iframe - * - * @example Basic usage - * ```ts source="./app-bridge.examples.ts#AppBridge_basicUsage" - * // Create MCP client for the server - * const client = new Client({ - * name: "MyHost", - * version: "1.0.0", - * }); - * await client.connect(serverTransport); - * - * // Create bridge for the View - * 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("View initialized"); - * // Now safe to send tool input - * bridge.sendToolInput({ arguments: { location: "NYC" } }); - * }; - * - * await bridge.connect(transport); - * ``` - */ -export class AppBridge extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppBridgeEventMap -> { + private _hostContext: McpUiHostContext; private _appCapabilities?: McpUiAppCapabilities; - private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; - protected readonly eventSchemas = { - sizechange: McpUiSizeChangedNotificationSchema, - sandboxready: McpUiSandboxProxyReadyNotificationSchema, - initialized: McpUiInitializedNotificationSchema, - requestteardown: McpUiRequestTeardownNotificationSchema, - loggingmessage: LoggingMessageNotificationSchema, - }; + /** Called when the view pings the host. */ + onping?: ( + params: PingRequest["params"], + extra: RequestHandlerExtra, + ) => void; + + /** Optional error handler. Mirrors the v1 `Protocol.onerror` slot. */ + onerror?: (error: Error) => void; + /** Called when the underlying transport closes. Mirrors v1 Protocol.onclose. */ + onclose?: () => void; - /** - * Create a new AppBridge instance. - * - * @param _client - MCP client connected to the server, or `null`. When provided, - * {@link connect `connect`} will automatically set up forwarding of MCP requests/notifications - * between the View and the server. When `null`, you must register handlers - * manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters. - * @param _hostInfo - Host application identification (name and version) - * @param _capabilities - Features and capabilities the host supports - * @param options - Configuration options (inherited from Protocol) - * - * @example With MCP client (automatic forwarding) - * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" - * const bridge = new AppBridge( - * mcpClient, - * { name: "MyHost", version: "1.0.0" }, - * { openLinks: {}, serverTools: {}, logging: {} }, - * ); - * ``` - * - * @example Without MCP client (manual handlers) - * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withoutMcpClient" - * const bridge = new AppBridge( - * null, - * { name: "MyHost", version: "1.0.0" }, - * { openLinks: {}, serverTools: {}, logging: {} }, - * ); - * bridge.oncalltool = async (params, extra) => { - * // Handle tool calls manually - * return { content: [] }; - * }; - * ``` - */ constructor( private _client: Client | null, private _hostInfo: Implementation, private _capabilities: McpUiHostCapabilities, options?: HostOptions, ) { - super(options); - + super(); this._hostContext = options?.hostContext || {}; - this.setRequestHandler(McpUiInitializeRequestSchema, (request) => - this._oninitialize(request), - ); - - this.setRequestHandler(PingRequestSchema, (request, extra) => { - this.onping?.(request.params, extra); - return {}; + this.server = new Server(_hostInfo, { + ...options, + capabilities: { + tools: {}, + resources: {}, + prompts: {}, + logging: {}, + }, + }); + this.server.onerror = (err) => this.onerror?.(err); + this.server.onclose = () => this.onclose?.(); + this.ui = this.server.extension(MCP_APPS_EXTENSION_ID, _capabilities, { + peerSchema: McpUiAppCapabilitiesSchema, }); - // Default handler for requestDisplayMode - returns current mode from host context. - // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - (request) => { - const currentMode = this._hostContext.displayMode ?? "inline"; - return { mode: currentMode }; + // ── ui/* request handlers ────────────────────────────────────────────── + + // ui/initialize — kept for v1-iframe wire compat (capabilities now also + // travel via MCP initialize via the ExtensionHandle). + this.ui.setRequestHandler( + McpUiInitializeRequestSchema.shape.method.value, + McpUiInitializeRequestSchema.shape.params, + (params) => this._oninitialize(params as McpUiInitializeRequest["params"]), + ); + + this.ui.setRequestHandler( + McpUiMessageRequestSchema.shape.method.value, + McpUiMessageRequestSchema.shape.params, + async (params, ctx) => { + if (!this._onmessage) + return { ok: false, error: "Host did not register onmessage" }; + return this._onmessage(params, toExtra(ctx)); + }, + ); + this.ui.setRequestHandler( + McpUiOpenLinkRequestSchema.shape.method.value, + McpUiOpenLinkRequestSchema.shape.params, + async (params, ctx) => { + if (!this._onopenlink) + return { opened: false, error: "Host did not register onopenlink" }; + return this._onopenlink(params, toExtra(ctx)); + }, + ); + this.ui.setRequestHandler( + McpUiDownloadFileRequestSchema.shape.method.value, + McpUiDownloadFileRequestSchema.shape.params, + async (params, ctx) => { + if (!this._ondownloadfile) + return { + downloaded: false, + error: "Host did not register ondownloadfile", + }; + return this._ondownloadfile(params, toExtra(ctx)); + }, + ); + this.ui.setRequestHandler( + McpUiRequestDisplayModeRequestSchema.shape.method.value, + McpUiRequestDisplayModeRequestSchema.shape.params, + async (params, ctx) => { + if (this._onrequestdisplaymode) + return this._onrequestdisplaymode(params, toExtra(ctx)); + return { mode: this._hostContext.displayMode ?? "inline" }; + }, + ); + this.ui.setRequestHandler( + McpUiUpdateModelContextRequestSchema.shape.method.value, + McpUiUpdateModelContextRequestSchema.shape.params, + async (params, ctx) => { + await this._onupdatemodelcontext?.(params, toExtra(ctx)); + return {}; }, ); - } - /** - * Get the view's capabilities discovered during initialization. - * - * Returns the capabilities that the view advertised during its - * initialization request. Returns `undefined` if called before - * initialization completes. - * - * @returns view capabilities, or `undefined` if not yet initialized - * - * @example Check view capabilities after initialization - * ```ts source="./app-bridge.examples.ts#AppBridge_getAppCapabilities_checkAfterInit" - * bridge.oninitialized = () => { - * const caps = bridge.getAppCapabilities(); - * if (caps?.tools) { - * console.log("View provides tools"); - * } - * }; - * ``` - * - * @see {@link McpUiAppCapabilities `McpUiAppCapabilities`} for the capabilities structure - */ - getAppCapabilities(): McpUiAppCapabilities | undefined { - return this._appCapabilities; + // ── ui/* notification → event dispatch ───────────────────────────────── + + for (const [event, { method, params }] of Object.entries( + BRIDGE_EVENT_NOTIFICATION_SCHEMAS, + )) { + this.ui.setNotificationHandler(method, params as never, (p) => + this.dispatchEvent(event as keyof AppBridgeEventMap, p), + ); + } + + // v1-compat: also accept legacy `notifications/message` from v1 iframes + // (v2 iframes send `ui/log`). See BREAKING.md § v1↔v2 interop. + this.server.setNotificationHandler("notifications/message", (n) => + this.dispatchEvent("loggingmessage", n.params), + ); + + // ── Standard MCP requests from iframe (proxy to real server) ─────────── + + this.server.setRequestHandler("tools/call", async (req, ctx) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(req.params, toExtra(ctx)); + }); + this.server.setRequestHandler("tools/list", async (req, ctx) => { + if (!this._onlisttools) return { tools: [] }; + return this._onlisttools(req.params, toExtra(ctx)); + }); + this.server.setRequestHandler("resources/list", async (req, ctx) => { + if (!this._onlistresources) return { resources: [] }; + return this._onlistresources(req.params, toExtra(ctx)); + }); + this.server.setRequestHandler( + "resources/templates/list", + async (req, ctx) => { + if (!this._onlistresourcetemplates) return { resourceTemplates: [] }; + return this._onlistresourcetemplates(req.params, toExtra(ctx)); + }, + ); + this.server.setRequestHandler("resources/read", async (req, ctx) => { + if (!this._onreadresource) + throw new Error("No onreadresource handler set"); + return this._onreadresource(req.params, toExtra(ctx)); + }); + this.server.setRequestHandler("prompts/list", async (req, ctx) => { + if (!this._onlistprompts) return { prompts: [] }; + return this._onlistprompts(req.params, toExtra(ctx)); + }); + this.server.setRequestHandler("ping", (req, ctx) => { + this.onping?.(req.params, toExtra(ctx)); + return {}; + }); } - /** - * Get the view's implementation info discovered during initialization. - * - * Returns the view's name and version as provided in its initialization - * request. Returns `undefined` if called before initialization completes. - * - * @returns view implementation info, or `undefined` if not yet initialized - * - * @example Log view information after initialization - * ```ts source="./app-bridge.examples.ts#AppBridge_getAppVersion_logAfterInit" - * bridge.oninitialized = () => { - * const appInfo = bridge.getAppVersion(); - * if (appInfo) { - * console.log(`View: ${appInfo.name} v${appInfo.version}`); - * } - * }; - * ``` - */ - getAppVersion(): Implementation | undefined { - return this._appInfo; + private _oninitialize( + params: McpUiInitializeRequest["params"], + ): McpUiInitializeResult { + this._appCapabilities = params.appCapabilities; + this._appInfo = params.appInfo; + return { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities: this._capabilities, + hostInfo: this._hostInfo, + hostContext: this._hostContext, + }; } - /** - * Optional handler for ping requests from the view. - * - * The View can send standard MCP `ping` requests to verify the connection - * is alive. The {@link AppBridge `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 - * ```ts source="./app-bridge.examples.ts#AppBridge_onping_handleRequest" - * bridge.onping = (params, extra) => { - * console.log("Received ping from view"); - * }; - * ``` - */ - onping?: (params: PingRequest["params"], extra: RequestHandlerExtra) => void; + // ── App info / capabilities ─────────────────────────────────────────────── - /** - * Register a handler for size change notifications from the view. - * - * The view sends `ui/notifications/size-changed` when its rendered content - * size changes, typically via `ResizeObserver`. Set this callback to dynamically - * adjust the iframe container dimensions based on the view's content. - * - * Note: This is for View → Host communication. To notify the View of - * host container dimension changes, use {@link setHostContext `setHostContext`}. - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onsizechange_handleResize" - * bridge.onsizechange = ({ width, height }) => { - * if (width != null) { - * iframe.style.width = `${width}px`; - * } - * if (height != null) { - * iframe.style.height = `${height}px`; - * } - * }; - * ``` - * - * @see {@link McpUiSizeChangedNotification `McpUiSizeChangedNotification`} for the notification type - * @see {@link app!App.sendSizeChanged `App.sendSizeChanged`} - the View method that sends these notifications - * @deprecated Use {@link addEventListener `addEventListener("sizechange", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - */ - get onsizechange(): - | ((params: McpUiSizeChangedNotification["params"]) => void) - | undefined { - return this.getEventHandler("sizechange"); - } - set onsizechange( - callback: - | ((params: McpUiSizeChangedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("sizechange", callback); + /** App capabilities advertised by the view. */ + get appCapabilities(): McpUiAppCapabilities | undefined { + return this.ui.getPeerSettings() ?? this._appCapabilities; } /** - * 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 `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 `McpUiSandboxProxyReadyNotification`} for the notification type - * @see {@link sendSandboxResourceReady `sendSandboxResourceReady`} for sending content to the sandbox - * @deprecated Use {@link addEventListener `addEventListener("sandboxready", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. + * `true` if the connected iframe advertised via `capabilities.extensions` + * (ext-apps v2). `false` if capabilities came only via `ui/initialize` + * (ext-apps v1). `undefined` before any handshake. */ - get onsandboxready(): - | ((params: McpUiSandboxProxyReadyNotification["params"]) => void) - | undefined { - return this.getEventHandler("sandboxready"); + private get _isV2Iframe(): boolean | undefined { + if (this.ui.getPeerSettings() !== undefined) return true; + if (this._appCapabilities !== undefined) return false; + return undefined; } - set onsandboxready( - callback: - | ((params: McpUiSandboxProxyReadyNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("sandboxready", callback); + /** @deprecated Use {@link appCapabilities `appCapabilities`}. */ + getAppCapabilities(): McpUiAppCapabilities | undefined { + return this.appCapabilities; } - /** - * Called when the view completes initialization. - * - * Set this callback to be notified when the view has finished its - * initialization handshake and is ready to receive tool input and other data. - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_oninitialized_sendToolInput" - * bridge.oninitialized = () => { - * console.log("View ready"); - * bridge.sendToolInput({ arguments: toolArgs }); - * }; - * ``` - * - * @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the notification type - * @see {@link sendToolInput `sendToolInput`} for sending tool arguments to the View - * @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - */ - get oninitialized(): - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined { - return this.getEventHandler("initialized"); + /** App implementation info from `ui/initialize`. */ + getAppVersion(): Implementation | undefined { + return this._appInfo; } - set oninitialized( - callback: - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("initialized", callback); + + /** Host capabilities passed to the constructor. */ + getCapabilities(): McpUiHostCapabilities { + return this._capabilities; } - /** - * Register a handler for message requests from the view. - * - * The view 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 - * ```ts source="./app-bridge.examples.ts#AppBridge_onmessage_logMessage" - * bridge.onmessage = async ({ role, content }, extra) => { - * try { - * await chatManager.addMessage({ role, content, source: "app" }); - * return {}; // Success - * } catch (error) { - * console.error("Failed to add message:", error); - * return { isError: true }; - * } - * }; - * ``` - * - * @see {@link McpUiMessageRequest `McpUiMessageRequest`} for the request type - * @see {@link McpUiMessageResult `McpUiMessageResult`} for the result type - */ + // ── Notification on* setters (DOM-style) ────────────────────────────────── + + get onsizechange() { return this.getEventHandler("sizechange"); } + set onsizechange(h) { this.setEventHandler("sizechange", h); } + + get onsandboxready() { return this.getEventHandler("sandboxready"); } + set onsandboxready(h) { this.setEventHandler("sandboxready", h); } + + get oninitialized() { return this.getEventHandler("initialized"); } + set oninitialized(h) { this.setEventHandler("initialized", h); } + + get onrequestteardown() { return this.getEventHandler("requestteardown"); } + set onrequestteardown(h) { this.setEventHandler("requestteardown", h); } + + get onloggingmessage() { return this.getEventHandler("loggingmessage"); } + set onloggingmessage(h) { this.setEventHandler("loggingmessage", h); } + + // ── Request on* setters ─────────────────────────────────────────────────── + private _onmessage?: ( params: McpUiMessageRequest["params"], extra: RequestHandlerExtra, - ) => Promise; - get onmessage() { - return this._onmessage; - } - set onmessage( - callback: - | (( - params: McpUiMessageRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); - this._onmessage = callback; - this.replaceRequestHandler( - McpUiMessageRequestSchema, - async (request, extra) => { - if (!this._onmessage) throw new Error("No onmessage handler set"); - return this._onmessage(request.params, extra); - }, - ); + ) => Promise | McpUiMessageResult; + get onmessage() { return this._onmessage; } + set onmessage(cb) { + this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, cb); + this._onmessage = cb; } - /** - * Register a handler for external link requests from the view. - * - * The view 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 - * ```ts source="./app-bridge.examples.ts#AppBridge_onopenlink_handleRequest" - * bridge.onopenlink = async ({ url }, extra) => { - * if (!isAllowedDomain(url)) { - * console.warn("Blocked external link:", url); - * return { isError: true }; - * } - * - * const confirmed = await showDialog({ - * message: `Open external link?\n${url}`, - * buttons: ["Open", "Cancel"], - * }); - * - * if (confirmed) { - * window.open(url, "_blank", "noopener,noreferrer"); - * return {}; - * } - * - * return { isError: true }; - * }; - * ``` - * - * @see {@link McpUiOpenLinkRequest `McpUiOpenLinkRequest`} for the request type - * @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for the result type - */ private _onopenlink?: ( params: McpUiOpenLinkRequest["params"], extra: RequestHandlerExtra, - ) => Promise; - get onopenlink() { - return this._onopenlink; - } - set onopenlink( - callback: - | (( - params: McpUiOpenLinkRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); - this._onopenlink = callback; - this.replaceRequestHandler( - McpUiOpenLinkRequestSchema, - async (request, extra) => { - if (!this._onopenlink) throw new Error("No onopenlink handler set"); - return this._onopenlink(request.params, extra); - }, - ); + ) => Promise | McpUiOpenLinkResult; + get onopenlink() { return this._onopenlink; } + set onopenlink(cb) { + this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, cb); + this._onopenlink = cb; } - /** - * Register a handler for file download requests from the View. - * - * The View sends `ui/download-file` requests when the user wants to - * download a file. The params contain an array of MCP resource content - * items — either `EmbeddedResource` (inline data) or `ResourceLink` - * (URI the host can fetch). The host should show a confirmation dialog - * and then trigger the download. - * - * @param callback - Handler that receives download params and returns a result - * - `params.contents` - Array of `EmbeddedResource` or `ResourceLink` items - * - `extra` - Request metadata (abort signal, session info) - * - Returns: `Promise` with optional `isError` flag - * - * @example - * ```ts - * bridge.ondownloadfile = async ({ contents }, extra) => { - * for (const item of contents) { - * if (item.type === "resource") { - * // EmbeddedResource — inline content - * const res = item.resource; - * const blob = res.blob - * ? new Blob([Uint8Array.from(atob(res.blob), c => c.charCodeAt(0))], { type: res.mimeType }) - * : new Blob([res.text ?? ""], { type: res.mimeType }); - * const url = URL.createObjectURL(blob); - * const link = document.createElement("a"); - * link.href = url; - * link.download = res.uri.split("/").pop() ?? "download"; - * link.click(); - * URL.revokeObjectURL(url); - * } else if (item.type === "resource_link") { - * // ResourceLink — host fetches or opens directly - * window.open(item.uri, "_blank"); - * } - * } - * return {}; - * }; - * ``` - * - * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for the request type - * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for the result type - */ private _ondownloadfile?: ( params: McpUiDownloadFileRequest["params"], extra: RequestHandlerExtra, - ) => Promise; - get ondownloadfile() { - return this._ondownloadfile; - } - set ondownloadfile( - callback: - | (( - params: McpUiDownloadFileRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced( - "ondownloadfile", - this._ondownloadfile, - callback, - ); - this._ondownloadfile = callback; - this.replaceRequestHandler( - McpUiDownloadFileRequestSchema, - async (request, extra) => { - if (!this._ondownloadfile) - throw new Error("No ondownloadfile handler set"); - return this._ondownloadfile(request.params, extra); - }, - ); + ) => Promise | McpUiDownloadFileResult; + get ondownloadfile() { return this._ondownloadfile; } + set ondownloadfile(cb) { + this.warnIfRequestHandlerReplaced("ondownloadfile", this._ondownloadfile, cb); + this._ondownloadfile = cb; } - /** - * Register a handler for app-initiated teardown request notifications from the view. - * - * The view sends `ui/notifications/request-teardown` when it wants the host to tear it down. - * If the host decides to proceed, it should send - * `ui/resource-teardown` (via {@link teardownResource `teardownResource`}) to allow - * the view to perform gracefull termination, then unmount the iframe after the view responds. - * - * @example - * ```typescript - * bridge.onrequestteardown = async (params) => { - * console.log("App requested teardown"); - * // Initiate teardown to allow the app to persist unsaved state - * // Alternatively, the callback can early return to prevent teardown - * await bridge.teardownResource({}); - * // Now safe to unmount the iframe - * iframe.remove(); - * }; - * ``` - * - * @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for the notification type - * @see {@link teardownResource `teardownResource`} for initiating teardown - * @deprecated Use {@link addEventListener `addEventListener("requestteardown", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - */ - get onrequestteardown(): - | ((params: McpUiRequestTeardownNotification["params"]) => void) - | undefined { - return this.getEventHandler("requestteardown"); - } - set onrequestteardown( - callback: - | ((params: McpUiRequestTeardownNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("requestteardown", callback); - } - - /** - * Register a handler for display mode change requests from the view. - * - * The view sends `ui/request-display-mode` requests when it wants to change - * its display mode (e.g., from "inline" to "fullscreen"). The handler should - * check if the requested mode is in `availableDisplayModes` from the host context, - * update the display mode if supported, and return the actual mode that was set. - * - * If the requested mode is not available, the handler should return the current - * display mode instead. - * - * By default, `AppBridge` returns the current `displayMode` from host context (or "inline"). - * Setting this property replaces that default behavior. - * - * @param callback - Handler that receives the requested mode and returns the actual mode set - * - `params.mode` - The display mode being requested ("inline" | "fullscreen" | "pip") - * - `extra` - Request metadata (abort signal, session info) - * - Returns: `Promise` with the actual mode set - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onrequestdisplaymode_handleRequest" - * bridge.onrequestdisplaymode = async ({ mode }, extra) => { - * if (availableDisplayModes.includes(mode)) { - * currentDisplayMode = mode; - * } - * return { mode: currentDisplayMode }; - * }; - * ``` - * - * @see {@link McpUiRequestDisplayModeRequest `McpUiRequestDisplayModeRequest`} for the request type - * @see {@link McpUiRequestDisplayModeResult `McpUiRequestDisplayModeResult`} for the result type - */ private _onrequestdisplaymode?: ( params: McpUiRequestDisplayModeRequest["params"], extra: RequestHandlerExtra, - ) => Promise; - get onrequestdisplaymode() { - return this._onrequestdisplaymode; - } - set onrequestdisplaymode( - callback: - | (( - params: McpUiRequestDisplayModeRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { + ) => Promise | McpUiRequestDisplayModeResult; + get onrequestdisplaymode() { return this._onrequestdisplaymode; } + set onrequestdisplaymode(cb) { this.warnIfRequestHandlerReplaced( "onrequestdisplaymode", this._onrequestdisplaymode, - callback, - ); - this._onrequestdisplaymode = callback; - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - async (request, extra) => { - if (!this._onrequestdisplaymode) - throw new Error("No onrequestdisplaymode handler set"); - return this._onrequestdisplaymode(request.params, extra); - }, + cb, ); + this._onrequestdisplaymode = cb; } - /** - * Register a handler for logging messages from the view. - * - * The view 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. - * - * The handler receives `LoggingMessageNotification["params"]`: - * - `level` — "debug" | "info" | "notice" | "warning" | "error" | "critical" | "alert" | "emergency" - * - `logger` — optional logger name/identifier - * - `data` — log message and optional structured data - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onloggingmessage_handleLog" - * bridge.onloggingmessage = ({ level, logger, data }) => { - * console[level === "error" ? "error" : "log"]( - * `[${logger ?? "View"}] ${level.toUpperCase()}:`, - * data, - * ); - * }; - * ``` - * @deprecated Use {@link addEventListener `addEventListener("loggingmessage", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - */ - get onloggingmessage(): - | ((params: LoggingMessageNotification["params"]) => void) - | undefined { - return this.getEventHandler("loggingmessage"); - } - set onloggingmessage( - callback: - | ((params: LoggingMessageNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("loggingmessage", callback); - } - - /** - * Register a handler for model context updates from the view. - * - * The view sends `ui/update-model-context` requests to update the Host's - * model context. Each request overwrites the previous context stored by the view. - * Unlike logging messages, context updates are intended to be available to - * the model in future turns. Unlike messages, context updates do not trigger follow-ups. - * - * The host will typically defer sending the context to the model until the - * next user message (including `ui/message`), and will only send the last - * update received. - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onupdatemodelcontext_storeContext" - * bridge.onupdatemodelcontext = async ( - * { content, structuredContent }, - * extra, - * ) => { - * // Store the context snapshot for inclusion in the next model request - * modelContextManager.update({ content, structuredContent }); - * return {}; - * }; - * ``` - * - * @see {@link McpUiUpdateModelContextRequest `McpUiUpdateModelContextRequest`} for the request type - */ private _onupdatemodelcontext?: ( params: McpUiUpdateModelContextRequest["params"], extra: RequestHandlerExtra, - ) => Promise; - get onupdatemodelcontext() { - return this._onupdatemodelcontext; - } - set onupdatemodelcontext( - callback: - | (( - params: McpUiUpdateModelContextRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { + ) => Promise | void | object; + get onupdatemodelcontext() { return this._onupdatemodelcontext; } + set onupdatemodelcontext(cb) { this.warnIfRequestHandlerReplaced( "onupdatemodelcontext", this._onupdatemodelcontext, - callback, - ); - this._onupdatemodelcontext = callback; - this.replaceRequestHandler( - McpUiUpdateModelContextRequestSchema, - async (request, extra) => { - if (!this._onupdatemodelcontext) - throw new Error("No onupdatemodelcontext handler set"); - return this._onupdatemodelcontext(request.params, extra); - }, + cb, ); + this._onupdatemodelcontext = cb; } - /** - * Register a handler for tool call requests from the view. - * - * The view sends `tools/call` requests to execute MCP server tools. This - * handler allows the host to intercept and process these requests, typically - * by forwarding them to the MCP server. - * - * @param callback - Handler that receives tool call params and returns a - * `CallToolResult` - * - `params` - Tool call parameters (name and arguments) - * - `extra` - Request metadata (abort signal, session info) - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_oncalltool_forwardToServer" - * bridge.oncalltool = async (params, extra) => { - * return mcpClient.request( - * { method: "tools/call", params }, - * CallToolResultSchema, - * { signal: extra.signal }, - * ); - * }; - * ``` - * - * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type - * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type - */ private _oncalltool?: ( params: CallToolRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get oncalltool() { - return this._oncalltool; - } - set oncalltool( - callback: - | (( - params: CallToolRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); - this._oncalltool = callback; - this.replaceRequestHandler( - CallToolRequestSchema, - async (request, extra) => { - if (!this._oncalltool) throw new Error("No oncalltool handler set"); - return this._oncalltool(request.params, extra); - }, - ); + get oncalltool() { return this._oncalltool; } + set oncalltool(cb) { + this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, cb); + this._oncalltool = cb; } - /** - * Notify the view that the MCP server's tool list has changed. - * - * The host sends `notifications/tools/list_changed` to the view when it - * receives this notification from the MCP server. This allows the view - * to refresh its tool cache or UI accordingly. - * - * @param params - Optional notification params (typically empty) - * - * @example - * ```typescript - * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { - * bridge.sendToolListChanged(); - * }); - * ``` - * - * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type - */ - sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { - return this.notification({ - method: "notifications/tools/list_changed" as const, - params, - }); + private _onlisttools?: ( + params: ListToolsRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get onlisttools() { return this._onlisttools; } + set onlisttools(cb) { + this.warnIfRequestHandlerReplaced("onlisttools", this._onlisttools, cb); + this._onlisttools = cb; } - /** - * Register a handler for list resources requests from the view. - * - * The view sends `resources/list` requests to enumerate available MCP - * resources. This handler allows the host to intercept and process these - * requests, typically by forwarding them to the MCP server. - * - * @param callback - Handler that receives list params and returns a - * `ListResourcesResult` - * - `params` - Request params (may include cursor for pagination) - * - `extra` - Request metadata (abort signal, session info) - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onlistresources_returnResources" - * bridge.onlistresources = async (params, extra) => { - * return mcpClient.request( - * { method: "resources/list", params }, - * ListResourcesResultSchema, - * { signal: extra.signal }, - * ); - * }; - * ``` - * - * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type - */ private _onlistresources?: ( params: ListResourcesRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get onlistresources() { - return this._onlistresources; - } - set onlistresources( - callback: - | (( - params: ListResourcesRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { + get onlistresources() { return this._onlistresources; } + set onlistresources(cb) { this.warnIfRequestHandlerReplaced( "onlistresources", this._onlistresources, - callback, - ); - this._onlistresources = callback; - this.replaceRequestHandler( - ListResourcesRequestSchema, - async (request, extra) => { - if (!this._onlistresources) - throw new Error("No onlistresources handler set"); - return this._onlistresources(request.params, extra); - }, + cb, ); + this._onlistresources = cb; } - /** - * Register a handler for list resource templates requests from the view. - * - * The view sends `resources/templates/list` requests to enumerate available - * MCP resource templates. This handler allows the host to intercept and process - * these requests, typically by forwarding them to the MCP server. - * - * @param callback - Handler that receives list params and returns a - * `ListResourceTemplatesResult` - * - `params` - Request params (may include cursor for pagination) - * - `extra` - Request metadata (abort signal, session info) - * - * @example - * ```typescript - * bridge.onlistresourcetemplates = async (params, extra) => { - * return mcpClient.request( - * { method: "resources/templates/list", params }, - * ListResourceTemplatesResultSchema, - * { signal: extra.signal } - * ); - * }; - * ``` - * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type - */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get onlistresourcetemplates() { - return this._onlistresourcetemplates; - } - set onlistresourcetemplates( - callback: - | (( - params: ListResourceTemplatesRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { + get onlistresourcetemplates() { return this._onlistresourcetemplates; } + set onlistresourcetemplates(cb) { this.warnIfRequestHandlerReplaced( "onlistresourcetemplates", this._onlistresourcetemplates, - callback, - ); - this._onlistresourcetemplates = callback; - this.replaceRequestHandler( - ListResourceTemplatesRequestSchema, - async (request, extra) => { - if (!this._onlistresourcetemplates) - throw new Error("No onlistresourcetemplates handler set"); - return this._onlistresourcetemplates(request.params, extra); - }, + cb, ); + this._onlistresourcetemplates = cb; } - /** - * Register a handler for read resource requests from the view. - * - * The view sends `resources/read` requests to retrieve the contents of an - * MCP resource. This handler allows the host to intercept and process these - * requests, typically by forwarding them to the MCP server. - * - * @param callback - Handler that receives read params and returns a - * `ReadResourceResult` - * - `params` - Read parameters including the resource URI - * - `extra` - Request metadata (abort signal, session info) - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onreadresource_returnResource" - * bridge.onreadresource = async (params, extra) => { - * return mcpClient.request( - * { method: "resources/read", params }, - * ReadResourceResultSchema, - * { signal: extra.signal }, - * ); - * }; - * ``` - * - * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type - */ private _onreadresource?: ( params: ReadResourceRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get onreadresource() { - return this._onreadresource; - } - set onreadresource( - callback: - | (( - params: ReadResourceRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { + get onreadresource() { return this._onreadresource; } + set onreadresource(cb) { this.warnIfRequestHandlerReplaced( "onreadresource", this._onreadresource, - callback, - ); - this._onreadresource = callback; - this.replaceRequestHandler( - ReadResourceRequestSchema, - async (request, extra) => { - if (!this._onreadresource) - throw new Error("No onreadresource handler set"); - return this._onreadresource(request.params, extra); - }, + cb, ); + this._onreadresource = cb; } - /** - * Notify the view that the MCP server's resource list has changed. - * - * The host sends `notifications/resources/list_changed` to the view when it - * receives this notification from the MCP server. This allows the view - * to refresh its resource cache or UI accordingly. - * - * @param params - Optional notification params (typically empty) - * - * @example - * ```typescript - * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { - * bridge.sendResourceListChanged(); - * }); - * ``` - * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type - */ - sendResourceListChanged( - params: ResourceListChangedNotification["params"] = {}, - ) { - return this.notification({ - method: "notifications/resources/list_changed" as const, - params, - }); - } - - /** - * Register a handler for list prompts requests from the view. - * - * The view sends `prompts/list` requests to enumerate available MCP - * prompts. This handler allows the host to intercept and process these - * requests, typically by forwarding them to the MCP server. - * - * @param callback - Handler that receives list params and returns a - * `ListPromptsResult` - * - `params` - Request params (may include cursor for pagination) - * - `extra` - Request metadata (abort signal, session info) - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_onlistprompts_returnPrompts" - * bridge.onlistprompts = async (params, extra) => { - * return mcpClient.request( - * { method: "prompts/list", params }, - * ListPromptsResultSchema, - * { signal: extra.signal }, - * ); - * }; - * ``` - * - * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type - */ private _onlistprompts?: ( params: ListPromptsRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get onlistprompts() { - return this._onlistprompts; - } - set onlistprompts( - callback: - | (( - params: ListPromptsRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced( - "onlistprompts", - this._onlistprompts, - callback, - ); - this._onlistprompts = callback; - this.replaceRequestHandler( - ListPromptsRequestSchema, - async (request, extra) => { - if (!this._onlistprompts) - throw new Error("No onlistprompts handler set"); - return this._onlistprompts(request.params, extra); - }, - ); + get onlistprompts() { return this._onlistprompts; } + set onlistprompts(cb) { + this.warnIfRequestHandlerReplaced("onlistprompts", this._onlistprompts, cb); + this._onlistprompts = cb; } - /** - * Notify the view that the MCP server's prompt list has changed. - * - * The host sends `notifications/prompts/list_changed` to the view when it - * receives this notification from the MCP server. This allows the view - * to refresh its prompt cache or UI accordingly. - * - * @param params - Optional notification params (typically empty) - * - * @example - * ```typescript - * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { - * bridge.sendPromptListChanged(); - * }); - * ``` - * - * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type - */ - sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { - return this.notification({ - method: "notifications/prompts/list_changed" as const, - params, - }); - } + // ── Outbound: standard MCP server→client notifications ──────────────────── - /** - * Verify that the guest supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO + /** Notify the view that the MCP server's tool list changed. */ + sendToolListChanged(_params?: object) { + return this.server.sendToolListChanged(); } - - /** - * Verify that a request handler is registered and supported for the given method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - // TODO + /** Notify the view that the MCP server's resource list changed. */ + sendResourceListChanged(_params?: object) { + return this.server.sendResourceListChanged(); } - - /** - * Verify that the host supports the capability required for the given notification method. - * @internal - */ - assertNotificationCapability(method: AppNotification["method"]): void { - // TODO + /** Notify the view that the MCP server's prompt list changed. */ + sendPromptListChanged(_params?: object) { + return this.server.sendPromptListChanged(); } - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } + // ── Outbound: ui/* notifications ────────────────────────────────────────── /** - * Verify that task handler is supported for the given method. - * @internal + * Update host context and notify the view of changed fields. + * Call this when theme, locale, displayMode, etc. change. */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); - } - - /** - * Get the host capabilities passed to the constructor. - * - * @returns Host capabilities object - * - * @see {@link McpUiHostCapabilities `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 { - const requestedVersion = request.params.protocolVersion; - - this._appCapabilities = request.params.appCapabilities; - this._appInfo = request.params.appInfo; - - const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( - requestedVersion, - ) - ? requestedVersion - : LATEST_PROTOCOL_VERSION; - - return { - protocolVersion, - hostCapabilities: this.getCapabilities(), - hostInfo: this._hostInfo, - hostContext: this._hostContext, - }; - } - - /** - * Update the host context and notify the view of changes. - * - * Compares fields present in the new context with the current context and sends a - * `ui/notifications/host-context-changed` notification containing only fields - * that have been added or modified. If no fields have changed, no notification is sent. - * The new context fully replaces the internal state. - * - * Common use cases include notifying the view when: - * - Theme changes (light/dark mode toggle) - * - Viewport size changes (window resize) - * - Display mode changes (inline/fullscreen) - * - Locale or timezone changes - * - * @param hostContext - The complete new host context state - * - * @example Update theme when user toggles dark mode - * ```ts source="./app-bridge.examples.ts#AppBridge_setHostContext_updateTheme" - * bridge.setHostContext({ theme: "dark" }); - * ``` - * - * @example Update multiple context fields - * ```ts source="./app-bridge.examples.ts#AppBridge_setHostContext_updateMultiple" - * bridge.setHostContext({ - * theme: "dark", - * containerDimensions: { maxHeight: 600, width: 800 }, - * }); - * ``` - * - * @see {@link McpUiHostContext `McpUiHostContext`} for the context structure - * @see {@link McpUiHostContextChangedNotification `McpUiHostContextChangedNotification`} for the notification type - */ - setHostContext(hostContext: McpUiHostContext) { - const changes: McpUiHostContext = {}; - let hasChanges = false; - for (const key of Object.keys(hostContext) as Array< - keyof McpUiHostContext - >) { - const oldValue = this._hostContext[key]; - const newValue = hostContext[key]; - if (deepEqual(oldValue, newValue)) { - continue; - } - changes[key] = newValue as any; - hasChanges = true; - } - if (hasChanges) { - this._hostContext = hostContext; - this.sendHostContextChange(changes); + setHostContext(context: Partial) { + const changed: Partial = {}; + for (const [k, v] of Object.entries(context) as [keyof McpUiHostContext, unknown][]) { + if (this._hostContext[k] !== v) (changed as Record)[k] = v; } + this._hostContext = { ...this._hostContext, ...context }; + if (Object.keys(changed).length === 0) return Promise.resolve(); + return this.sendHostContextChanged(changed); } - /** - * Low-level method to notify the view of host context changes. - * - * Most hosts should use {@link setHostContext `setHostContext`} instead, which automatically - * detects changes and calls this method with only the modified fields. - * Use this directly only when you need fine-grained control over change detection. - * - * @param params - The context fields that have changed (partial update) - */ - sendHostContextChange( + /** Low-level: send a host-context-changed notification with the given diff. */ + sendHostContextChanged( params: McpUiHostContextChangedNotification["params"], - ): Promise | void { - return this.notification({ - method: "ui/notifications/host-context-changed" as const, + ) { + return this.ui.sendNotification( + "ui/notifications/host-context-changed", params, - }); + ); } - /** - * Send complete tool arguments to the view. - * - * The host MUST send this notification after the View completes initialization - * (after {@link oninitialized `oninitialized`} callback fires) and complete tool arguments become available. - * This notification is sent exactly once and is required before {@link sendToolResult `sendToolResult`}. - * - * @param params - Complete tool call arguments - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolInput_afterInit" - * bridge.oninitialized = () => { - * bridge.sendToolInput({ - * arguments: { location: "New York", units: "metric" }, - * }); - * }; - * ``` - * - * @see {@link McpUiToolInputNotification `McpUiToolInputNotification`} for the notification type - * @see {@link oninitialized `oninitialized`} for the initialization callback - * @see {@link sendToolResult `sendToolResult`} for sending results after execution - */ + /** Send tool input arguments to the view (after streaming completes). */ sendToolInput(params: McpUiToolInputNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-input" as const, - params, - }); + return this.ui.sendNotification("ui/notifications/tool-input", params); } - /** - * Send streaming partial tool arguments to the view. - * - * The host MAY send this notification zero or more times while tool arguments - * are being streamed, before {@link sendToolInput `sendToolInput`} is called with complete - * arguments. This enables progressive rendering of tool arguments in the - * view. - * - * The arguments represent best-effort recovery of incomplete JSON. views - * SHOULD handle missing or changing fields gracefully between notifications. - * - * @param params - Partial tool call arguments (may be incomplete) - * - * @example Stream partial arguments as they arrive - * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolInputPartial_streaming" - * // As streaming progresses... - * bridge.sendToolInputPartial({ arguments: { loc: "N" } }); - * bridge.sendToolInputPartial({ arguments: { location: "New" } }); - * bridge.sendToolInputPartial({ arguments: { location: "New York" } }); - * - * // When complete, send final input - * bridge.sendToolInput({ - * arguments: { location: "New York", units: "metric" }, - * }); - * ``` - * - * @see {@link McpUiToolInputPartialNotification `McpUiToolInputPartialNotification`} for the notification type - * @see {@link sendToolInput `sendToolInput`} for sending complete arguments - */ + /** Send partial (still-streaming) tool input arguments to the view. */ sendToolInputPartial(params: McpUiToolInputPartialNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-input-partial" as const, + return this.ui.sendNotification( + "ui/notifications/tool-input-partial", params, - }); + ); } - /** - * Send tool execution result to the view. - * - * The host MUST send this notification when tool execution completes successfully, - * provided the view is still displayed. If the view was closed before execution - * completes, the host MAY skip this notification. This must be sent after - * {@link sendToolInput `sendToolInput`}. - * - * @param params - Standard MCP tool execution result - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution" - * const result = await mcpClient.request( - * { method: "tools/call", params: { name: "get_weather", arguments: args } }, - * CallToolResultSchema, - * ); - * bridge.sendToolResult(result); - * ``` - * - * @see {@link McpUiToolResultNotification `McpUiToolResultNotification`} for the notification type - * @see {@link sendToolInput `sendToolInput`} for sending tool arguments before results - */ + /** Send tool execution result to the view. */ sendToolResult(params: McpUiToolResultNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-result" as const, - params, - }); + return this.ui.sendNotification("ui/notifications/tool-result", params); } - /** - * Notify the view that tool execution was cancelled. - * - * The host MUST send this notification if tool execution was cancelled for any - * reason, including user action, sampling error, classifier intervention, or - * any other interruption. This allows the view to update its state and - * display appropriate feedback to the user. - * - * @param params - Cancellation details object - * - `reason`: Human-readable explanation for why the tool was cancelled - * - * @example User-initiated cancellation - * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolCancelled_userInitiated" - * // User clicked "Cancel" button - * bridge.sendToolCancelled({ reason: "User cancelled the operation" }); - * ``` - * - * @example System-level cancellation - * ```ts source="./app-bridge.examples.ts#AppBridge_sendToolCancelled_systemLevel" - * // Sampling error or timeout - * bridge.sendToolCancelled({ reason: "Request timeout after 30 seconds" }); - * - * // Classifier intervention - * bridge.sendToolCancelled({ reason: "Content policy violation detected" }); - * ``` - * - * @see {@link McpUiToolCancelledNotification `McpUiToolCancelledNotification`} for the notification type - * @see {@link sendToolResult `sendToolResult`} for sending successful results - * @see {@link sendToolInput `sendToolInput`} for sending tool arguments - */ + /** Notify the view that the tool call was cancelled. */ sendToolCancelled(params: McpUiToolCancelledNotification["params"]) { - return this.notification({ - method: "ui/notifications/tool-cancelled" as const, - params, - }); + return this.ui.sendNotification("ui/notifications/tool-cancelled", params); } - /** - * 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 `onsandboxready`} for handling the sandbox proxy ready notification - */ + /** Tell the sandbox proxy that the resource HTML is ready to load. */ sendSandboxResourceReady( params: McpUiSandboxResourceReadyNotification["params"], ) { - return this.notification({ - method: "ui/notifications/sandbox-resource-ready" as const, + return this.ui.sendNotification( + "ui/notifications/sandbox-resource-ready", params, - }); + ); } + // ── Outbound: ui/* and view-tool requests ───────────────────────────────── + /** - * Request graceful shutdown of the view. - * - * The host MUST send this request before tearing down the UI resource (before - * unmounting the iframe). This gives the view 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 view confirms readiness for teardown - * - * @example - * ```ts source="./app-bridge.examples.ts#AppBridge_teardownResource_gracefulShutdown" - * try { - * await bridge.teardownResource({}); - * // View is ready, safe to unmount iframe - * iframe.remove(); - * } catch (error) { - * console.error("Teardown failed:", error); - * } - * ``` + * Ask the view to clean up before unmount. Await before removing the iframe. */ teardownResource( params: McpUiResourceTeardownRequest["params"], options?: RequestOptions, ) { - return this.request( - { - method: "ui/resource-teardown" as const, - params, - }, + return this.ui.sendRequest( + "ui/resource-teardown", + params, McpUiResourceTeardownResultSchema, options, ); } - - /** @deprecated Use {@link teardownResource `teardownResource`} instead */ - sendResourceTeardown: AppBridge["teardownResource"] = this.teardownResource; + /** @deprecated Use {@link teardownResource `teardownResource`}. */ + get sendResourceTeardown() { return this.teardownResource; } /** - * Call a tool on the view. - * - * Sends a `tools/call` request to the view and returns the result. + * Call a tool the **view** exposes (see {@link app!App.oncalltool}). * - * @param params - Tool call parameters (name and arguments) - * @param options - Request options (timeout, abort signal, etc.) - * @returns Promise resolving to the tool call result + * Wire method: `ui/call-view-tool` (renamed from `tools/call` in v2). */ callTool(params: CallToolRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/call", params }, - CallToolResultSchema, + if (this._isV2Iframe === false) { + throw new Error( + "bridge.callTool(): connected iframe is ext-apps v1 (capabilities arrived via ui/initialize only). " + + "Host→iframe tool calls use the v2-only 'ui/call-view-tool' wire method; upgrade the iframe to ext-apps@2.", + ); + } + return this.ui.sendRequest( + "ui/call-view-tool", + params, + specTypeSchema("CallToolResult"), options, ); } /** - * List tools available on the view. - * - * Sends a `tools/list` request to the view and returns the result. + * List tools the **view** exposes (see {@link app!App.onlisttools}). * - * @param params - List tools parameters (may include cursor for pagination) - * @param options - Request options (timeout, abort signal, etc.) - * @returns Promise resolving to the list of tools + * Wire method: `ui/list-view-tools` (renamed from `tools/list` in v2). */ - listTools(params: ListToolsRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/list", params }, - ListToolsResultSchema, + listTools(params?: ListToolsRequest["params"], options?: RequestOptions) { + if (this._isV2Iframe === false) { + throw new Error( + "bridge.listTools(): connected iframe is ext-apps v1 (capabilities arrived via ui/initialize only). " + + "Host→iframe tool listing uses the v2-only 'ui/list-view-tools' wire method; upgrade the iframe to ext-apps@2.", + ); + } + return this.ui.sendRequest( + "ui/list-view-tools", + params, + specTypeSchema("ListToolsResult"), options, ); } + // ── Lifecycle ───────────────────────────────────────────────────────────── + /** - * Connect to the view via transport and optionally set up message forwarding. - * - * This method establishes the transport connection. If an MCP client was passed - * to the constructor, it also automatically sets up request/notification forwarding - * based on the MCP server's capabilities, proxying the following to the view: - * - Tools (tools/call, notifications/tools/list_changed) - * - Resources (resources/list, resources/read, resources/templates/list, notifications/resources/list_changed) - * - Prompts (prompts/list, notifications/prompts/list_changed) - * - * If no client was passed to the constructor, no automatic forwarding is set up - * and you must register handlers manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, - * etc. setters. - * - * After calling connect, wait for the {@link oninitialized `oninitialized`} callback before sending - * tool input and other data to the View. - * - * @param transport - Transport layer (typically {@link PostMessageTransport `PostMessageTransport`}) - * @returns Promise resolving when connection is established - * - * @throws {Error} If a client was passed but 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 With MCP client (automatic forwarding) - * ```ts source="./app-bridge.examples.ts#AppBridge_connect_withMcpClient" - * const bridge = new AppBridge(mcpClient, hostInfo, capabilities); - * const transport = new PostMessageTransport( - * iframe.contentWindow!, - * iframe.contentWindow!, - * ); + * Connect to the iframe. * - * bridge.oninitialized = () => { - * console.log("View ready"); - * bridge.sendToolInput({ arguments: toolArgs }); - * }; - * - * await bridge.connect(transport); - * ``` - * - * @example Without MCP client (manual handlers) - * ```ts source="./app-bridge.examples.ts#AppBridge_connect_withoutMcpClient" - * const bridge = new AppBridge(null, hostInfo, capabilities); - * - * // Register handlers manually - * bridge.oncalltool = async (params, extra) => { - * // Custom tool call handling - * return { content: [] }; - * }; - * - * await bridge.connect(transport); - * ``` + * If an `mcpClient` was passed to the constructor, automatically wires the + * standard-MCP proxy handlers (`oncalltool`, `onreadresource`, …) to forward + * to that client, and relays `list_changed` notifications. */ - async connect(transport: Transport) { - if (this.transport) { + async connect(transport: Transport): Promise { + if (this.server.transport) { throw new Error( "AppBridge is already connected. Call close() before connecting again.", ); } if (this._client) { - // When a client was passed to the constructor, automatically forward - // MCP requests/notifications between the view and the server - const serverCapabilities = this._client.getServerCapabilities(); - if (!serverCapabilities) { - throw new Error("Client server capabilities not available"); - } - - if (serverCapabilities.tools) { - this.oncalltool = async (params, extra) => { - return this._client!.request( - { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, - ); - }; - if (serverCapabilities.tools.listChanged) { + const sc = this._client.getServerCapabilities(); + if (!sc) throw new Error("Client server capabilities not available"); + + if (sc.tools) { + this.oncalltool = async (params, extra) => + this._client!.callTool(params, { + signal: extra.signal, + }) as Promise; + this.onlisttools = async (params, extra) => + this._client!.listTools(params, { signal: extra.signal }); + if (sc.tools.listChanged) { this._client.setNotificationHandler( - ToolListChangedNotificationSchema, - (n) => this.sendToolListChanged(n.params), + "notifications/tools/list_changed", + () => this.sendToolListChanged(), ); } } - if (serverCapabilities.resources) { - this.onlistresources = async (params, extra) => { - return this._client!.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, - ); - }; - this.onlistresourcetemplates = async (params, extra) => { - return this._client!.request( - { method: "resources/templates/list", params }, - ListResourceTemplatesResultSchema, - { signal: extra.signal }, - ); - }; - this.onreadresource = async (params, extra) => { - return this._client!.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, - ); - }; - if (serverCapabilities.resources.listChanged) { + if (sc.resources) { + this.onlistresources = async (params, extra) => + this._client!.listResources(params, { signal: extra.signal }); + this.onlistresourcetemplates = async (params, extra) => + this._client!.listResourceTemplates(params, { + signal: extra.signal, + }); + this.onreadresource = async (params, extra) => + this._client!.readResource(params, { signal: extra.signal }); + if (sc.resources.listChanged) { this._client.setNotificationHandler( - ResourceListChangedNotificationSchema, - (n) => this.sendResourceListChanged(n.params), + "notifications/resources/list_changed", + () => this.sendResourceListChanged(), ); } } - if (serverCapabilities.prompts) { - this.onlistprompts = async (params, extra) => { - return this._client!.request( - { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, - ); - }; - if (serverCapabilities.prompts.listChanged) { + if (sc.prompts) { + this.onlistprompts = async (params, extra) => + this._client!.listPrompts(params, { signal: extra.signal }); + if (sc.prompts.listChanged) { this._client.setNotificationHandler( - PromptListChangedNotificationSchema, - (n) => this.sendPromptListChanged(n.params), + "notifications/prompts/list_changed", + () => this.sendPromptListChanged(), ); } } } + return this.server.connect(transport); + } - // MCP-UI specific handlers are registered by the host component - // after the proxy is created. The standard MCP initialization - // (via oninitialized callback set in constructor) handles the ready signal. - - return super.connect(transport); + /** Close the connection. */ + async close(): Promise { + return this.server.close(); } -} -function deepEqual(a: any, b: any): boolean { - return JSON.stringify(a) === JSON.stringify(b); + /** Underlying transport (for diagnostics). */ + get transport(): Transport | undefined { + return this.server.transport; + } } diff --git a/src/app.examples.ts b/src/app.examples.ts index a0a582076..939a02536 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -7,11 +7,11 @@ * @module */ -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; +import type { Tool } from "@modelcontextprotocol/client"; import type { McpServer, ToolCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +} from "@modelcontextprotocol/server"; import { App, PostMessageTransport, @@ -551,10 +551,7 @@ async function App_setupAutoResize_manual(transport: PostMessageTransport) { await app.connect(transport); // Later, enable auto-resize manually - const cleanup = app.setupSizeChangedNotifications(); - - // Clean up when done - cleanup(); + app.setupSizeChangedNotifications(); //#endregion App_setupAutoResize_manual } diff --git a/src/app.ts b/src/app.ts index f5f97b37c..e11a35cac 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,52 +1,48 @@ import { + Client, + type CallToolRequest, + type ClientContext, + type CallToolResult, + type ExtensionHandle, + type Implementation, + type ListResourcesRequest, + type ListResourcesResult, + type ListToolsRequest, + type ListToolsResult, + type LoggingMessageNotification, + type ProtocolOptions, + type ReadResourceRequest, + type ReadResourceResult, type RequestOptions, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; + type Transport, +} from "@modelcontextprotocol/client"; -import { - CallToolRequest, - CallToolRequestSchema, - CallToolResult, - CallToolResultSchema, - EmptyResultSchema, - Implementation, - ListResourcesRequest, - ListResourcesResult, - ListResourcesResultSchema, - ListToolsRequest, - ListToolsRequestSchema, - ListToolsResult, - LoggingMessageNotification, - PingRequestSchema, - ReadResourceRequest, - ReadResourceResult, - ReadResourceResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; -import { AppNotification, AppRequest, AppResult } from "./types"; -import { ProtocolWithEvents } from "./events"; -export { ProtocolWithEvents }; +import { EventDispatcher } from "./events"; +export { EventDispatcher, ProtocolWithEvents } from "./events"; import { PostMessageTransport } from "./message-transport"; +import { specTypeSchema } from "@modelcontextprotocol/client"; import { LATEST_PROTOCOL_VERSION, McpUiAppCapabilities, - McpUiUpdateModelContextRequest, + McpUiAppCapabilitiesSchema, + McpUiDownloadFileRequest, + McpUiDownloadFileResultSchema, McpUiHostCapabilities, + McpUiHostCapabilitiesSchema, McpUiHostContext, McpUiHostContextChangedNotification, McpUiHostContextChangedNotificationSchema, - McpUiInitializedNotification, - McpUiInitializeRequest, McpUiInitializeResultSchema, McpUiMessageRequest, McpUiMessageResultSchema, McpUiOpenLinkRequest, McpUiOpenLinkResultSchema, - McpUiDownloadFileRequest, - McpUiDownloadFileResultSchema, + McpUiRequestDisplayModeRequest, + McpUiRequestDisplayModeResultSchema, + McpUiRequestTeardownNotification, McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, - McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, McpUiToolCancelledNotificationSchema, @@ -56,117 +52,88 @@ import { McpUiToolInputPartialNotificationSchema, McpUiToolResultNotification, McpUiToolResultNotificationSchema, - McpUiRequestDisplayModeRequest, - McpUiRequestDisplayModeResultSchema, + McpUiUpdateModelContextRequest, } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; export { - applyHostStyleVariables, + applyDocumentTheme, applyHostFonts, + applyHostStyleVariables, getDocumentTheme, - applyDocumentTheme, } from "./styles"; +/** SEP-2133 extension identifier for MCP Apps. */ +export const MCP_APPS_EXTENSION_ID = "io.modelcontextprotocol/ui"; + /** * Metadata key for associating a UI resource URI with a tool. * * MCP servers include this key in tool definition metadata (via `tools/list`) * to indicate which UI resource should be displayed when the tool is called. - * When hosts see a tool with this metadata, they fetch and render the - * corresponding {@link App `App`}. - * - * **Note**: This constant is provided for reference and backwards compatibility. - * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`} - * with the `_meta.ui.resourceUri` format instead. Host developers must check both - * formats for compatibility. * - * @example Modern format (server-side, not in Apps) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat" - * // Preferred: Use registerAppTool with nested ui.resourceUri - * registerAppTool( - * server, - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * ui: { resourceUri: "ui://weather/forecast" }, - * }, - * }, - * handler, - * ); - * ``` - * - * @example Legacy format (deprecated, for backwards compatibility) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat" - * // Deprecated: Direct use of RESOURCE_URI_META_KEY - * server.registerTool( - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * [RESOURCE_URI_META_KEY]: "ui://weather/forecast", - * }, - * }, - * handler, - * ); - * ``` - * - * @example How hosts check for this metadata (must support both formats) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide" - * // Hosts should check both modern and legacy formats - * const meta = tool._meta; - * const uiMeta = meta?.ui as McpUiToolMeta | undefined; - * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined; - * const uiUri = uiMeta?.resourceUri ?? legacyUri; - * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { - * // Fetch the resource and display the UI - * } - * ``` + * **Note**: Prefer the nested `_meta.ui.resourceUri` format via + * {@link server-helpers!registerAppTool `registerAppTool`}. This flat key is + * kept for backwards compatibility. */ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; +/** MIME type identifying an MCP App HTML resource. */ +export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; + /** - * MIME type for MCP UI resources. + * Extract UI resource URI from tool metadata. * - * Identifies HTML content as an MCP App UI resource. - * - * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. + * Supports both the nested `_meta.ui.resourceUri` format and the deprecated + * flat `_meta["ui/resourceUri"]` format. */ -export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; +export function getToolUiResourceUri(tool: { + _meta?: Record; +}): string | undefined { + const meta = tool._meta; + if (!meta) return undefined; + const ui = meta.ui as { resourceUri?: unknown } | undefined; + const hasNested = ui != null && "resourceUri" in ui; + const candidate = hasNested ? ui.resourceUri : meta[RESOURCE_URI_META_KEY]; + if (candidate === undefined && !hasNested) return undefined; + if (typeof candidate !== "string" || !candidate.startsWith("ui://")) { + throw new Error( + 'Invalid UI resource URI (must be a string starting with "ui://"): ' + + JSON.stringify(candidate), + ); + } + return candidate; +} /** - * Options for configuring {@link App `App`} behavior. - * - * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration. + * Handler context passed to `on*` request handlers. * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options + * In v2 this is a slimmed adapter over the SDK's `BaseContext`. Most v1 + * consumers only used `signal`. */ -type AppOptions = ProtocolOptions & { +export type RequestHandlerExtra = { + /** AbortSignal that fires if the request is cancelled. */ + signal: AbortSignal; +}; + +function toExtra(ctx: { mcpReq: { signal: AbortSignal } }): RequestHandlerExtra { + return { signal: ctx.mcpReq.signal }; +} + +/** + * Options for constructing an {@link App `App`}. + */ +export interface AppOptions extends ProtocolOptions { /** - * Automatically report size changes to the host using `ResizeObserver`. - * - * When enabled, the {@link App `App`} monitors `document.body` and `document.documentElement` - * for size changes and automatically sends `ui/notifications/size-changed` - * notifications to the host. - * - * @default true + * Automatically observe document size and emit + * `ui/notifications/size-changed` to the host. Default: `true`. */ autoResize?: boolean; -}; - -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +} /** - * Maps DOM-style event names to their notification `params` types. - * - * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`}) - * to provide type-safe `addEventListener` / `removeEventListener` and - * singular `on*` handler support. + * Event map for {@link App `App`} — notifications the host pushes to the view. */ export type AppEventMap = { toolinput: McpUiToolInputNotification["params"]; @@ -176,675 +143,218 @@ export type AppEventMap = { hostcontextchanged: McpUiHostContextChangedNotification["params"]; }; +const APP_EVENT_NOTIFICATION_SCHEMAS = { + toolinput: McpUiToolInputNotificationSchema, + toolinputpartial: McpUiToolInputPartialNotificationSchema, + toolresult: McpUiToolResultNotificationSchema, + toolcancelled: McpUiToolCancelledNotificationSchema, + hostcontextchanged: McpUiHostContextChangedNotificationSchema, +} as const; + /** - * 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 - * - * Views (Apps) act as MCP clients connecting to the host via {@link PostMessageTransport `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. **Teardown**: Host sends teardown request before unmounting - * - * ## Inherited Methods + * The View side of the MCP Apps protocol — runs inside the iframe. * - * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits: - * - `setRequestHandler()` - Register handlers for requests from host - * - `setNotificationHandler()` - Register handlers for notifications from host - * - `addEventListener()` - Append a listener for a notification event (multi-listener) - * - `removeEventListener()` - Remove a previously added listener - * - * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system - * - * ## Notification Setters (DOM-model `on*` handlers) - * - * For common notifications, the `App` class provides getter/setter properties - * that follow DOM-model replace semantics (like `el.onclick`): - * - `ontoolinput` - Complete tool arguments from host - * - `ontoolinputpartial` - Streaming partial tool arguments - * - `ontoolresult` - Tool execution results - * - `ontoolcancelled` - Tool execution was cancelled by user or host - * - `onhostcontextchanged` - Host context changes (theme, locale, etc.) - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use `addEventListener` to attach multiple listeners without replacing. - * - * @example Basic usage with PostMessageTransport - * ```ts source="./app.examples.ts#App_basicUsage" - * const app = new App( - * { name: "WeatherApp", version: "1.0.0" }, - * {}, // capabilities - * ); - * - * // Register handlers before connecting to ensure no notifications are missed - * app.ontoolinput = (params) => { - * console.log("Tool arguments:", params.arguments); - * }; + * `App` composes an MCP {@link Client `Client`} (the iframe is the MCP client + * on the postMessage wire, per SEP-1865) and an + * {@link ExtensionHandle `ExtensionHandle`} for the `ui/*` extension methods. + * Standard MCP methods (`tools/call`, `resources/read`, …) are proxied through + * the host to the real MCP server via the underlying `Client`. * + * @example + * ```ts + * const app = new App({ name: "MyApp", version: "1.0.0" }, {}); + * app.ontoolresult = (r) => render(r); * await app.connect(); * ``` */ -export class App extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppEventMap -> { - private _hostCapabilities?: McpUiHostCapabilities; - private _hostInfo?: Implementation; - private _hostContext?: McpUiHostContext; - - protected readonly eventSchemas = { - toolinput: McpUiToolInputNotificationSchema, - toolinputpartial: McpUiToolInputPartialNotificationSchema, - toolresult: McpUiToolResultNotificationSchema, - toolcancelled: McpUiToolCancelledNotificationSchema, - hostcontextchanged: McpUiHostContextChangedNotificationSchema, - }; +export class App extends EventDispatcher { + /** Underlying MCP Client (iframe → host wire). */ + readonly client: Client; + /** SEP-2133 extension handle for `ui/*` methods. */ + readonly ui: ExtensionHandle; - protected override onEventDispatch( - event: K, - params: AppEventMap[K], - ): void { - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; - } - } + private _hostContext?: McpUiHostContext; + private _hostInfo?: Implementation; + private _resizeObserver?: ResizeObserver; /** - * 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 - * ```ts source="./app.examples.ts#App_constructor_basic" - * const app = new App( - * { name: "MyApp", version: "1.0.0" }, - * { tools: { listChanged: true } }, // capabilities - * { autoResize: true }, // options - * ); - * ``` + * Optional error handler. Called when the underlying transport surfaces an + * error. Mirrors the v1 `Protocol.onerror` slot. */ + onerror?: (error: Error) => void; + /** Called when the underlying transport closes. Mirrors v1 Protocol.onclose. */ + onclose?: () => void; + constructor( private _appInfo: Implementation, private _capabilities: McpUiAppCapabilities = {}, - private options: AppOptions = { autoResize: true }, + readonly options: AppOptions = { autoResize: true }, ) { - super(options); - - this.setRequestHandler(PingRequestSchema, (request) => { - console.log("Received ping:", request.params); - return {}; + super(); + this.client = new Client(_appInfo, { + ...options, + capabilities: { roots: undefined }, + }); + this.client.onerror = (err) => this.onerror?.(err); + this.client.onclose = () => this.onclose?.(); + this.ui = this.client.extension(MCP_APPS_EXTENSION_ID, _capabilities, { + peerSchema: McpUiHostCapabilitiesSchema, }); - // Eagerly register the hostcontextchanged event slot so that - // onEventDispatch (which merges into _hostContext) fires even if the - // user never assigns onhostcontextchanged or calls addEventListener. - this.setEventHandler("hostcontextchanged", undefined); + // Wire incoming ui/* notifications to the DOM-style event system. + for (const [event, schema] of Object.entries( + APP_EVENT_NOTIFICATION_SCHEMAS, + )) { + this.ui.setNotificationHandler( + schema.shape.method.value as string, + (schema.shape.params ?? schema) as never, + (params) => this.dispatchEvent(event as keyof AppEventMap, params), + ); + } + + // ui/resource-teardown (host → app request) + this.ui.setRequestHandler( + McpUiResourceTeardownRequestSchema.shape.method.value, + McpUiResourceTeardownRequestSchema.shape.params, + async (params, ctx) => { + if (this._onteardown) return this._onteardown(params, toExtra(ctx)); + return {}; + }, + ); + + // Non-spec host→iframe tool surface (renamed from tools/call & tools/list). + this.ui.setRequestHandler( + "ui/call-view-tool", + specTypeSchema("CallToolRequestParams"), + async (params, ctx) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(params, toExtra(ctx)); + }, + ); + this.ui.setRequestHandler( + "ui/list-view-tools", + specTypeSchema("PaginatedRequestParams"), + async (params, ctx) => { + if (!this._onlisttools) throw new Error("No onlisttools handler set"); + return this._onlisttools(params, toExtra(ctx)); + }, + ); } + /** Merge `hostcontextchanged` params into cached state before listeners fire. */ + protected override onEventDispatch( + event: K, + params: AppEventMap[K], + ): void { + if (event === "hostcontextchanged") { + this._hostContext = { ...(this._hostContext ?? {}), ...(params as McpUiHostContext) }; + } + } + + // ── Host info / capabilities / context ──────────────────────────────────── + /** - * Get the host's capabilities discovered during initialization. - * - * Returns the capabilities that the host advertised during the - * {@link connect `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 - * ```ts source="./app.examples.ts#App_getHostCapabilities_checkAfterConnection" - * await app.connect(); - * if (app.getHostCapabilities()?.serverTools) { - * console.log("Host supports server tool calls"); - * } - * ``` - * - * @see {@link connect `connect`} for the initialization handshake - * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure + * Host capabilities advertised via `capabilities.extensions[io.modelcontextprotocol/ui]` + * during the MCP `initialize` handshake. */ + get hostCapabilities(): McpUiHostCapabilities | undefined { + return this.ui.getPeerSettings(); + } + /** @deprecated Use {@link hostCapabilities `hostCapabilities`}. */ getHostCapabilities(): McpUiHostCapabilities | undefined { - return this._hostCapabilities; + 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 `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 - * ```ts source="./app.examples.ts#App_getHostVersion_logAfterConnection" - * await app.connect(transport); - * const { name, version } = app.getHostVersion() ?? {}; - * console.log(`Connected to ${name} v${version}`); - * ``` - * - * @see {@link connect `connect`} for the initialization handshake - */ + /** Host implementation info from `ui/initialize`. */ getHostVersion(): Implementation | undefined { return this._hostInfo; } /** - * Get the host context discovered during initialization. - * - * Returns the host context that was provided in the initialization response, - * including tool info, theme, locale, and other environment details. - * This context is automatically updated when the host sends - * `ui/notifications/host-context-changed` notifications. - * - * Returns `undefined` if called before connection is established. - * - * @returns Host context, or `undefined` if not yet connected - * - * @example Access host context after connection - * ```ts source="./app.examples.ts#App_getHostContext_accessAfterConnection" - * await app.connect(transport); - * const context = app.getHostContext(); - * if (context?.theme === "dark") { - * document.body.classList.add("dark-theme"); - * } - * if (context?.toolInfo) { - * console.log("Tool:", context.toolInfo.tool.name); - * } - * ``` - * - * @see {@link connect `connect`} for the initialization handshake - * @see {@link onhostcontextchanged `onhostcontextchanged`} for context change notifications - * @see {@link McpUiHostContext `McpUiHostContext`} for the context structure + * Current host context (theme, locale, displayMode, hostStyles, …). Updated + * automatically by `ui/notifications/host-context-changed`. */ getHostContext(): McpUiHostContext | undefined { return this._hostContext; } - /** - * 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. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use {@link addEventListener `addEventListener`} to attach multiple listeners - * without replacing. - * - * Register handlers before calling {@link connect `connect`} to avoid missing notifications. - * - * @example - * ```ts source="./app.examples.ts#App_ontoolinput_setter" - * // Register before connecting to ensure no notifications are missed - * app.ontoolinput = (params) => { - * console.log("Tool:", params.arguments); - * // Update your UI with the tool arguments - * }; - * await app.connect(); - * ``` - * - * @deprecated Use {@link addEventListener `addEventListener("toolinput", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - * @see {@link McpUiToolInputNotification `McpUiToolInputNotification`} for the notification structure - */ - get ontoolinput(): - | ((params: McpUiToolInputNotification["params"]) => void) - | undefined { - return this.getEventHandler("toolinput"); - } - set ontoolinput( - callback: - | ((params: McpUiToolInputNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("toolinput", callback); - } + // ── Notification on* setters (DOM-style) ────────────────────────────────── - /** - * 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. - * - * **Important:** Partial arguments are "healed" JSON — the host closes unclosed - * brackets/braces to produce valid JSON. This means objects may be incomplete - * (e.g., the last item in an array may be truncated). Use partial data only - * for preview UI, not for critical operations. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use {@link addEventListener `addEventListener`} to attach multiple listeners - * without replacing. - * - * Register handlers before calling {@link connect `connect`} to avoid missing notifications. - * - * @example Progressive rendering of tool arguments - * ```ts source="./app.examples.ts#App_ontoolinputpartial_progressiveRendering" - * const codePreview = document.querySelector("#code-preview")!; - * const canvas = document.querySelector("#canvas")!; - * - * app.ontoolinputpartial = (params) => { - * codePreview.textContent = (params.arguments?.code as string) ?? ""; - * codePreview.style.display = "block"; - * canvas.style.display = "none"; - * }; - * - * app.ontoolinput = (params) => { - * codePreview.style.display = "none"; - * canvas.style.display = "block"; - * render(params.arguments?.code as string); - * }; - * ``` - * - * @deprecated Use {@link addEventListener `addEventListener("toolinputpartial", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - * @see {@link McpUiToolInputPartialNotification `McpUiToolInputPartialNotification`} for the notification structure - * @see {@link ontoolinput `ontoolinput`} for the complete tool input handler - */ - get ontoolinputpartial(): - | ((params: McpUiToolInputPartialNotification["params"]) => void) - | undefined { - return this.getEventHandler("toolinputpartial"); - } - set ontoolinputpartial( - callback: - | ((params: McpUiToolInputPartialNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("toolinputpartial", callback); - } + get ontoolinput() { return this.getEventHandler("toolinput"); } + set ontoolinput(h) { this.setEventHandler("toolinput", h); } - /** - * 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. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use {@link addEventListener `addEventListener`} to attach multiple listeners - * without replacing. - * - * Register handlers before calling {@link connect `connect`} to avoid missing notifications. - * - * @example Display tool execution results - * ```ts source="./app.examples.ts#App_ontoolresult_displayResults" - * app.ontoolresult = (params) => { - * if (params.isError) { - * console.error("Tool execution failed:", params.content); - * } else if (params.content) { - * console.log("Tool output:", params.content); - * } - * }; - * ``` - * - * @deprecated Use {@link addEventListener `addEventListener("toolresult", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - * @see {@link McpUiToolResultNotification `McpUiToolResultNotification`} for the notification structure - * @see {@link ontoolinput `ontoolinput`} for the initial tool input handler - */ - get ontoolresult(): - | ((params: McpUiToolResultNotification["params"]) => void) - | undefined { - return this.getEventHandler("toolresult"); - } - set ontoolresult( - callback: - | ((params: McpUiToolResultNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("toolresult", callback); - } + get ontoolinputpartial() { return this.getEventHandler("toolinputpartial"); } + set ontoolinputpartial(h) { this.setEventHandler("toolinputpartial", h); } - /** - * Convenience handler for receiving tool cancellation notifications from the host. - * - * Set this property to register a handler that will be called when the host - * notifies that tool execution was cancelled. This can occur for various reasons - * including user action, sampling error, classifier intervention, or other - * interruptions. Apps should update their state and display appropriate feedback. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use {@link addEventListener `addEventListener`} to attach multiple listeners - * without replacing. - * - * Register handlers before calling {@link connect `connect`} to avoid missing notifications. - * - * @example Handle tool cancellation - * ```ts source="./app.examples.ts#App_ontoolcancelled_handleCancellation" - * app.ontoolcancelled = (params) => { - * console.log("Tool cancelled:", params.reason); - * // Update your UI to show cancellation state - * }; - * ``` - * - * @deprecated Use {@link addEventListener `addEventListener("toolcancelled", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - * @see {@link McpUiToolCancelledNotification `McpUiToolCancelledNotification`} for the notification structure - * @see {@link ontoolresult `ontoolresult`} for successful tool completion - */ - get ontoolcancelled(): - | ((params: McpUiToolCancelledNotification["params"]) => void) - | undefined { - return this.getEventHandler("toolcancelled"); - } - set ontoolcancelled( - callback: - | ((params: McpUiToolCancelledNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("toolcancelled", callback); - } + get ontoolresult() { return this.getEventHandler("toolresult"); } + set ontoolresult(h) { this.setEventHandler("toolresult", h); } - /** - * Convenience handler for host context changes (theme, 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), locale changes, or - * other environmental updates. Apps should respond by updating their UI - * accordingly. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * Use {@link addEventListener `addEventListener`} to attach multiple listeners - * without replacing. - * - * Notification params are automatically merged into the internal host context - * via {@link onEventDispatch `onEventDispatch`} before any handler or listener - * fires. This means {@link getHostContext `getHostContext`} will return the - * updated values even before your callback runs. - * - * Register handlers before calling {@link connect `connect`} to avoid missing notifications. - * - * @example Respond to theme changes - * ```ts source="./app.examples.ts#App_onhostcontextchanged_respondToTheme" - * app.onhostcontextchanged = (ctx) => { - * if (ctx.theme === "dark") { - * document.body.classList.add("dark-theme"); - * } else { - * document.body.classList.remove("dark-theme"); - * } - * }; - * ``` - * - * @deprecated Use {@link addEventListener `addEventListener("hostcontextchanged", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}. - * @see {@link McpUiHostContextChangedNotification `McpUiHostContextChangedNotification`} for the notification structure - * @see {@link McpUiHostContext `McpUiHostContext`} for the full context structure - */ - get onhostcontextchanged(): - | ((params: McpUiHostContextChangedNotification["params"]) => void) - | undefined { - return this.getEventHandler("hostcontextchanged"); - } - set onhostcontextchanged( - callback: - | ((params: McpUiHostContextChangedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("hostcontextchanged", callback); - } + get ontoolcancelled() { return this.getEventHandler("toolcancelled"); } + set ontoolcancelled(h) { this.setEventHandler("toolcancelled", h); } + + get onhostcontextchanged() { return this.getEventHandler("hostcontextchanged"); } + set onhostcontextchanged(h) { this.setEventHandler("hostcontextchanged", h); } + + // ── Request on* setters ─────────────────────────────────────────────────── - /** - * Convenience handler for graceful shutdown requests from the host. - * - * Set this property to register a handler that will be called when the host - * requests the app to prepare for teardown. This allows the app to perform - * cleanup operations (save state, close connections, etc.) before being unmounted. - * - * The handler can be sync or async. The host will wait for the returned promise - * to resolve before proceeding with teardown. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * - * Register handlers before calling {@link connect `connect`} to avoid missing requests. - * - * @param callback - Function called when teardown is requested. - * Must return `McpUiResourceTeardownResult` (can be an empty object `{}`) or a Promise resolving to it. - * - * @example Perform cleanup before teardown - * ```ts source="./app.examples.ts#App_onteardown_performCleanup" - * app.onteardown = async () => { - * await saveState(); - * closeConnections(); - * console.log("App ready for teardown"); - * return {}; - * }; - * ``` - * - * @see {@link McpUiResourceTeardownRequest `McpUiResourceTeardownRequest`} for the request structure - */ private _onteardown?: ( params: McpUiResourceTeardownRequest["params"], extra: RequestHandlerExtra, - ) => McpUiResourceTeardownResult | Promise; - get onteardown() { - return this._onteardown; - } - set onteardown( - callback: - | (( - params: McpUiResourceTeardownRequest["params"], - extra: RequestHandlerExtra, - ) => McpUiResourceTeardownResult | Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); - this._onteardown = callback; - this.replaceRequestHandler( - McpUiResourceTeardownRequestSchema, - (request, extra) => { - if (!this._onteardown) throw new Error("No onteardown handler set"); - return this._onteardown(request.params, extra); - }, - ); - } - + ) => Promise | McpUiResourceTeardownResult; /** - * 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. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * - * Register handlers before calling {@link connect `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 - * ```ts source="./app.examples.ts#App_oncalltool_handleFromHost" - * app.oncalltool = async (params, extra) => { - * if (params.name === "greet") { - * const name = params.arguments?.name ?? "World"; - * return { content: [{ type: "text", text: `Hello, ${name}!` }] }; - * } - * throw new Error(`Unknown tool: ${params.name}`); - * }; - * ``` + * Handler for `ui/resource-teardown` — called by the host before the iframe + * is unmounted. Return after any cleanup is complete. */ + get onteardown() { return this._onteardown; } + set onteardown(cb) { + this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, cb); + this._onteardown = cb; + } + private _oncalltool?: ( params: CallToolRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get oncalltool() { - return this._oncalltool; - } - set oncalltool( - callback: - | (( - params: CallToolRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); - this._oncalltool = callback; - this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { - if (!this._oncalltool) throw new Error("No oncalltool handler set"); - return this._oncalltool(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. - * - * Assigning replaces the previous handler; assigning `undefined` clears it. - * - * Register handlers before calling {@link connect `connect`} to avoid missing requests. - * - * @param callback - Async function that returns a {@link ListToolsResult `ListToolsResult`}. - * Registration is always allowed; capability validation occurs when handlers - * are invoked. + * Handler for tools the **iframe** exposes to the host (e.g., + * `get_current_selection`). The host calls these via + * {@link app-bridge!AppBridge.callTool `AppBridge.callTool`}. * - * @example Return available tools - * ```ts source="./app.examples.ts#App_onlisttools_returnTools" - * app.onlisttools = async (params, extra) => { - * return { - * tools: [ - * { name: "greet", inputSchema: { type: "object" as const } }, - * { name: "calculate", inputSchema: { type: "object" as const } }, - * { name: "format", inputSchema: { type: "object" as const } }, - * ], - * }; - * }; - * ``` - * - * @see {@link oncalltool `oncalltool`} for handling tool execution + * Wire method: `ui/call-view-tool` (renamed from `tools/call` in v2; not part + * of SEP-1865's standard-MCP-messages set). */ + get oncalltool() { return this._oncalltool; } + set oncalltool(cb) { + this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, cb); + this._oncalltool = cb; + } + private _onlisttools?: ( params: ListToolsRequest["params"], extra: RequestHandlerExtra, ) => Promise; - get onlisttools() { - return this._onlisttools; - } - set onlisttools( - callback: - | (( - params: ListToolsRequest["params"], - extra: RequestHandlerExtra, - ) => Promise) - | undefined, - ) { - this.warnIfRequestHandlerReplaced( - "onlisttools", - this._onlisttools, - callback, - ); - this._onlisttools = callback; - this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { - if (!this._onlisttools) throw new Error("No onlisttools handler set"); - return this._onlisttools(request.params, extra); - }); - } - - /** - * Verify that the host supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that the app declared the capability required for the given request method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - switch (method) { - case "tools/call": - case "tools/list": - if (!this._capabilities.tools) { - throw new Error( - `Client does not support tool capability (required for ${method})`, - ); - } - return; - case "ping": - case "ui/resource-teardown": - return; - default: - throw new Error(`No handler for method ${method} registered`); - } - } - /** - * Verify that the app supports the capability required for the given notification method. - * @internal + * Handler that lists tools the **iframe** exposes. See {@link oncalltool}. + * + * Wire method: `ui/list-view-tools` (renamed from `tools/list` in v2). */ - assertNotificationCapability(method: AppNotification["method"]): void { - // TODO + get onlisttools() { return this._onlisttools; } + set onlisttools(cb) { + this.warnIfRequestHandlerReplaced("onlisttools", this._onlisttools, cb); + this._onlisttools = cb; } - /** - * Verify that task creation is supported for the given request method. - * @internal - */ - protected assertTaskCapability(_method: string): void { - throw new Error("Tasks are not supported in MCP Apps"); - } - - /** - * Verify that task handler is supported for the given method. - * @internal - */ - protected assertTaskHandlerCapability(_method: string): void { - throw new Error("Task handlers are not supported in MCP Apps"); - } + // ── Outbound: standard MCP (proxied through host to real server) ────────── /** * 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 - * ```ts source="./app.examples.ts#App_callServerTool_fetchWeather" - * try { - * const result = await app.callServerTool({ - * name: "get_weather", - * arguments: { location: "Tokyo" }, - * }); - * if (result.isError) { - * console.error("Tool returned error:", result.content); - * } else { - * console.log(result.content); - * } - * } catch (error) { - * console.error("Tool call failed:", error); - * } + * @example + * ```ts + * const result = await app.callServerTool({ + * name: "search", + * arguments: { query: "weather" }, + * }); * ``` */ async callServerTool( @@ -857,652 +367,205 @@ export class App extends ProtocolWithEvents< `Did you mean: callServerTool({ name: "${params}", arguments: { ... } })?`, ); } - return await this.request( - { method: "tools/call", params }, - CallToolResultSchema, - options, - ); + return this.client.callTool(params, options) as Promise; } - /** - * Read a resource from the originating MCP server (proxied through the host). - * - * Apps can read resources to access files, data, or other content provided by - * the MCP server. Resources are identified by URI (e.g., `file:///path/to/file` - * or custom schemes like `videos://bunny-1mb`). The host proxies the request to - * the actual MCP server and returns the resource content. - * - * @param params - Resource URI to read - * @param options - Request options (timeout, etc.) - * @returns Resource content with URI, name, description, mimeType, and contents array - * - * @throws {Error} If the resource 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 - * - * @example Read a video resource and play it - * ```ts source="./app.examples.ts#App_readServerResource_playVideo" - * try { - * const result = await app.readServerResource({ - * uri: "videos://bunny-1mb", - * }); - * const content = result.contents[0]; - * if (content && "blob" in content) { - * const binary = Uint8Array.from(atob(content.blob), (c) => - * c.charCodeAt(0), - * ); - * const url = URL.createObjectURL( - * new Blob([binary], { type: content.mimeType || "video/mp4" }), - * ); - * videoElement.src = url; - * videoElement.play(); - * } - * } catch (error) { - * console.error("Failed to read resource:", error); - * } - * ``` - * - * @see {@link listServerResources `listServerResources`} to discover available resources - */ + /** Read a resource from the originating MCP server (proxied through the host). */ async readServerResource( params: ReadResourceRequest["params"], options?: RequestOptions, ): Promise { - return await this.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - options, - ); + return this.client.readResource(params, options); } - /** - * List available resources from the originating MCP server (proxied through the host). - * - * Apps can list resources to discover what content is available on the MCP server. - * This enables dynamic resource discovery and building resource browsers or pickers. - * The host proxies the request to the actual MCP server and returns the resource list. - * - * Results may be paginated using the `cursor` parameter for servers with many resources. - * - * @param params - Optional parameters (omit for all resources, or `{ cursor }` for pagination) - * @param options - Request options (timeout, etc.) - * @returns List of resources with their URIs, names, descriptions, mimeTypes, and optional pagination cursor - * - * @throws {Error} If the request times out or the connection is lost - * @throws {Error} If the host rejects the request - * - * @example Discover available videos and build a picker UI - * ```ts source="./app.examples.ts#App_listServerResources_buildPicker" - * try { - * const result = await app.listServerResources(); - * const videoResources = result.resources.filter((r) => - * r.mimeType?.startsWith("video/"), - * ); - * videoResources.forEach((resource) => { - * const option = document.createElement("option"); - * option.value = resource.uri; - * option.textContent = resource.description || resource.name; - * selectElement.appendChild(option); - * }); - * } catch (error) { - * console.error("Failed to list resources:", error); - * } - * ``` - * - * @see {@link readServerResource `readServerResource`} to read a specific resource - */ + /** List resources from the originating MCP server (proxied through the host). */ async listServerResources( params?: ListResourcesRequest["params"], options?: RequestOptions, ): Promise { - return await this.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - options, - ); + return this.client.listResources(params, options); } + // ── Outbound: ui/* requests ─────────────────────────────────────────────── + /** - * 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 with optional `isError` flag indicating host rejection - * - * @throws {Error} If the request times out or the connection is lost - * - * @example Send a text message from user interaction - * ```ts source="./app.examples.ts#App_sendMessage_textFromInteraction" - * try { - * const result = await app.sendMessage({ - * role: "user", - * content: [{ type: "text", text: "Show me details for item #42" }], - * }); - * if (result.isError) { - * console.error("Host rejected the message"); - * // Handle rejection appropriately for your app - * } - * } catch (error) { - * console.error("Failed to send message:", error); - * // Handle transport/protocol error - * } - * ``` - * - * @example Send follow-up message after offloading large data to model context - * ```ts source="./app.examples.ts#App_sendMessage_withLargeContext" - * const markdown = `--- - * word-count: ${fullTranscript.split(/\s+/).length} - * speaker-names: ${speakerNames.join(", ")} - * --- - * - * ${fullTranscript}`; - * - * // Offload long transcript to model context - * await app.updateModelContext({ content: [{ type: "text", text: markdown }] }); - * - * // Send brief trigger message - * await app.sendMessage({ - * role: "user", - * content: [{ type: "text", text: "Summarize the key points" }], - * }); - * ``` - * - * @see {@link McpUiMessageRequest `McpUiMessageRequest`} for request structure + * Send a message to the host that should be treated as if the user typed it. + * Typically pre-fills or submits the chat input. */ - sendMessage(params: McpUiMessageRequest["params"], options?: RequestOptions) { - return this.request( - { - method: "ui/message", - params, - }, + sendMessage( + params: McpUiMessageRequest["params"], + options?: RequestOptions, + ) { + return this.ui.sendRequest( + "ui/message", + params, McpUiMessageResultSchema, options, ); } /** - * Send log messages to the host for debugging and telemetry. + * Send a log message to the host. * - * 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 - * ```ts source="./app.examples.ts#App_sendLog_debugState" - * app.sendLog({ - * level: "info", - * data: "Weather data refreshed", - * logger: "WeatherApp", - * }); - * ``` - * - * @returns Promise that resolves when the log notification is sent + * Wire method: `ui/log` (renamed from `notifications/message` in v2; the v1 + * direction conflicted with core MCP semantics). */ sendLog(params: LoggingMessageNotification["params"]) { - return this.notification({ - method: "notifications/message", - params, - }); + return this.ui.sendNotification("ui/log", params); } /** - * Update the host's model context with app state. - * - * Context updates are intended to be available to the model in future - * turns, without triggering an immediate model response (unlike {@link sendMessage `sendMessage`}). - * - * The host will typically defer sending the context to the model until the - * next user message — either from the actual user or via `sendMessage`. Only - * the last update is sent; each call overwrites any previous context. - * - * @param params - Context content and/or structured content - * @param options - Request options (timeout, etc.) - * - * @throws {Error} If the host rejects the context update (e.g., unsupported content type) - * @throws {Error} If the request times out or the connection is lost - * - * @example Update model context with current app state - * ```ts source="./app.examples.ts#App_updateModelContext_appState" - * const markdown = `--- - * item-count: ${itemList.length} - * total-cost: ${totalCost} - * currency: ${currency} - * --- - * - * User is viewing their shopping cart with ${itemList.length} items selected: - * - * ${itemList.map((item) => `- ${item}`).join("\n")}`; - * - * await app.updateModelContext({ - * content: [{ type: "text", text: markdown }], - * }); - * ``` - * - * @example Report runtime error to model - * ```ts source="./app.examples.ts#App_updateModelContext_reportError" - * try { - * const _stream = await navigator.mediaDevices.getUserMedia({ audio: true }); - * // ... use _stream for transcription - * } catch (err) { - * // Inform the model that the app is in a degraded state - * await app.updateModelContext({ - * content: [ - * { - * type: "text", - * text: "Error: transcription unavailable", - * }, - * ], - * }); - * } - * ``` - * - * @returns Promise that resolves when the context update is acknowledged + * Update the host's model context with app state. The host stashes this and + * includes it in the next prompt turn. */ updateModelContext( params: McpUiUpdateModelContextRequest["params"], options?: RequestOptions, ) { - return this.request( - { - method: "ui/update-model-context", - params, - }, - EmptyResultSchema, + return this.ui.sendRequest( + "ui/update-model-context", + params, + specTypeSchema("EmptyResult"), options, ); } - /** - * 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 by checking `result.isError`. - * - * @param params - URL to open - * @param options - Request options (timeout, etc.) - * @returns Result with `isError: true` if the host denied the request (e.g., blocked domain, user cancelled) - * - * @throws {Error} If the request times out or the connection is lost - * - * @example Open documentation link - * ```ts source="./app.examples.ts#App_openLink_documentation" - * const { isError } = await app.openLink({ url: "https://docs.example.com" }); - * if (isError) { - * // Host denied the request (e.g., blocked domain, user cancelled) - * // Optionally show fallback: display URL for manual copy - * console.warn("Link request denied"); - * } - * ``` - * - * @see {@link McpUiOpenLinkRequest `McpUiOpenLinkRequest`} for request structure - * @see {@link McpUiOpenLinkResult `McpUiOpenLinkResult`} for result structure - */ - openLink(params: McpUiOpenLinkRequest["params"], options?: RequestOptions) { - return this.request( - { - method: "ui/open-link", - params, - }, + /** Ask the host to open a URL in the user's browser. */ + openLink( + params: McpUiOpenLinkRequest["params"], + options?: RequestOptions, + ) { + return this.ui.sendRequest( + "ui/open-link", + params, McpUiOpenLinkResultSchema, options, ); } + /** @deprecated Use {@link openLink `openLink`}. */ + get sendOpenLink() { return this.openLink; } - /** @deprecated Use {@link openLink `openLink`} instead */ - sendOpenLink: App["openLink"] = this.openLink; - - /** - * Request the host to download a file. - * - * Since MCP Apps run in sandboxed iframes where direct downloads are blocked, - * this provides a host-mediated mechanism for file exports. The host will - * typically show a confirmation dialog before initiating the download. - * - * Uses standard MCP resource types: `EmbeddedResource` for inline content - * and `ResourceLink` for content the host can fetch directly. - * - * @param params - Resource contents to download - * @param options - Request options (timeout, etc.) - * @returns Result with `isError: true` if the host denied the request (e.g., user cancelled) - * - * @throws {Error} If the request times out or the connection is lost - * - * @example Download a JSON file (embedded text resource) - * ```ts - * const data = JSON.stringify({ items: selectedItems }, null, 2); - * const { isError } = await app.downloadFile({ - * contents: [{ - * type: "resource", - * resource: { - * uri: "file:///export.json", - * mimeType: "application/json", - * text: data, - * }, - * }], - * }); - * if (isError) { - * console.warn("Download denied or cancelled"); - * } - * ``` - * - * @example Download binary content (embedded blob resource) - * ```ts - * const { isError } = await app.downloadFile({ - * contents: [{ - * type: "resource", - * resource: { - * uri: "file:///image.png", - * mimeType: "image/png", - * blob: base64EncodedPng, - * }, - * }], - * }); - * ``` - * - * @example Download via resource link (host fetches) - * ```ts - * const { isError } = await app.downloadFile({ - * contents: [{ - * type: "resource_link", - * uri: "https://api.example.com/reports/q4.pdf", - * name: "Q4 Report", - * mimeType: "application/pdf", - * }], - * }); - * ``` - * - * @see {@link McpUiDownloadFileRequest `McpUiDownloadFileRequest`} for request structure - * @see {@link McpUiDownloadFileResult `McpUiDownloadFileResult`} for result structure - */ + /** Ask the host to download a file to the user's machine. */ downloadFile( params: McpUiDownloadFileRequest["params"], options?: RequestOptions, ) { - return this.request( - { - method: "ui/download-file", - params, - }, + return this.ui.sendRequest( + "ui/download-file", + params, McpUiDownloadFileResultSchema, options, ); } - /** - * Request the host to tear down this app. - * - * Apps call this method to request that the host tear them down. The host - * decides whether to proceed - if approved, the host will send - * `ui/resource-teardown` to allow the app to perform gracefull termination before being - * unmounted. This piggybacks on the existing teardown mechanism, ensuring - * the app only needs a single shutdown procedure (via {@link onteardown `onteardown`}) - * regardless of whether the teardown was initiated by the app or the host. - * - * This is a fire-and-forget notification - no response is expected. - * If the host approves, the app will receive a `ui/resource-teardown` - * request via the {@link onteardown `onteardown`} handler to persist unsaved state. - * - * @param params - Empty params object (reserved for future use) - * @returns Promise that resolves when the notification is sent - * - * @example App-initiated teardown after user action - * ```typescript - * // User clicks "Done" button in the app - * async function handleDoneClick() { - * // Request the host to tear down the app - * await app.requestTeardown(); - * // If host approves, onteardown handler will be called for termination - * } - * - * // Set up teardown handler (called for both app-initiated and host-initiated teardown) - * app.onteardown = async () => { - * await saveState(); - * closeConnections(); - * return {}; - * }; - * ``` - * - * @see {@link McpUiRequestTeardownNotification `McpUiRequestTeardownNotification`} for notification structure - * @see {@link onteardown `onteardown`} for the graceful termination handler - */ - requestTeardown(params: McpUiRequestTeardownNotification["params"] = {}) { - return this.notification({ - method: "ui/notifications/request-teardown", - params, - }); - } - - /** - * Request a change to the display mode. - * - * Requests the host to change the UI container to the specified display mode - * (e.g., "inline", "fullscreen", "pip"). The host will respond with the actual - * display mode that was set, which may differ from the requested mode if - * the requested mode is not available (check `availableDisplayModes` in host context). - * - * @param params - The display mode being requested - * @param options - Request options (timeout, etc.) - * @returns Result containing the actual display mode that was set - * - * @example Toggle display mode - * ```ts source="./app.examples.ts#App_requestDisplayMode_toggle" - * const container = document.getElementById("main")!; - * const ctx = app.getHostContext(); - * const newMode = ctx?.displayMode === "inline" ? "fullscreen" : "inline"; - * if (ctx?.availableDisplayModes?.includes(newMode)) { - * const result = await app.requestDisplayMode({ mode: newMode }); - * container.classList.toggle("fullscreen", result.mode === "fullscreen"); - * } - * ``` - * - * @see {@link McpUiRequestDisplayModeRequest `McpUiRequestDisplayModeRequest`} for request structure - * @see {@link McpUiHostContext `McpUiHostContext`} for checking availableDisplayModes - */ + /** Ask the host to change the iframe's display mode (inline ↔ fullscreen ↔ pip). */ requestDisplayMode( params: McpUiRequestDisplayModeRequest["params"], options?: RequestOptions, ) { - return this.request( - { - method: "ui/request-display-mode", - params, - }, + return this.ui.sendRequest( + "ui/request-display-mode", + params, McpUiRequestDisplayModeResultSchema, options, ); } - /** - * 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 - * ```ts source="./app.examples.ts#App_sendSizeChanged_manual" - * app.sendSizeChanged({ - * width: 400, - * height: 600, - * }); - * ``` - * - * @returns Promise that resolves when the notification is sent - * - * @see {@link McpUiSizeChangedNotification `McpUiSizeChangedNotification`} for notification structure - */ + // ── Outbound: ui/* notifications ────────────────────────────────────────── + + /** Ask the host to tear down this view (e.g., user clicked a close button). */ + requestTeardown( + params?: McpUiRequestTeardownNotification["params"], + ) { + return this.ui.sendNotification("ui/notifications/request-teardown", params); + } + + /** Notify the host that the iframe's content size changed. */ sendSizeChanged(params: McpUiSizeChangedNotification["params"]) { - return this.notification({ - method: "ui/notifications/size-changed", - params, - }); + return this.ui.sendNotification("ui/notifications/size-changed", params); } + /** @deprecated Use {@link sendSizeChanged `sendSizeChanged`}. */ + get notifySizeChanged() { return this.sendSizeChanged; } /** - * Set up automatic size change notifications using ResizeObserver. - * - * Observes both `document.documentElement` and `document.body` for size changes - * and automatically sends `ui/notifications/size-changed` 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 - * ```ts source="./app.examples.ts#App_setupAutoResize_manual" - * const app = new App( - * { name: "MyApp", version: "1.0.0" }, - * {}, - * { autoResize: false }, - * ); - * await app.connect(transport); - * - * // Later, enable auto-resize manually - * const cleanup = app.setupSizeChangedNotifications(); - * - * // Clean up when done - * cleanup(); - * ``` + * Start observing document size and emitting size-changed notifications. + * Called automatically by {@link connect `connect`} when + * `options.autoResize` is true. */ - setupSizeChangedNotifications() { - let scheduled = false; - let lastWidth = 0; - let lastHeight = 0; - - const sendBodySizeChanged = () => { - if (scheduled) { - return; - } - scheduled = true; - requestAnimationFrame(() => { - scheduled = false; - const html = document.documentElement; - - // Measure actual content height by temporarily overriding html sizing. - // Height uses max-content because fit-content would clamp to the viewport - // height when content is taller than the iframe, causing internal scrolling. - // - // Width uses window.innerWidth instead of measuring via fit-content. - // Setting html.style.width to fit-content forces a synchronous reflow at - // 0px width for responsive apps (whose content derives width from the - // container rather than having intrinsic width). This causes the browser - // to clamp scrollLeft on any horizontal scroll containers to 0, permanently - // destroying their scroll positions. - const originalHeight = html.style.height; - html.style.height = "max-content"; - const height = Math.ceil(html.getBoundingClientRect().height); - html.style.height = originalHeight; - - const width = Math.ceil(window.innerWidth); - - // Only send if size actually changed (prevents feedback loops from style changes) - if (width !== lastWidth || height !== lastHeight) { - lastWidth = width; - lastHeight = height; - this.sendSizeChanged({ width, height }); - } + setupSizeChangedNotifications(): void { + if (typeof ResizeObserver === "undefined" || this._resizeObserver) return; + this._resizeObserver = new ResizeObserver(() => { + void this.sendSizeChanged({ + width: document.documentElement.scrollWidth, + height: document.documentElement.scrollHeight, }); - }; - - sendBodySizeChanged(); - - const resizeObserver = new ResizeObserver(sendBodySizeChanged); - // Observe both html and body to catch all size changes - resizeObserver.observe(document.documentElement); - resizeObserver.observe(document.body); - - return () => resizeObserver.disconnect(); + }); + this._resizeObserver.observe(document.documentElement); } + // ── Lifecycle ───────────────────────────────────────────────────────────── + /** - * 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 setupSizeChangedNotifications `setupSizeChangedNotifications`} if enabled (default) + * Connect to the host. * - * If initialization fails, the connection is automatically closed and an error - * is thrown. + * Performs the MCP `initialize` handshake (which carries + * `capabilities.extensions[io.modelcontextprotocol/ui]`), then a slimmed + * `ui/initialize` to receive the initial `hostContext`, then + * `ui/notifications/initialized`. * - * @param transport - Transport layer (typically {@link PostMessageTransport `PostMessageTransport`}) - * @param options - Request options for the initialize request - * - * @throws {Error} If initialization fails or connection is lost - * - * @example Connect with PostMessageTransport - * ```ts source="./app.examples.ts#App_connect_withPostMessageTransport" - * const app = new App({ name: "MyApp", version: "1.0.0" }, {}); - * - * try { - * await app.connect(new PostMessageTransport(window.parent, window.parent)); - * console.log("Connected successfully!"); - * } catch (error) { - * console.error("Failed to connect:", error); - * } - * ``` - * - * @see {@link McpUiInitializeRequest `McpUiInitializeRequest`} for the initialization request structure - * @see {@link McpUiInitializedNotification `McpUiInitializedNotification`} for the initialized notification - * @see {@link PostMessageTransport `PostMessageTransport`} for the typical transport implementation + * @param transport - Defaults to a `PostMessageTransport` to `window.parent`. */ - override async connect( + async connect( transport: Transport = new PostMessageTransport( window.parent, window.parent, ), options?: RequestOptions, ): Promise { - if (this.transport) { + if (this.client.transport) { throw new Error( "App is already connected. Call close() before connecting again.", ); } - await super.connect(transport); + await this.client.connect(transport, options); try { - const result = await this.request( - { - method: "ui/initialize", - params: { - appCapabilities: this._capabilities, - appInfo: this._appInfo, - protocolVersion: LATEST_PROTOCOL_VERSION, - }, + const result = await this.ui.sendRequest( + "ui/initialize", + { + appCapabilities: this._capabilities, + appInfo: this._appInfo, + protocolVersion: LATEST_PROTOCOL_VERSION, }, McpUiInitializeResultSchema, options, ); - if (result === undefined) { - throw new Error(`Server sent invalid initialize result: ${result}`); + throw new Error(`Host sent invalid ui/initialize result: ${result}`); } + this._hostInfo = result.hostInfo as Implementation; + this._hostContext = (result.hostContext ?? {}) as McpUiHostContext; - this._hostCapabilities = result.hostCapabilities; - this._hostInfo = result.hostInfo; - this._hostContext = result.hostContext; - - await this.notification({ - method: "ui/notifications/initialized", - }); + await this.ui.sendNotification("ui/notifications/initialized", undefined); if (this.options?.autoResize) { this.setupSizeChangedNotifications(); } } catch (error) { - // Disconnect if initialization fails. void this.close(); throw error; } } + + /** Close the connection and release resources. */ + async close(): Promise { + this._resizeObserver?.disconnect(); + this._resizeObserver = undefined; + return this.client.close(); + } + + /** Underlying transport (for diagnostics). */ + get transport(): Transport | undefined { + return this.client.transport; + } } diff --git a/src/events.ts b/src/events.ts index 4d17ca4f2..251ea9183 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,13 +1,3 @@ -import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { - Request, - Notification, - Result, -} from "@modelcontextprotocol/sdk/types.js"; -import { ZodLiteral, ZodObject } from "zod/v4"; - -type MethodSchema = ZodObject<{ method: ZodLiteral }>; - /** * Per-event state: a singular `on*` handler (replace semantics) plus a * listener array (`addEventListener` semantics), mirroring the DOM model @@ -19,13 +9,9 @@ interface EventSlot { } /** - * Intermediate base class that adds DOM-style event support on top of the - * MCP SDK's `Protocol`. + * Standalone DOM-style event dispatcher base class. * - * The base `Protocol` class stores one handler per method: - * `setRequestHandler()` and `setNotificationHandler()` replace any existing - * handler for the same method silently. This class introduces a two-channel - * event model inspired by the DOM: + * Provides a two-channel event model: * * ### Singular `on*` handler (like `el.onclick`) * @@ -42,41 +28,26 @@ interface EventSlot { * * ### Dispatch order * - * When a notification arrives for a mapped event: + * When {@link dispatchEvent `dispatchEvent`} is called for a mapped event: * 1. {@link onEventDispatch `onEventDispatch`} (subclass side-effects) * 2. The singular `on*` handler (if set) * 3. All `addEventListener` listeners in insertion order * - * ### Double-set protection - * - * Direct calls to {@link setRequestHandler `setRequestHandler`} / - * {@link setNotificationHandler `setNotificationHandler`} throw if a handler - * for the same method has already been registered (through any path), so - * accidental overwrites surface as errors instead of silent bugs. + * Unlike the v1 `ProtocolWithEvents`, this class has no coupling to the MCP + * SDK. Subclasses are responsible for wiring incoming notifications to + * {@link dispatchEvent `dispatchEvent`} themselves (typically via + * `ExtensionHandle.setNotificationHandler`). * * @typeParam EventMap - Maps event names to the listener's `params` type. */ -export abstract class ProtocolWithEvents< - SendRequestT extends Request, - SendNotificationT extends Notification, - SendResultT extends Result, +export abstract class EventDispatcher< EventMap extends Record, -> extends Protocol { - private _registeredMethods = new Set(); +> { private _eventSlots = new Map(); /** - * Event name → notification schema. Subclasses populate this so that - * the event system can lazily register a dispatcher with the correct - * schema on first use. - */ - protected abstract readonly eventSchemas: { - [K in keyof EventMap]: MethodSchema; - }; - - /** - * Called once per incoming notification, before any handlers or listeners - * fire. Subclasses may override to perform side effects such as merging + * Called once per dispatch, before any handlers or listeners fire. + * Subclasses may override to perform side effects such as merging * notification params into cached state. */ protected onEventDispatch( @@ -87,9 +58,7 @@ export abstract class ProtocolWithEvents< // ── Event system (DOM model) ──────────────────────────────────────── /** - * Lazily create the event slot and register a single dispatcher with the - * base `Protocol`. The dispatcher fans out to the `on*` handler and all - * `addEventListener` listeners. + * Lazily create the event slot for a given event name. */ private _ensureEventSlot( event: K, @@ -98,31 +67,32 @@ export abstract class ProtocolWithEvents< | EventSlot | undefined; if (!slot) { - const schema = this.eventSchemas[event]; - if (!schema) { - throw new Error(`Unknown event: ${String(event)}`); - } slot = { listeners: [] }; this._eventSlots.set(event, slot as EventSlot); - - // Claim this method so direct setNotificationHandler calls throw. - const method = schema.shape.method.value; - this._registeredMethods.add(method); - - const s = slot; // stable reference for the closure - super.setNotificationHandler(schema, (n) => { - const params = (n as { params: EventMap[K] }).params; - this.onEventDispatch(event, params); - // 1. Singular on* handler - s.onHandler?.(params); - // 2. addEventListener listeners — snapshot to tolerate removal during - // dispatch (e.g., a listener that calls removeEventListener on itself) - for (const l of [...s.listeners]) l(params); - }); } return slot; } + /** + * Dispatch an event to its `on*` handler and all `addEventListener` + * listeners. Subclasses call this when a mapped notification arrives. + */ + protected dispatchEvent( + event: K, + params: EventMap[K], + ): void { + this.onEventDispatch(event, params); + const slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) return; + // 1. Singular on* handler + slot.onHandler?.(params); + // 2. addEventListener listeners — snapshot to tolerate removal during + // dispatch (e.g., a listener that calls removeEventListener on itself) + for (const l of [...slot.listeners]) l(params); + } + /** * Set or clear the singular `on*` handler for an event. * @@ -160,11 +130,7 @@ export abstract class ProtocolWithEvents< * * Unlike the singular `on*` handler, calling this multiple times appends * listeners rather than replacing them. All registered listeners fire in - * insertion order after the `on*` handler when the notification arrives. - * - * Registration is lazy: the first call (for a given event, from either - * this method or the `on*` setter) registers a dispatcher with the base - * `Protocol`. + * insertion order after the `on*` handler when the event is dispatched. * * @param event - Event name (a key of the `EventMap` type parameter). * @param handler - Listener invoked with the notification `params`. @@ -177,9 +143,7 @@ export abstract class ProtocolWithEvents< } /** - * Remove a previously registered event listener. The dispatcher stays - * registered even if the listener array becomes empty; future - * notifications simply have no listeners to call. + * Remove a previously registered event listener. */ removeEventListener( event: K, @@ -193,46 +157,7 @@ export abstract class ProtocolWithEvents< if (idx !== -1) slot.listeners.splice(idx, 1); } - // ── Handler registration with double-set protection ───────────────── - - // The two overrides below are arrow-function class fields rather than - // prototype methods so that Protocol's constructor — which registers its - // own ping/cancelled/progress handlers via `this.setRequestHandler` - // before our fields initialize — hits the base implementation and skips - // tracking. Converting these to proper methods would crash with - // `_registeredMethods` undefined during super(). - - /** - * Registers a request handler. Throws if a handler for the same method - * has already been registered — use the `on*` setter (replace semantics) - * or `addEventListener` (multi-listener) for notification events. - * - * @throws {Error} if a handler for this method is already registered. - */ - override setRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; - - /** - * Registers a notification handler. Throws if a handler for the same - * method has already been registered — use the `on*` setter (replace - * semantics) or `addEventListener` (multi-listener) for mapped events. - * - * @throws {Error} if a handler for this method is already registered. - */ - override setNotificationHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setNotificationHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setNotificationHandler"); - super.setNotificationHandler(schema, handler); - }; + // ── Compat shim for request-handler `on*` setters ────────────────── /** * Warn if a request handler `on*` setter is replacing a previously-set @@ -250,30 +175,11 @@ export abstract class ProtocolWithEvents< ); } } - - /** - * Replace a request handler, bypassing double-set protection. Used by - * `on*` request-handler setters that need replace semantics. - */ - protected replaceRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - const method = (schema as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; - - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; - if (this._registeredMethods.has(method)) { - throw new Error( - `Handler for "${method}" already registered (via ${via}). ` + - `Use addEventListener() to attach multiple listeners, ` + - `or the on* setter for replace semantics.`, - ); - } - this._registeredMethods.add(method); - } } + +/** + * @deprecated Renamed to {@link EventDispatcher `EventDispatcher`} in v2. + * This alias is kept for source-level compatibility and will be removed in a + * future release. + */ +export const ProtocolWithEvents = EventDispatcher; diff --git a/src/generated/schema.json b/src/generated/schema.json index d66e55760..bfc9ef3a0 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -63,6 +63,9 @@ }, "additionalProperties": false }, + "McpUiContentBlock": { + "$schema": "https://json-schema.org/draft/2020-12/schema" + }, "McpUiDisplayMode": { "$schema": "https://json-schema.org/draft/2020-12/schema", "anyOf": [ @@ -95,182 +98,7 @@ "contents": { "type": "array", "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource" - }, - "resource": { - "anyOf": [ - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "text": { - "type": "string" - } - }, - "required": ["uri", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "blob": { - "type": "string" - } - }, - "required": ["uri", "blob"], - "additionalProperties": false - } - ] - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "resource"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "size": { - "type": "number" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "properties": {}, - "additionalProperties": {} - }, - "type": { - "type": "string", - "const": "resource_link" - } - }, - "required": ["name", "uri", "type"], - "additionalProperties": false - } - ] + "anyOf": [{}, {}] }, "description": "Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types." } @@ -503,7 +331,8 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Capabilities supported by the host application." }, "McpUiHostContextChangedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -521,145 +350,9 @@ "type": "object", "properties": { "id": { - "description": "JSON-RPC id of the tools/call request.", - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - ] + "description": "JSON-RPC id of the tools/call request." }, "tool": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "description": { - "type": "string" - }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "annotations": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "readOnlyHint": { - "type": "boolean" - }, - "destructiveHint": { - "type": "boolean" - }, - "idempotentHint": { - "type": "boolean" - }, - "openWorldHint": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "execution": { - "type": "object", - "properties": { - "taskSupport": { - "type": "string", - "enum": ["required", "optional", "forbidden"] - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["name", "inputSchema"], - "additionalProperties": false, "description": "Tool definition including name, inputSchema, etc." } }, @@ -1284,145 +977,9 @@ "type": "object", "properties": { "id": { - "description": "JSON-RPC id of the tools/call request.", - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - ] + "description": "JSON-RPC id of the tools/call request." }, "tool": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "description": { - "type": "string" - }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "annotations": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "readOnlyHint": { - "type": "boolean" - }, - "destructiveHint": { - "type": "boolean" - }, - "idempotentHint": { - "type": "boolean" - }, - "openWorldHint": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "execution": { - "type": "object", - "properties": { - "taskSupport": { - "type": "string", - "enum": ["required", "optional", "forbidden"] - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["name", "inputSchema"], - "additionalProperties": false, "description": "Tool definition including name, inputSchema, etc." } }, @@ -2472,52 +2029,6 @@ "type": "object", "properties": { "appInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "version": { - "type": "string" - }, - "websiteUrl": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name", "version"], - "additionalProperties": false, "description": "App identification (name and version)." }, "appCapabilities": { @@ -2586,52 +2097,6 @@ "description": "Negotiated protocol version string (e.g., \"2025-11-21\")." }, "hostInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "version": { - "type": "string" - }, - "websiteUrl": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": ["name", "version"], - "additionalProperties": false, "description": "Host application identification and version." }, "hostCapabilities": { @@ -2854,145 +2319,9 @@ "type": "object", "properties": { "id": { - "description": "JSON-RPC id of the tools/call request.", - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - ] + "description": "JSON-RPC id of the tools/call request." }, "tool": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "description": { - "type": "string" - }, - "inputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "outputSchema": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["type"], - "additionalProperties": {} - }, - "annotations": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "readOnlyHint": { - "type": "boolean" - }, - "destructiveHint": { - "type": "boolean" - }, - "idempotentHint": { - "type": "boolean" - }, - "openWorldHint": { - "type": "boolean" - } - }, - "additionalProperties": false - }, - "execution": { - "type": "object", - "properties": { - "taskSupport": { - "type": "string", - "enum": ["required", "optional", "forbidden"] - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["name", "inputSchema"], - "additionalProperties": false, "description": "Tool definition including name, inputSchema, etc." } }, @@ -3648,322 +2977,7 @@ }, "content": { "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "image" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "audio" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "size": { - "type": "number" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "properties": {}, - "additionalProperties": {} - }, - "type": { - "type": "string", - "const": "resource_link" - } - }, - "required": ["name", "uri", "type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource" - }, - "resource": { - "anyOf": [ - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "text": { - "type": "string" - } - }, - "required": ["uri", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "blob": { - "type": "string" - } - }, - "required": ["uri", "blob"], - "additionalProperties": false - } - ] - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "resource"], - "additionalProperties": false - } - ] - }, + "items": {}, "description": "Message content blocks (text, image, etc.)." } }, @@ -4130,7 +3144,8 @@ } } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Content Security Policy configuration for UI resources.\n\nServers declare which origins their UI requires. Hosts use this to enforce appropriate CSP headers.\n\n> [!IMPORTANT]\n> MCP App HTML runs in a sandboxed iframe with no same-origin server.\n> **All** origins must be declared—including where your bundled JS/CSS is\n> served from (`localhost` in dev, your CDN in production)." }, "McpUiResourceMeta": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -4242,7 +3257,8 @@ "additionalProperties": false } }, - "additionalProperties": false + "additionalProperties": false, + "description": "Sandbox permissions requested by the UI resource.\n\nServers declare which browser capabilities their UI needs.\nHosts MAY honor these by setting appropriate iframe `allow` attributes.\nApps SHOULD NOT assume permissions are granted; use JS feature detection as fallback." }, "McpUiResourceTeardownRequest": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -5287,369 +4303,6 @@ "const": "ui/notifications/tool-result" }, "params": { - "type": "object", - "properties": { - "_meta": { - "type": "object", - "properties": { - "progressToken": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - ] - }, - "io.modelcontextprotocol/related-task": { - "type": "object", - "properties": { - "taskId": { - "type": "string" - } - }, - "required": ["taskId"], - "additionalProperties": false - } - }, - "additionalProperties": {} - }, - "content": { - "default": [], - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "image" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "audio" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "size": { - "type": "number" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "properties": {}, - "additionalProperties": {} - }, - "type": { - "type": "string", - "const": "resource_link" - } - }, - "required": ["name", "uri", "type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource" - }, - "resource": { - "anyOf": [ - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "text": { - "type": "string" - } - }, - "required": ["uri", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "blob": { - "type": "string" - } - }, - "required": ["uri", "blob"], - "additionalProperties": false - } - ] - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "resource"], - "additionalProperties": false - } - ] - } - }, - "structuredContent": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "isError": { - "type": "boolean" - } - }, - "required": ["content"], - "additionalProperties": {}, "description": "Standard MCP tool execution result." } }, @@ -5684,322 +4337,7 @@ "content": { "description": "Context content blocks (text, image, etc.).", "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "image" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "audio" - }, - "data": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "data", "mimeType"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "title": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "sizes": { - "type": "array", - "items": { - "type": "string" - } - }, - "theme": { - "type": "string", - "enum": ["light", "dark"] - } - }, - "required": ["src"], - "additionalProperties": false - } - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "size": { - "type": "number" - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "properties": {}, - "additionalProperties": {} - }, - "type": { - "type": "string", - "const": "resource_link" - } - }, - "required": ["name", "uri", "type"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "resource" - }, - "resource": { - "anyOf": [ - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "text": { - "type": "string" - } - }, - "required": ["uri", "text"], - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "blob": { - "type": "string" - } - }, - "required": ["uri", "blob"], - "additionalProperties": false - } - ] - }, - "annotations": { - "type": "object", - "properties": { - "audience": { - "type": "array", - "items": { - "type": "string", - "enum": ["user", "assistant"] - } - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "lastModified": { - "type": "string", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$" - } - }, - "additionalProperties": false - }, - "_meta": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": ["type", "resource"], - "additionalProperties": false - } - ] - } + "items": {} }, "structuredContent": { "description": "Structured content for machine-readable context data.", diff --git a/src/generated/schema.test.ts b/src/generated/schema.test.ts index 57d989fd0..298d066b6 100644 --- a/src/generated/schema.test.ts +++ b/src/generated/schema.test.ts @@ -167,6 +167,10 @@ export type McpUiInitializeResultSchemaInferredType = z.infer< typeof generated.McpUiInitializeResultSchema >; +export type McpUiContentBlockSchemaInferredType = z.infer< + typeof generated.McpUiContentBlockSchema +>; + expectType({} as McpUiThemeSchemaInferredType); expectType({} as spec.McpUiTheme); expectType({} as McpUiDisplayModeSchemaInferredType); @@ -361,3 +365,5 @@ expectType( expectType( {} as spec.McpUiInitializeResult, ); +expectType({} as McpUiContentBlockSchemaInferredType); +expectType({} as spec.McpUiContentBlock); diff --git a/src/generated/schema.ts b/src/generated/schema.ts index aed1f9651..5fcc39681 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -2,15 +2,16 @@ // Post-processed for Zod v3/v4 compatibility and MCP SDK integration // Run: npm run generate:schemas import { z } from "zod/v4"; -import { - ContentBlockSchema, - CallToolResultSchema, - EmbeddedResourceSchema, - ImplementationSchema, - RequestIdSchema, - ResourceLinkSchema, - ToolSchema, -} from "@modelcontextprotocol/sdk/types.js"; +import { isSpecType } from "@modelcontextprotocol/client"; +import type { + ContentBlock, + CallToolResult, + EmbeddedResource, + Implementation, + RequestId, + ResourceLink, + Tool, +} from "@modelcontextprotocol/client"; /** * @description Color theme preference for the host environment. @@ -212,77 +213,81 @@ export const McpUiSandboxProxyReadyNotificationSchema = z.object({ * > **All** origins must be declared—including where your bundled JS/CSS is * > served from (`localhost` in dev, your CDN in production). */ -export const McpUiResourceCspSchema = z.object({ - /** - * @description Origins for network requests (fetch/XHR/WebSocket). - * - * - Maps to CSP `connect-src` directive - * - Empty or omitted → no network connections (secure default) - * - * @example - * ```ts - * ["https://api.weather.com", "wss://realtime.service.com"] - * ``` - */ - connectDomains: z - .array(z.string()) - .optional() - .describe( - "Origins for network requests (fetch/XHR/WebSocket).\n\n- Maps to CSP `connect-src` directive\n- Empty or omitted \u2192 no network connections (secure default)", - ), - /** - * @description Origins for static resources (images, scripts, stylesheets, fonts, media). - * - * - Maps to CSP `img-src`, `script-src`, `style-src`, `font-src`, `media-src` directives - * - Wildcard subdomains supported: `https://*.example.com` - * - Empty or omitted → no network resources (secure default) - * - * @example - * ```ts - * ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"] - * ``` - */ - resourceDomains: z - .array(z.string()) - .optional() - .describe( - "Origins for static resources (images, scripts, stylesheets, fonts, media).\n\n- Maps to CSP `img-src`, `script-src`, `style-src`, `font-src`, `media-src` directives\n- Wildcard subdomains supported: `https://*.example.com`\n- Empty or omitted \u2192 no network resources (secure default)", - ), - /** - * @description Origins for nested iframes. - * - * - Maps to CSP `frame-src` directive - * - Empty or omitted → no nested iframes allowed (`frame-src 'none'`) - * - * @example - * ```ts - * ["https://www.youtube.com", "https://player.vimeo.com"] - * ``` - */ - frameDomains: z - .array(z.string()) - .optional() - .describe( - "Origins for nested iframes.\n\n- Maps to CSP `frame-src` directive\n- Empty or omitted \u2192 no nested iframes allowed (`frame-src 'none'`)", - ), - /** - * @description Allowed base URIs for the document. - * - * - Maps to CSP `base-uri` directive - * - Empty or omitted → only same origin allowed (`base-uri 'self'`) - * - * @example - * ```ts - * ["https://cdn.example.com"] - * ``` - */ - baseUriDomains: z - .array(z.string()) - .optional() - .describe( - "Allowed base URIs for the document.\n\n- Maps to CSP `base-uri` directive\n- Empty or omitted \u2192 only same origin allowed (`base-uri 'self'`)", - ), -}); +export const McpUiResourceCspSchema = z + .object({ + /** + * @description Origins for network requests (fetch/XHR/WebSocket). + * + * - Maps to CSP `connect-src` directive + * - Empty or omitted → no network connections (secure default) + * + * @example + * ```ts + * ["https://api.weather.com", "wss://realtime.service.com"] + * ``` + */ + connectDomains: z + .array(z.string()) + .optional() + .describe( + "Origins for network requests (fetch/XHR/WebSocket).\n\n- Maps to CSP `connect-src` directive\n- Empty or omitted \u2192 no network connections (secure default)", + ), + /** + * @description Origins for static resources (images, scripts, stylesheets, fonts, media). + * + * - Maps to CSP `img-src`, `script-src`, `style-src`, `font-src`, `media-src` directives + * - Wildcard subdomains supported: `https://*.example.com` + * - Empty or omitted → no network resources (secure default) + * + * @example + * ```ts + * ["https://cdn.jsdelivr.net", "https://*.cloudflare.com"] + * ``` + */ + resourceDomains: z + .array(z.string()) + .optional() + .describe( + "Origins for static resources (images, scripts, stylesheets, fonts, media).\n\n- Maps to CSP `img-src`, `script-src`, `style-src`, `font-src`, `media-src` directives\n- Wildcard subdomains supported: `https://*.example.com`\n- Empty or omitted \u2192 no network resources (secure default)", + ), + /** + * @description Origins for nested iframes. + * + * - Maps to CSP `frame-src` directive + * - Empty or omitted → no nested iframes allowed (`frame-src 'none'`) + * + * @example + * ```ts + * ["https://www.youtube.com", "https://player.vimeo.com"] + * ``` + */ + frameDomains: z + .array(z.string()) + .optional() + .describe( + "Origins for nested iframes.\n\n- Maps to CSP `frame-src` directive\n- Empty or omitted \u2192 no nested iframes allowed (`frame-src 'none'`)", + ), + /** + * @description Allowed base URIs for the document. + * + * - Maps to CSP `base-uri` directive + * - Empty or omitted → only same origin allowed (`base-uri 'self'`) + * + * @example + * ```ts + * ["https://cdn.example.com"] + * ``` + */ + baseUriDomains: z + .array(z.string()) + .optional() + .describe( + "Allowed base URIs for the document.\n\n- Maps to CSP `base-uri` directive\n- Empty or omitted \u2192 only same origin allowed (`base-uri 'self'`)", + ), + }) + .describe( + "Content Security Policy configuration for UI resources.\n\nServers declare which origins their UI requires. Hosts use this to enforce appropriate CSP headers.\n\n> [!IMPORTANT]\n> MCP App HTML runs in a sandboxed iframe with no same-origin server.\n> **All** origins must be declared\u2014including where your bundled JS/CSS is\n> served from (`localhost` in dev, your CDN in production).", + ); /** * @description Sandbox permissions requested by the UI resource. @@ -291,52 +296,56 @@ export const McpUiResourceCspSchema = z.object({ * Hosts MAY honor these by setting appropriate iframe `allow` attributes. * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ -export const McpUiResourcePermissionsSchema = z.object({ - /** - * @description Request camera access. - * - * Maps to Permission Policy `camera` feature. - */ - camera: z - .object({}) - .optional() - .describe( - "Request camera access.\n\nMaps to Permission Policy `camera` feature.", - ), - /** - * @description Request microphone access. - * - * Maps to Permission Policy `microphone` feature. - */ - microphone: z - .object({}) - .optional() - .describe( - "Request microphone access.\n\nMaps to Permission Policy `microphone` feature.", - ), - /** - * @description Request geolocation access. - * - * Maps to Permission Policy `geolocation` feature. - */ - geolocation: z - .object({}) - .optional() - .describe( - "Request geolocation access.\n\nMaps to Permission Policy `geolocation` feature.", - ), - /** - * @description Request clipboard write access. - * - * Maps to Permission Policy `clipboard-write` feature. - */ - clipboardWrite: z - .object({}) - .optional() - .describe( - "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", - ), -}); +export const McpUiResourcePermissionsSchema = z + .object({ + /** + * @description Request camera access. + * + * Maps to Permission Policy `camera` feature. + */ + camera: z + .object({}) + .optional() + .describe( + "Request camera access.\n\nMaps to Permission Policy `camera` feature.", + ), + /** + * @description Request microphone access. + * + * Maps to Permission Policy `microphone` feature. + */ + microphone: z + .object({}) + .optional() + .describe( + "Request microphone access.\n\nMaps to Permission Policy `microphone` feature.", + ), + /** + * @description Request geolocation access. + * + * Maps to Permission Policy `geolocation` feature. + */ + geolocation: z + .object({}) + .optional() + .describe( + "Request geolocation access.\n\nMaps to Permission Policy `geolocation` feature.", + ), + /** + * @description Request clipboard write access. + * + * Maps to Permission Policy `clipboard-write` feature. + */ + clipboardWrite: z + .object({}) + .optional() + .describe( + "Request clipboard write access.\n\nMaps to Permission Policy `clipboard-write` feature.", + ), + }) + .describe( + "Sandbox permissions requested by the UI resource.\n\nServers declare which browser capabilities their UI needs.\nHosts MAY honor these by setting appropriate iframe `allow` attributes.\nApps SHOULD NOT assume permissions are granted; use JS feature detection as fallback.", + ); /** * @description Notification of UI size changes (View -> Host). @@ -495,70 +504,72 @@ export const McpUiRequestTeardownNotificationSchema = z.object({ * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities */ -export const McpUiHostCapabilitiesSchema = z.object({ - /** @description Experimental features (structure TBD). */ - experimental: z - .object({}) - .optional() - .describe("Experimental features (structure TBD)."), - /** @description Host supports opening external URLs. */ - openLinks: z - .object({}) - .optional() - .describe("Host supports opening external URLs."), - /** @description Host supports file downloads via ui/download-file. */ - downloadFile: z - .object({}) - .optional() - .describe("Host supports file downloads via ui/download-file."), - /** @description Host can proxy tool calls to the MCP server. */ - serverTools: z - .object({ - /** @description Host supports tools/list_changed notifications. */ - listChanged: z - .boolean() - .optional() - .describe("Host supports tools/list_changed notifications."), - }) - .optional() - .describe("Host can proxy tool calls to the MCP server."), - /** @description Host can proxy resource reads to the MCP server. */ - serverResources: z - .object({ - /** @description Host supports resources/list_changed notifications. */ - listChanged: z - .boolean() - .optional() - .describe("Host supports resources/list_changed notifications."), - }) - .optional() - .describe("Host can proxy resource reads to the MCP server."), - /** @description Host accepts log messages. */ - logging: z.object({}).optional().describe("Host accepts log messages."), - /** @description Sandbox configuration applied by the host. */ - sandbox: z - .object({ - /** @description Permissions granted by the host (camera, microphone, geolocation). */ - permissions: McpUiResourcePermissionsSchema.optional().describe( - "Permissions granted by the host (camera, microphone, geolocation).", - ), - /** @description CSP domains approved by the host. */ - csp: McpUiResourceCspSchema.optional().describe( - "CSP domains approved by the host.", +export const McpUiHostCapabilitiesSchema = z + .object({ + /** @description Experimental features (structure TBD). */ + experimental: z + .object({}) + .optional() + .describe("Experimental features (structure TBD)."), + /** @description Host supports opening external URLs. */ + openLinks: z + .object({}) + .optional() + .describe("Host supports opening external URLs."), + /** @description Host supports file downloads via ui/download-file. */ + downloadFile: z + .object({}) + .optional() + .describe("Host supports file downloads via ui/download-file."), + /** @description Host can proxy tool calls to the MCP server. */ + serverTools: z + .object({ + /** @description Host supports tools/list_changed notifications. */ + listChanged: z + .boolean() + .optional() + .describe("Host supports tools/list_changed notifications."), + }) + .optional() + .describe("Host can proxy tool calls to the MCP server."), + /** @description Host can proxy resource reads to the MCP server. */ + serverResources: z + .object({ + /** @description Host supports resources/list_changed notifications. */ + listChanged: z + .boolean() + .optional() + .describe("Host supports resources/list_changed notifications."), + }) + .optional() + .describe("Host can proxy resource reads to the MCP server."), + /** @description Host accepts log messages. */ + logging: z.object({}).optional().describe("Host accepts log messages."), + /** @description Sandbox configuration applied by the host. */ + sandbox: z + .object({ + /** @description Permissions granted by the host (camera, microphone, geolocation). */ + permissions: McpUiResourcePermissionsSchema.optional().describe( + "Permissions granted by the host (camera, microphone, geolocation).", + ), + /** @description CSP domains approved by the host. */ + csp: McpUiResourceCspSchema.optional().describe( + "CSP domains approved by the host.", + ), + }) + .optional() + .describe("Sandbox configuration applied by the host."), + /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ + updateModelContext: + McpUiSupportedContentBlockModalitiesSchema.optional().describe( + "Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns.", ), - }) - .optional() - .describe("Sandbox configuration applied by the host."), - /** @description Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns. */ - updateModelContext: - McpUiSupportedContentBlockModalitiesSchema.optional().describe( - "Host accepts context updates (ui/update-model-context) to be included in the model's context for future turns.", + /** @description Host supports receiving content messages (ui/message) from the view. */ + message: McpUiSupportedContentBlockModalitiesSchema.optional().describe( + "Host supports receiving content messages (ui/message) from the view.", ), - /** @description Host supports receiving content messages (ui/message) from the view. */ - message: McpUiSupportedContentBlockModalitiesSchema.optional().describe( - "Host supports receiving content messages (ui/message) from the view.", - ), -}); + }) + .describe("Capabilities supported by the host application."); /** * @description Capabilities provided by the View ({@link app!App `App`}). @@ -734,6 +745,30 @@ export const McpUiClientCapabilitiesSchema = z.object({ ), }); +const EmbeddedResourceSchema = z.custom((v) => + isSpecType("EmbeddedResource", v), +); + +const ResourceLinkSchema = z.custom((v) => + isSpecType("ResourceLink", v), +); + +const ContentBlockSchema = z.custom((v) => + isSpecType("ContentBlock", v), +); + +const CallToolResultSchema = z.custom((v) => + isSpecType("CallToolResult", v), +); + +const RequestIdSchema = z.custom((v) => isSpecType("RequestId", v)); + +const ToolSchema = z.custom((v) => isSpecType("Tool", v)); + +const ImplementationSchema = z.custom((v) => + isSpecType("Implementation", v), +); + /** * @description Request to download a file through the host. * @@ -1018,3 +1053,6 @@ export const McpUiInitializeResultSchema = z ), }) .passthrough(); + +/** Content block for ui/update-model-context. */ +export const McpUiContentBlockSchema = ContentBlockSchema; diff --git a/src/message-transport.ts b/src/message-transport.ts index 9d195435a..3cc5f797c 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -1,12 +1,9 @@ import { - JSONRPCMessage, - JSONRPCMessageSchema, - MessageExtraInfo, -} from "@modelcontextprotocol/sdk/types.js"; -import { - Transport, - TransportSendOptions, -} from "@modelcontextprotocol/sdk/shared/transport.js"; + type JSONRPCMessage, + parseJSONRPCMessage, + type Transport, + type TransportSendOptions, +} from "@modelcontextprotocol/client"; import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; /** @@ -79,28 +76,29 @@ export class PostMessageTransport implements Transport { console.debug("Ignoring message from unknown source", event); return; } - const parsed = JSONRPCMessageSchema.safeParse(event.data); - if (parsed.success) { - console.debug("Parsed message", parsed.data); - this.onmessage?.(parsed.data); - } else if (event.data?.jsonrpc !== "2.0") { - // Not a JSON-RPC message at all (e.g. internal frames injected by - // the host environment). Ignore silently so the transport stays alive. - console.debug( - "Ignoring non-JSON-RPC message", - parsed.error.message, - event, - ); - } else { - // Has jsonrpc: "2.0" but is otherwise malformed — surface as a real - // protocol error. - console.error("Failed to parse message", parsed.error.message, event); - this.onerror?.( - new Error( - "Invalid JSON-RPC message received: " + parsed.error.message, - ), - ); + let parsed: JSONRPCMessage; + try { + parsed = parseJSONRPCMessage(event.data); + } catch (err) { + if (event.data?.jsonrpc !== "2.0") { + // Not a JSON-RPC message at all (e.g. internal frames injected by + // the host environment). Ignore silently so the transport stays alive. + console.debug("Ignoring non-JSON-RPC message", err, event); + } else { + // Has jsonrpc: "2.0" but is otherwise malformed — surface as a real + // protocol error. + console.error("Failed to parse message", err, event); + this.onerror?.( + new Error( + "Invalid JSON-RPC message received: " + + (err instanceof Error ? err.message : String(err)), + ), + ); + } + return; } + console.debug("Parsed message", parsed); + this.onmessage?.(parsed); }; } @@ -166,9 +164,8 @@ export class PostMessageTransport implements Transport { * 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; + onmessage?: (message: JSONRPCMessage) => void; /** * Optional session identifier for this transport connection. diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index d6dcfbb79..cfb624609 100644 --- a/src/react/useApp.tsx +++ b/src/react/useApp.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from "react"; -import { Implementation } from "@modelcontextprotocol/sdk/types.js"; -import { Client } from "@modelcontextprotocol/sdk/client"; +import { Implementation } from "@modelcontextprotocol/client"; import { App, McpUiAppCapabilities, PostMessageTransport } from "../app"; export * from "../app"; diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index d583ae85c..29442524d 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -13,7 +13,7 @@ import type { McpServer, ToolCallback, ReadResourceCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +} from "@modelcontextprotocol/server"; import { z } from "zod"; import { registerAppTool, @@ -74,7 +74,7 @@ function registerAppTool_basicUsage(server: McpServer) { { title: "Get Weather", description: "Get current weather for a location", - inputSchema: { location: z.string() }, + inputSchema: z.object({ location: z.string() }), _meta: { ui: { resourceUri: "ui://weather/view.html" }, }, @@ -122,7 +122,7 @@ function registerAppTool_appOnlyVisibility(server: McpServer) { "update-quantity", { description: "Update item quantity in cart", - inputSchema: { itemId: z.string(), quantity: z.number() }, + inputSchema: z.object({ itemId: z.string(), quantity: z.number() }), _meta: { ui: { resourceUri: "ui://shop/cart.html", diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 1853cc05a..47fd9a3b0 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -7,7 +7,7 @@ import { getUiCapability, EXTENSION_ID, } from "./index"; -import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer } from "@modelcontextprotocol/server"; describe("registerAppTool", () => { it("should pass through config to server.registerTool", () => { @@ -31,7 +31,7 @@ describe("registerAppTool", () => { }); registerAppTool( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "my-tool", { title: "My Tool", @@ -72,7 +72,7 @@ describe("registerAppTool", () => { }; registerAppTool( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "my-tool", { _meta: { @@ -107,7 +107,7 @@ describe("registerAppTool", () => { }; registerAppTool( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "my-tool", { _meta: { @@ -142,7 +142,7 @@ describe("registerAppTool", () => { }; registerAppTool( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "my-tool", { _meta: { @@ -176,7 +176,7 @@ describe("registerAppTool", () => { }; registerAppTool( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "my-tool", { _meta: { @@ -225,7 +225,7 @@ describe("registerAppResource", () => { }); registerAppResource( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "My Resource", "ui://test/view.html", { @@ -255,7 +255,7 @@ describe("registerAppResource", () => { }; registerAppResource( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "My Resource", "ui://test/view.html", { @@ -306,7 +306,7 @@ describe("registerAppResource", () => { const callback = mock(async () => expectedResult); registerAppResource( - mockServer as unknown as Pick, + mockServer as unknown as McpServer, "My Resource", "ui://test/view.html", { _meta: { ui: {} } }, diff --git a/src/server/index.ts b/src/server/index.ts index 87209ef39..7d16f49ad 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -32,45 +32,42 @@ */ import { - RESOURCE_URI_META_KEY, - RESOURCE_MIME_TYPE, + McpUiClientCapabilities, McpUiResourceCsp, McpUiResourceMeta, McpUiToolMeta, - McpUiClientCapabilities, + RESOURCE_MIME_TYPE, + RESOURCE_URI_META_KEY, } from "../app.js"; import type { - BaseToolCallback, + ClientCapabilities, McpServer, + ReadResourceCallback, + ReadResourceResult, + RegisteredResource, RegisteredTool, ResourceMetadata, - ToolCallback, - ReadResourceCallback as _ReadResourceCallback, - RegisteredResource, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - AnySchema, - ZodRawShapeCompat, -} from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import type { - ClientCapabilities, - ReadResourceResult, + StandardSchemaWithJSON, ToolAnnotations, -} from "@modelcontextprotocol/sdk/types.js"; + ToolCallback, +} from "@modelcontextprotocol/server"; // Re-exports for convenience -export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; +export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY }; export type { ResourceMetadata, ToolCallback }; /** * Base tool configuration matching the standard MCP server tool options. * Extended by {@link McpUiAppToolConfig `McpUiAppToolConfig`} to add UI metadata requirements. */ -export interface ToolConfig { +export interface ToolConfig< + Input extends StandardSchemaWithJSON | undefined = undefined, + Output extends StandardSchemaWithJSON | undefined = undefined, +> { title?: string; description?: string; - inputSchema?: ZodRawShapeCompat | AnySchema; - outputSchema?: ZodRawShapeCompat | AnySchema; + inputSchema?: Input; + outputSchema?: Output; annotations?: ToolAnnotations; _meta?: Record; } @@ -85,370 +82,136 @@ export interface ToolConfig { * * @see {@link registerAppTool `registerAppTool`} for the recommended way to register app tools */ -export interface McpUiAppToolConfig extends ToolConfig { +export type McpUiAppToolConfig< + Input extends StandardSchemaWithJSON | undefined = undefined, + Output extends StandardSchemaWithJSON | undefined = undefined, +> = ToolConfig & { _meta: { [key: string]: unknown; } & ( - | { - ui: McpUiToolMeta; - } + | { ui: McpUiToolMeta } | { /** * URI of the UI resource to display for this tool. - * This is converted to `_meta["ui/resourceUri"]`. + * Converted to `_meta["ui/resourceUri"]` for wire compat. * * @example "ui://weather/view.html" - * * @deprecated Use `_meta.ui.resourceUri` instead. */ - [RESOURCE_URI_META_KEY]?: string; + [RESOURCE_URI_META_KEY]: string; } ); -} +}; -/** - * MCP App Resource configuration for {@link registerAppResource `registerAppResource`}. - * - * Extends the base MCP SDK `ResourceMetadata` with optional UI metadata - * for configuring security policies and rendering preferences. - * - * The `_meta.ui` field here is included in the `resources/list` response and serves as - * a static default for hosts to review at connection time. When the `resources/read` - * content item also includes `_meta.ui`, the content-item value takes precedence. - * - * @see {@link registerAppResource `registerAppResource`} for usage - */ -export interface McpUiAppResourceConfig extends ResourceMetadata { - /** - * Optional UI metadata for the resource. - * - * This appears on the resource entry in `resources/list` and acts as a listing-level - * fallback. Individual content items returned by `resources/read` may include their - * own `_meta.ui` which takes precedence over this value. - */ - _meta?: { - /** - * UI-specific metadata including CSP configuration and rendering preferences. - */ - ui?: McpUiResourceMeta; - // Allow additional metadata properties for extensibility. - [key: string]: unknown; - }; +function normalizeAppToolMeta( + meta: McpUiAppToolConfig["_meta"], +): Record { + const out: Record = { ...meta }; + const ui = { ...((meta as { ui?: McpUiToolMeta }).ui ?? {}) } as McpUiToolMeta; + const flat = out[RESOURCE_URI_META_KEY]; + // Sync both directions so hosts checking either format see the resource URI. + if (ui.resourceUri && !(RESOURCE_URI_META_KEY in out)) { + out[RESOURCE_URI_META_KEY] = ui.resourceUri; + } else if (typeof flat === "string" && !ui.resourceUri) { + ui.resourceUri = flat; + } + out.ui = ui; + return out; } /** - * Register an app tool with the MCP server. - * - * This is a convenience wrapper around `server.registerTool` that normalizes - * UI metadata: if `_meta.ui.resourceUri` is set, the legacy `_meta["ui/resourceUri"]` - * key is also populated (and vice versa) for compatibility with older hosts. - * - * @param server - The MCP server instance - * @param name - Tool name/identifier - * @param config - Tool configuration with `_meta` field containing UI metadata - * @param cb - Tool handler function - * - * @example Basic usage - * ```ts source="./index.examples.ts#registerAppTool_basicUsage" - * registerAppTool( - * server, - * "get-weather", - * { - * title: "Get Weather", - * description: "Get current weather for a location", - * inputSchema: { location: z.string() }, - * _meta: { - * ui: { resourceUri: "ui://weather/view.html" }, - * }, - * }, - * async (args) => { - * const weather = await fetchWeather(args.location); - * return { content: [{ type: "text", text: JSON.stringify(weather) }] }; - * }, - * ); - * ``` - * - * @example Tool visible to model but not callable by UI - * ```ts source="./index.examples.ts#registerAppTool_modelOnlyVisibility" - * registerAppTool( - * server, - * "show-cart", - * { - * description: "Display the user's shopping cart", - * _meta: { - * ui: { - * resourceUri: "ui://shop/cart.html", - * visibility: ["model"], - * }, - * }, - * }, - * async () => { - * const cart = await getCart(); - * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; - * }, - * ); - * ``` - * - * @example Tool hidden from model, only callable by UI - * ```ts source="./index.examples.ts#registerAppTool_appOnlyVisibility" - * registerAppTool( - * server, - * "update-quantity", - * { - * description: "Update item quantity in cart", - * inputSchema: { itemId: z.string(), quantity: z.number() }, - * _meta: { - * ui: { - * resourceUri: "ui://shop/cart.html", - * visibility: ["app"], - * }, - * }, - * }, - * async ({ itemId, quantity }) => { - * const cart = await updateCartItem(itemId, quantity); - * return { content: [{ type: "text", text: JSON.stringify(cart) }] }; - * }, - * ); - * ``` + * Register a tool whose result is rendered as an interactive App UI. * - * @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool + * Thin wrapper over `McpServer.registerTool` that normalizes the + * `_meta.ui` block and ensures both nested and flat resource-URI keys are + * present for maximum host compatibility. */ export function registerAppTool< - OutputArgs extends ZodRawShapeCompat | AnySchema, - InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, + Input extends StandardSchemaWithJSON | undefined = undefined, + Output extends StandardSchemaWithJSON | undefined = undefined, >( - server: Pick, + server: McpServer, name: string, - config: McpUiAppToolConfig & { - inputSchema?: InputArgs; - outputSchema?: OutputArgs; - }, - cb: ToolCallback, + config: McpUiAppToolConfig, + callback: ToolCallback, ): RegisteredTool { - // Normalize metadata for backward compatibility: - // - If _meta.ui.resourceUri is set, also set the legacy flat key - // - If the legacy flat key is set, also set _meta.ui.resourceUri - const meta = config._meta; - const uiMeta = meta.ui as McpUiToolMeta | undefined; - const legacyUri = meta[RESOURCE_URI_META_KEY] as string | undefined; - - let normalizedMeta = meta; - if (uiMeta?.resourceUri && !legacyUri) { - // New format -> also set legacy key - normalizedMeta = { ...meta, [RESOURCE_URI_META_KEY]: uiMeta.resourceUri }; - } else if (legacyUri && !uiMeta?.resourceUri) { - // Legacy format -> also set new format - normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; - } - - return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); + return server.registerTool( + name, + { + ...config, + _meta: normalizeAppToolMeta(config._meta), + } as Parameters[1], + callback as Parameters[2], + ); } -export type McpUiReadResourceResult = ReadResourceResult & { +/** + * Metadata for an App UI resource. Adds the optional `_meta.ui` CSP/permissions + * block on top of {@link ResourceMetadata `ResourceMetadata`}. + */ +export type McpUiAppResourceMetadata = ResourceMetadata & { _meta?: { - ui?: McpUiResourceMeta; [key: string]: unknown; + ui?: McpUiResourceMeta; }; }; -export type McpUiReadResourceCallback = ( - uri: URL, - extra: Parameters<_ReadResourceCallback>[1], -) => McpUiReadResourceResult | Promise; -export type ReadResourceCallback = McpUiReadResourceCallback; /** - * Register an app resource with the MCP server. - * - * This is a convenience wrapper around `server.registerResource` that: - * - Defaults the MIME type to {@link RESOURCE_MIME_TYPE `RESOURCE_MIME_TYPE`} (`"text/html;profile=mcp-app"`) - * - Provides a cleaner API matching the SDK's callback signature + * Register an App UI resource (the `ui://` HTML the host renders in an iframe). * - * @param server - The MCP server instance - * @param name - Human-readable resource name - * @param uri - Resource URI (should match the `_meta.ui` field in tool config) - * @param config - Resource configuration - * @param readCallback - Callback that returns the resource contents - * - * @example Basic usage - * ```ts source="./index.examples.ts#registerAppResource_basicUsage" - * registerAppResource( - * server, - * "Weather View", - * "ui://weather/view.html", - * { - * description: "Interactive weather display", - * }, - * async () => ({ - * contents: [ - * { - * uri: "ui://weather/view.html", - * mimeType: RESOURCE_MIME_TYPE, - * text: await fs.readFile("dist/view.html", "utf-8"), - * }, - * ], - * }), - * ); - * ``` - * - * @example With CSP configuration for network access - * ```ts source="./index.examples.ts#registerAppResource_withCsp" - * registerAppResource( - * server, - * "Music Player", - * "ui://music/player.html", - * { - * description: "Audio player with external soundfonts", - * }, - * async () => ({ - * contents: [ - * { - * uri: "ui://music/player.html", - * mimeType: RESOURCE_MIME_TYPE, - * text: musicPlayerHtml, - * _meta: { - * ui: { - * csp: { - * resourceDomains: ["https://cdn.example.com"], // For scripts/styles/images - * connectDomains: ["https://api.example.com"], // For fetch/WebSocket - * }, - * }, - * }, - * }, - * ], - * }), - * ); - * ``` - * - * @example With stable origin for external API CORS allowlists - * ```ts source="./index.examples.ts#registerAppResource_withDomain" - * // Computes a stable origin from an MCP server URL for hosting in Claude. - * function computeAppDomainForClaude(mcpServerUrl: string): string { - * const hash = crypto - * .createHash("sha256") - * .update(mcpServerUrl) - * .digest("hex") - * .slice(0, 32); - * return `${hash}.claudemcpcontent.com`; - * } - * - * const APP_DOMAIN = computeAppDomainForClaude("https://example.com/mcp"); - * - * registerAppResource( - * server, - * "Company Dashboard", - * "ui://dashboard/view.html", - * { - * description: "Internal dashboard with company data", - * }, - * async () => ({ - * contents: [ - * { - * uri: "ui://dashboard/view.html", - * mimeType: RESOURCE_MIME_TYPE, - * text: dashboardHtml, - * _meta: { - * ui: { - * // CSP: tell browser the app is allowed to make requests - * csp: { - * connectDomains: ["https://api.example.com"], - * }, - * // CORS: give app a stable origin for the API server to allowlist - * // - * // (Public APIs that use `Access-Control-Allow-Origin: *` or API - * // key auth don't need this.) - * domain: APP_DOMAIN, - * }, - * }, - * }, - * ], - * }), - * ); - * ``` - * - * @see {@link McpUiResourceMeta `McpUiResourceMeta`} for `_meta.ui` configuration options - * @see {@link McpUiResourceCsp `McpUiResourceCsp`} for CSP domain allowlist configuration - * @see {@link registerAppTool `registerAppTool`} to register tools that reference this resource + * Thin wrapper over `McpServer.registerResource` that defaults `mimeType` to + * {@link RESOURCE_MIME_TYPE `RESOURCE_MIME_TYPE`} and ensures resource contents + * carry that MIME type. */ export function registerAppResource( - server: Pick, + server: McpServer, name: string, uri: string, - config: McpUiAppResourceConfig, - readCallback: McpUiReadResourceCallback, + metadata: McpUiAppResourceMetadata, + readCallback: ReadResourceCallback, ): RegisteredResource { + const wrappedCallback: ReadResourceCallback = async (u, ctx) => { + const result = await readCallback(u, ctx); + return { + ...result, + contents: result.contents.map((c) => ({ + mimeType: RESOURCE_MIME_TYPE, + ...c, + })), + } as ReadResourceResult; + }; return server.registerResource( name, uri, - { - // Default MIME type for MCP App UI resources (can still be overridden by config below) - mimeType: RESOURCE_MIME_TYPE, - ...config, - }, - readCallback, + { mimeType: RESOURCE_MIME_TYPE, ...metadata }, + wrappedCallback, ); } /** - * Extension identifier for MCP Apps capability negotiation. - * - * Used as the key in `extensions` to advertise MCP Apps support. + * Type guard: returns true if `caps.extensions` declares MCP Apps support. */ +export function clientSupportsMcpApps( + caps: ClientCapabilities | undefined, +): caps is ClientCapabilities & { + extensions: { "io.modelcontextprotocol/ui": McpUiClientCapabilities }; +} { + return !!caps?.extensions?.["io.modelcontextprotocol/ui"]; +} + +export type { McpUiResourceCsp, McpUiResourceMeta, McpUiToolMeta }; + +/** SEP-2133 extension identifier for MCP Apps. Same value as MCP_APPS_EXTENSION_ID. */ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; /** - * Get MCP Apps capability settings from client capabilities. - * - * This helper retrieves the capability object from the `extensions` field - * where MCP Apps advertises its support. - * - * Note: The `clientCapabilities` parameter extends the SDK's `ClientCapabilities` - * type with an `extensions` field (pending SEP-1724). Once `extensions` is added - * to the SDK, this can use `ClientCapabilities` directly. - * - * @param clientCapabilities - The client capabilities from the initialize response - * @returns The MCP Apps capability settings, or `undefined` if not supported - * - * @example Check for MCP Apps support in server initialization - * ```ts source="./index.examples.ts#getUiCapability_checkSupport" - * server.server.oninitialized = () => { - * const clientCapabilities = server.server.getClientCapabilities(); - * const uiCap = getUiCapability(clientCapabilities); - * - * if (uiCap?.mimeTypes?.includes(RESOURCE_MIME_TYPE)) { - * // App-enhanced tool - * registerAppTool( - * server, - * "weather", - * { - * description: "Get weather information with interactive dashboard", - * _meta: { ui: { resourceUri: "ui://weather/dashboard" } }, - * }, - * weatherHandler, - * ); - * } else { - * // Text-only fallback - * server.registerTool( - * "weather", - * { - * description: "Get weather information", - * }, - * textWeatherHandler, - * ); - * } - * }; - * ``` + * Returns the MCP Apps capability blob from ClientCapabilities.extensions, or + * undefined if the client does not declare MCP Apps support. */ export function getUiCapability( - clientCapabilities: - | (ClientCapabilities & { extensions?: Record }) - | null - | undefined, + clientCapabilities: ClientCapabilities | undefined | null, ): McpUiClientCapabilities | undefined { - if (!clientCapabilities) { - return undefined; - } - - return clientCapabilities.extensions?.[EXTENSION_ID] as + return clientCapabilities?.extensions?.[EXTENSION_ID] as | McpUiClientCapabilities | undefined; } diff --git a/src/spec.types.ts b/src/spec.types.ts index 9f83fa972..fe6c77af9 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -11,14 +11,14 @@ */ import type { + RequestId, CallToolResult, ContentBlock, EmbeddedResource, Implementation, - RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. @@ -459,7 +459,7 @@ export interface McpUiResourceTeardownResult { [key: string]: unknown; } -export interface McpUiSupportedContentBlockModalities { +export type McpUiSupportedContentBlockModalities = { /** @description Host supports text content blocks. */ text?: {}; /** @description Host supports image content blocks. */ @@ -491,7 +491,7 @@ export interface McpUiRequestTeardownNotification { * @description Capabilities supported by the host application. * @see {@link McpUiInitializeResult `McpUiInitializeResult`} for the initialization result that includes these capabilities */ -export interface McpUiHostCapabilities { +export type McpUiHostCapabilities = { /** @description Experimental features (structure TBD). */ experimental?: {}; /** @description Host supports opening external URLs. */ @@ -527,7 +527,7 @@ export interface McpUiHostCapabilities { * @description Capabilities provided by the View ({@link app!App `App`}). * @see {@link McpUiInitializeRequest `McpUiInitializeRequest`} for the initialization request that includes these capabilities */ -export interface McpUiAppCapabilities { +export type McpUiAppCapabilities = { /** @description Experimental features (structure TBD). */ experimental?: {}; /** @description App exposes MCP-style tools that the host can call. */ @@ -594,7 +594,7 @@ export interface McpUiInitializedNotification { * > **All** origins must be declared—including where your bundled JS/CSS is * > served from (`localhost` in dev, your CDN in production). */ -export interface McpUiResourceCsp { +export type McpUiResourceCsp = { /** * @description Origins for network requests (fetch/XHR/WebSocket). * @@ -653,7 +653,7 @@ export interface McpUiResourceCsp { * Hosts MAY honor these by setting appropriate iframe `allow` attributes. * Apps SHOULD NOT assume permissions are granted; use JS feature detection as fallback. */ -export interface McpUiResourcePermissions { +export type McpUiResourcePermissions = { /** * @description Request camera access. * @@ -831,10 +831,13 @@ export const REQUEST_DISPLAY_MODE_METHOD: McpUiRequestDisplayModeRequest["method * capabilities during MCP initialization. Servers can check for MCP Apps * support using {@link server-helpers!getUiCapability}. */ -export interface McpUiClientCapabilities { +export type McpUiClientCapabilities = { /** * @description Array of supported MIME types for UI resources. * Must include `"text/html;profile=mcp-app"` for MCP Apps support. */ mimeTypes?: string[]; } + +/** Content block for ui/update-model-context. */ +export type McpUiContentBlock = ContentBlock; diff --git a/src/types.ts b/src/types.ts index 24bffecee..fcec84c41 100644 --- a/src/types.ts +++ b/src/types.ts @@ -67,6 +67,7 @@ export { type McpUiToolVisibility, type McpUiToolMeta, type McpUiClientCapabilities, + type McpUiContentBlock, } from "./spec.types.js"; // Import types needed for protocol type unions (not re-exported, just used internally) @@ -156,7 +157,7 @@ import { ReadResourceResult, ResourceListChangedNotification, ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; /** * All request types in the MCP Apps protocol. diff --git a/tsconfig.json b/tsconfig.json index 08ff1103f..eb5eb7a44 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,10 @@ "compilerOptions": { "target": "ES2020", "module": "ESNext", - "lib": ["ES2020", "DOM"], + "lib": [ + "ES2020", + "DOM" + ], "jsx": "react-jsx", "declaration": true, "emitDeclarationOnly": true, @@ -15,6 +18,16 @@ "skipLibCheck": true, "resolveJsonModule": true }, - "include": ["src/**/*.ts", "src/**/*.tsx", "docs/**/*.ts", "docs/**/*.tsx"], - "exclude": ["node_modules", "dist", "examples/**/*.ts"] + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "docs/**/*.ts", + "docs/**/*.tsx" + ], + "exclude": [ + "dist", + "examples", + "examples/**/*.ts", + "node_modules" + ] } diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 5775caec7..c84ebd634 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -26,9 +26,17 @@ const config = { "src/message-transport.ts", "src/types.ts", ], + externalSymbolLinkMappings: { + "@modelcontextprotocol/client": { + "*": "https://modelcontextprotocol.github.io/typescript-sdk/", + }, + "@modelcontextprotocol/server": { + "*": "https://modelcontextprotocol.github.io/typescript-sdk/", + }, + }, excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["AppOptions", "MethodSchema"], + intentionallyNotExported: [], blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false,