diff --git a/AGENTS.md b/AGENTS.md index 6bfc8cdd6..f850aa3e5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \ ### Key Source Files -- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging -- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests +- `src/app.ts` - `App` subclasses the base MCP SDK `Client`, handles View initialization, tool calls, and messaging +- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Server` for the inner iframe channel and proxies through a separate outer `Client` - `src/server/index.ts` - Helpers for MCP servers to register tools/resources with UI metadata - `src/types.ts` - Protocol types re-exported from `spec.types.ts` and Zod schemas from `generated/schema.ts` (auto-generated during build) - `src/message-transport.ts` - `PostMessageTransport` for iframe communication @@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \ ### Protocol Flow ``` -View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server +View (App/Client) <--PostMessageTransport--> Host (AppBridge/Server) <--separate MCP Client--> MCP Server ``` 1. Host creates iframe with view HTML 2. View creates `App` instance and calls `connect()` with `PostMessageTransport` -3. View sends `ui/initialize` request, receives host capabilities and context +3. View and Host complete the temporary inner base MCP handshake, then the View sends `ui/initialize` and receives authoritative Apps capabilities and context 4. Host sends `sendToolInput()` with tool arguments after initialization 5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()` 6. Host sends `sendToolResult()` when tool execution completes diff --git a/README.md b/README.md index 2fd89ffee..b3f68e77c 100644 --- a/README.md +++ b/README.md @@ -122,9 +122,17 @@ resources: ## Getting Started ```bash -npm install -S @modelcontextprotocol/ext-apps +npm install -S @modelcontextprotocol/ext-apps \ + @modelcontextprotocol/client@2.0.0-beta.4 \ + @modelcontextprotocol/server@2.0.0-beta.4 \ + @modelcontextprotocol/core@2.0.0-beta.4 \ + zod@^4.2.0 ``` +This release uses the split base MCP SDK v2 packages. Install all three at the +exact published beta.4 version so the Apps SDK and your MCP client/server share +one compatible protocol implementation. + **New here?** Start with the [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) to build your first MCP App. diff --git a/build.bun.ts b/build.bun.ts index a129d5ddd..837279ff1 100644 --- a/build.bun.ts +++ b/build.bun.ts @@ -31,9 +31,15 @@ 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"]; +// Peer dependencies stay external in the standard entry points so consumers +// share one base MCP SDK and Zod instance. The *-with-deps entry points keep +// bundling these dependencies for standalone browser use. +const PEER_EXTERNALS = [ + "@modelcontextprotocol/client", + "@modelcontextprotocol/core", + "@modelcontextprotocol/server", + "zod", +]; await Promise.all([ buildJs("src/app.ts", { diff --git a/docs/authorization.md b/docs/authorization.md index 59fc26cde..df7adc6af 100644 --- a/docs/authorization.md +++ b/docs/authorization.md @@ -150,7 +150,7 @@ registerAppTool( "get_account_balance", { description: "Get account balance", - inputSchema: { accountId: z.string() }, + inputSchema: z.object({ accountId: z.string() }), }, async ({ accountId }) => { if (!authInfo) { diff --git a/docs/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md index d91281b41..22d5d9f0c 100644 --- a/docs/migrate_from_openai_apps.md +++ b/docs/migrate_from_openai_apps.md @@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun #### Before (OpenAI) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { z } from "zod"; function createServer() { @@ -78,7 +78,7 @@ function createServer() { { title: "Shopping Cart", description: "Display the user's shopping cart", - inputSchema: { userId: z.string() }, + inputSchema: z.object({ userId: z.string() }), annotations: { readOnlyHint: true }, _meta: { "openai/outputTemplate": "ui://view/cart.html", @@ -126,7 +126,7 @@ function createServer() { #### After (MCP Apps) ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer } from "@modelcontextprotocol/server"; import { registerAppTool, registerAppResource, @@ -144,7 +144,7 @@ function createServer() { { title: "Shopping Cart", description: "Display the user's shopping cart", - inputSchema: { userId: z.string() }, + inputSchema: z.object({ userId: z.string() }), annotations: { readOnlyHint: true }, _meta: { ui: { resourceUri: "ui://view/cart.html" } }, }, diff --git a/docs/overview.md b/docs/overview.md index 28f5b2379..317ab4300 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -56,7 +56,11 @@ flowchart LR - **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them. - **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat. -The View acts as an MCP client, the Host acts as a proxy, and the Server is a standard MCP server. +The View's `App` subclasses the base MCP SDK `Client`. The Host's `AppBridge` +subclasses the base MCP SDK `Server` for the inner iframe channel, while a +separate outer `Client` connects the Host to the actual MCP Server. Keeping +those two connections separate prevents iframe negotiation and capabilities +from leaking into the server connection. ## Lifecycle @@ -93,7 +97,7 @@ sequenceDiagram ``` 1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server. -2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data. +2. **Initialization** — When a UI tool is called, the Host renders the iframe. The SDK first completes its temporary inner base MCP handshake, then the View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). `ui/initialize` remains authoritative for Apps capabilities and identity, and `ui/notifications/initialized` remains the public View-ready signal. 3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context. 4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context. 5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources. diff --git a/docs/patterns.md b/docs/patterns.md index 87a6d8232..884de2651 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,7 @@ 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..bb9eaa492 100644 --- a/docs/patterns.tsx +++ b/docs/patterns.tsx @@ -17,16 +17,12 @@ import { randomUUID } from "node:crypto"; import type { CallToolResult, ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; -import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import type { McpUiHostContext } from "../src/types.js"; import { useEffect, useState } from "react"; import { useApp } from "../src/react/index.js"; import { registerAppTool } from "../src/server/index.js"; -import { - McpServer, - ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server"; import { z } from "zod"; /** @@ -117,14 +113,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 +248,7 @@ 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..eb6b096cb 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -21,7 +21,7 @@ This tutorial assumes you've built an MCP server before and are comfortable with We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server. -You'll also need Node.js 18+. +You'll also need Node.js 20+. ## 1. Set up the project @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` @@ -178,9 +178,10 @@ 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"; +import { z } from "zod"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -205,7 +206,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async () => { @@ -240,10 +241,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 { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -263,7 +264,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index df1396c5f..e5d82b53e 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -28,6 +28,10 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex cd examples/basic-host ``` + The root install runs the package build, including type-checking the + documentation snippets against the exact base MCP SDK beta.4 packages. Do + not bypass a failed build before testing the host. + 2. Start basic-host, pointing it to your MCP server: ```bash diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 1b45dcf18..583d2ffa9 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -12,7 +12,7 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" diff --git a/examples/basic-host/src/implementation.ts b/examples/basic-host/src/implementation.ts index 31e36983e..5f680ffdc 100644 --- a/examples/basic-host/src/implementation.ts +++ b/examples/basic-host/src/implementation.ts @@ -1,8 +1,12 @@ 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, + SSEClientTransport, + StreamableHTTPClientTransport, + type CallToolResult, + type Resource, + type Tool, +} from "@modelcontextprotocol/client"; import { getTheme, onThemeChange } from "./theme"; import { HOST_STYLE_VARIABLES } from "./host-styles"; diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 3d488a792..00c0d52b2 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/client"; 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..ffdff5266 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 512720205..0187827f8 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", diff --git a/examples/basic-server-preact/server.ts b/examples/basic-server-preact/server.ts index bf14ea599..dfec94aa1 100644 --- a/examples/basic-server-preact/server.ts +++ b/examples/basic-server-preact/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type 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"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _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..351a8ec21 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/client"; 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..21c5e18af 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 { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index e4d12333f..dae7665e9 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -34,8 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/basic-server-react/server.ts b/examples/basic-server-react/server.ts index 23f6dda5a..d77c908c4 100644 --- a/examples/basic-server-react/server.ts +++ b/examples/basic-server-react/server.ts @@ -1,8 +1,12 @@ 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, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") @@ -29,7 +33,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _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..2b5fbef9a 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/client"; 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..095ef8dfd 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 8f75ac8c2..5d9140761 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", diff --git a/examples/basic-server-solid/server.ts b/examples/basic-server-solid/server.ts index 2ed2d9356..addb52260 100644 --- a/examples/basic-server-solid/server.ts +++ b/examples/basic-server-solid/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type 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"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _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..2d78b7b33 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/client"; 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..734ec0cdb 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index f877bf7c8..18bf7abed 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", diff --git a/examples/basic-server-svelte/server.ts b/examples/basic-server-svelte/server.ts index da603ca8d..941428514 100644 --- a/examples/basic-server-svelte/server.ts +++ b/examples/basic-server-svelte/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type 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"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte index fde128dde..cabcc317d 100644 --- a/examples/basic-server-svelte/src/App.svelte +++ b/examples/basic-server-svelte/src/App.svelte @@ -7,7 +7,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts index 286fa34f0..4de733187 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 5d47f2cdc..6bb75af46 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/basic-server-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts index 5b2daf704..693d86148 100644 --- a/examples/basic-server-vanillajs/server.ts +++ b/examples/basic-server-vanillajs/server.ts @@ -1,6 +1,9 @@ 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, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -29,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), 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..6b5084a3a 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/client"; 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..d77426919 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index d27ddc91b..3b34aa686 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", diff --git a/examples/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts index 5aa72de03..9adb9111e 100644 --- a/examples/basic-server-vue/server.ts +++ b/examples/basic-server-vue/server.ts @@ -1,8 +1,12 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type 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"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -28,7 +32,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time as an ISO 8601 string.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, // Links this tool to its UI resource }, async (): Promise => { diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue index 196ad64da..a45c26208 100644 --- a/examples/basic-server-vue/src/App.vue +++ b/examples/basic-server-vue/src/App.vue @@ -7,7 +7,7 @@ import { applyHostStyleVariables, type McpUiHostContext, } from "@modelcontextprotocol/ext-apps"; -import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js"; +import type { CallToolResult } from "@modelcontextprotocol/client"; function extractTime(result: CallToolResult): string { const { text } = result.content?.find((c) => c.type === "text")!; diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts index cc488f0b1..02c03cb59 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 19a1d2554..5d4fe8ebb 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts index 04fba11fe..01f5dbfb6 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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. The widget is interactive and exposes tools for reading/modifying allocations, adjusting budgets, and comparing against industry benchmarks.", - inputSchema: {}, + inputSchema: z.object({}), outputSchema: BudgetDataResponseSchema, _meta: { ui: { resourceUri } }, }, diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts index d59903b34..8b33603ba 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index 8d1268b5c..c54040a53 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts index 6d07144a2..129e2dc9e 100644 --- a/examples/cohort-heatmap-server/server.ts +++ b/examples/cohort-heatmap-server/server.ts @@ -1,5 +1,7 @@ -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -178,8 +180,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..c78875777 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index 4511de903..ef14b3264 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts index 244415d1f..7cc1c1fe2 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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..9fb0825f8 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 3417a8fe9..0e3f508f7 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -35,7 +35,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "zod": "^4.1.13" }, "devDependencies": { diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts index fd3452a19..068f94da3 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import { appendFileSync } from "node:fs"; import path from "node:path"; diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts index c45787e2b..9a42842df 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index f6715a4ff..56fc8eda6 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -15,8 +15,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/integration-server/server.ts b/examples/integration-server/server.ts index e44689d24..346f4f7a8 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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: {}, + inputSchema: z.object({}), 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..281a35674 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/client"; import { StrictMode, useCallback, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import styles from "./mcp-app.module.css"; diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index f3ed6cd27..6aef3e12f 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -23,10 +23,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", - "jose": "^6.0.0" + "jose": "^6.0.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts index fe7b1acb5..01535ebfd 100644 --- a/examples/lazy-auth-server/server.ts +++ b/examples/lazy-auth-server/server.ts @@ -21,13 +21,13 @@ import { registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import { + McpServer, + UrlElicitationRequiredError, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import cors from "cors"; import express, { type Express, type Request, type Response } from "express"; import { SignJWT, jwtVerify } from "jose"; @@ -35,6 +35,7 @@ import crypto from "node:crypto"; import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js). Derived // from import.meta.url rather than import.meta.filename/dirname, which are @@ -605,7 +606,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Show Auth Button", description: "Public tool: shows a button that triggers the protected get_secret tool.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri: buttonUri } }, }, async (): Promise => ({ @@ -638,7 +639,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Get Secret", description: "Protected tool: returns secret data. Requires authentication.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri: secretUri } }, }, async (): Promise => { @@ -682,7 +683,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Revoke Auth Token", description: "Protected tool: revokes the caller's entire auth session (access + refresh token).", - inputSchema: {}, + inputSchema: z.object({}), }, async (): Promise => { if (!authInfo) { @@ -720,7 +721,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Elicit URL (session-callback)", description: "URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.", - inputSchema: {}, + inputSchema: z.object({}), }, async (_args, extra): Promise => { const eid = crypto.randomUUID(); @@ -735,7 +736,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { message: "Please open this URL to continue.", elicitationId: eid, }, - { relatedRequestId: extra.requestId, timeout: 300_000 }, + { relatedRequestId: extra.mcpReq.id, timeout: 300_000 }, ); // Send completion notification before returning — lands on the same SSE // stream as this tool response. For Streamable HTTP this is the ONLY @@ -767,7 +768,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer { title: "Elicit by -32042 Error", description: "Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.", - inputSchema: {}, + inputSchema: z.object({}), }, async (): Promise => { // Retry path: check if the user completed the elicitation we last issued. @@ -1031,7 +1032,7 @@ export function createApp(): Express { } const server = createServer(authInfo, req); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); res.on("close", () => { diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts index f91f57b6e..80383942d 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/map-server/package.json b/examples/map-server/package.json index 5dc23fe20..a3639ce5e 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -26,8 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/map-server/server.ts b/examples/map-server/server.ts index 10125a641..5767d46ba 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.", - 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/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts index 39d7c2a22..d3090c7a4 100644 --- a/examples/map-server/src/mcp-app.ts +++ b/examples/map-server/src/mcp-app.ts @@ -6,7 +6,7 @@ * a navigate-to tool for the host to control navigation. */ import { App } from "@modelcontextprotocol/ext-apps"; -import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js"; +import type { ContentBlock } from "@modelcontextprotocol/client"; import { z } from "zod"; // TypeScript declaration for Cesium loaded from CDN diff --git a/examples/pdf-server/main.ts b/examples/pdf-server/main.ts index 1ff848a3e..bb7b57f42 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { @@ -39,7 +39,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 4d960f175..a7eb4bb95 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -26,8 +26,11 @@ }, "dependencies": { "@cantoo/pdf-lib": "^2.6.5", + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", diff --git a/examples/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts index 33a03f1fd..0b2a5eb4d 100644 --- a/examples/pdf-server/server.test.ts +++ b/examples/pdf-server/server.test.ts @@ -2,8 +2,7 @@ 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, InMemoryTransport } from "@modelcontextprotocol/client"; import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs"; import { PDFDocument } from "@cantoo/pdf-lib"; import { makeRandomJpeg } from "../../tests/helpers/range-counting-server"; diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts index 93dedfdc9..e67cd4731 100644 --- a/examples/pdf-server/server.ts +++ b/examples/pdf-server/server.ts @@ -13,18 +13,19 @@ 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 { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; import { - RootsListChangedNotificationSchema, + McpServer, type CallToolResult, + type ElicitResult, + type PrimitiveSchemaDefinition, type ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; + type Server, +} 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. @@ -62,10 +63,6 @@ class FetchStandardFontDataFactory { return new Uint8Array(await res.arrayBuffer()); } } -import type { - PrimitiveSchemaDefinition, - ElicitResult, -} from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; // ============================================================================= @@ -1282,7 +1279,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer { refreshRoots(server.server); }; server.server.setNotificationHandler( - RootsListChangedNotificationSchema, + "notifications/roots/list_changed", async () => { await refreshRoots(server.server); }, @@ -1292,10 +1289,12 @@ export function createServer(options: CreateServerOptions = {}): McpServer { const { readPdfRange } = sharedPdfCache; // 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", + inputSchema: z.object({}), + }, async (): Promise => { const seen = new Set(); const localFiles: string[] = []; @@ -1377,7 +1376,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 @@ -1386,7 +1385,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"), @@ -1473,7 +1472,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) @@ -1489,7 +1488,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() @@ -2426,7 +2425,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( @@ -2523,7 +2522,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 ( { @@ -2600,7 +2599,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 @@ -2657,7 +2656,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"), @@ -2670,7 +2669,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 => { @@ -2700,7 +2699,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"), @@ -2709,7 +2708,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 => { @@ -2739,7 +2738,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"), @@ -2751,7 +2750,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 => { @@ -2781,9 +2780,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 => { @@ -2828,10 +2827,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 a953e242a..45e0c7458 100644 --- a/examples/pdf-server/src/mcp-app.ts +++ b/examples/pdf-server/src/mcp-app.ts @@ -12,8 +12,10 @@ 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 { z } from "zod"; import * as pdfjsLib from "pdfjs-dist"; import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist"; diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index a5cbf5b22..cb4e010ab 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -8,7 +8,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } } diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts index 06895a49a..7f370a868 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 { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -21,7 +21,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 99948cc2d..f741c6e29 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -16,9 +16,12 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts index eb3c3b1ab..611d0f072 100644 --- a/examples/quickstart/server.ts +++ b/examples/quickstart/server.ts @@ -3,9 +3,10 @@ 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"; +import { z } from "zod"; const DIST_DIR = path.join(import.meta.dirname, "dist"); @@ -30,7 +31,7 @@ export function createServer(): McpServer { { title: "Get Time", description: "Returns the current server time.", - inputSchema: {}, + inputSchema: z.object({}), _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 a0076e6b9..2ebc099e6 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -15,7 +15,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } } diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts index dbd3ab6ae..cc20e639a 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index aa1e668ef..63a5d4871 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts index b59851c40..c5fa05d35 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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..7e388f0a6 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index ccf15a45c..318f01760 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -25,7 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts index 5238c40b7..50c27bc66 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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..d7bf3a0b6 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 { createMcpExpressApp } from "@modelcontextprotocol/express"; +import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node"; +import type { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index ca9fa909f..10d7c71bc 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -25,7 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts index cfd319b51..be7214ae0 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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..c55c79973 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index 12e8cea06..5bc4a9585 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -27,7 +27,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts index 8fac5f2f9..a17bcf6c9 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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, + inputSchema: z.object({}), + 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, + inputSchema: z.object({}), + outputSchema: PollStatsSchema, _meta: { ui: { visibility: ["app"] } }, }, async (): Promise => { diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts index 13a2084a8..ef16e26e2 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index cfb5686a6..fb97d333b 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -26,8 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts index ae320df80..0a13c19ef 100644 --- a/examples/threejs-server/server.ts +++ b/examples/threejs-server/server.ts @@ -8,8 +8,10 @@ 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, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -161,7 +163,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 +174,7 @@ export function createServer(): McpServer { .positive() .default(400) .describe("Height in pixels"), - }, + }), outputSchema: z.object({ success: z.boolean(), }), @@ -192,7 +194,7 @@ export function createServer(): McpServer { { title: "Learn Three.js", description: "Get documentation and examples for using the Three.js View", - inputSchema: {}, + inputSchema: z.object({}), }, async () => { return { diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx index 34194b15f..44fee0c67 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/client"; import { StrictMode, useState, useCallback, useEffect, useRef } from "react"; import { createRoot } from "react-dom/client"; import { z } from "zod"; diff --git a/examples/transcript-server/main.ts b/examples/transcript-server/main.ts index 68db8dc7d..e86ad7c16 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -25,7 +25,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index dcba3d7c0..eea4a80b5 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -25,7 +25,9 @@ }, "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/transcript-server/server.ts b/examples/transcript-server/server.ts index 56bf88d99..863eb2922 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { @@ -10,6 +10,7 @@ import { registerAppResource, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server"; +import { z } from "zod"; // Works both from source (server.ts) and compiled (dist/server.js) const DIST_DIR = import.meta.filename.endsWith(".ts") ? path.join(import.meta.dirname, "dist") @@ -33,7 +34,7 @@ export function createServer(): McpServer { title: "Transcribe Speech", description: "Opens a live speech transcription interface using the Web Speech API.", - inputSchema: {}, + inputSchema: z.object({}), _meta: { ui: { resourceUri } }, }, async (): Promise => { diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts index 082fa5abc..1983373c6 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index b0d04aff1..ea816b93c 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -24,8 +24,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" diff --git a/examples/video-resource-server/server.ts b/examples/video-resource-server/server.ts index 2cb317e41..8ae624029 100644 --- a/examples/video-resource-server/server.ts +++ b/examples/video-resource-server/server.ts @@ -12,11 +12,9 @@ import { import { McpServer, ResourceTemplate, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; + type CallToolResult, + type ReadResourceResult, +} from "@modelcontextprotocol/server"; import fs from "node:fs/promises"; import path from "node:path"; import { z } from "zod"; @@ -135,14 +133,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..2a3f8f91d 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/client"; 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..bfeeffbaa 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 } from "@modelcontextprotocol/node"; import cors from "cors"; import type { Request, Response } from "express"; import { createServer } from "./server.js"; @@ -27,7 +27,7 @@ export async function startStreamableHTTPServer( app.all("/mcp", async (req: Request, res: Response) => { const server = createServer(); - const transport = new StreamableHTTPServerTransport({ + const transport = new NodeStreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index fe511ad0f..bd31d6f78 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -26,8 +26,11 @@ "serve": "bun --watch main.ts" }, "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/express": "2.0.0-beta.4", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", diff --git a/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts index 59d5e18ee..8bc4eb7cb 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 type { - CallToolResult, - ReadResourceResult, -} from "@modelcontextprotocol/sdk/types.js"; +import { + McpServer, + type CallToolResult, + type ReadResourceResult, +} 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 291292dee..9833d9c25 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/client"; import { z } from "zod"; import { forceCenter, diff --git a/package-lock.json b/package-lock.json index 29e905875..9e0a71b5f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ }, "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -44,16 +46,18 @@ "typedoc": "^0.28.14", "typedoc-github-theme": "^0.4.0", "typescript": "^5.9.3", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "engines": { "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "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" + "zod": "^4.2.0" }, "peerDependenciesMeta": { "react": { @@ -68,8 +72,8 @@ "name": "@modelcontextprotocol/ext-apps-basic-host", "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", "react": "^19.2.0", "react-dom": "^19.2.0", "zod": "^4.1.13" @@ -111,8 +115,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", @@ -155,8 +162,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -202,8 +212,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", @@ -246,8 +259,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", @@ -290,8 +306,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -332,8 +351,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", @@ -376,8 +398,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -419,8 +443,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -466,8 +492,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -509,8 +537,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "zod": "^4.1.13" }, "bin": { @@ -549,8 +579,11 @@ "examples/integration-server": { "version": "1.7.4", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -596,10 +629,12 @@ "license": "MIT", "dependencies": { "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", - "jose": "^6.0.0" + "jose": "^6.0.0", + "zod": "^4.1.13" }, "bin": { "mcp-server-lazy-auth": "dist/index.js" @@ -637,8 +672,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -680,8 +718,11 @@ "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", @@ -722,8 +763,7 @@ "name": "@modelcontextprotocol/server-qr", "version": "1.7.4", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } }, "examples/quickstart": { @@ -731,10 +771,13 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", - "express": "^5.1.0" + "express": "^5.1.0", + "zod": "^4.1.13" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -763,8 +806,7 @@ "version": "1.7.4", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0" + "@modelcontextprotocol/ext-apps": "^1.7.0" } }, "examples/scenario-modeler-server": { @@ -772,8 +814,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -820,8 +864,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -862,8 +908,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", @@ -905,8 +953,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -949,8 +999,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", @@ -998,8 +1051,10 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1041,8 +1096,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cors": "^2.8.5", "express": "^5.1.0", "zod": "^4.1.13" @@ -1083,8 +1141,11 @@ "version": "1.7.4", "license": "MIT", "dependencies": { + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/express": "2.0.0-beta.4", "@modelcontextprotocol/ext-apps": "^1.7.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/node": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", @@ -2576,6 +2637,52 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0-beta.4.tgz", + "integrity": "sha512-VNHA/UXDk7mCpVl+jOg5B4WMRRD2OEl+it360Lhi6HiCbrIB2f6pZ0cqSDKXQVUtZvqnhdQHzZAM9h8EKWhq/A==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0-beta.4", + "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.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0-beta.4.tgz", + "integrity": "sha512-nsMXd4wQBKzmph6r+WOhum+mXjDYljTAqwY/XUg3hLtvNOQ8+JVqBSJOVCMJvx9lXhTpTOPrGZ3BuNiaNPjSvg==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/express": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0-beta.4.tgz", + "integrity": "sha512-7hI8bEomz6rMn8sKYRVIZgQJsrWIjIS9WiPyIp3X2pMyLSCC7hB5km0kg+9SygWygTIRnPfte0tXevRB3mXEHQ==", + "license": "MIT", + "dependencies": { + "cors": "^2.8.5" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-beta.4", + "express": "^4.18.0 || ^5.0.0" + } + }, "node_modules/@modelcontextprotocol/ext-apps": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", @@ -2609,6 +2716,27 @@ "resolved": "examples/basic-host", "link": true }, + "node_modules/@modelcontextprotocol/node": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0-beta.4.tgz", + "integrity": "sha512-OqbrRVNYNfLxBB2BRkP5oIHKHa0IPHluMbaLgw7YCRKcj5be9ExzyWFJY3KVMb+1F2AVn+0SRoH0XKcNzh3HYQ==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@modelcontextprotocol/server": "^2.0.0-beta.4", + "hono": "^4.11.4" + }, + "peerDependenciesMeta": { + "hono": { + "optional": true + } + } + }, "node_modules/@modelcontextprotocol/quickstart": { "resolved": "examples/quickstart", "link": true @@ -2618,6 +2746,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", @@ -2653,6 +2782,19 @@ } } }, + "node_modules/@modelcontextprotocol/server": { + "version": "2.0.0-beta.4", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0-beta.4.tgz", + "integrity": "sha512-pjMZcNEt1dOq0aJCcY3b7w1Ayh+qmQN2xlfTELcGV9yiAjEGIczBPBLWWW0k0zzo5T8kiv15jtKPsWeHGxLvLg==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0-beta.4", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/@modelcontextprotocol/server-basic-preact": { "resolved": "examples/basic-server-preact", "link": true @@ -4264,6 +4406,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", @@ -4280,6 +4423,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" }, @@ -5815,6 +5959,7 @@ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.2.tgz", "integrity": "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^10.2.0" }, @@ -5832,7 +5977,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.2", @@ -5848,7 +5994,8 @@ "url": "https://opencollective.com/fastify" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/fflate": { "version": "0.8.2", @@ -6175,6 +6322,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.23.tgz", "integrity": "sha512-eIaZ9qDgu7XV0pxOCrg7/WhnQ6Ivm22UcxhXx/A3dcbqbbYgBEkc6e/J/s7j2tS96zoB0S9VBdLwQNCWwUo4LA==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -6316,6 +6464,7 @@ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", "license": "MIT", + "peer": true, "engines": { "node": ">= 12" } @@ -6511,13 +6660,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", @@ -7493,6 +7644,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" } @@ -9619,6 +9771,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 8c6ead57b..ecbf386a8 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,9 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -105,13 +107,15 @@ "typedoc": "^0.28.14", "typedoc-github-theme": "^0.4.0", "typescript": "^5.9.3", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/client": "2.0.0-beta.4", + "@modelcontextprotocol/core": "2.0.0-beta.4", + "@modelcontextprotocol/server": "2.0.0-beta.4", "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" + "zod": "^4.2.0" }, "peerDependenciesMeta": { "react": { diff --git a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index 02d09a4e2..69e41f836 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -77,13 +77,15 @@ Before writing any code, analyze the server's existing tools and determine which ## Step 2: Add Dependencies ```bash -npm install @modelcontextprotocol/ext-apps +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 npm install -D vite vite-plugin-singlefile ``` Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React). -Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory. +Use the exact base MCP SDK prerelease required by ext-apps. Existing servers +that still import `@modelcontextprotocol/sdk` v1 must migrate those imports and +handler schemas before adding the App integration. ## Step 3: Set Up the Build Pipeline @@ -146,10 +148,14 @@ Transform plain MCP tools into App tools with UI. **Before** (plain MCP tool): ```typescript -server.tool("my-tool", { param: z.string() }, async (args) => { - const data = await fetchData(args.param); - return { content: [{ type: "text", text: JSON.stringify(data) }] }; -}); +server.registerTool( + "my-tool", + { inputSchema: z.object({ param: z.string() }) }, + async ({ param }) => { + const data = await fetchData(param); + return { content: [{ type: "text", text: JSON.stringify(data) }] }; + }, +); ``` **After** (App tool with UI): @@ -158,17 +164,22 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@model const resourceUri = "ui://my-tool/mcp-app.html"; -registerAppTool(server, "my-tool", { - description: "Shows data with an interactive UI", - inputSchema: { param: z.string() }, - _meta: { ui: { resourceUri } }, -}, async (args) => { - const data = await fetchData(args.param); - return { - content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts - structuredContent: { data }, // structured data for the UI - }; -}); +registerAppTool( + server, + "my-tool", + { + description: "Shows data with an interactive UI", + inputSchema: z.object({ param: z.string() }), + _meta: { ui: { resourceUri } }, + }, + async ({ param }) => { + const data = await fetchData(param); + return { + content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts + structuredContent: { data }, // structured data for the UI + }; + }, +); ``` Key guidance: @@ -187,17 +198,21 @@ import path from "node:path"; const resourceUri = "ui://my-tool/mcp-app.html"; -registerAppResource(server, { - uri: resourceUri, - name: "My Tool UI", - mimeType: RESOURCE_MIME_TYPE, -}, async () => { - const html = await fs.readFile( - path.resolve(import.meta.dirname, "dist", "mcp-app.html"), - "utf-8", - ); - return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; -}); +registerAppResource( + server, + "My Tool UI", + resourceUri, + {}, + async () => { + const html = await fs.readFile( + path.resolve(import.meta.dirname, "dist", "mcp-app.html"), + "utf-8", + ); + return { + contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }], + }; + }, +); ``` If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration. @@ -271,25 +286,32 @@ registerAppTool(server, "poll-data", { }); ``` -The UI calls these via `app.callServerTool("poll-data", {})`. +The UI calls these via +`app.callServerTool({ name: "poll-data", arguments: {} })`. ### CSP Configuration If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains: ```typescript -registerAppResource(server, { - uri: resourceUri, - name: "My Tool UI", - mimeType: RESOURCE_MIME_TYPE, - _meta: { - ui: { - connectDomains: ["api.example.com"], // fetch/XHR targets - resourceDomains: ["cdn.example.com"], // scripts, styles, images - frameDomains: ["embed.example.com"], // nested iframes +registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({ + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + _meta: { + ui: { + csp: { + connectDomains: ["https://api.example.com"], // fetch/XHR targets + resourceDomains: ["https://cdn.example.com"], // scripts, styles, images + frameDomains: ["https://embed.example.com"], // nested iframes + }, + }, + }, }, - }, -}, async () => { /* ... */ }); + ], +})); ``` ### Streaming Partial Input @@ -326,7 +348,14 @@ server.server.oninitialized = () => { }, appToolHandler); } else { // Text-only client — register plain tool - server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler); + server.registerTool( + "my-tool", + { + description: "Shows data", + inputSchema: z.object({ param: z.string() }), + }, + plainToolHandler, + ); } }; ``` diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md index 5db2d946c..22686ef5e 100644 --- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md +++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md @@ -115,19 +115,20 @@ Create a new MCP server with tool and resource registration. This wraps the exis ### Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 zod@^4.2.0 npm install -D tsx vite vite-plugin-singlefile ``` -Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory. +Use `npm install` with the exact base MCP SDK prerelease required by ext-apps. +Do not substitute unpublished local packages or guess a different prerelease. ### Server Code Create `server.ts`: ```typescript -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; +import { McpServer } from "@modelcontextprotocol/server"; +import { StdioServerTransport } from "@modelcontextprotocol/server/stdio"; import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server"; import fs from "node:fs/promises"; import path from "node:path"; @@ -140,7 +141,9 @@ const resourceUri = "ui://my-app/mcp-app.html"; // Register the tool — inputSchema maps to the app's data sources registerAppTool(server, "show-app", { description: "Displays the app with the given parameters", - inputSchema: { query: z.string().describe("The search query") }, + inputSchema: z.object({ + query: z.string().describe("The search query"), + }), _meta: { ui: { resourceUri } }, }, async (args) => { // Process args server-side if needed @@ -151,19 +154,29 @@ registerAppTool(server, "show-app", { }); // Register the HTML resource -registerAppResource(server, { - uri: resourceUri, - name: "My App UI", - mimeType: RESOURCE_MIME_TYPE, - // Add CSP domains from Step 2 if needed: - // _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } }, -}, async () => { - const html = await fs.readFile( - path.resolve(import.meta.dirname, "dist", "mcp-app.html"), - "utf-8", - ); - return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] }; -}); +registerAppResource( + server, + "My App UI", + resourceUri, + {}, + async () => { + const html = await fs.readFile( + path.resolve(import.meta.dirname, "dist", "mcp-app.html"), + "utf-8", + ); + return { + contents: [ + { + uri: resourceUri, + mimeType: RESOURCE_MIME_TYPE, + text: html, + // Add CSP domains from Step 2 if needed: + // _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"] } } }, + }, + ], + }; + }, +); // Start the server const transport = new StdioServerTransport(); @@ -310,7 +323,10 @@ const data = await fetch("/api/data").then(r => r.json()); // After (hybrid): async function fetchData(): Promise { if (isMcpApp) { - const result = await app.callServerTool("fetch-data", {}); + const result = await app.callServerTool({ + name: "fetch-data", + arguments: {}, + }); return result.structuredContent; } return fetch("/api/data").then(r => r.json()); @@ -437,7 +453,8 @@ registerAppTool(server, "refresh-data", { }); ``` -The UI calls these via `app.callServerTool("refresh-data", {})`. +The UI calls these via +`app.callServerTool({ name: "refresh-data", arguments: {} })`. ### Streaming Partial Input diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md index 5bd12191d..fc41e70cd 100644 --- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md +++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md @@ -110,11 +110,12 @@ See `/tmp/mcp-ext-apps/docs/patterns.md` for detailed recipes: **Always** use `npm install` to add dependencies rather than manually writing version numbers: ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.4 @modelcontextprotocol/core@2.0.0-beta.4 @modelcontextprotocol/server@2.0.0-beta.4 @modelcontextprotocol/node@2.0.0-beta.4 @modelcontextprotocol/express@2.0.0-beta.4 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors ``` -This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory. +Use the exact base MCP SDK prerelease required by ext-apps. Do not substitute +unpublished local packages or guess a different prerelease. ### TypeScript Server Execution diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 109f35247..85225f5c5 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -12,7 +12,7 @@ * * ts-to-zod generates `import { z } from "zod"`. We rewrite this to * `import { z } from "zod/v4"` because the generated schemas compose with - * schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK + * schemas imported from `@modelcontextprotocol/core`, which the SDK * constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime * fails with errors like `keyValidator._parse is not a function` (v3 internals * calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by @@ -22,7 +22,7 @@ * * **Problem**: ts-to-zod cannot resolve types imported from external packages. * When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`, - * `RequestId`, and `Tool` from `@modelcontextprotocol/sdk`, it generates `z.any()` + * `RequestId`, and `Tool` from `@modelcontextprotocol/core`, it generates `z.any()` * as a placeholder. * * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders. @@ -193,7 +193,7 @@ function postProcess(content: string): string { `import { z } from "zod/v4"; import { ${mcpImports}, -} from "@modelcontextprotocol/sdk/types.js";`, +} from "@modelcontextprotocol/core";`, ); // 2. Remove z.any() placeholders for external types (now imported from MCP SDK) diff --git a/scripts/typedoc-plugin-base-sdk-links.mjs b/scripts/typedoc-plugin-base-sdk-links.mjs new file mode 100644 index 000000000..609891828 --- /dev/null +++ b/scripts/typedoc-plugin-base-sdk-links.mjs @@ -0,0 +1,16 @@ +/** + * Resolve module-local links inherited from the base MCP SDK declarations. + * + * TypeDoc's externalSymbolLinkMappings handles package-qualified symbols, but + * the base SDK writes this particular reference as `index.inputRequired`. + */ + +const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/"; + +/** @param {import("typedoc").Application} app */ +export function load(app) { + app.converter.addUnknownSymbolResolver((ref) => { + const name = ref.symbolReference?.path?.map((part) => part.path).join("."); + if (name === "index.inputRequired") return BASE_SDK_DOCS; + }); +} diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts index d5dc5bc74..47ad6234f 100644 --- a/src/app-bridge.examples.ts +++ b/src/app-bridge.examples.ts @@ -7,17 +7,11 @@ * @module */ -import { Client } from "@modelcontextprotocol/sdk/client/index.js"; -import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; -import { - CallToolResult, - CallToolResultSchema, +import { Client, type Transport } from "@modelcontextprotocol/client"; +import type { CreateMessageRequest, CreateMessageResult, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResultSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; import { AppBridge, PostMessageTransport } from "./app-bridge.js"; import type { McpUiDisplayMode } from "./types.js"; @@ -223,8 +217,7 @@ function AppBridge_oncalltool_forwardToServer( bridge.oncalltool = async (params, extra) => { return mcpClient.request( { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_oncalltool_forwardToServer @@ -245,7 +238,9 @@ function AppBridge_oncreatesamplingmessage_forwardToLlm( //#region AppBridge_oncreatesamplingmessage_forwardToLlm bridge.oncreatesamplingmessage = async (params, extra) => { // Apply rate limiting, user approval, cost controls here - return await myLlmProvider.complete(params, { signal: extra.signal }); + return await myLlmProvider.complete(params, { + signal: extra.mcpReq.signal, + }); }; //#endregion AppBridge_oncreatesamplingmessage_forwardToLlm } @@ -261,8 +256,7 @@ function AppBridge_onlistresources_returnResources( bridge.onlistresources = async (params, extra) => { return mcpClient.request( { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onlistresources_returnResources @@ -279,8 +273,7 @@ function AppBridge_onreadresource_returnResource( bridge.onreadresource = async (params, extra) => { return mcpClient.request( { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onreadresource_returnResource @@ -297,8 +290,7 @@ function AppBridge_onlistprompts_returnPrompts( bridge.onlistprompts = async (params, extra) => { return mcpClient.request( { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; //#endregion AppBridge_onlistprompts_returnPrompts @@ -444,10 +436,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.request({ + method: "tools/call", + params: { name: "get_weather", arguments: args }, + }); bridge.sendToolResult(result); //#endregion AppBridge_sendToolResult_afterExecution } diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index a4fe1de1e..81e072fd6 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1,17 +1,6 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } 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 { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { Server, type ServerCapabilities } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; import { App } from "./app"; @@ -22,6 +11,7 @@ import { getToolUiResourceUri, isToolVisibilityModelOnly, isToolVisibilityAppOnly, + McpUiOpenLinkResultSchema, type McpUiHostCapabilities, } from "./app-bridge"; @@ -34,11 +24,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0)); */ function createMockClient( serverCapabilities: ServerCapabilities = {}, -): Pick { +): Pick< + Client, + | "getServerCapabilities" + | "request" + | "notification" + | "setNotificationHandler" +> { return { getServerCapabilities: () => serverCapabilities, request: async () => ({}) as never, notification: async () => {}, + setNotificationHandler: () => {}, }; } @@ -85,6 +82,58 @@ describe("App <-> AppBridge integration", () => { expect(initializedFired).toBe(true); }); + it("keeps standard initialization separate from the Apps-ready gate", async () => { + const methods: string[] = []; + let releaseAppsInitialized!: () => void; + let reachedAppsInitialized!: () => void; + const appsInitializedGate = new Promise((resolve) => { + releaseAppsInitialized = resolve; + }); + const reachedGate = new Promise((resolve) => { + reachedAppsInitialized = resolve; + }); + const originalSend = appTransport.send.bind(appTransport); + appTransport.send = async (message, options) => { + if ("method" in message) methods.push(message.method); + if ( + "method" in message && + message.method === "ui/notifications/initialized" + ) { + reachedAppsInitialized(); + await appsInitializedGate; + } + return originalSend(message, options); + }; + + let singularCalls = 0; + let listenerCalls = 0; + bridge.oninitialized = () => singularCalls++; + bridge.addEventListener("initialized", () => listenerCalls++); + + await bridge.connect(bridgeTransport); + const connecting = app.connect(appTransport); + await reachedGate; + + expect(methods).toEqual([ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ]); + expect(app.getNegotiatedProtocolVersion()).toBe("2025-11-25"); + expect(bridge.getNegotiatedProtocolVersion()).toBe("2025-11-25"); + expect(bridge.getClientVersion()).toEqual(testAppInfo); + expect(bridge.getAppVersion()).toEqual(testAppInfo); + expect(singularCalls).toBe(0); + expect(listenerCalls).toBe(0); + + releaseAppsInitialized(); + await connecting; + + expect(singularCalls).toBe(1); + expect(listenerCalls).toBe(1); + }); + it("App receives host info and capabilities after connect", async () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -94,6 +143,8 @@ describe("App <-> AppBridge integration", () => { const hostCaps = app.getHostCapabilities(); expect(hostCaps).toEqual(testHostCapabilities); + expect(bridge.getHostCapabilities()).toEqual(testHostCapabilities); + expect(bridge.getCapabilities()).toEqual({}); }); it("Bridge receives app info and capabilities after initialization", async () => { @@ -614,6 +665,30 @@ describe("App <-> AppBridge integration", () => { expect(result.isError).toBe(true); }); + + it("rejects invalid params for a custom ui request", async () => { + bridge.onopenlink = async () => ({}); + await app.connect(appTransport); + + await expect( + app.request( + { + method: "ui/open-link", + params: { url: 42 }, + }, + McpUiOpenLinkResultSchema, + ), + ).rejects.toThrow(); + }); + + it("rejects invalid results from a custom ui handler", async () => { + bridge.onopenlink = async () => ({ isError: "yes" }) as never; + await app.connect(appTransport); + + await expect( + app.openLink({ url: "https://example.com" }), + ).rejects.toThrow(); + }); }); describe("deprecated method aliases", () => { @@ -683,10 +758,7 @@ describe("App <-> AppBridge integration", () => { await app.connect(appTransport); // Bridge can send ping via the protocol's request method - const result = await bridge.request( - { method: "ping", params: {} }, - EmptyResultSchema, - ); + const result = await bridge.request({ method: "ping", params: {} }); expect(result).toEqual({}); }); @@ -801,7 +873,7 @@ describe("App <-> AppBridge integration", () => { }); await bridge.callTool({ name: "noargs", arguments: {} }); expect(receivedExtra).toBeDefined(); - expect(receivedExtra.signal).toBeInstanceOf(AbortSignal); + expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal); }); it("isError result skips output schema validation", async () => { @@ -2183,10 +2255,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.request({ + method: "resources/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2227,10 +2299,10 @@ 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.request({ + method: "resources/read", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2278,10 +2350,10 @@ 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.request({ + method: "resources/templates/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2301,10 +2373,10 @@ 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.request({ + method: "prompts/list", + params: requestParams, + }); expect(receivedRequests).toHaveLength(1); expect(receivedRequests[0]).toMatchObject(requestParams); @@ -2313,9 +2385,13 @@ describe("App <-> AppBridge integration", () => { it("sendToolListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + bridge.oncalltool = async () => ({ content: [] }); + app.setNotificationHandler( + "notifications/tools/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2328,9 +2404,13 @@ describe("App <-> AppBridge integration", () => { it("sendResourceListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + bridge.onlistresources = async () => ({ resources: [] }); + app.setNotificationHandler( + "notifications/resources/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2343,9 +2423,13 @@ describe("App <-> AppBridge integration", () => { it("sendPromptListChanged sends notification to app", async () => { const receivedNotifications: unknown[] = []; - app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => { - receivedNotifications.push(n.params); - }); + bridge.onlistprompts = async () => ({ prompts: [] }); + app.setNotificationHandler( + "notifications/prompts/list_changed", + (notification) => { + receivedNotifications.push(notification.params); + }, + ); await bridge.connect(bridgeTransport); await app.connect(appTransport); @@ -2358,6 +2442,184 @@ describe("App <-> AppBridge integration", () => { }); }); +describe("AppBridge outer v2 Client proxy", () => { + let outerClient: Client; + let outerServer: Server; + let outerClientTransport: InMemoryTransport; + let outerServerTransport: InMemoryTransport; + let outerInitializeCount: number; + + beforeEach(async () => { + [outerClientTransport, outerServerTransport] = + InMemoryTransport.createLinkedPair(); + outerInitializeCount = 0; + const originalSend = outerClientTransport.send.bind(outerClientTransport); + outerClientTransport.send = async (message, options) => { + if ("method" in message && message.method === "initialize") { + outerInitializeCount++; + } + return originalSend(message, options); + }; + + outerServer = new Server( + { name: "ActualServer", version: "1.0.0" }, + { + capabilities: { + tools: { listChanged: true }, + resources: { listChanged: true }, + prompts: { listChanged: true }, + }, + supportedProtocolVersions: ["2025-11-25"], + }, + ); + outerServer.setRequestHandler("tools/call", async (request) => ({ + content: [ + { + type: "text", + text: `outer:${request.params.name}`, + }, + ], + })); + + outerClient = new Client( + { name: "HostOuterClient", version: "1.0.0" }, + { + capabilities: {}, + versionNegotiation: { mode: "legacy" }, + supportedProtocolVersions: ["2025-11-25"], + }, + ); + await outerServer.connect(outerServerTransport); + await outerClient.connect(outerClientTransport); + }); + + afterEach(async () => { + await outerClient.close().catch(() => {}); + await outerServer.close().catch(() => {}); + }); + + it("proxies through the outer Client without reconnecting it", async () => { + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + let [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + expect( + await app.callServerTool({ name: "first", arguments: {} }), + ).toMatchObject({ + content: [{ type: "text", text: "outer:first" }], + }); + expect(outerInitializeCount).toBe(1); + + await app.close(); + await bridge.close(); + [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect( + await app.callServerTool({ name: "second", arguments: {} }), + ).toMatchObject({ + content: [{ type: "text", text: "outer:second" }], + }); + expect(outerInitializeCount).toBe(1); + expect(outerClient.getServerVersion()).toMatchObject({ + name: "ActualServer", + version: "1.0.0", + }); + + await app.close(); + await bridge.close(); + }); + + it("propagates View cancellation to the actual outer Server", async () => { + let started!: () => void; + let aborted!: () => void; + const startedPromise = new Promise((resolve) => { + started = resolve; + }); + const abortedPromise = new Promise((resolve) => { + aborted = resolve; + }); + outerServer.setRequestHandler("tools/call", async (_request, context) => { + started(); + await new Promise((resolve) => { + context.mcpReq.signal.addEventListener( + "abort", + () => { + aborted(); + resolve(); + }, + { once: true }, + ); + }); + return { content: [] }; + }); + + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + const controller = new AbortController(); + const result = app.callServerTool( + { name: "cancel-me", arguments: {} }, + { signal: controller.signal }, + ); + await startedPromise; + controller.abort("test cancellation"); + await abortedPromise; + await expect(result).rejects.toThrow(); + + await app.close(); + await bridge.close(); + }); + + it("forwards outer list-changed notifications to the App", async () => { + const bridge = new AppBridge( + outerClient, + testHostInfo, + testHostCapabilities, + ); + const app = new App(testAppInfo, {}, { autoResize: false }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + const received: string[] = []; + app.setNotificationHandler("notifications/tools/list_changed", () => { + received.push("tools"); + }); + app.setNotificationHandler("notifications/resources/list_changed", () => { + received.push("resources"); + }); + app.setNotificationHandler("notifications/prompts/list_changed", () => { + received.push("prompts"); + }); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + await outerServer.sendToolListChanged(); + await outerServer.sendResourceListChanged(); + await outerServer.sendPromptListChanged(); + await flush(); + + expect(received).toEqual(["tools", "resources", "prompts"]); + + await app.close(); + await bridge.close(); + }); +}); + describe("getToolUiResourceUri", () => { describe("new nested format (_meta.ui.resourceUri)", () => { it("extracts resourceUri from _meta.ui.resourceUri", () => { @@ -2713,7 +2975,7 @@ describe("isToolVisibilityAppOnly", () => { expect(a).toEqual([]); }); - it("App.onEventDispatch merges hostcontext before listeners fire", async () => { + it("App merges hostcontext before listeners fire", async () => { let seen: unknown; app.addEventListener("hostcontextchanged", () => { seen = app.getHostContext(); @@ -2752,38 +3014,16 @@ describe("isToolVisibilityAppOnly", () => { expect(app.onteardown).toBe(handler); }); - it("direct setRequestHandler throws when called twice", () => { + it("direct setRequestHandler uses base SDK replacement semantics", () => { 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", () => {}); + bridge2.setRequestHandler("ping", () => ({})); expect(() => { - app2.setNotificationHandler( - // @ts-expect-error — exercising throw path with raw schema - { - shape: { method: { value: "ui/notifications/tool-input" } }, - }, - () => {}, - ); - }).toThrow(/already registered/); + bridge2.setRequestHandler("ping", () => ({})); + }).not.toThrow(); }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 23383c40f..d353cf71e 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1,58 +1,51 @@ -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, - CreateMessageRequest, - CreateMessageRequestSchema, - CreateMessageResult, - CreateMessageResultWithTools, - 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 CreateMessageRequest, + type CreateMessageResult, + type CreateMessageResultWithTools, + type EmptyResult, + type Implementation, + type JSONRPCRequest, + type ListPromptsRequest, + type ListPromptsResult, + type ListResourcesRequest, + type ListResourcesResult, + type ListResourceTemplatesRequest, + type ListResourceTemplatesResult, + type ListToolsRequest, + type LoggingMessageNotification, + type PingRequest, + type PromptListChangedNotification, + type ReadResourceRequest, + type ReadResourceResult, + type RequestOptions, + type ResourceListChangedNotification, + type Result, + type ServerCapabilities, + type ServerContext, + type ServerOptions, + type Tool, + type ToolListChangedNotification, + type Transport, +} from "@modelcontextprotocol/server"; import { - Protocol, - ProtocolOptions, - RequestOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; -import { ProtocolWithEvents } from "./events"; + EmptyResultSchema, + LoggingMessageNotificationSchema, +} from "@modelcontextprotocol/core"; +import { EventDispatcher } from "./events"; +import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; + +type MethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; import { - type AppNotification, - type AppRequest, - type AppResult, type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, type McpUiToolCancelledNotification, @@ -71,15 +64,19 @@ import { McpUiInitializeRequest, McpUiInitializeRequestSchema, McpUiInitializeResult, + McpUiInitializeResultSchema, McpUiMessageRequest, McpUiMessageRequestSchema, McpUiMessageResult, + McpUiMessageResultSchema, McpUiOpenLinkRequest, McpUiOpenLinkRequestSchema, McpUiOpenLinkResult, + McpUiOpenLinkResultSchema, McpUiDownloadFileRequest, McpUiDownloadFileRequestSchema, McpUiDownloadFileResult, + McpUiDownloadFileResultSchema, McpUiResourceTeardownRequest, McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, @@ -90,6 +87,7 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeRequestSchema, McpUiRequestDisplayModeResult, + McpUiRequestDisplayModeResultSchema, McpUiResourcePermissions, McpUiToolMeta, } from "./types"; @@ -98,6 +96,20 @@ export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app"; import { RESOURCE_URI_META_KEY } from "./app"; export { PostMessageTransport } from "./message-transport"; +function proxiedInnerCapabilities( + capabilities: ServerCapabilities, +): ServerCapabilities { + return { + ...(capabilities.tools && { tools: capabilities.tools }), + ...(capabilities.resources && { + resources: { + listChanged: capabilities.resources.listChanged, + }, + }), + ...(capabilities.prompts && { prompts: capabilities.prompts }), + }; +} + /** * Extract UI resource URI from tool metadata. * @@ -203,10 +215,13 @@ export function buildAllowAttribute( * * @property hostContext - Optional initial host context to provide to the view * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options + * @see `ServerOptions` from @modelcontextprotocol/server for available options * @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure */ -export type HostOptions = ProtocolOptions & { +export type HostOptions = Omit< + ServerOptions, + "capabilities" | "supportedProtocolVersions" +> & { hostContext?: McpUiHostContext; }; @@ -221,15 +236,12 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; /** * Extra metadata passed to request handlers. * - * This type represents the additional context provided by the `Protocol` class - * when handling requests, including abort signals and session information. - * It is extracted from the MCP SDK's request handler signature. + * This type represents the additional context provided by the base MCP SDK + * `Server` when handling requests. * * @internal */ -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +type RequestHandlerExtra = ServerContext; /** * Maps DOM-style event names to their notification `params` types. @@ -249,7 +261,7 @@ export type AppBridgeEventMap = { /** * 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 + * `AppBridge` extends the base MCP SDK's public `Server` 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 @@ -303,47 +315,57 @@ export type AppBridgeEventMap = { * await bridge.connect(transport); * ``` */ -export class AppBridge extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppBridgeEventMap -> { +export class AppBridge extends Server { private _appCapabilities?: McpUiAppCapabilities; private _hostContext: McpUiHostContext = {}; private _appInfo?: Implementation; private _initializedReceived = false; + private readonly _registeredEvents = new Set(); + private readonly _events = new EventDispatcher(); /** - * Wrap every handler registered via `replaceRequestHandler` with a check - * that the View has sent `ui/notifications/initialized`. Warns (never - * throws) so lenient hosts keep working while still surfacing the - * misordering that leaves strict hosts with a permanently hidden iframe. - * `ui/initialize` and `ping` use `setRequestHandler` directly and are - * intentionally exempt. + * The base MCP SDK calls this hook for every standard and custom request + * handler registration. AppBridge uses it to preserve the existing warning + * when a View calls host methods before `ui/notifications/initialized`, + * without adding overload-forwarding adapters around `setRequestHandler`. + * Chaining through `super._wrapHandler()` retains the base `Server`'s own + * role-specific behavior, including tool-result validation. The two + * initialization methods and `ping` are exempt because they are valid before + * the Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch. * * @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149} */ - private _baseReplaceRequestHandler = this.replaceRequestHandler; - protected override replaceRequestHandler: Protocol< - AppRequest, - AppNotification, - AppResult - >["setRequestHandler"] = (schema, handler) => { - this._baseReplaceRequestHandler(schema, (request, extra) => { + protected override _wrapHandler( + method: string, + handler: ( + request: JSONRPCRequest, + context: ServerContext, + ) => Promise, + ): (request: JSONRPCRequest, context: ServerContext) => Promise { + const wrapped = super._wrapHandler(method, handler); + if ( + method === "initialize" || + method === "ui/initialize" || + method === "ping" + ) { + return wrapped; + } + return async (request, context) => { if (!this._initializedReceived) { console.warn( - `[ext-apps] AppBridge received '${request.method}' before ` + + `[ext-apps] AppBridge received '${method}' before ` + `ui/notifications/initialized. The View is calling host ` + `methods before completing the handshake; it should await ` + `app.connect() first.`, ); } - return handler(request, extra); - }); - }; + return wrapped(request, context); + }; + } - protected readonly eventSchemas = { + private readonly eventSchemas: { + [K in keyof AppBridgeEventMap]: MethodSchema; + } = { sizechange: McpUiSizeChangedNotificationSchema, sandboxready: McpUiSandboxProxyReadyNotificationSchema, initialized: McpUiInitializedNotificationSchema, @@ -351,6 +373,80 @@ export class AppBridge extends ProtocolWithEvents< loggingmessage: LoggingMessageNotificationSchema, }; + protected setEventHandler( + event: K, + handler: ((params: AppBridgeEventMap[K]) => void) | undefined, + ): void { + this._ensureEventSlot(event); + this._events.setHandler(event, handler); + } + + protected getEventHandler( + event: K, + ): ((params: AppBridgeEventMap[K]) => void) | undefined { + return this._events.getHandler(event); + } + + addEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._ensureEventSlot(event); + this._events.addEventListener(event, handler); + } + + removeEventListener( + event: K, + handler: (params: AppBridgeEventMap[K]) => void, + ): void { + this._events.removeEventListener(event, handler); + } + + private _ensureEventSlot(event: K): void { + if (this._registeredEvents.has(event)) return; + const schema = this.eventSchemas[event]; + if (!schema) throw new Error(`Unknown event: ${String(event)}`); + + const dispatch = (params: AppBridgeEventMap[K]) => { + if (event === "initialized") this._initializedReceived = true; + this._events.dispatch(event, params); + }; + + if (event === "loggingmessage") { + this.setNotificationHandler("notifications/message", (notification) => { + dispatch(notification.params as AppBridgeEventMap[K]); + }); + } else { + this.setNotificationHandler( + schema.shape.method.value, + { params: schema.shape.params }, + (params) => dispatch(params as AppBridgeEventMap[K]), + ); + } + this._registeredEvents.add(event); + } + + protected warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, + ): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); + } + } + + private ensureInnerCapabilities(capabilities: ServerCapabilities): void { + const current = super.getCapabilities(); + const missing = Object.keys(capabilities).some( + (key) => current[key as keyof ServerCapabilities] === undefined, + ); + if (missing) this.registerCapabilities(capabilities); + } + /** * Create a new AppBridge instance. * @@ -359,8 +455,8 @@ export class AppBridge extends ProtocolWithEvents< * 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) + * @param _hostCapabilities - Features and capabilities the host supports + * @param options - Configuration options (inherited from Server) * * @example With MCP client (automatic forwarding) * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" @@ -387,31 +483,61 @@ export class AppBridge extends ProtocolWithEvents< constructor( private _client: Client | null, private _hostInfo: Implementation, - private _capabilities: McpUiHostCapabilities, + private _hostCapabilities: McpUiHostCapabilities, options?: HostOptions, ) { - super(options); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 + super(_hostInfo, { + ...options, + capabilities: {}, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }); - this.addEventListener("initialized", () => { - this._initializedReceived = true; + // The base Server's standard initialized callback is MCP-ready state. The + // public AppBridge callback remains exclusively the Apps View-ready gate. + super.setNotificationHandler("notifications/initialized", () => {}); + Object.defineProperty(this, "oninitialized", { + configurable: true, + enumerable: true, + get: () => this.getEventHandler("initialized"), + set: (callback: AppBridge["oninitialized"]) => + this.setEventHandler("initialized", callback), }); + this._ensureEventSlot("initialized"); + this._hostContext = options?.hostContext || {}; - this.setRequestHandler(McpUiInitializeRequestSchema, (request) => - this._oninitialize(request), + this.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + (params) => this._onAppsInitialize(params), ); - this.setRequestHandler(PingRequestSchema, (request, extra) => { + this.setRequestHandler("ping", (request, extra) => { this.onping?.(request.params, extra); return {}; }); // Default handler for requestDisplayMode - returns current mode from host context. // Hosts can override this by setting bridge.onrequestdisplaymode = ... - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - (request) => { + this.setRequestHandler( + "ui/request-display-mode", + { + params: McpUiRequestDisplayModeRequestSchema.shape.params, + result: McpUiRequestDisplayModeResultSchema, + }, + () => { const currentMode = this._hostContext.displayMode ?? "inline"; return { mode: currentMode }; }, @@ -541,8 +667,7 @@ export class AppBridge extends ProtocolWithEvents< * ```typescript * bridge.onsandboxready = async () => { * const resource = await mcpClient.request( - * { method: "resources/read", params: { uri: "ui://my-app" } }, - * ReadResourceResultSchema + * { method: "resources/read", params: { uri: "ui://my-app" } } * ); * * bridge.sendSandboxResourceReady({ @@ -588,18 +713,9 @@ export class AppBridge extends ProtocolWithEvents< * @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"); - } - set oninitialized( - callback: - | ((params: McpUiInitializedNotification["params"]) => void) - | undefined, - ) { - this.setEventHandler("initialized", callback); - } + override oninitialized?: ( + params?: McpUiInitializedNotification["params"], + ) => void; /** * Register a handler for message requests from the view. @@ -652,11 +768,15 @@ export class AppBridge extends ProtocolWithEvents< ) { this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this._onmessage = callback; - this.replaceRequestHandler( - McpUiMessageRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/message", + { + params: McpUiMessageRequestSchema.shape.params, + result: McpUiMessageResultSchema, + }, + async (params, extra) => { if (!this._onmessage) throw new Error("No onmessage handler set"); - return this._onmessage(request.params, extra); + return this._onmessage(params, extra); }, ); } @@ -721,11 +841,15 @@ export class AppBridge extends ProtocolWithEvents< ) { this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this._onopenlink = callback; - this.replaceRequestHandler( - McpUiOpenLinkRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/open-link", + { + params: McpUiOpenLinkRequestSchema.shape.params, + result: McpUiOpenLinkResultSchema, + }, + async (params, extra) => { if (!this._onopenlink) throw new Error("No onopenlink handler set"); - return this._onopenlink(request.params, extra); + return this._onopenlink(params, extra); }, ); } @@ -793,12 +917,16 @@ export class AppBridge extends ProtocolWithEvents< callback, ); this._ondownloadfile = callback; - this.replaceRequestHandler( - McpUiDownloadFileRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/download-file", + { + params: McpUiDownloadFileRequestSchema.shape.params, + result: McpUiDownloadFileResultSchema, + }, + async (params, extra) => { if (!this._ondownloadfile) throw new Error("No ondownloadfile handler set"); - return this._ondownloadfile(request.params, extra); + return this._ondownloadfile(params, extra); }, ); } @@ -893,12 +1021,16 @@ export class AppBridge extends ProtocolWithEvents< callback, ); this._onrequestdisplaymode = callback; - this.replaceRequestHandler( - McpUiRequestDisplayModeRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/request-display-mode", + { + params: McpUiRequestDisplayModeRequestSchema.shape.params, + result: McpUiRequestDisplayModeResultSchema, + }, + async (params, extra) => { if (!this._onrequestdisplaymode) throw new Error("No onrequestdisplaymode handler set"); - return this._onrequestdisplaymode(request.params, extra); + return this._onrequestdisplaymode(params, extra); }, ); } @@ -990,12 +1122,16 @@ export class AppBridge extends ProtocolWithEvents< callback, ); this._onupdatemodelcontext = callback; - this.replaceRequestHandler( - McpUiUpdateModelContextRequestSchema, - async (request, extra) => { + this.setRequestHandler( + "ui/update-model-context", + { + params: McpUiUpdateModelContextRequestSchema.shape.params, + result: EmptyResultSchema, + }, + async (params, extra) => { if (!this._onupdatemodelcontext) throw new Error("No onupdatemodelcontext handler set"); - return this._onupdatemodelcontext(request.params, extra); + return this._onupdatemodelcontext(params, extra); }, ); } @@ -1017,14 +1153,13 @@ export class AppBridge extends ProtocolWithEvents< * bridge.oncalltool = async (params, extra) => { * return mcpClient.request( * { method: "tools/call", params }, - * CallToolResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type - * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type + * @see `CallToolRequest` from @modelcontextprotocol/server for the request type + * @see `CallToolResult` from @modelcontextprotocol/server for the result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1043,13 +1178,11 @@ export class AppBridge extends ProtocolWithEvents< ) { 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); - }, - ); + this.ensureInnerCapabilities({ tools: {} }); + this.setRequestHandler("tools/call", async (request, extra) => { + if (!this._oncalltool) throw new Error("No oncalltool handler set"); + return this._oncalltool(request.params, extra); + }); } /** @@ -1074,12 +1207,14 @@ export class AppBridge extends ProtocolWithEvents< * ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm" * bridge.oncreatesamplingmessage = async (params, extra) => { * // Apply rate limiting, user approval, cost controls here - * return await myLlmProvider.complete(params, { signal: extra.signal }); + * return await myLlmProvider.complete(params, { + * signal: extra.mcpReq.signal, + * }); * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/server for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/server for result types */ set oncreatesamplingmessage( callback: ( @@ -1087,12 +1222,9 @@ export class AppBridge extends ProtocolWithEvents< extra: RequestHandlerExtra, ) => Promise, ) { - this.setRequestHandler( - CreateMessageRequestSchema, - async (request, extra) => { - return callback(request.params, extra); - }, - ); + this.setRequestHandler("sampling/createMessage", async (request, extra) => { + return callback(request.params, extra); + }); } /** @@ -1112,7 +1244,7 @@ export class AppBridge extends ProtocolWithEvents< * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1138,14 +1270,13 @@ export class AppBridge extends ProtocolWithEvents< * bridge.onlistresources = async (params, extra) => { * return mcpClient.request( * { method: "resources/list", params }, - * ListResourcesResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourcesRequest` from @modelcontextprotocol/server for the request type + * @see `ListResourcesResult` from @modelcontextprotocol/server for the result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1168,14 +1299,12 @@ export class AppBridge extends ProtocolWithEvents< 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); - }, - ); + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler("resources/list", async (request, extra) => { + if (!this._onlistresources) + throw new Error("No onlistresources handler set"); + return this._onlistresources(request.params, extra); + }); } /** @@ -1195,14 +1324,13 @@ export class AppBridge extends ProtocolWithEvents< * bridge.onlistresourcetemplates = async (params, extra) => { * return mcpClient.request( * { method: "resources/templates/list", params }, - * ListResourceTemplatesResultSchema, - * { signal: extra.signal } + * { signal: extra.mcpReq.signal } * ); * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/server for the request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/server for the result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1225,8 +1353,9 @@ export class AppBridge extends ProtocolWithEvents< callback, ); this._onlistresourcetemplates = callback; - this.replaceRequestHandler( - ListResourceTemplatesRequestSchema, + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler( + "resources/templates/list", async (request, extra) => { if (!this._onlistresourcetemplates) throw new Error("No onlistresourcetemplates handler set"); @@ -1252,14 +1381,13 @@ export class AppBridge extends ProtocolWithEvents< * bridge.onreadresource = async (params, extra) => { * return mcpClient.request( * { method: "resources/read", params }, - * ReadResourceResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type + * @see `ReadResourceRequest` from @modelcontextprotocol/server for the request type + * @see `ReadResourceResult` from @modelcontextprotocol/server for the result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1282,14 +1410,12 @@ export class AppBridge extends ProtocolWithEvents< 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); - }, - ); + this.ensureInnerCapabilities({ resources: {} }); + this.setRequestHandler("resources/read", async (request, extra) => { + if (!this._onreadresource) + throw new Error("No onreadresource handler set"); + return this._onreadresource(request.params, extra); + }); } /** @@ -1309,7 +1435,7 @@ export class AppBridge extends ProtocolWithEvents< * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1337,14 +1463,13 @@ export class AppBridge extends ProtocolWithEvents< * bridge.onlistprompts = async (params, extra) => { * return mcpClient.request( * { method: "prompts/list", params }, - * ListPromptsResultSchema, - * { signal: extra.signal }, + * { signal: extra.mcpReq.signal }, * ); * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type + * @see `ListPromptsRequest` from @modelcontextprotocol/server for the request type + * @see `ListPromptsResult` from @modelcontextprotocol/server for the result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1367,14 +1492,11 @@ export class AppBridge extends ProtocolWithEvents< 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); - }, - ); + this.ensureInnerCapabilities({ prompts: {} }); + this.setRequestHandler("prompts/list", async (request, extra) => { + if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); + return this._onlistprompts(request.params, extra); + }); } /** @@ -1394,7 +1516,7 @@ export class AppBridge extends ProtocolWithEvents< * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/server for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ @@ -1403,46 +1525,6 @@ export class AppBridge extends ProtocolWithEvents< }); } - /** - * Verify that the guest supports the capability required for the given request method. - * @internal - */ - assertCapabilityForMethod(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that a request handler is registered and supported for the given method. - * @internal - */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { - // TODO - } - - /** - * Verify that the host supports the capability required for the given notification method. - * @internal - */ - assertNotificationCapability(method: AppNotification["method"]): void { - // TODO - } - - /** - * 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"); - } - /** * Get the host capabilities passed to the constructor. * @@ -1450,18 +1532,18 @@ export class AppBridge extends ProtocolWithEvents< * * @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure */ - getCapabilities(): McpUiHostCapabilities { - return this._capabilities; + getHostCapabilities(): McpUiHostCapabilities { + return this._hostCapabilities; } /** * Handle the ui/initialize request from the guest. * @internal */ - private async _oninitialize( - request: McpUiInitializeRequest, + private async _onAppsInitialize( + params: McpUiInitializeRequest["params"], ): Promise { - const requestedVersion = request.params.protocolVersion; + const requestedVersion = params.protocolVersion; if (this._appInfo !== undefined) { console.warn( @@ -1472,8 +1554,8 @@ export class AppBridge extends ProtocolWithEvents< ); } - this._appCapabilities = request.params.appCapabilities; - this._appInfo = request.params.appInfo; + this._appCapabilities = params.appCapabilities; + this._appInfo = params.appInfo; const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes( requestedVersion, @@ -1483,7 +1565,7 @@ export class AppBridge extends ProtocolWithEvents< return { protocolVersion, - hostCapabilities: this.getCapabilities(), + hostCapabilities: this.getHostCapabilities(), hostInfo: this._hostInfo, hostContext: this._hostContext, }; @@ -1636,10 +1718,10 @@ export class AppBridge extends ProtocolWithEvents< * * @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, - * ); + * const result = await mcpClient.request({ + * method: "tools/call", + * params: { name: "get_weather", arguments: args }, + * }); * bridge.sendToolResult(result); * ``` * @@ -1765,11 +1847,7 @@ export class AppBridge extends ProtocolWithEvents< * @returns Promise resolving to the tool call result */ callTool(params: CallToolRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/call", params }, - CallToolResultSchema, - options, - ); + return this.request({ method: "tools/call", params }, options); } /** @@ -1782,11 +1860,7 @@ export class AppBridge extends ProtocolWithEvents< * @returns Promise resolving to the list of tools */ listTools(params: ListToolsRequest["params"], options?: RequestOptions) { - return this.request( - { method: "tools/list", params }, - ListToolsResultSchema, - options, - ); + return this.request({ method: "tools/list", params }, options); } /** @@ -1857,18 +1931,18 @@ export class AppBridge extends ProtocolWithEvents< if (!serverCapabilities) { throw new Error("Client server capabilities not available"); } + this.registerCapabilities(proxiedInnerCapabilities(serverCapabilities)); if (serverCapabilities.tools) { this.oncalltool = async (params, extra) => { return this._client!.request( { method: "tools/call", params }, - CallToolResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.tools.listChanged) { this._client.setNotificationHandler( - ToolListChangedNotificationSchema, + "notifications/tools/list_changed", (n) => this.sendToolListChanged(n.params), ); } @@ -1877,27 +1951,24 @@ export class AppBridge extends ProtocolWithEvents< this.onlistresources = async (params, extra) => { return this._client!.request( { method: "resources/list", params }, - ListResourcesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; this.onlistresourcetemplates = async (params, extra) => { return this._client!.request( { method: "resources/templates/list", params }, - ListResourceTemplatesResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; this.onreadresource = async (params, extra) => { return this._client!.request( { method: "resources/read", params }, - ReadResourceResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.resources.listChanged) { this._client.setNotificationHandler( - ResourceListChangedNotificationSchema, + "notifications/resources/list_changed", (n) => this.sendResourceListChanged(n.params), ); } @@ -1906,22 +1977,20 @@ export class AppBridge extends ProtocolWithEvents< this.onlistprompts = async (params, extra) => { return this._client!.request( { method: "prompts/list", params }, - ListPromptsResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, ); }; if (serverCapabilities.prompts.listChanged) { this._client.setNotificationHandler( - PromptListChangedNotificationSchema, + "notifications/prompts/list_changed", (n) => this.sendPromptListChanged(n.params), ); } } } - // 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. + // The standard MCP handshake only establishes the temporary inner Server + // role. ui/notifications/initialized remains the public View-ready signal. return super.connect(transport); } diff --git a/src/app.examples.ts b/src/app.examples.ts index 81bdd4440..7d475efa0 100644 --- a/src/app.examples.ts +++ b/src/app.examples.ts @@ -7,11 +7,8 @@ * @module */ -import type { Tool } from "@modelcontextprotocol/sdk/types.js"; -import type { - McpServer, - ToolCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { SamplingMessage, Tool } from "@modelcontextprotocol/client"; +import type { McpServer, ToolCallback } from "@modelcontextprotocol/server"; import { App, PostMessageTransport, @@ -369,7 +366,7 @@ async function App_createSamplingMessage_simple(app: App) { */ async function App_createSamplingMessage_withTools( app: App, - messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[], + messages: SamplingMessage[], ) { //#region App_createSamplingMessage_withTools if (!app.getHostCapabilities()?.sampling?.tools) return; diff --git a/src/app.test.ts b/src/app.test.ts new file mode 100644 index 000000000..dcf2d5287 --- /dev/null +++ b/src/app.test.ts @@ -0,0 +1,300 @@ +import { afterEach, describe, expect, it } from "bun:test"; +import { + InMemoryTransport, + Server, + type JSONRPCMessage, +} from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app"; +import { + LATEST_PROTOCOL_VERSION, + McpUiInitializeRequestSchema, + McpUiInitializeResultSchema, + McpUiInitializedNotificationSchema, +} from "./types"; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +type ConnectedPair = { + app: App; + server: Server; + sentByApp: JSONRPCMessage[]; + appsInitialized: Promise; +}; + +async function connectPair( + app = new App( + { name: "view-app", version: "1.0.0" }, + { tools: { listChanged: true } }, + { autoResize: false }, + ), +): Promise { + const server = new Server( + { name: "inner-mcp-server", version: "0.0.0" }, + { + capabilities: { resources: {}, tools: {} }, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }, + ); + const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + const sentByApp: JSONRPCMessage[] = []; + const send = appTransport.send.bind(appTransport); + appTransport.send = async (message, options) => { + sentByApp.push(message); + await send(message, options); + }; + + let resolveAppsInitialized!: () => void; + const appsInitialized = new Promise((resolve) => { + resolveAppsInitialized = resolve; + }); + + server.setRequestHandler( + "ui/initialize", + { + params: McpUiInitializeRequestSchema.shape.params, + result: McpUiInitializeResultSchema, + }, + (params) => { + expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION); + return { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities: { openLinks: {} }, + hostInfo: { name: "apps-host", version: "2.0.0" }, + hostContext: { theme: "dark" as const, locale: "en-US" }, + }; + }, + ); + server.setNotificationHandler( + "ui/notifications/initialized", + { params: McpUiInitializedNotificationSchema.shape.params }, + () => resolveAppsInitialized(), + ); + server.setRequestHandler("tools/call", (request) => ({ + content: [{ type: "text", text: `called ${request.params.name}` }], + })); + server.setRequestHandler("resources/read", (request) => ({ + contents: [{ uri: request.params.uri, text: "resource body" }], + })); + server.setRequestHandler("resources/list", () => ({ + resources: [{ uri: "test://resource", name: "Test resource" }], + })); + server.setRequestHandler("sampling/createMessage", () => ({ + model: "test-model", + role: "assistant", + content: { type: "text", text: "sampled" }, + })); + + await server.connect(serverTransport); + await app.connect(appTransport); + await appsInitialized; + + return { app, server, sentByApp, appsInitialized }; +} + +const connected: Array> = []; + +afterEach(async () => { + await Promise.all( + connected + .splice(0) + .flatMap(({ app, server }) => [app.close(), server.close()]), + ); +}); + +describe("App base MCP SDK v2 Client migration", () => { + it("completes the legacy MCP handshake before the authoritative Apps handshake", async () => { + const pair = await connectPair(); + connected.push(pair); + + const lifecycleMethods = pair.sentByApp + .filter( + (message): message is JSONRPCMessage & { method: string } => + "method" in message, + ) + .map((message) => message.method) + .filter((method) => + [ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ].includes(method), + ); + + expect(lifecycleMethods).toEqual([ + "initialize", + "notifications/initialized", + "ui/initialize", + "ui/notifications/initialized", + ]); + expect(pair.app.getNegotiatedProtocolVersion()).toBe( + INNER_MCP_PROTOCOL_VERSION, + ); + }); + + it("uses ui/initialize as the authority for Apps host state", async () => { + const pair = await connectPair(); + connected.push(pair); + + expect(pair.app.getServerVersion()).toEqual({ + name: "inner-mcp-server", + version: "0.0.0", + }); + expect(pair.app.getHostVersion()).toEqual({ + name: "apps-host", + version: "2.0.0", + }); + expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} }); + expect(pair.app.getHostContext()).toEqual({ + theme: "dark", + locale: "en-US", + }); + }); + + it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => { + const pair = await connectPair(); + connected.push(pair); + let receivedSignal: AbortSignal | undefined; + + pair.app.registerTool( + "greet", + { description: "Greets a person" }, + async (extra) => { + receivedSignal = extra.mcpReq.signal; + return { content: [{ type: "text", text: "hello" }] }; + }, + ); + + const listed = await pair.server.request({ + method: "tools/list", + params: {}, + }); + const result = await pair.server.request({ + method: "tools/call", + params: { name: "greet", arguments: {} }, + }); + + expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]); + expect(result.content).toEqual([{ type: "text", text: "hello" }]); + expect(receivedSignal).toBeInstanceOf(AbortSignal); + }); + + it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => { + const pair = await connectPair(); + connected.push(pair); + + const toolResult = await pair.app.callServerTool({ + name: "refresh", + arguments: {}, + }); + const resource = await pair.app.readServerResource({ + uri: "test://resource", + }); + const resources = await pair.app.listServerResources(); + const sample = await pair.app.createSamplingMessage({ + messages: [{ role: "user", content: { type: "text", text: "hello" } }], + maxTokens: 20, + }); + + expect(toolResult.content).toEqual([ + { type: "text", text: "called refresh" }, + ]); + expect(resource.contents[0]).toEqual({ + uri: "test://resource", + text: "resource body", + }); + expect(resources.resources.map(({ uri }) => uri)).toEqual([ + "test://resource", + ]); + expect(sample.content).toEqual({ type: "text", text: "sampled" }); + }); + + it("preserves reconnect behavior after close", async () => { + const first = await connectPair(); + await first.app.close(); + await first.server.close(); + + const second = await connectPair(first.app); + connected.push(second); + + expect(second.app.getNegotiatedProtocolVersion()).toBe( + INNER_MCP_PROTOCOL_VERSION, + ); + expect(second.app.getHostVersion()).toEqual({ + name: "apps-host", + version: "2.0.0", + }); + }); + + it("preserves auto-resize setup after Apps initialization", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: true }, + ); + let setupCalls = 0; + app.setupSizeChangedNotifications = () => { + setupCalls += 1; + return () => {}; + }; + + const pair = await connectPair(app); + connected.push(pair); + + expect(setupCalls).toBe(1); + }); + + it("preserves strict late-handler timing errors", async () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false, strict: true }, + ); + const pair = await connectPair(app); + connected.push(pair); + + expect(() => { + app.ontoolinput = () => {}; + }).toThrow(/handler registered after connect/); + }); + + it("uses base MCP SDK replacement semantics for request handlers", async () => { + const pair = await connectPair(); + connected.push(pair); + let handledBy = 0; + + pair.app.setRequestHandler("ping", () => { + handledBy = 1; + return {}; + }); + pair.app.setRequestHandler("ping", () => { + handledBy = 2; + return {}; + }); + + await pair.server.request({ method: "ping", params: {} }); + expect(handledBy).toBe(2); + }); + + it("uses base MCP SDK replacement semantics for non-event notifications", async () => { + const pair = await connectPair(); + connected.push(pair); + const calls: number[] = []; + const params = z.object({}); + + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(1); + }); + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(2); + }); + await pair.server.notification({ + method: "test/notification", + params: {}, + }); + + expect(calls).toEqual([2]); + }); +}); diff --git a/src/app.ts b/src/app.ts index adfad5c77..7e752cec3 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,39 +1,29 @@ import { + Client, + type ClientContext, + type ClientOptions, type RequestOptions, - mergeCapabilities, - ProtocolOptions, -} from "@modelcontextprotocol/sdk/shared/protocol.js"; - -import { - CallToolRequest, - CallToolRequestSchema, - CallToolResult, - CallToolResultSchema, - CreateMessageRequest, - CreateMessageResult, - CreateMessageResultSchema, - CreateMessageResultWithTools, - CreateMessageResultWithToolsSchema, - EmptyResultSchema, - Implementation, - ListResourcesRequest, - ListResourcesResult, - ListResourcesResultSchema, - ListToolsRequest, - ListToolsRequestSchema, - ListToolsResult, - LoggingMessageNotification, - PingRequestSchema, - ReadResourceRequest, - ReadResourceResult, - ReadResourceResultSchema, - Tool, - ToolAnnotations, - ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; -import { AppNotification, AppRequest, AppResult } from "./types"; -import { ProtocolWithEvents } from "./events"; -export { ProtocolWithEvents }; + type CallToolRequest, + type CallToolResult, + type CreateMessageRequest, + type CreateMessageResult, + type CreateMessageResultWithTools, + type Implementation, + type ListResourcesRequest, + type ListResourcesResult, + type ListToolsRequest, + type ListToolsResult, + type LoggingMessageNotification, + type ReadResourceRequest, + type ReadResourceResult, + type Tool, + type ToolAnnotations, + type ToolListChangedNotification, + type Transport, +} from "@modelcontextprotocol/client"; +import { EmptyResultSchema } from "@modelcontextprotocol/core"; +import { EventDispatcher } from "./events"; +export { EventDispatcher } from "./events"; import { PostMessageTransport } from "./message-transport"; import { LATEST_PROTOCOL_VERSION, @@ -55,6 +45,7 @@ import { McpUiResourceTeardownRequest, McpUiResourceTeardownRequestSchema, McpUiResourceTeardownResult, + McpUiResourceTeardownResultSchema, McpUiRequestTeardownNotification, McpUiSizeChangedNotification, McpUiToolCancelledNotification, @@ -68,13 +59,36 @@ import { McpUiRequestDisplayModeRequest, McpUiRequestDisplayModeResultSchema, } from "./types"; -import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import { StandardSchemaV1, standardSchemaToJsonSchema, validateStandardSchema, } from "./standard-schema"; -import { z } from "zod/v4"; +import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; + +type MethodSchema = ZodObject<{ + method: ZodLiteral; + params: ZodType; +}>; + +const INNER_MCP_PROTOCOL_VERSION = "2025-11-25"; + +function mergeAppCapabilities( + base: McpUiAppCapabilities, + additional: McpUiAppCapabilities, +): McpUiAppCapabilities { + const result = { ...base, ...additional }; + if (additional.experimental !== undefined) { + result.experimental = { + ...base.experimental, + ...additional.experimental, + }; + } + if (additional.tools !== undefined) { + result.tools = { ...base.tools, ...additional.tools }; + } + return result; +} export type { StandardSchemaV1, @@ -160,11 +174,14 @@ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; /** * Options for configuring {@link App `App`} behavior. * - * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration. + * Extends `ClientOptions` from the base MCP SDK with `App`-specific configuration. * - * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options + * @see `ClientOptions` from @modelcontextprotocol/client for inherited options */ -export type AppOptions = ProtocolOptions & { +export type AppOptions = Omit< + ClientOptions, + "capabilities" | "supportedProtocolVersions" | "versionNegotiation" +> & { /** * Automatically report size changes to the host using `ResizeObserver`. * @@ -204,9 +221,7 @@ export type AppOptions = ProtocolOptions & { allowUnsafeEval?: boolean; }; -type RequestHandlerExtra = Parameters< - Parameters[1] ->[1]; +type RequestHandlerExtra = ClientContext; /** * Result of an app-registered tool callback. When `Out` is provided, @@ -228,7 +243,7 @@ export type AppToolResult< * validated/parsed input; when `In` is `undefined`, the callback receives only * `extra`. When `Out` is provided, the return's `structuredContent` is typed. * - * Mirrors `ToolCallback` from `@modelcontextprotocol/sdk/server/mcp.js` but is + * Mirrors `ToolCallback` from `@modelcontextprotocol/server` but is * parameterized over {@link StandardSchemaV1} instead of zod, so any * Standard-Schema-compatible library (Zod, ArkType, Valibot, …) can be used. */ @@ -246,7 +261,7 @@ export type AppToolCallback< /** * Handle returned by {@link App.registerTool}. Mirrors `RegisteredTool` from - * `@modelcontextprotocol/sdk/server/mcp.js` but stores + * `@modelcontextprotocol/server` but stores * {@link StandardSchemaV1} schemas. */ export type RegisteredAppTool = { @@ -271,9 +286,9 @@ export type RegisteredAppTool = { /** * 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. + * Used by {@link App `App`} and its composed {@link EventDispatcher} to + * provide type-safe `addEventListener` / `removeEventListener` and singular + * `on*` handler support. */ export type AppEventMap = { toolinput: McpUiToolInputNotification["params"]; @@ -287,9 +302,8 @@ export type AppEventMap = { * 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. + * that run inside host applications. It extends the base MCP SDK's public + * `Client` class and adds the Apps handshake, lifecycle, and high-level APIs. * * ## Architecture * @@ -304,16 +318,14 @@ export type AppEventMap = { * 3. **Interactive**: Send requests, receive notifications, call tools * 4. **Teardown**: Host sends teardown request before unmounting * - * ## Inherited Methods + * ## Protocol and event methods * - * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits: + * As a subclass of the base MCP SDK's `Client`, `App` provides: * - `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 @@ -342,17 +354,14 @@ export type AppEventMap = { * await app.connect(); * ``` */ -export class App extends ProtocolWithEvents< - AppRequest, - AppNotification, - AppResult, - AppEventMap -> { +export class App extends Client { private _hostCapabilities?: McpUiHostCapabilities; private _hostInfo?: Implementation; private _hostContext?: McpUiHostContext; private _registeredTools: { [name: string]: RegisteredAppTool } = {}; private _initializedSent = false; + private readonly _registeredEvents = new Set(); + private readonly _events = new EventDispatcher(); /** * Warn if a host-bound method is called before {@link connect `connect`} has @@ -378,7 +387,9 @@ export class App extends ProtocolWithEvents< console.warn(`${msg}. This will throw in a future release.`); } - protected readonly eventSchemas = { + private readonly eventSchemas: { + [K in keyof AppEventMap]: MethodSchema; + } = { toolinput: McpUiToolInputNotificationSchema, toolinputpartial: McpUiToolInputPartialNotificationSchema, toolresult: McpUiToolResultNotificationSchema, @@ -427,28 +438,65 @@ export class App extends ProtocolWithEvents< console.warn(msg); } - protected override setEventHandler( + protected setEventHandler( event: K, handler: ((params: AppEventMap[K]) => void) | undefined, ): void { if (handler) this._assertHandlerTiming(event); - super.setEventHandler(event, handler); + this._ensureEventSlot(event); + this._events.setHandler(event, handler); } - override addEventListener( + addEventListener( event: K, handler: (params: AppEventMap[K]) => void, ): void { this._assertHandlerTiming(event); - super.addEventListener(event, handler); + this._ensureEventSlot(event); + this._events.addEventListener(event, handler); } - protected override onEventDispatch( + removeEventListener( event: K, - params: AppEventMap[K], + handler: (params: AppEventMap[K]) => void, ): void { - if (event === "hostcontextchanged") { - this._hostContext = { ...this._hostContext, ...params }; + this._events.removeEventListener(event, handler); + } + + private _ensureEventSlot(event: K): void { + if (this._registeredEvents.has(event)) return; + const schema = this.eventSchemas[event]; + if (!schema) throw new Error(`Unknown event: ${String(event)}`); + this.setNotificationHandler( + schema.shape.method.value, + { params: schema.shape.params }, + (params) => { + const eventParams = params as AppEventMap[K]; + if (event === "hostcontextchanged") { + this._hostContext = { ...this._hostContext, ...eventParams }; + } + this._events.dispatch(event, eventParams); + }, + ); + this._registeredEvents.add(event); + } + + protected getEventHandler( + event: K, + ): ((params: AppEventMap[K]) => void) | undefined { + return this._events.getHandler(event); + } + + protected warnIfRequestHandlerReplaced( + name: string, + previous: unknown, + next: unknown, + ): void { + if (previous && next) { + console.warn( + `[MCP Apps] ${name} handler replaced. ` + + `Previous handler will no longer be called.`, + ); } } @@ -456,7 +504,7 @@ export class App extends ProtocolWithEvents< * Create a new MCP App instance. * * @param _appInfo - App identification (name and version) - * @param _capabilities - Features and capabilities this app provides + * @param _appCapabilities - Features and capabilities this app provides * @param options - Configuration options including `autoResize` behavior * * @example @@ -470,33 +518,48 @@ export class App extends ProtocolWithEvents< */ constructor( private _appInfo: Implementation, - private _capabilities: McpUiAppCapabilities = {}, + private _appCapabilities: McpUiAppCapabilities = {}, private options: AppOptions = { autoResize: true }, ) { - super(options); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 + super(_appInfo, { + ...options, + capabilities: {}, + versionNegotiation: { mode: "legacy" }, + supportedProtocolVersions: [INNER_MCP_PROTOCOL_VERSION], + }); if (!options.allowUnsafeEval) { z.config({ jitless: true }); } - this.setRequestHandler(PingRequestSchema, (request) => { + this.setRequestHandler("ping", (request) => { console.log("Received ping:", request.params); return {}; }); - // Eagerly register the hostcontextchanged event slot so that - // onEventDispatch (which merges into _hostContext) fires even if the - // user never assigns onhostcontextchanged or calls addEventListener. + // Eagerly register hostcontextchanged so the owner-side context merge runs + // even if the user never assigns a handler or adds a listener. this.setEventHandler("hostcontextchanged", undefined); } - private registerCapabilities(capabilities: McpUiAppCapabilities): void { + private registerAppCapabilities(capabilities: McpUiAppCapabilities): void { if (this.transport) { throw new Error( "Cannot register capabilities after transport is established", ); } - this._capabilities = mergeCapabilities(this._capabilities, capabilities); + this._appCapabilities = mergeAppCapabilities( + this._appCapabilities, + capabilities, + ); } registerTool< @@ -519,7 +582,7 @@ export class App extends ProtocolWithEvents< } const app = this; const notify = () => { - if (app._initializedSent && app._capabilities.tools?.listChanged) { + if (app._initializedSent && app._appCapabilities.tools?.listChanged) { void app.sendToolListChanged(); } }; @@ -591,8 +654,8 @@ export class App extends ProtocolWithEvents< // to declare { tools: {} } in the constructor just to use registerTool. // Only do this pre-connect; post-connect the capability was already // advertised (or wasn't) and can't change. - if (!this._capabilities.tools && !this.transport) { - this.registerCapabilities({ tools: { listChanged: true } }); + if (!this._appCapabilities.tools && !this.transport) { + this.registerAppCapabilities({ tools: { listChanged: true } }); } this.ensureToolHandlersInitialized(); @@ -924,9 +987,9 @@ export class App extends ProtocolWithEvents< * 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. + * 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. * @@ -1004,11 +1067,15 @@ export class App extends ProtocolWithEvents< ) { this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.replaceRequestHandler( - McpUiResourceTeardownRequestSchema, - (request, extra) => { + this.setRequestHandler( + "ui/resource-teardown", + { + params: McpUiResourceTeardownRequestSchema.shape.params, + result: McpUiResourceTeardownResultSchema, + }, + (params, extra) => { if (!this._onteardown) throw new Error("No onteardown handler set"); - return this._onteardown(request.params, extra); + return this._onteardown(params, extra); }, ); } @@ -1058,7 +1125,7 @@ export class App extends ProtocolWithEvents< ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => { + this.setRequestHandler("tools/call", (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1129,7 +1196,7 @@ export class App extends ProtocolWithEvents< callback, ); this._onlisttools = callback; - this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => { + this.setRequestHandler("tools/list", (request, extra) => { if (!this._onlisttools) throw new Error("No onlisttools handler set"); return this._onlisttools(request.params, extra); }); @@ -1139,7 +1206,7 @@ export class App extends ProtocolWithEvents< * Verify that the host supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: AppRequest["method"]): void { + protected override assertCapabilityForMethod(method: string): void { switch (method) { case "sampling/createMessage": if (!this._hostCapabilities?.sampling) { @@ -1155,11 +1222,11 @@ export class App extends ProtocolWithEvents< * Verify that the app declared the capability required for the given request method. * @internal */ - assertRequestHandlerCapability(method: AppRequest["method"]): void { + protected override assertRequestHandlerCapability(method: string): void { switch (method) { case "tools/call": case "tools/list": - if (!this._capabilities.tools) { + if (!this._appCapabilities.tools) { throw new Error( `Client does not support tool capability (required for ${method})`, ); @@ -1177,26 +1244,10 @@ export class App extends ProtocolWithEvents< * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(_method: AppNotification["method"]): void { + protected override assertNotificationCapability(_method: string): void { // TODO } - /** - * 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"); - } - /** * Call a tool on the originating MCP server (proxied through the host). * @@ -1245,7 +1296,6 @@ export class App extends ProtocolWithEvents< } return await this.request( { method: "tools/call", params }, - CallToolResultSchema, { // Hosts may interpose long-running or user-interactive steps before the // tool result arrives. Opting in here lets a host heartbeat keep the @@ -1302,11 +1352,7 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("readServerResource"); - return await this.request( - { method: "resources/read", params }, - ReadResourceResultSchema, - options, - ); + return await this.request({ method: "resources/read", params }, options); } /** @@ -1350,11 +1396,7 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("listServerResources"); - return await this.request( - { method: "resources/list", params }, - ListResourcesResultSchema, - options, - ); + return await this.request({ method: "resources/list", params }, options); } /** @@ -1415,8 +1457,8 @@ export class App extends ProtocolWithEvents< * } * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ async createSamplingMessage( params: CreateMessageRequest["params"] & { tools?: undefined }, @@ -1431,12 +1473,8 @@ export class App extends ProtocolWithEvents< options?: RequestOptions, ): Promise { this._assertInitialized("createSamplingMessage"); - const resultSchema = params.tools - ? CreateMessageResultWithToolsSchema - : CreateMessageResultSchema; return await this.request( { method: "sampling/createMessage", params }, - resultSchema, options, ); } @@ -1953,14 +1991,23 @@ export class App extends ProtocolWithEvents< ); } this._initializedSent = false; - await super.connect(transport); + // TEMPORARY INNER-CHANNEL NEGOTIATION: + // App and AppBridge use the public v2 Client/Server roles, so the iframe + // channel first completes the SDK's standard legacy MCP initialization. + // ui/initialize remains authoritative for Apps capabilities, identity, and + // host context, and ui/notifications/initialized remains the View-ready gate. + // Keep the inner MCP era pinned to 2025-11-25. Revisit this sequence only + // after the MCP Apps negotiation specification is resolved: + // https://github.com/modelcontextprotocol/ext-apps/issues/708 try { + await super.connect(transport); + const result = await this.request( { method: "ui/initialize", params: { - appCapabilities: this._capabilities, + appCapabilities: this._appCapabilities, appInfo: this._appInfo, protocolVersion: LATEST_PROTOCOL_VERSION, }, diff --git a/src/events.test.ts b/src/events.test.ts new file mode 100644 index 000000000..7fd87087f --- /dev/null +++ b/src/events.test.ts @@ -0,0 +1,82 @@ +import { describe, expect, it, spyOn } from "bun:test"; + +import { EventDispatcher } from "./events"; + +type TestEventMap = { + change: { value: number }; +}; + +describe("EventDispatcher", () => { + it("replaces the singular handler", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + const first = () => calls.push("first"); + const second = () => calls.push("second"); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + + try { + dispatcher.setHandler("change", first); + dispatcher.setHandler("change", second); + + expect(dispatcher.getHandler("change")).toBe(second); + dispatcher.dispatch("change", { value: 1 }); + expect(calls).toEqual(["second"]); + } finally { + warn.mockRestore(); + } + }); + + it("dispatches the singular handler before listeners in insertion order", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + + dispatcher.setHandler("change", () => calls.push("handler")); + dispatcher.addEventListener("change", () => calls.push("listener-1")); + dispatcher.addEventListener("change", () => calls.push("listener-2")); + + dispatcher.dispatch("change", { value: 1 }); + + expect(calls).toEqual(["handler", "listener-1", "listener-2"]); + }); + + it("uses a listener snapshot when a listener is removed during dispatch", () => { + const dispatcher = new EventDispatcher(); + const calls: string[] = []; + const second = () => calls.push("second"); + const first = () => { + calls.push("first"); + dispatcher.removeEventListener("change", second); + }; + + dispatcher.addEventListener("change", first); + dispatcher.addEventListener("change", second); + + dispatcher.dispatch("change", { value: 1 }); + dispatcher.dispatch("change", { value: 2 }); + + expect(calls).toEqual(["first", "second", "first"]); + }); + + it("warns only when one singular handler replaces another", () => { + const dispatcher = new EventDispatcher(); + const warn = spyOn(console, "warn").mockImplementation(() => {}); + const first = () => {}; + const second = () => {}; + + try { + dispatcher.setHandler("change", first); + dispatcher.setHandler("change", undefined); + dispatcher.setHandler("change", first); + expect(warn).not.toHaveBeenCalled(); + + dispatcher.setHandler("change", second); + + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0]?.[0])).toContain( + "onchange handler replaced", + ); + } finally { + warn.mockRestore(); + } + }); +}); diff --git a/src/events.ts b/src/events.ts index 4d17ca4f2..aff144605 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,17 +1,6 @@ -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 - * where `el.onclick` and `el.addEventListener("click", …)` coexist. + * Per-event state: a singular `on*` handler (replace semantics) plus an + * ordered listener array (`addEventListener` semantics). */ interface EventSlot { onHandler?: ((params: T) => void) | undefined; @@ -19,118 +8,19 @@ interface EventSlot { } /** - * Intermediate base class that adds DOM-style event support on top of the - * MCP SDK's `Protocol`. - * - * 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: - * - * ### Singular `on*` handler (like `el.onclick`) - * - * Subclasses expose `get`/`set` pairs that delegate to - * {@link setEventHandler `setEventHandler`} / - * {@link getEventHandler `getEventHandler`}. Assigning replaces the previous - * handler; assigning `undefined` clears it. `addEventListener` listeners are - * unaffected. - * - * ### Multi-listener (`addEventListener` / `removeEventListener`) - * - * Append to a per-event listener array. Listeners fire in insertion order - * after the singular `on*` handler. - * - * ### Dispatch order - * - * When a notification arrives 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 + * Protocol-independent DOM-style event fan-out. * - * ### Double-set protection + * Owners perform event-specific state updates before calling + * {@link dispatch}. Dispatch then invokes the singular handler followed by a + * snapshot of listeners in insertion order. * - * 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. - * - * @typeParam EventMap - Maps event names to the listener's `params` type. + * @typeParam EventMap - Maps event names to their listener parameter types. */ -export abstract class ProtocolWithEvents< - SendRequestT extends Request, - SendNotificationT extends Notification, - SendResultT extends Result, - 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 - * notification params into cached state. - */ - protected onEventDispatch( - _event: K, - _params: EventMap[K], - ): void {} - - // ── 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. - */ - private _ensureEventSlot( - event: K, - ): EventSlot { - let slot = this._eventSlots.get(event) as - | 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); +export class EventDispatcher> { + private readonly _eventSlots = new Map(); - 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; - } - - /** - * Set or clear the singular `on*` handler for an event. - * - * Replace semantics — like the DOM's `el.onclick = fn`. Assigning - * `undefined` clears the handler without affecting `addEventListener` - * listeners. - */ - protected setEventHandler( + /** Set, replace, or clear the singular handler for an event. */ + setHandler( event: K, handler: ((params: EventMap[K]) => void) | undefined, ): void { @@ -144,31 +34,15 @@ export abstract class ProtocolWithEvents< slot.onHandler = handler; } - /** - * Get the singular `on*` handler for an event, or `undefined` if none is - * set. `addEventListener` listeners are not reflected here. - */ - protected getEventHandler( + /** Get the singular handler for an event, if one is set. */ + getHandler( event: K, ): ((params: EventMap[K]) => void) | undefined { return (this._eventSlots.get(event) as EventSlot | undefined) ?.onHandler; } - /** - * Add a listener for a notification event. - * - * 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`. - * - * @param event - Event name (a key of the `EventMap` type parameter). - * @param handler - Listener invoked with the notification `params`. - */ + /** Add a listener. Listeners run in insertion order after the handler. */ addEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -176,11 +50,7 @@ export abstract class ProtocolWithEvents< this._ensureEventSlot(event).listeners.push(handler); } - /** - * 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 the first matching listener, if present. */ removeEventListener( event: K, handler: (params: EventMap[K]) => void, @@ -193,87 +63,29 @@ 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); - }; - /** - * Warn if a request handler `on*` setter is replacing a previously-set - * handler. Call from each request setter before updating the backing field. + * Dispatch an event to its singular handler and a listener snapshot. + * Listener mutations during dispatch take effect on the next dispatch. */ - protected warnIfRequestHandlerReplaced( - name: string, - previous: unknown, - next: unknown, - ): void { - if (previous && next) { - console.warn( - `[MCP Apps] ${name} handler replaced. ` + - `Previous handler will no longer be called.`, - ); - } + dispatch(event: K, params: EventMap[K]): void { + const slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) return; + slot.onHandler?.(params); + for (const l of [...slot.listeners]) l(params); } - /** - * 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.`, - ); + private _ensureEventSlot( + event: K, + ): EventSlot { + let slot = this._eventSlots.get(event) as + | EventSlot + | undefined; + if (!slot) { + slot = { listeners: [] }; + this._eventSlots.set(event, slot as EventSlot); } - this._registeredMethods.add(method); + return slot; } } diff --git a/src/generated/schema.json b/src/generated/schema.json index 80b4ac60d..5f2124a95 100644 --- a/src/generated/schema.json +++ b/src/generated/schema.json @@ -596,7 +596,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -611,25 +613,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -1286,7 +1273,40 @@ } }, "required": ["method", "params"], - "additionalProperties": false + "additionalProperties": false, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostContext": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -1359,7 +1379,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -1374,25 +1396,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -2044,7 +2051,40 @@ "additionalProperties": false } }, - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiHostCss": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2942,7 +2982,9 @@ "propertyNames": { "type": "string" }, - "additionalProperties": {} + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } }, "required": { "type": "array", @@ -2957,25 +2999,10 @@ "outputSchema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "object" - }, - "properties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "required": { - "type": "array", - "items": { - "type": "string" - } + "$schema": { + "type": "string" } }, - "required": ["type"], "additionalProperties": {} }, "annotations": { @@ -3637,7 +3664,40 @@ "hostCapabilities", "hostContext" ], - "additionalProperties": {} + "additionalProperties": {}, + "$defs": { + "__schema0": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "null" + }, + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/$defs/__schema0" + } + }, + { + "type": "array", + "items": { + "$ref": "#/$defs/__schema0" + } + } + ] + } + } }, "McpUiInitializedNotification": { "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -5669,13 +5729,7 @@ ] } }, - "structuredContent": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, + "structuredContent": {}, "isError": { "type": "boolean" } diff --git a/src/generated/schema.ts b/src/generated/schema.ts index 43687374e..341c7b488 100644 --- a/src/generated/schema.ts +++ b/src/generated/schema.ts @@ -10,7 +10,7 @@ import { RequestIdSchema, ResourceLinkSchema, ToolSchema, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/core"; /** * @description Color theme preference for the host environment. diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts index d5f509204..2a115f3f9 100644 --- a/src/message-transport.test.ts +++ b/src/message-transport.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test"; -import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; +import type { JSONRPCMessage } from "@modelcontextprotocol/client"; import { PostMessageTransport } from "./message-transport"; diff --git a/src/message-transport.ts b/src/message-transport.ts index 9d195435a..94835401f 100644 --- a/src/message-transport.ts +++ b/src/message-transport.ts @@ -1,12 +1,10 @@ -import { +import { JSONRPCMessageSchema } from "@modelcontextprotocol/core"; +import type { JSONRPCMessage, - JSONRPCMessageSchema, MessageExtraInfo, -} from "@modelcontextprotocol/sdk/types.js"; -import { Transport, TransportSendOptions, -} from "@modelcontextprotocol/sdk/shared/transport.js"; +} from "@modelcontextprotocol/client"; import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types"; /** diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx index 61892464a..0e64a74d6 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 type { Implementation } from "@modelcontextprotocol/client"; import { App, AppOptions, diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts index d583ae85c..9ceebcebb 100644 --- a/src/server/index.examples.ts +++ b/src/server/index.examples.ts @@ -9,17 +9,14 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs/promises"; -import type { - McpServer, - ToolCallback, - ReadResourceCallback, -} from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { McpServer, ToolCallback } from "@modelcontextprotocol/server"; import { z } from "zod"; import { registerAppTool, registerAppResource, getUiCapability, RESOURCE_MIME_TYPE, + type ReadResourceCallback, } from "./index.js"; // Stubs for external functions used in examples @@ -74,7 +71,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 +119,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..5de341f9e 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", () => { diff --git a/src/server/index.ts b/src/server/index.ts index c90514acd..61e743831 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -40,24 +40,17 @@ import { McpUiClientCapabilities, } from "../app.js"; import type { - BaseToolCallback, + ClientCapabilities, McpServer, + ReadResourceResult, RegisteredTool, ResourceMetadata, + StandardSchemaWithJSON, + ToolAnnotations, ToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, -} from "@modelcontextprotocol/sdk/server/mcp.js"; -import type { - AnySchema, - ZodRawShapeCompat, -} from "@modelcontextprotocol/sdk/server/zod-compat.js"; -import type { StandardSchemaWithJSON } from "../standard-schema"; -import type { - ClientCapabilities, - ReadResourceResult, - ToolAnnotations, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/server"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; @@ -70,8 +63,8 @@ export type { ResourceMetadata, ToolCallback }; export interface ToolConfig { title?: string; description?: string; - inputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; - outputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON; + inputSchema?: StandardSchemaWithJSON; + outputSchema?: StandardSchemaWithJSON; annotations?: ToolAnnotations; _meta?: Record; } @@ -157,7 +150,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * { * 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" }, * }, @@ -197,7 +190,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * "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", @@ -215,9 +208,8 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool */ export function registerAppTool< - OutputArgs extends ZodRawShapeCompat | StandardSchemaWithJSON, - InputArgs extends undefined | ZodRawShapeCompat | StandardSchemaWithJSON = - undefined, + OutputArgs extends StandardSchemaWithJSON, + InputArgs extends StandardSchemaWithJSON | undefined = undefined, >( server: Pick, name: string, @@ -225,15 +217,7 @@ export function registerAppTool< inputSchema?: InputArgs; outputSchema?: OutputArgs; }, - // The widened constraint signals the v2 API shape, but NOTE: McpServer in - // sdk@1.x still calls zod internals at runtime, so non-zod schemas will fail - // here until we depend on sdk v2. Zod (which all current callers use) is - // unaffected. The cast below bridges the 1.x type signature. - cb: ToolCallback< - InputArgs extends undefined | ZodRawShapeCompat | AnySchema - ? InputArgs - : AnySchema - >, + cb: ToolCallback, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -251,14 +235,7 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - // Cast bridges the widened StandardSchemaWithJSON constraint to the - // sdk@1.x zod-typed signature. Drops once we depend on sdk v2. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return server.registerTool( - name, - { ...config, _meta: normalizedMeta } as any, - cb as any, - ); + return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } export type McpUiReadResourceResult = ReadResourceResult & { @@ -418,10 +395,6 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; * 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 * @@ -456,10 +429,7 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui"; * ``` */ export function getUiCapability( - clientCapabilities: - | (ClientCapabilities & { extensions?: Record }) - | null - | undefined, + clientCapabilities: ClientCapabilities | null | undefined, ): McpUiClientCapabilities | undefined { if (!clientCapabilities) { return undefined; diff --git a/src/spec.types.ts b/src/spec.types.ts index 7a8b33761..8b03de94f 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "@modelcontextprotocol/sdk/types.js"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. @@ -163,8 +163,8 @@ export interface McpUiOpenLinkResult { /** @description True if the host failed to open the URL (e.g., due to security policy). */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -195,8 +195,8 @@ export interface McpUiDownloadFileResult { /** @description True if the download failed (e.g., user cancelled or host denied). */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -223,8 +223,8 @@ export interface McpUiMessageResult { /** @description True if the host rejected or failed to deliver the message. */ isError?: boolean; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -454,7 +454,7 @@ export interface McpUiResourceTeardownRequest { */ export interface McpUiResourceTeardownResult { /** - * Index signature required for MCP SDK `Protocol` class compatibility. + * Additional result properties are preserved for protocol extensibility. */ [key: string]: unknown; } @@ -577,8 +577,8 @@ export interface McpUiInitializeResult { /** @description Rich context about the host environment. */ hostContext: McpUiHostContext; /** - * Index signature required for MCP SDK `Protocol` class compatibility. - * Note: The generated schema uses passthrough() to allow additional properties. + * Additional result properties are preserved for protocol extensibility. + * The generated schema uses passthrough() to allow them. */ [key: string]: unknown; } @@ -752,7 +752,7 @@ export interface McpUiRequestDisplayModeResult { /** @description The display mode that was actually set. May differ from requested if not supported. */ mode: McpUiDisplayMode; /** - * Index signature required for MCP SDK `Protocol` class compatibility. + * Additional result properties are preserved for protocol extensibility. * Note: The generated schema uses passthrough() to allow additional properties. */ [key: string]: unknown; diff --git a/src/types.ts b/src/types.ts index 7fc6b7188..655812bd3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -69,33 +69,6 @@ export { type McpUiClientCapabilities, } from "./spec.types.js"; -// Import types needed for protocol type unions (not re-exported, just used internally) -import type { - McpUiInitializeRequest, - McpUiOpenLinkRequest, - McpUiDownloadFileRequest, - McpUiMessageRequest, - McpUiUpdateModelContextRequest, - McpUiResourceTeardownRequest, - McpUiRequestDisplayModeRequest, - McpUiHostContextChangedNotification, - McpUiToolInputNotification, - McpUiToolInputPartialNotification, - McpUiToolResultNotification, - McpUiToolCancelledNotification, - McpUiSandboxResourceReadyNotification, - McpUiInitializedNotification, - McpUiSizeChangedNotification, - McpUiSandboxProxyReadyNotification, - McpUiRequestTeardownNotification, - McpUiInitializeResult, - McpUiOpenLinkResult, - McpUiDownloadFileResult, - McpUiMessageResult, - McpUiResourceTeardownResult, - McpUiRequestDisplayModeResult, -} from "./spec.types.js"; - // Re-export all schemas from generated/schema.ts (already PascalCase) export { McpUiThemeSchema, @@ -135,106 +108,3 @@ export { McpUiToolVisibilitySchema, McpUiToolMetaSchema, } from "./generated/schema.js"; - -// Re-export SDK types used in protocol type unions -import { - CallToolRequest, - CallToolResult, - CreateMessageRequest, - CreateMessageResult, - CreateMessageResultWithTools, - EmptyResult, - ListPromptsRequest, - ListPromptsResult, - ListResourcesRequest, - ListResourcesResult, - ListResourceTemplatesRequest, - ListResourceTemplatesResult, - ListToolsRequest, - ListToolsResult, - LoggingMessageNotification, - PingRequest, - PromptListChangedNotification, - ReadResourceRequest, - ReadResourceResult, - ResourceListChangedNotification, - ToolListChangedNotification, -} from "@modelcontextprotocol/sdk/types.js"; - -/** - * All request types in the MCP Apps protocol. - * - * Includes: - * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode) - * - MCP server requests forwarded from the app (tools/call, tools/list, resources/list, - * resources/templates/list, resources/read, prompts/list) - * - MCP client requests forwarded to the host (sampling/createMessage) - * - Protocol requests (ping) - */ -export type AppRequest = - | McpUiInitializeRequest - | McpUiOpenLinkRequest - | McpUiDownloadFileRequest - | McpUiMessageRequest - | McpUiUpdateModelContextRequest - | McpUiResourceTeardownRequest - | McpUiRequestDisplayModeRequest - | CallToolRequest - | ListToolsRequest - | ListResourcesRequest - | ListResourceTemplatesRequest - | ReadResourceRequest - | ListPromptsRequest - | CreateMessageRequest - | PingRequest; - -/** - * All notification types in the MCP Apps protocol. - * - * Host to app: - * - Tool lifecycle (input, input-partial, result, cancelled) - * - Host context changes - * - MCP list changes (tools, resources, prompts) - * - Sandbox resource ready - * - * App to host: - * - Initialized, size-changed, sandbox-proxy-ready, request-teardown - * - Logging messages - */ -export type AppNotification = - // Sent to app - | McpUiHostContextChangedNotification - | McpUiToolInputNotification - | McpUiToolInputPartialNotification - | McpUiToolResultNotification - | McpUiToolCancelledNotification - | McpUiSandboxResourceReadyNotification - | ToolListChangedNotification - | ResourceListChangedNotification - | PromptListChangedNotification - // Received from app - | McpUiInitializedNotification - | McpUiSizeChangedNotification - | McpUiSandboxProxyReadyNotification - | McpUiRequestTeardownNotification - | LoggingMessageNotification; - -/** - * All result types in the MCP Apps protocol. - */ -export type AppResult = - | McpUiInitializeResult - | McpUiOpenLinkResult - | McpUiDownloadFileResult - | McpUiMessageResult - | McpUiResourceTeardownResult - | McpUiRequestDisplayModeResult - | CallToolResult - | ListToolsResult - | ListResourcesResult - | ListResourceTemplatesResult - | ReadResourceResult - | ListPromptsResult - | CreateMessageResult - | CreateMessageResultWithTools - | EmptyResult; diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 4c5baa320..041bf5e67 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -1,5 +1,7 @@ import { OptionDefaults } from "typedoc"; +const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/"; + /** @type {Partial} */ const config = { name: "MCP Apps", @@ -28,7 +30,29 @@ const config = { ], excludePrivate: true, excludeInternal: false, - intentionallyNotExported: ["MethodSchema", "RequestHandlerExtra"], + intentionallyNotExported: ["RequestHandlerExtra"], + externalSymbolLinkMappings: { + "@modelcontextprotocol/client": { + ClientOptions: BASE_SDK_DOCS, + ConnectOptions: BASE_SDK_DOCS, + DiscoverResult: BASE_SDK_DOCS, + ProtocolError: BASE_SDK_DOCS, + "Protocol.close": BASE_SDK_DOCS, + "Protocol.notification": BASE_SDK_DOCS, + ResponseCacheStore: BASE_SDK_DOCS, + SdkError: BASE_SDK_DOCS, + "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS, + "__type.enforceStrictCapabilities": BASE_SDK_DOCS, + }, + "@modelcontextprotocol/server": { + LoggingMessageNotification: BASE_SDK_DOCS, + "McpServer.registerTool": BASE_SDK_DOCS, + "MessageExtraInfo.classification": BASE_SDK_DOCS, + "Protocol.close": BASE_SDK_DOCS, + "Protocol.notification": BASE_SDK_DOCS, + "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS, + }, + }, blockTags: [...OptionDefaults.blockTags, "@description"], jsDocCompatibility: { exampleTag: false, @@ -49,6 +73,7 @@ const config = { out: "docs/api", plugin: [ "typedoc-github-theme", + "./scripts/typedoc-plugin-base-sdk-links.mjs", "./scripts/typedoc-plugin-fix-mermaid-entities.mjs", "./scripts/typedoc-plugin-seo.mjs", "./scripts/typedoc-plugin-mcpstyle.mjs",