diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb419382c..756f25918 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,21 @@ jobs: grep -q "Ready" /tmp/mcpb-smoke.log working-directory: examples/pdf-server + dependency-isolation: + name: Dependency isolation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: "22" + cache: npm + + - run: npm ci + + - run: npm run test:dependency-isolation + e2e: runs-on: ubuntu-latest steps: diff --git a/AGENTS.md b/AGENTS.md index 4b0faa066..219b94718 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` subclasses the base MCP SDK `Protocol`, handles View initialization, tool calls, and messaging -- `src/app-bridge.ts` - `AppBridge` subclasses the base MCP SDK `Protocol` for the iframe channel and proxies through a separate outer `Client` +- `src/app.ts` - `App` subclasses `Protocol` from `@modelcontextprotocol/client`, handles View initialization, tool calls, and messaging +- `src/app-bridge.ts` - `AppBridge` subclasses `Protocol` from `@modelcontextprotocol/client` for the 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 diff --git a/README.md b/README.md index ff47e1e9d..acfc6617b 100644 --- a/README.md +++ b/README.md @@ -122,27 +122,33 @@ resources: ## Getting Started +Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of +`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you). + For a View or host: ```bash npm install -S @modelcontextprotocol/ext-apps \ - @modelcontextprotocol/client@2.0.0-beta.5 \ - @modelcontextprotocol/core@2.0.0-beta.5 \ + @modelcontextprotocol/client@^2.0.0 \ zod@^4.2.0 ``` -For an MCP server: +For an MCP server, add the server package (and, for HTTP transports, the Node +and Express adapters): ```bash npm install -S @modelcontextprotocol/ext-apps \ - @modelcontextprotocol/server@2.0.0-beta.5 \ - @modelcontextprotocol/core@2.0.0-beta.5 \ + @modelcontextprotocol/client@^2.0.0 \ + @modelcontextprotocol/server@^2.0.0 \ + @modelcontextprotocol/node@^2.0.0 \ + @modelcontextprotocol/express@^2.0.0 \ zod@^4.2.0 ``` -Applications that implement both roles should install both `client` and -`server`. Keep all installed base MCP SDK packages on the exact same published -beta so they share one compatible protocol implementation. +The wire protocol is unchanged between `ext-apps` 1.x and 2.x: a 2.x View works +in a 1.x host and a 2.x host renders 1.x Views. See the +[migration guide](https://apps.extensions.modelcontextprotocol.io/api/documents/migrate-to-v2.html) +when upgrading from 1.x. **New here?** Start with the [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html) diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md new file mode 100644 index 000000000..2164b8334 --- /dev/null +++ b/docs/migrate-to-2.md @@ -0,0 +1,102 @@ +--- +title: Migrate to v2 +group: Getting Started +description: Upgrade from ext-apps 1.x to 2.x — new base MCP SDK peer packages, API changes for Views and hosts, and what stays wire-compatible. +--- + +# Migrating from ext-apps 1.x to 2.x + +ext-apps 2.x is built on the base MCP TypeScript SDK 2.x, which replaced the +single `@modelcontextprotocol/sdk` package with `@modelcontextprotocol/client`, +`server`, `core`, `node` and `express`. The MCP Apps wire protocol did not +change; the breaking changes are in dependencies and in the TypeScript API. + +## Host compatibility + +The `ui/*` messages exchanged over the iframe channel are byte-identical to +1.x. A 2.x View runs in a 1.x host and a 2.x host renders 1.x Views. The only +host-side deltas are in error responses (see below). + +## Peer dependencies by role + +| Role | Install | +| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| View author | `@modelcontextprotocol/ext-apps`, `@modelcontextprotocol/client@^2.0.0`, `zod@^4.2.0` (+ `react`/`react-dom` for `./react`) | +| Host author | same as View author | +| MCP server author | View author packages + `@modelcontextprotocol/server@^2.0.0`; `@modelcontextprotocol/node` and `express` for HTTP transports | +| CDN / `*-with-deps` | nothing extra: `./app-with-deps` and `./react-with-deps` bundle client, core and zod (about 25% larger than the 1.x bundles) | + +`@modelcontextprotocol/client` is a required peer (`App` and `AppBridge` extend +its `Protocol` class); `@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it without you listing it; +`@modelcontextprotocol/server` stays optional and is only needed for the +`./server` helpers. Node.js 20+ is required. + +## Breaking changes + +- **Peer packages.** `@modelcontextprotocol/sdk@^1` is replaced by the split + 2.x packages above, all at `^2.0.0`. Remove the 1.x package from your + project; the two SDKs do not interoperate. +- **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must + implement Standard JSON Schema (`~standard.jsonSchema`): zod v4, ArkType, + Valibot. Raw zod shapes (`{ q: z.string() }`) still work with + `registerAppTool` but are deprecated; wrap them with `z.object({...})`. +- **`App` / `AppBridge` extend `Protocol` from `@modelcontextprotocol/client`.** + `ProtocolWithEvents`, `AppRequest`, `AppNotification` and `AppResult` are + gone; use the SDK's `Protocol`, `Request`, `Notification` and `Result`. +- **Handler context.** Custom handlers receive the SDK 2.x context: + `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` → + `extra.mcpReq.id`. +- **`setRequestHandler` / `setNotificationHandler` take method names.** + `app.setRequestHandler(SomeRequestSchema, handler)` becomes + `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)` + for custom methods (the handler receives the parsed params); the two-argument + `setRequestHandler("tools/call", handler)` form exists only for spec-defined + method names. +- **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); + local conditions are `SdkError` with a string `code`: request timeout → + `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a + request with an `AbortSignal` also rejects with `"REQUEST_TIMEOUT"` (the + message is the abort reason). Messages no longer carry the `MCP error N:` + prefix. + +## Host-side wire deltas + +Observed when a 2.x `AppBridge` answers a View; a 1.x View sees these too. + +| Situation | 1.x host | 2.x host | +| --------------------------------------------------------- | ------------------------------ | --------------------------------------------------- | +| A handler throws `-32002` (resource not found) | `error.code: -32002` | `error.code: -32602` (the SDK never emits `-32002`) | +| Invalid params on a `ui/*` request | `-32603` with a zod issue dump | `-32602 Invalid params for : …` | +| Error message text | `MCP error -32602: …` prefix | plain message | +| `tools/call` to an unknown tool through a 2.x `McpServer` | `result.isError: true` | JSON-RPC error `-32602` (`callServerTool` rejects) | + +## `schema.json` + +The published `./schema.json` export is regenerated from the 2.x core schemas: + +- `McpUiToolResultNotification.params.structuredContent` is any JSON value (was + `type: "object"`). +- `McpUiToolResultNotification.params._meta` documents + `io.modelcontextprotocol/serverInfo` and no longer lists `progressToken` / + `related-task` (both still pass through). +- `McpUiHostContext.toolInfo.tool.outputSchema` is a loose object (only + `$schema` is documented); `inputSchema.properties` values are now typed as + JSON values. +- A recursive JSON-value definition (`__schema0`) is added under the `$defs` of + `McpUiHostContext`, `McpUiHostContextChangedNotification` and + `McpUiInitializeResult`. + +## Checklist + +1. `npm uninstall @modelcontextprotocol/sdk` and install the packages for your + role from the table above. +2. Replace `sdk/...` imports with the split packages (`sdk/server/mcp.js` → + `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` → + `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`, + `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js` + → `@modelcontextprotocol/client` or `server` for the types, + `@modelcontextprotocol/core` for the zod schemas). +3. Wrap raw zod shapes with `z.object({...})`. +4. Update custom handlers to the `extra.mcpReq.*` context and method-keyed + `setRequestHandler` calls. +5. Replace `McpError` / numeric-code checks with `ProtocolError` / `SdkError`. diff --git a/docs/overview.md b/docs/overview.md index 5f3852dd6..8eda95d6a 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -56,8 +56,8 @@ 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's `App` and the Host's `AppBridge` both subclass the base MCP SDK's -public `Protocol` for the iframe channel. A separate outer `Client` connects +The View's `App` and the Host's `AppBridge` both subclass the `Protocol` class +exported by `@modelcontextprotocol/client` for the iframe channel. A separate outer `Client` connects the Host to the actual MCP Server. Keeping those two connections separate preserves the Apps-only iframe handshake and prevents iframe capabilities from leaking into the server connection. diff --git a/docs/quickstart.md b/docs/quickstart.md index 3cac8727d..2039dc165 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -37,7 +37,7 @@ Install the dependencies you'll need: ```bash npm init -y -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 @modelcontextprotocol/node@^2.0.0 @modelcontextprotocol/express@^2.0.0 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile @types/express @types/cors @types/node tsx concurrently cross-env ``` diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md index e5d82b53e..a8eb33399 100644 --- a/docs/testing-mcp-apps.md +++ b/docs/testing-mcp-apps.md @@ -29,7 +29,7 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex ``` The root install runs the package build, including type-checking the - documentation snippets against the exact base MCP SDK beta.4 packages. Do + documentation snippets against the pinned base MCP SDK 2.x packages. Do not bypass a failed build before testing the host. 2. Start basic-host, pointing it to your MCP server: diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json index 06d1a0514..cefe9d50b 100644 --- a/examples/basic-host/package.json +++ b/examples/basic-host/package.json @@ -1,7 +1,7 @@ { "homepage": "https://github.com/modelcontextprotocol/ext-apps/tree/main/examples/basic-host", "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "scripts": { "build": "tsc --noEmit && concurrently \"cross-env INPUT=index.html vite build\" \"cross-env INPUT=sandbox.html vite build\"", @@ -11,11 +11,11 @@ "dev": "cross-env NODE_ENV=development concurrently \"npm run watch\" \"npm run serve\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/express": "^5.0.0", diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json index 82a30bbd2..c88a3062f 100644 --- a/examples/basic-server-preact/package.json +++ b/examples/basic-server-preact/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Preact", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@preact/preset-vite": "^2.0.0", diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json index b2b95403f..07a9d3b8c 100644 --- a/examples/basic-server-react/package.json +++ b/examples/basic-server-react/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using React", "repository": { @@ -35,15 +35,15 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json index 2e3bc79b1..a459ebc36 100644 --- a/examples/basic-server-solid/package.json +++ b/examples/basic-server-solid/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Solid", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json index cd7fb4158..3e0279fd5 100644 --- a/examples/basic-server-svelte/package.json +++ b/examples/basic-server-svelte/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Svelte", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^5.0.0", diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json index 50f0f4241..1404b654f 100644 --- a/examples/basic-server-vanillajs/package.json +++ b/examples/basic-server-vanillajs/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using vanilla JavaScript", "repository": { @@ -25,13 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json index 2ff3e0adb..32083c699 100644 --- a/examples/basic-server-vue/package.json +++ b/examples/basic-server-vue/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Basic MCP App Server example using Vue", "repository": { @@ -25,14 +25,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json index 9208e91c9..e44a14735 100644 --- a/examples/budget-allocator-server/package.json +++ b/examples/budget-allocator-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Budget allocator MCP App Server with interactive visualization", "repository": { @@ -26,14 +26,14 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json index eb58415dc..de20f1365 100644 --- a/examples/cohort-heatmap-server/package.json +++ b/examples/cohort-heatmap-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Cohort heatmap MCP App Server for retention analysis", "repository": { @@ -26,15 +26,15 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json index f6999ee3a..643db17df 100644 --- a/examples/customer-segmentation-server/package.json +++ b/examples/customer-segmentation-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Customer segmentation MCP App Server with filtering", "repository": { @@ -26,14 +26,14 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json index 33cede44e..5be929186 100644 --- a/examples/debug-server/package.json +++ b/examples/debug-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Debug MCP App Server for testing all SDK capabilities", "repository": { @@ -34,11 +34,11 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json index 18f22e66c..7e04f4aeb 100644 --- a/examples/integration-server/package.json +++ b/examples/integration-server/package.json @@ -1,6 +1,6 @@ { "name": "integration-server", - "version": "1.7.5", + "version": "2.0.0", "private": true, "type": "module", "scripts": { @@ -16,15 +16,15 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json index 6e7aaa7c5..b12b29ba8 100644 --- a/examples/lazy-auth-server/package.json +++ b/examples/lazy-auth-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App example demonstrating lazy (on-demand) OAuth: public tools work unauthenticated, protected tools return 401 + WWW-Authenticate so the host runs the OAuth flow only when needed", "repository": { @@ -22,13 +22,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/map-server/package.json b/examples/map-server/package.json index a59445e8e..dc6a37fc8 100644 --- a/examples/map-server/package.json +++ b/examples/map-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-map", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server example with CesiumJS 3D globe and geocoding", "repository": { @@ -27,13 +27,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs index c1c346909..29173341d 100644 --- a/examples/pdf-server/build-mcpb.mjs +++ b/examples/pdf-server/build-mcpb.mjs @@ -43,6 +43,21 @@ writeFileSync( path.join(stage, "manifest.json"), JSON.stringify(manifest, null, 2), ); +// The bundle must ship the ext-apps build from this checkout, not whatever +// the registry has: pack the repo root and point the staged manifest at it. +const repoRoot = path.resolve(here, "..", ".."); +const packed = JSON.parse( + execSync( + "npm pack --json --ignore-scripts --pack-destination " + + JSON.stringify(stage), + { + cwd: repoRoot, + encoding: "utf8", + }, + ).replace(/^[^[]*/, ""), +); +pkg.dependencies["@modelcontextprotocol/ext-apps"] = + "file:./" + packed[0].filename; writeFileSync(path.join(stage, "package.json"), JSON.stringify(pkg, null, 2)); const run = (cmd) => execSync(cmd, { cwd: stage, stdio: "inherit" }); diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json index 8c2c2ee66..ae0870a4b 100644 --- a/examples/pdf-server/package.json +++ b/examples/pdf-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP server for loading and extracting text from PDF files with chunked pagination and interactive viewer", "repository": { @@ -27,14 +27,14 @@ "dependencies": { "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json index e1b849b31..425728eb4 100644 --- a/examples/qr-server/package.json +++ b/examples/qr-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.5", + "version": "2.0.0", "private": true, "scripts": { "start": "uv run server.py", @@ -8,6 +8,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } } diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json index 6874b498a..eecb08000 100644 --- a/examples/quickstart/package.json +++ b/examples/quickstart/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "private": true, "description": "Quickstart MCP App Server example", @@ -15,13 +15,13 @@ "start": "concurrently --raw \"cross-env NODE_ENV=development INPUT=mcp-app.html vite build --watch\" \"tsx watch main.ts\"" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/say-server/package.json b/examples/say-server/package.json index 5f0f73818..cff7024b7 100644 --- a/examples/say-server/package.json +++ b/examples/say-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-say", - "version": "1.7.5", + "version": "2.0.0", "private": true, "description": "Streaming TTS MCP App Server with karaoke-style text highlighting", "repository": { @@ -15,6 +15,6 @@ "build": "echo 'No build step needed for Python server'" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } } diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json index 76d692af4..d82424df1 100644 --- a/examples/scenario-modeler-server/package.json +++ b/examples/scenario-modeler-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Financial scenario modeling MCP App Server", "repository": { @@ -26,8 +26,8 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", @@ -35,7 +35,7 @@ "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json index 57edf087a..3e16a923f 100644 --- a/examples/shadertoy-server/package.json +++ b/examples/shadertoy-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server example for rendering ShaderToy-compatible GLSL shaders", "repository": { @@ -24,13 +24,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json index 55c2870d7..1b233bbe9 100644 --- a/examples/sheet-music-server/package.json +++ b/examples/sheet-music-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server for rendering and playing sheet music from ABC notation", "repository": { @@ -24,14 +24,14 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json index b0c2f201f..f3a5763a8 100644 --- a/examples/system-monitor-server/package.json +++ b/examples/system-monitor-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "System monitor MCP App Server with real-time stats", "repository": { @@ -26,15 +26,15 @@ "serve": "bun --watch main.ts" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "systeminformation": "^5.31.6", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json index 40d1c2541..f8df10f5a 100644 --- a/examples/threejs-server/package.json +++ b/examples/threejs-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Three.js 3D visualization MCP App Server", "repository": { @@ -27,8 +27,8 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", @@ -36,7 +36,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "three": "^0.181.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/transcript-server/README.md b/examples/transcript-server/README.md index 3b3fa4221..559ffadef 100644 --- a/examples/transcript-server/README.md +++ b/examples/transcript-server/README.md @@ -56,7 +56,7 @@ To test local modifications, use this configuration (replace `~/code/ext-apps` w ### Prerequisites -- Node.js 18+ +- Node.js 20+ - Chrome, Edge, or Safari (Web Speech API support) ### Installation diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json index e7c5dbac6..405f85fe1 100644 --- a/examples/transcript-server/package.json +++ b/examples/transcript-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server for live speech transcription", "repository": { @@ -24,13 +24,13 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json index 199a373df..dea8fcb1b 100644 --- a/examples/video-resource-server/package.json +++ b/examples/video-resource-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "MCP App Server demonstrating video resources served as base64 blobs", "repository": { @@ -25,13 +25,13 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json index 090a8519c..08316707d 100644 --- a/examples/wiki-explorer-server/package.json +++ b/examples/wiki-explorer-server/package.json @@ -1,6 +1,6 @@ { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.5", + "version": "2.0.0", "type": "module", "description": "Wikipedia link explorer MCP App Server with graph visualization", "repository": { @@ -27,14 +27,14 @@ }, "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", "@modelcontextprotocol/express": "2.0.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", diff --git a/package-lock.json b/package-lock.json index 49b131502..f63b3d84f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "workspaces": [ "examples/*" @@ -52,17 +52,14 @@ "node": ">=20" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/core": "2.0.0", - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" }, "peerDependenciesMeta": { - "@modelcontextprotocol/client": { - "optional": true - }, "@modelcontextprotocol/server": { "optional": true }, @@ -76,13 +73,13 @@ }, "examples/basic-host": { "name": "@modelcontextprotocol/ext-apps-basic-host", - "version": "1.7.5", + "version": "2.0.0", "dependencies": { "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/express": "^5.0.0", @@ -99,20 +96,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-host/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-host/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-preact": { "name": "@modelcontextprotocol/server-basic-preact", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "preact": "^10.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-preact": "dist/index.js" @@ -129,21 +143,38 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-preact/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-preact/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-react": { "name": "@modelcontextprotocol/server-basic-react", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-react": "dist/index.js" @@ -162,20 +193,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-react/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-react/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-solid": { "name": "@modelcontextprotocol/server-basic-solid", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "solid-js": "1.9.10", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-solid": "dist/index.js" @@ -192,20 +240,37 @@ "vite-plugin-solid": "^2.11.12" } }, + "examples/basic-server-solid/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-solid/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-svelte": { "name": "@modelcontextprotocol/server-basic-svelte", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "svelte": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-svelte": "dist/index.js" @@ -222,19 +287,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-svelte/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-svelte/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vanillajs": { "name": "@modelcontextprotocol/server-basic-vanillajs", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-vanillajs": "dist/index.js" @@ -250,20 +332,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vanillajs/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vanillajs/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/basic-server-vue": { "name": "@modelcontextprotocol/server-basic-vue", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "vue": "^3.5.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-basic-vue": "dist/index.js" @@ -280,19 +379,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/basic-server-vue/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/basic-server-vue/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/budget-allocator-server": { "name": "@modelcontextprotocol/server-budget-allocator", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-budget-allocator-server": "dist/index.js" @@ -308,20 +424,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/budget-allocator-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/budget-allocator-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/cohort-heatmap-server": { "name": "@modelcontextprotocol/server-cohort-heatmap", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-cohort-heatmap-server": "dist/index.js" @@ -340,19 +473,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/cohort-heatmap-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/cohort-heatmap-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/customer-segmentation-server": { "name": "@modelcontextprotocol/server-customer-segmentation", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-customer-segmentation-server": "dist/index.js" @@ -368,16 +518,33 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/customer-segmentation-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/customer-segmentation-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/debug-server": { "name": "@modelcontextprotocol/server-debug", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-debug": "dist/index.js" @@ -395,19 +562,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/debug-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/debug-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/integration-server": { - "version": "1.7.5", + "version": "2.0.0", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-integration-server": "dist/index.js" @@ -425,18 +609,35 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/integration-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/integration-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/lazy-auth-server": { "name": "@modelcontextprotocol/server-lazy-auth", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "jose": "^6.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-server-lazy-auth": "dist/index.js" @@ -452,19 +653,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/lazy-auth-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/lazy-auth-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/map-server": { "name": "@modelcontextprotocol/server-map", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-map-server": "dist/index.js" @@ -480,21 +698,38 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/map-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/map-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/pdf-server": { "name": "@modelcontextprotocol/server-pdf", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@cantoo/pdf-lib": "^2.6.5", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", "pdfjs-dist": "^5.0.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-pdf-server": "dist/index.js" @@ -510,25 +745,42 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/pdf-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/pdf-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/qr-server": { "name": "@modelcontextprotocol/server-qr", - "version": "1.7.5", + "version": "2.0.0", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } }, "examples/quickstart": { "name": "@modelcontextprotocol/quickstart", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "devDependencies": { "@types/cors": "^2.8.19", @@ -542,21 +794,31 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/quickstart/node_modules/@types/node": { + "version": "22.19.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.5.tgz", + "integrity": "sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "examples/say-server": { "name": "@modelcontextprotocol/server-say", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { - "@modelcontextprotocol/ext-apps": "^1.7.0" + "@modelcontextprotocol/ext-apps": "^2.0.0" } }, "examples/scenario-modeler-server": { "name": "@modelcontextprotocol/server-scenario-modeler", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", @@ -564,7 +826,7 @@ "express": "^5.1.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-scenario-modeler-server": "dist/index.js" @@ -583,18 +845,35 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/scenario-modeler-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/scenario-modeler-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/shadertoy-server": { "name": "@modelcontextprotocol/server-shadertoy", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-shadertoy-server": "dist/index.js" @@ -610,19 +889,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/shadertoy-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/shadertoy-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/sheet-music-server": { "name": "@modelcontextprotocol/server-sheet-music", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "abcjs": "^6.4.4", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-sheet-music-server": "dist/index.js" @@ -638,20 +934,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/sheet-music-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/sheet-music-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/system-monitor-server": { "name": "@modelcontextprotocol/server-system-monitor", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "chart.js": "^4.4.0", "cors": "^2.8.5", "express": "^5.1.0", "systeminformation": "^5.31.6", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-system-monitor-server": "dist/index.js" @@ -667,14 +980,31 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/system-monitor-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/system-monitor-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/threejs-server": { "name": "@modelcontextprotocol/server-threejs", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", @@ -682,7 +1012,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "three": "^0.181.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-threejs-server": "dist/index.js" @@ -702,18 +1032,35 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/threejs-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/threejs-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/transcript-server": { "name": "@modelcontextprotocol/server-transcript", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-transcript-server": "dist/index.js" @@ -730,19 +1077,36 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/transcript-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/transcript-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/video-resource-server": { "name": "@modelcontextprotocol/server-video-resource", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-video-resource-server": "dist/index.js" @@ -758,20 +1122,37 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/video-resource-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/video-resource-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "examples/wiki-explorer-server": { "name": "@modelcontextprotocol/server-wiki-explorer", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/express": "2.0.0", - "@modelcontextprotocol/ext-apps": "^1.7.0", + "@modelcontextprotocol/ext-apps": "^2.0.0", "@modelcontextprotocol/node": "2.0.0", "@modelcontextprotocol/server": "2.0.0", "cheerio": "^1.0.0", "cors": "^2.8.5", "express": "^5.1.0", - "zod": "^4.1.13" + "zod": "^4.2.0" }, "bin": { "mcp-wiki-explorer-server": "dist/index.js" @@ -788,6 +1169,23 @@ "vite-plugin-singlefile": "^2.3.0" } }, + "examples/wiki-explorer-server/node_modules/@types/node": { + "version": "22.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.0.tgz", + "integrity": "sha512-XC70cRZVElFHfIUB40FgZOBbgJYFKKMa5nb9lxcwYstFG/Mi+/Y0bGS+rs6Dmhmkpq4pnNiLiuZAbc02YCOnmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "examples/wiki-explorer-server/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -2326,33 +2724,8 @@ } }, "node_modules/@modelcontextprotocol/ext-apps": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.1.tgz", - "integrity": "sha512-J3WdG1A4JSSKnSWKyU+895dBVYBV2Utgtf7fUsUK45mlkETm53a/1DR6Pm3hUGKqLLQthZLmpxOg8VPzJi/lyg==", - "license": "MIT", - "workspaces": [ - "examples/*" - ], - "dependencies": { - "@standard-schema/spec": "^1.1.0" - }, - "engines": { - "node": ">=20" - }, - "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } + "resolved": "", + "link": true }, "node_modules/@modelcontextprotocol/ext-apps-basic-host": { "resolved": "examples/basic-host", @@ -2383,47 +2756,6 @@ "resolved": "examples/quickstart", "link": true }, - "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "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", - "ajv-formats": "^3.0.1", - "content-type": "^1.0.5", - "cors": "^2.8.5", - "cross-spawn": "^7.0.5", - "eventsource": "^3.0.2", - "eventsource-parser": "^3.0.0", - "express": "^5.2.1", - "express-rate-limit": "^8.2.1", - "hono": "^4.11.4", - "jose": "^6.1.3", - "json-schema-typed": "^8.0.2", - "pkce-challenge": "^5.0.0", - "raw-body": "^3.0.0", - "zod": "^3.25 || ^4.0", - "zod-to-json-schema": "^3.25.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@cfworker/json-schema": "^4.1.1", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "@cfworker/json-schema": { - "optional": true - }, - "zod": { - "optional": false - } - } - }, "node_modules/@modelcontextprotocol/server": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz", @@ -4043,41 +4375,6 @@ "node": ">=0.4.0" } }, - "node_modules/ajv": { - "version": "8.18.0", - "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", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "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" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, "node_modules/ansi-escapes": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", @@ -5640,49 +5937,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/express-rate-limit": { - "version": "8.5.2", - "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" - }, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": ">= 4.11" - } - }, - "node_modules/fast-deep-equal": { - "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", - "peer": true - }, - "node_modules/fast-uri": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", - "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause", - "peer": true - }, "node_modules/fflate": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", @@ -6145,16 +6399,6 @@ "node": ">=12" } }, - "node_modules/ip-address": { - "version": "10.2.0", - "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" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6342,20 +6586,6 @@ "node": ">=6" } }, - "node_modules/json-schema-traverse": { - "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", - "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", - "peer": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -7348,16 +7578,6 @@ "node": ">=0.10.0" } }, - "node_modules/require-from-string": { - "version": "2.0.2", - "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" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -9496,16 +9716,6 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.1", - "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 fc887fab6..da82a665c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "url": "https://github.com/modelcontextprotocol/ext-apps" }, "homepage": "https://github.com/modelcontextprotocol/ext-apps", - "version": "1.7.5", + "version": "2.0.0", "license": "MIT", "description": "MCP Apps SDK — Enable MCP servers to display interactive user interfaces in conversational clients.", "type": "module", @@ -111,17 +111,14 @@ "zod": "^4.2.0" }, "peerDependencies": { - "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/core": "2.0.0", - "@modelcontextprotocol/server": "2.0.0", + "@modelcontextprotocol/client": "^2.0.0", + "@modelcontextprotocol/core": "^2.0.0", + "@modelcontextprotocol/server": "^2.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0", "zod": "^4.2.0" }, "peerDependenciesMeta": { - "@modelcontextprotocol/client": { - "optional": true - }, "@modelcontextprotocol/server": { "optional": true }, 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 fda72b071..5a473f02d 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -77,15 +77,28 @@ Before writing any code, analyze the server's existing tools and determine which ## Step 2: Add Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 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 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. +Plus framework-specific dependencies if needed (e.g., `react`, `react-dom`, `@vitejs/plugin-react` for React), and `@modelcontextprotocol/node` / `@modelcontextprotocol/express` if the server uses HTTP transports. + +ext-apps 2.x requires the split base MCP SDK packages at `^2.0.0` +(`@modelcontextprotocol/core` comes in transitively). Existing servers that +still import `@modelcontextprotocol/sdk` v1 must migrate those imports and +handler schemas before adding the App integration: + +| SDK v1 (`@modelcontextprotocol/sdk`) | SDK v2 | +|---|---| +| `sdk/server/mcp.js` (`McpServer`) | `@modelcontextprotocol/server` | +| `sdk/server/streamableHttp.js` (`StreamableHTTPServerTransport`) | `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node` | +| Express wiring by hand | `createMcpExpressApp` from `@modelcontextprotocol/express` | +| `sdk/server/stdio.js` | `@modelcontextprotocol/server/stdio` | +| `sdk/types.js` types (`CallToolResult`, …) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `sdk/types.js` zod schemas (`CallToolResultSchema`, …) | `@modelcontextprotocol/core` | +| Raw zod shapes: `inputSchema: { q: z.string() }` | `inputSchema: z.object({ q: z.string() })` | +| `extra.signal` in tool callbacks | `extra.mcpReq.signal` | +| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", { params: ParamsSchema }, handler)` (the 2-arg form is only for spec methods such as `"tools/call"`) | ## Step 3: Set Up the Build Pipeline diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md index 03fc8d73a..8fb7fa07e 100644 --- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md +++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md @@ -115,12 +115,14 @@ Create a new MCP server with tool and resource registration. This wraps the exis ### Dependencies ```bash -npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 zod@^4.2.0 +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0 npm install -D tsx vite vite-plugin-singlefile ``` -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. +Use `npm install` so the package manager resolves versions; ext-apps 2.x needs +the split base MCP SDK packages at `^2.0.0` (`@modelcontextprotocol/core` comes +in transitively). Do not add the legacy `@modelcontextprotocol/sdk` v1 package +or substitute unpublished local packages. ### Server Code diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md index 6f1c466cf..d73dc90a3 100644 --- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md +++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md @@ -110,12 +110,14 @@ 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/client@2.0.0-beta.5 @modelcontextprotocol/core@2.0.0-beta.5 @modelcontextprotocol/server@2.0.0-beta.5 @modelcontextprotocol/node@2.0.0-beta.5 @modelcontextprotocol/express@2.0.0-beta.5 zod@^4.2.0 express cors +npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 @modelcontextprotocol/node@^2.0.0 @modelcontextprotocol/express@^2.0.0 zod@^4.2.0 express cors npm install -D typescript vite vite-plugin-singlefile concurrently cross-env @types/node @types/express @types/cors ``` -Use the exact base MCP SDK prerelease required by ext-apps. Do not substitute -unpublished local packages or guess a different prerelease. +ext-apps 2.x requires the split base MCP SDK packages (`@modelcontextprotocol/client`, +`server`, `node`, `express`) at `^2.0.0`; `@modelcontextprotocol/core` comes in +transitively. Do not add the legacy `@modelcontextprotocol/sdk` v1 package or +substitute unpublished local packages. ### TypeScript Server Execution diff --git a/scripts/check-dependency-isolation.mjs b/scripts/check-dependency-isolation.mjs index 2ec28cef0..3809224eb 100644 --- a/scripts/check-dependency-isolation.mjs +++ b/scripts/check-dependency-isolation.mjs @@ -2,6 +2,7 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, mkdirSync, + readdirSync, readFileSync, rmSync, writeFileSync, @@ -16,56 +17,102 @@ const packageJson = JSON.parse( const client = "@modelcontextprotocol/client"; const server = "@modelcontextprotocol/server"; +// App and AppBridge extend the client package's Protocol class, so client is +// a required peer for every consumer. The server helpers are only needed by +// server authors, so server stays optional: View-only consumers must not have +// it installed or bundled. for (const role of [client, server]) { if (!packageJson.peerDependencies?.[role]) { throw new Error(`${role} must remain a peer dependency`); } - if (packageJson.peerDependenciesMeta?.[role]?.optional !== true) { - throw new Error(`${role} must be an optional peer dependency`); +} +if (packageJson.peerDependenciesMeta?.[client]?.optional) { + throw new Error(`${client} must be a required peer dependency`); +} +if (packageJson.peerDependenciesMeta?.[server]?.optional !== true) { + throw new Error(`${server} must be an optional peer dependency`); +} + +/** + * Exact version for a synthetic consumer dependency. Read from + * devDependencies so the consumers exercise the same SDK version the + * repository tests against; the check would silently drift if a range were + * allowed here. + */ +function exactDevDependency(name) { + const version = packageJson.devDependencies?.[name]; + if (!/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(version ?? "")) { + throw new Error( + `devDependencies["${name}"] must be an exact version, got ${JSON.stringify(version)}`, + ); } + return version; +} + +/** Exact version of a package as installed in this repository's node_modules. */ +function installedVersion(name) { + return JSON.parse( + readFileSync(join(root, "node_modules", name, "package.json"), "utf8"), + ).version; } const temporaryRoot = mkdtempSync(join(tmpdir(), "ext-apps-role-peers-")); try { + // Minimal environment: nothing inherited from the caller's npm config + // (registry overrides, auth tokens, npm_config_* set by an outer `npm run`) + // can leak into the synthetic consumers. const npmEnvironment = { - ...process.env, + PATH: process.env.PATH ?? process.env.Path, + HOME: process.env.HOME ?? process.env.USERPROFILE, npm_config_cache: join(temporaryRoot, "npm-cache"), }; - const packOutput = JSON.parse( - execFileSync( - "npm", - [ - "pack", - "--ignore-scripts", - "--json", - "--pack-destination", - temporaryRoot, - ], - { cwd: root, encoding: "utf8", env: npmEnvironment }, - ), + const packDestination = join(temporaryRoot, "pack"); + mkdirSync(packDestination); + // `npm pack` runs the package's `prepare` script even with --ignore-scripts + // (pacote's directory fetcher), and its output can pollute stdout, so do not + // rely on `--json`: locate the tarball on disk instead. + execFileSync( + "npm", + ["pack", "--ignore-scripts", "--pack-destination", packDestination], + { cwd: root, stdio: "pipe", env: npmEnvironment }, + ); + const tarballs = readdirSync(packDestination).filter((name) => + name.endsWith(".tgz"), ); - const tarball = join(temporaryRoot, packOutput[0].filename); + if (tarballs.length !== 1) { + throw new Error(`expected exactly one tarball, found ${tarballs}`); + } + const tarball = join(packDestination, tarballs[0]); const consumers = [ { + // View / host author: ext-apps + client (+ react for the hooks entry). name: "app-only", dependencies: { - "@types/node": packageJson.devDependencies["@types/node"], - [client]: packageJson.devDependencies[client], + "@types/node": exactDevDependency("@types/node"), + "@types/react": installedVersion("@types/react"), + [client]: exactDevDependency(client), "@modelcontextprotocol/ext-apps": `file:${tarball}`, + react: installedVersion("react"), }, absent: server, + // server must be neither installed (it is an optional peer) nor bundled. + mustNotInstall: true, entry: - 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; console.log(App, AppBridge);', + 'import { App } from "@modelcontextprotocol/ext-apps"; import { AppBridge } from "@modelcontextprotocol/ext-apps/app-bridge"; import { useApp } from "@modelcontextprotocol/ext-apps/react"; console.log(App, AppBridge, useApp);', }, { + // Server author: ext-apps + server. npm auto-installs client as a + // required peer (its types back the shared wire types), but the server + // entry must not pull it into a runtime bundle. name: "server-only", dependencies: { - "@types/node": packageJson.devDependencies["@types/node"], + "@types/node": exactDevDependency("@types/node"), "@modelcontextprotocol/ext-apps": `file:${tarball}`, - [server]: packageJson.devDependencies[server], + [server]: exactDevDependency(server), }, absent: client, + mustNotInstall: false, entry: 'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);', }, @@ -91,22 +138,24 @@ try { "--no-audit", "--no-fund", ], - { cwd: directory, stdio: "pipe", env: npmEnvironment }, + { cwd: directory, stdio: "inherit", env: npmEnvironment }, ); - const absentPath = join( - directory, - "node_modules", - ...consumer.absent.split("/"), - "package.json", - ); - try { - readFileSync(absentPath); - throw new Error( - `${consumer.name} unexpectedly installed ${consumer.absent}`, + if (consumer.mustNotInstall) { + const absentPath = join( + directory, + "node_modules", + ...consumer.absent.split("/"), + "package.json", ); - } catch (error) { - if (error?.code !== "ENOENT") throw error; + try { + readFileSync(absentPath); + throw new Error( + `${consumer.name} unexpectedly installed ${consumer.absent}`, + ); + } catch (error) { + if (error?.code !== "ENOENT") throw error; + } } writeFileSync(join(directory, "entry.ts"), consumer.entry); @@ -114,6 +163,7 @@ try { join(directory, "tsconfig.json"), JSON.stringify({ compilerOptions: { + jsx: "react-jsx", lib: ["ES2020", "DOM"], module: "ESNext", moduleResolution: "bundler", @@ -141,7 +191,7 @@ try { "--outfile=bundle.js", `--metafile=${metafile}`, ], - { cwd: directory, stdio: "pipe" }, + { cwd: directory, stdio: "inherit" }, ); const bundleInputs = Object.keys( JSON.parse(readFileSync(metafile, "utf8")).inputs, diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts index 85225f5c5..0ff9f049f 100644 --- a/scripts/generate-schemas.ts +++ b/scripts/generate-schemas.ts @@ -22,10 +22,12 @@ * * **Problem**: ts-to-zod cannot resolve types imported from external packages. * When it encounters types like `ContentBlock`, `CallToolResult`, `Implementation`, - * `RequestId`, and `Tool` from `@modelcontextprotocol/core`, it generates `z.any()` - * as a placeholder. + * `RequestId`, and `Tool` (imported by spec.types.ts from + * `@modelcontextprotocol/client`), it generates `z.any()` as a placeholder. * - * **Solution**: Import the schemas from MCP SDK and remove the z.any() placeholders. + * **Solution**: Import the corresponding Zod schemas from + * `@modelcontextprotocol/core` (the role-neutral package both client and + * server re-export) and remove the z.any() placeholders. * * ### 3. Index Signatures (`z.record().and()` → `z.object().passthrough()`) * diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 397e12f90..a87b0e7db 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -1551,40 +1551,9 @@ describe("App <-> AppBridge integration", () => { ).rejects.toThrow(/Invalid input for tool translate/); }); - it("falls back to z.toJSONSchema for zod schemas lacking ~standard.jsonSchema (zod v3.25.x)", async () => { - // zod v3.25 implements ~standard.validate but not ~standard.jsonSchema. - // Simulate by stripping jsonSchema from a real zod schema. - const v4Schema = z.object({ q: z.string() }); - const zod3LikeSchema = Object.assign(Object.create(v4Schema), { - "~standard": { - version: 1 as const, - vendor: "zod", - validate: v4Schema["~standard"].validate, - types: undefined as - | undefined - | { - readonly input: { q: string }; - readonly output: { q: string }; - }, - // no jsonSchema - }, - }); - + it("rejects listTools when a tool schema does not implement Standard JSON Schema", async () => { const appCapabilities = { tools: { listChanged: true } }; app = new App(testAppInfo, appCapabilities, { autoResize: false }); - app.registerTool( - "search", - { inputSchema: zod3LikeSchema }, - async ({ q }: { q: string }) => ({ - content: [{ type: "text" as const, text: q }], - }), - ); - await app.connect(appTransport); - - const list = await bridge.listTools({}); - expect(list.tools[0].inputSchema.properties).toHaveProperty("q"); - - // Non-zod schema without jsonSchema → listTools rejects with guidance. app.registerTool( "broken", { @@ -1598,6 +1567,8 @@ describe("App <-> AppBridge integration", () => { }, async () => ({ content: [] }), ); + await app.connect(appTransport); + expect(bridge.listTools({})).rejects.toThrow( /does not implement Standard JSON Schema/, ); @@ -3016,16 +2987,95 @@ describe("isToolVisibilityAppOnly", () => { expect(app.onteardown).toBe(handler); }); - it("direct setRequestHandler uses base SDK replacement semantics", () => { + it("direct setRequestHandler throws when called twice", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + const params = z.object({}); + bridge2.setRequestHandler("test/method", { params }, () => ({})); + expect(() => { + bridge2.setRequestHandler("test/method", { params }, () => ({})); + }).toThrow(/already registered/); + }); + + it("direct setRequestHandler cannot silently replace an on* host handler", () => { const bridge2 = new AppBridge( createMockClient() as Client, testHostInfo, testHostCapabilities, ); - bridge2.setRequestHandler("ping", () => ({})); + bridge2.onopenlink = async () => ({}); expect(() => { - bridge2.setRequestHandler("ping", () => ({})); + bridge2.setRequestHandler( + "ui/open-link", + { params: z.object({}) }, + () => ({}), + ); + }).toThrow(/already registered/); + }); + + it("direct setNotificationHandler throws for event-mapped methods", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.onsizechange = () => {}; + expect(() => { + bridge2.setNotificationHandler( + "ui/notifications/size-changed", + { params: z.object({}) }, + () => {}, + ); + }).toThrow(/already registered/); + }); + + it("removeRequestHandler releases the method so an on* setter can re-register", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + bridge2.removeRequestHandler("tools/call"); + expect(() => { + bridge2.setRequestHandler("tools/call", async () => ({ content: [] })); }).not.toThrow(); + expect(() => { + bridge2.oncalltool = async () => ({ content: [] }); + }).not.toThrow(); + }); + + it("oncreatesamplingmessage has a getter, replace semantics, and a replace warning", () => { + const bridge2 = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + expect(bridge2.oncreatesamplingmessage).toBeUndefined(); + const first = async () => ({ + role: "assistant" as const, + content: { type: "text" as const, text: "" }, + model: "m", + }); + bridge2.oncreatesamplingmessage = first; + expect(bridge2.oncreatesamplingmessage).toBe(first); + + const warn = spyOn(console, "warn").mockImplementation(() => {}); + try { + expect(() => { + bridge2.oncreatesamplingmessage = first; + }).not.toThrow(); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("oncreatesamplingmessage handler replaced"), + ); + } finally { + warn.mockRestore(); + } + + bridge2.oncreatesamplingmessage = undefined; + expect(bridge2.oncreatesamplingmessage).toBeUndefined(); }); }); }); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 8e4867731..3d145d413 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -34,7 +34,7 @@ import { EmptyResultSchema, LoggingMessageNotificationSchema, } from "@modelcontextprotocol/core"; -import { EventDispatcher } from "./events.js"; +import { EventDispatcher, MethodRegistry } from "./events.js"; import type { ZodLiteral, ZodObject, ZodType } from "zod/v4"; type MethodSchema = ZodObject<{ @@ -91,6 +91,8 @@ import { export * from "./types.js"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js"; import { RESOURCE_URI_META_KEY } from "./constants.js"; + +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; export { PostMessageTransport } from "./message-transport.js"; /** @@ -216,8 +218,8 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION]; /** * Extra metadata passed to request handlers. * - * This type represents the additional context provided by the base MCP SDK - * `Server` when handling requests. + * This type represents the additional context (`BaseContext`) provided by the + * base MCP SDK `Protocol` when handling requests. * * @internal */ @@ -302,6 +304,81 @@ export class AppBridge extends Protocol { private _initializedReceived = false; private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); + private readonly _methods = new MethodRegistry(); + + // ── Handler registration with double-set protection ───────────────── + // + // The base SDK `Protocol` silently replaces an existing handler. The four + // overrides below restore the v1 behaviour: a direct `setRequestHandler` / + // `setNotificationHandler` for a method that already has a handler throws, + // so a stray registration cannot silently replace a host's `on*` handler + // (for example the URL allow-listing in `onopenlink`) or disconnect + // `addEventListener` listeners. They are arrow-function class fields rather + // than prototype methods so that `Protocol`'s constructor — which registers + // its own ping/cancelled/progress handlers before our fields initialize — + // hits the base implementation and skips tracking. + + /** + * Registers a request handler. Throws if a handler for the same method has + * already been registered — use the `on*` setter for replace semantics. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setRequestHandler: Protocol["setRequestHandler"] = ( + method: string, + ...rest: unknown[] + ) => { + this._methods.claim(method, "setRequestHandler"); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + /** + * 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["setNotificationHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.claim(method, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + override removeRequestHandler: Protocol["removeRequestHandler"] = + (method: string) => { + this._methods.release(method); + super.removeRequestHandler(method); + }; + + override removeNotificationHandler: Protocol["removeNotificationHandler"] = + (method: string) => { + this._methods.release(method); + super.removeNotificationHandler(method); + }; + + /** + * Register a request handler with replace semantics, bypassing the + * double-set protection of {@link setRequestHandler `setRequestHandler`}. + * Used by the `on*` request-handler setters. + */ + protected replaceRequestHandler: Protocol["setRequestHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.replace(method); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; /** * The base MCP SDK calls this hook for every standard and custom request @@ -419,7 +496,7 @@ export class AppBridge extends Protocol { * manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters. * @param _hostInfo - Host application identification (name and version) * @param _hostCapabilities - Features and capabilities the host supports - * @param options - Configuration options (inherited from Server) + * @param options - Configuration options (`ProtocolOptions` from the base MCP SDK plus `hostContext`) * * @example With MCP client (automatic forwarding) * ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient" @@ -450,12 +527,14 @@ export class AppBridge extends Protocol { options?: HostOptions, ) { super(options); + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); this._ensureEventSlot("initialized"); this._hostContext = options?.hostContext || {}; - this.setRequestHandler( + this.replaceRequestHandler( "ui/initialize", { params: McpUiInitializeRequestSchema.shape.params, @@ -464,14 +543,14 @@ export class AppBridge extends Protocol { (params) => this._onAppsInitialize(params), ); - this.setRequestHandler("ping", (request, extra) => { + this.replaceRequestHandler("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.setRequestHandler( + this.replaceRequestHandler( "ui/request-display-mode", { params: McpUiRequestDisplayModeRequestSchema.shape.params, @@ -746,7 +825,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback); this._onmessage = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/message", { params: McpUiMessageRequestSchema.shape.params, @@ -819,7 +898,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback); this._onopenlink = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/open-link", { params: McpUiOpenLinkRequestSchema.shape.params, @@ -895,7 +974,7 @@ export class AppBridge extends Protocol { callback, ); this._ondownloadfile = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/download-file", { params: McpUiDownloadFileRequestSchema.shape.params, @@ -999,7 +1078,7 @@ export class AppBridge extends Protocol { callback, ); this._onrequestdisplaymode = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/request-display-mode", { params: McpUiRequestDisplayModeRequestSchema.shape.params, @@ -1100,7 +1179,7 @@ export class AppBridge extends Protocol { callback, ); this._onupdatemodelcontext = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/update-model-context", { params: McpUiUpdateModelContextRequestSchema.shape.params, @@ -1136,8 +1215,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/server for the request type - * @see `CallToolResult` from @modelcontextprotocol/server for the result type + * @see `CallToolRequest` from @modelcontextprotocol/client for the request type + * @see `CallToolResult` from @modelcontextprotocol/client for the result type */ private _oncalltool?: ( params: CallToolRequest["params"], @@ -1156,7 +1235,7 @@ export class AppBridge extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.setRequestHandler("tools/call", async (request, extra) => { + this.replaceRequestHandler("tools/call", async (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1190,18 +1269,39 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/server for the request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/server for result types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ + private _oncreatesamplingmessage?: ( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise; + get oncreatesamplingmessage() { + return this._oncreatesamplingmessage; + } set oncreatesamplingmessage( - callback: ( - params: CreateMessageRequest["params"], - extra: RequestHandlerExtra, - ) => Promise, + callback: + | (( + params: CreateMessageRequest["params"], + extra: RequestHandlerExtra, + ) => Promise) + | undefined, ) { - this.setRequestHandler("sampling/createMessage", async (request, extra) => { - return callback(request.params, extra); - }); + this.warnIfRequestHandlerReplaced( + "oncreatesamplingmessage", + this._oncreatesamplingmessage, + callback, + ); + this._oncreatesamplingmessage = callback; + this.replaceRequestHandler( + "sampling/createMessage", + async (request, extra) => { + if (!this._oncreatesamplingmessage) { + throw new Error("No oncreatesamplingmessage handler set"); + } + return this._oncreatesamplingmessage(request.params, extra); + }, + ); } /** @@ -1216,12 +1316,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => { * bridge.sendToolListChanged(); * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1252,8 +1352,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/server for the request type - * @see `ListResourcesResult` from @modelcontextprotocol/server for the result type + * @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourcesResult` from @modelcontextprotocol/client for the result type */ private _onlistresources?: ( params: ListResourcesRequest["params"], @@ -1276,7 +1376,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistresources = callback; - this.setRequestHandler("resources/list", async (request, extra) => { + this.replaceRequestHandler("resources/list", async (request, extra) => { if (!this._onlistresources) throw new Error("No onlistresources handler set"); return this._onlistresources(request.params, extra); @@ -1305,8 +1405,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/server for the request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/server for the result type + * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type + * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type */ private _onlistresourcetemplates?: ( params: ListResourceTemplatesRequest["params"], @@ -1329,7 +1429,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistresourcetemplates = callback; - this.setRequestHandler( + this.replaceRequestHandler( "resources/templates/list", async (request, extra) => { if (!this._onlistresourcetemplates) @@ -1361,8 +1461,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/server for the request type - * @see `ReadResourceResult` from @modelcontextprotocol/server for the result type + * @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type + * @see `ReadResourceResult` from @modelcontextprotocol/client for the result type */ private _onreadresource?: ( params: ReadResourceRequest["params"], @@ -1385,7 +1485,7 @@ export class AppBridge extends Protocol { callback, ); this._onreadresource = callback; - this.setRequestHandler("resources/read", async (request, extra) => { + this.replaceRequestHandler("resources/read", async (request, extra) => { if (!this._onreadresource) throw new Error("No onreadresource handler set"); return this._onreadresource(request.params, extra); @@ -1404,12 +1504,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => { * bridge.sendResourceListChanged(); * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1442,8 +1542,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/server for the request type - * @see `ListPromptsResult` from @modelcontextprotocol/server for the result type + * @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type + * @see `ListPromptsResult` from @modelcontextprotocol/client for the result type */ private _onlistprompts?: ( params: ListPromptsRequest["params"], @@ -1466,7 +1566,7 @@ export class AppBridge extends Protocol { callback, ); this._onlistprompts = callback; - this.setRequestHandler("prompts/list", async (request, extra) => { + this.replaceRequestHandler("prompts/list", async (request, extra) => { if (!this._onlistprompts) throw new Error("No onlistprompts handler set"); return this._onlistprompts(request.params, extra); }); @@ -1484,12 +1584,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => { * bridge.sendPromptListChanged(); * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/server for the notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ diff --git a/src/app.test.ts b/src/app.test.ts index 9057710f4..8420e81ea 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -6,13 +6,13 @@ import { } from "@modelcontextprotocol/server"; import { z } from "zod/v4"; -import { App } from "./app"; +import { App } from "./app.js"; import { LATEST_PROTOCOL_VERSION, McpUiInitializeRequestSchema, McpUiInitializeResultSchema, McpUiInitializedNotificationSchema, -} from "./types"; +} from "./types.js"; type ConnectedPair = { app: App; @@ -248,25 +248,62 @@ describe("App base MCP SDK v2 Protocol migration", () => { }).toThrow(/handler registered after connect/); }); - it("uses base MCP SDK replacement semantics for request handlers", async () => { + it("direct setRequestHandler throws when a handler is already registered", async () => { const pair = await connectPair(); connected.push(pair); + const params = z.object({}); let handledBy = 0; - pair.app.setRequestHandler("ping", () => { + pair.app.setRequestHandler("test/method", { params }, () => { handledBy = 1; return {}; }); - pair.app.setRequestHandler("ping", () => { + expect(() => { + pair.app.setRequestHandler("test/method", { params }, () => { + handledBy = 2; + return {}; + }); + }).toThrow(/already registered/); + + await pair.server.request( + { method: "test/method", params: {} }, + z.object({}), + ); + expect(handledBy).toBe(1); + }); + + it("direct setRequestHandler throws for methods owned by the App itself", async () => { + const pair = await connectPair(); + connected.push(pair); + expect(() => { + pair.app.setRequestHandler("ping", () => ({})); + }).toThrow(/already registered/); + }); + + it("removeRequestHandler releases the method for re-registration", async () => { + const pair = await connectPair(); + connected.push(pair); + const params = z.object({}); + let handledBy = 0; + + pair.app.setRequestHandler("test/method", { params }, () => { + handledBy = 1; + return {}; + }); + pair.app.removeRequestHandler("test/method"); + pair.app.setRequestHandler("test/method", { params }, () => { handledBy = 2; return {}; }); - await pair.server.request({ method: "ping", params: {} }); + await pair.server.request( + { method: "test/method", params: {} }, + z.object({}), + ); expect(handledBy).toBe(2); }); - it("uses base MCP SDK replacement semantics for non-event notifications", async () => { + it("direct setNotificationHandler throws when a handler is already registered", async () => { const pair = await connectPair(); connected.push(pair); const calls: number[] = []; @@ -275,14 +312,70 @@ describe("App base MCP SDK v2 Protocol migration", () => { pair.app.setNotificationHandler("test/notification", { params }, () => { calls.push(1); }); - pair.app.setNotificationHandler("test/notification", { params }, () => { - calls.push(2); - }); + expect(() => { + pair.app.setNotificationHandler("test/notification", { params }, () => { + calls.push(2); + }); + }).toThrow(/already registered/); await pair.server.notification({ method: "test/notification", params: {}, }); - expect(calls).toEqual([2]); + expect(calls).toEqual([1]); + }); + + it("direct setNotificationHandler throws for event-mapped methods (listener first)", () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false }, + ); + app.addEventListener("toolinput", () => {}); + expect(() => { + app.setNotificationHandler( + "ui/notifications/tool-input", + { params: z.object({}) }, + () => {}, + ); + }).toThrow(/already registered/); + }); + + it("event registration throws when a direct handler already owns the method", () => { + const app = new App( + { name: "view-app", version: "1.0.0" }, + {}, + { autoResize: false }, + ); + app.setNotificationHandler( + "ui/notifications/tool-input", + { params: z.object({}) }, + () => {}, + ); + expect(() => { + app.ontoolinput = () => {}; + }).toThrow(/already registered/); + expect(() => { + app.addEventListener("toolinput", () => {}); + }).toThrow(/already registered/); + }); + + it("on* request setters keep replace semantics", async () => { + const pair = await connectPair(); + connected.push(pair); + let handledBy = 0; + pair.app.onteardown = async () => { + handledBy = 1; + return {}; + }; + pair.app.onteardown = async () => { + handledBy = 2; + return {}; + }; + await pair.server.request( + { method: "ui/resource-teardown", params: {} }, + z.object({}), + ); + expect(handledBy).toBe(2); }); }); diff --git a/src/app.ts b/src/app.ts index 3fe84d682..58ea9da1c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -23,8 +23,9 @@ import { } from "@modelcontextprotocol/client"; import { EmptyResultSchema } from "@modelcontextprotocol/core"; export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; -import { EventDispatcher } from "./events.js"; +import { EventDispatcher, MethodRegistry } from "./events.js"; export { EventDispatcher } from "./events.js"; + import { PostMessageTransport } from "./message-transport.js"; import { LATEST_PROTOCOL_VERSION, @@ -67,6 +68,8 @@ import { } from "./standard-schema.js"; import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; + type MethodSchema = ZodObject<{ method: ZodLiteral; params: ZodType; @@ -103,71 +106,6 @@ export { applyDocumentTheme, } from "./styles.js"; -/** - * Metadata key for associating a UI resource URI with a tool. - * - * MCP servers include this key in tool definition metadata (via `tools/list`) - * to indicate which UI resource should be displayed when the tool is called. - * When hosts see a tool with this metadata, they fetch and render the - * corresponding {@link App `App`}. - * - * **Note**: This constant is provided for reference and backwards compatibility. - * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`} - * with the `_meta.ui.resourceUri` format instead. Host developers must check both - * formats for compatibility. - * - * @example Modern format (server-side, not in Apps) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat" - * // Preferred: Use registerAppTool with nested ui.resourceUri - * registerAppTool( - * server, - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * ui: { resourceUri: "ui://weather/forecast" }, - * }, - * }, - * handler, - * ); - * ``` - * - * @example Legacy format (deprecated, for backwards compatibility) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat" - * // Deprecated: Direct use of RESOURCE_URI_META_KEY - * server.registerTool( - * "weather", - * { - * description: "Get weather forecast", - * _meta: { - * [RESOURCE_URI_META_KEY]: "ui://weather/forecast", - * }, - * }, - * handler, - * ); - * ``` - * - * @example How hosts check for this metadata (must support both formats) - * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide" - * // Hosts should check both modern and legacy formats - * const meta = tool._meta; - * const uiMeta = meta?.ui as McpUiToolMeta | undefined; - * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined; - * const uiUri = uiMeta?.resourceUri ?? legacyUri; - * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { - * // Fetch the resource and display the UI - * } - * ``` - */ - -/** - * MIME type for MCP UI resources. - * - * Identifies HTML content as an MCP App UI resource. - * - * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. - */ - /** * Options for configuring {@link App `App`} behavior. * @@ -359,6 +297,80 @@ export class App extends Protocol { private _initializedSent = false; private readonly _registeredEvents = new Set(); private readonly _events = new EventDispatcher(); + private readonly _methods = new MethodRegistry(); + + // ── Handler registration with double-set protection ───────────────── + // + // The base SDK `Protocol` silently replaces an existing handler. The four + // overrides below restore the v1 behaviour: a direct `setRequestHandler` / + // `setNotificationHandler` for a method that already has a handler throws, + // so a stray registration cannot silently disconnect `on*` handlers or + // `addEventListener` listeners. They are arrow-function class fields rather + // than prototype methods so that `Protocol`'s constructor — which registers + // its own ping/cancelled/progress handlers before our fields initialize — + // hits the base implementation and skips tracking. + + /** + * Registers a request handler. Throws if a handler for the same method has + * already been registered — use the `on*` setter for replace semantics. + * + * @throws {Error} if a handler for this method is already registered. + */ + override setRequestHandler: Protocol["setRequestHandler"] = ( + method: string, + ...rest: unknown[] + ) => { + this._methods.claim(method, "setRequestHandler"); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + /** + * 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["setNotificationHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.claim(method, "setNotificationHandler"); + (super.setNotificationHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; + + override removeRequestHandler: Protocol["removeRequestHandler"] = + (method: string) => { + this._methods.release(method); + super.removeRequestHandler(method); + }; + + override removeNotificationHandler: Protocol["removeNotificationHandler"] = + (method: string) => { + this._methods.release(method); + super.removeNotificationHandler(method); + }; + + /** + * Register a request handler with replace semantics, bypassing the + * double-set protection of {@link setRequestHandler `setRequestHandler`}. + * Used by the `on*` request-handler setters. + */ + protected replaceRequestHandler: Protocol["setRequestHandler"] = + (method: string, ...rest: unknown[]) => { + this._methods.replace(method); + (super.setRequestHandler as unknown as UntypedHandlerSetter).call( + this, + method, + ...rest, + ); + }; /** * Warn if a host-bound method is called before {@link connect `connect`} has @@ -519,12 +531,16 @@ export class App extends Protocol { private options: AppOptions = { autoResize: true }, ) { super(options); + // Claim the handlers the base Protocol constructor installed so a direct + // setNotificationHandler cannot silently replace them either. + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); if (!options.allowUnsafeEval) { z.config({ jitless: true }); } - this.setRequestHandler("ping", (request) => { + this.replaceRequestHandler("ping", (request) => { console.log("Received ping:", request.params); return {}; }); @@ -1056,7 +1072,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback); this._onteardown = callback; - this.setRequestHandler( + this.replaceRequestHandler( "ui/resource-teardown", { params: McpUiResourceTeardownRequestSchema.shape.params, @@ -1114,7 +1130,7 @@ export class App extends Protocol { ) { this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback); this._oncalltool = callback; - this.setRequestHandler("tools/call", (request, extra) => { + this.replaceRequestHandler("tools/call", (request, extra) => { if (!this._oncalltool) throw new Error("No oncalltool handler set"); return this._oncalltool(request.params, extra); }); @@ -1185,7 +1201,7 @@ export class App extends Protocol { callback, ); this._onlisttools = callback; - this.setRequestHandler("tools/list", (request, extra) => { + this.replaceRequestHandler("tools/list", (request, extra) => { if (!this._onlisttools) throw new Error("No onlisttools handler set"); return this._onlisttools(request.params, extra); }); @@ -1221,11 +1237,11 @@ export class App extends Protocol { ); } return; - case "ping": - case "ui/resource-teardown": - return; default: - throw new Error(`No handler for method ${method} registered`); + // `ping`, `ui/*`, and custom (vendor-prefixed) methods need no + // declared capability. The base SDK explicitly supports custom + // request handlers via the `{ params, result }` form. + return; } } diff --git a/src/constants.ts b/src/constants.ts index f69cfa533..c879a03a2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,66 @@ -/** Legacy metadata key associating an MCP tool with its App resource. */ +/** + * Metadata key for associating a UI resource URI with a tool. + * + * MCP servers include this key in tool definition metadata (via `tools/list`) + * to indicate which UI resource should be displayed when the tool is called. + * When hosts see a tool with this metadata, they fetch and render the + * corresponding {@link app!App `App`}. + * + * **Note**: This constant is provided for reference and backwards compatibility. + * Server developers should use {@link server-helpers!registerAppTool `registerAppTool`} + * with the `_meta.ui.resourceUri` format instead. Host developers must check both + * formats for compatibility. + * + * @example Modern format (server-side, not in Apps) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_modernFormat" + * // Preferred: Use registerAppTool with nested ui.resourceUri + * registerAppTool( + * server, + * "weather", + * { + * description: "Get weather forecast", + * _meta: { + * ui: { resourceUri: "ui://weather/forecast" }, + * }, + * }, + * handler, + * ); + * ``` + * + * @example Legacy format (deprecated, for backwards compatibility) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_legacyFormat" + * // Deprecated: Direct use of RESOURCE_URI_META_KEY + * server.registerTool( + * "weather", + * { + * description: "Get weather forecast", + * _meta: { + * [RESOURCE_URI_META_KEY]: "ui://weather/forecast", + * }, + * }, + * handler, + * ); + * ``` + * + * @example How hosts check for this metadata (must support both formats) + * ```ts source="./app.examples.ts#RESOURCE_URI_META_KEY_hostSide" + * // Hosts should check both modern and legacy formats + * const meta = tool._meta; + * const uiMeta = meta?.ui as McpUiToolMeta | undefined; + * const legacyUri = meta?.[RESOURCE_URI_META_KEY] as string | undefined; + * const uiUri = uiMeta?.resourceUri ?? legacyUri; + * if (typeof uiUri === "string" && uiUri.startsWith("ui://")) { + * // Fetch the resource and display the UI + * } + * ``` + */ export const RESOURCE_URI_META_KEY = "ui/resourceUri"; -/** MIME type for MCP App HTML resources. */ +/** + * MIME type for MCP UI resources. + * + * Identifies HTML content as an MCP App UI resource. + * + * Used by {@link server-helpers!registerAppResource `registerAppResource`} as the default MIME type for app resources. + */ export const RESOURCE_MIME_TYPE = "text/html;profile=mcp-app"; diff --git a/src/core-types.ts b/src/core-types.ts deleted file mode 100644 index 19392d24e..000000000 --- a/src/core-types.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - CallToolResultSchema, - ContentBlockSchema, - EmbeddedResourceSchema, - ImplementationSchema, - RequestIdSchema, - ResourceLinkSchema, - ToolSchema, -} from "@modelcontextprotocol/core"; -import type { z } from "zod/v4"; - -// Infer shared wire types from the public role-neutral schemas so declarations -// used by every entrypoint do not acquire a client or server package edge. -export type CallToolResult = z.infer; -export type ContentBlock = z.infer; -export type EmbeddedResource = z.infer; -export type Implementation = z.infer; -export type RequestId = z.infer; -export type ResourceLink = z.infer; -export type Tool = z.infer; diff --git a/src/events.test.ts b/src/events.test.ts index 7fd87087f..60e7c355f 100644 --- a/src/events.test.ts +++ b/src/events.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, spyOn } from "bun:test"; -import { EventDispatcher } from "./events"; +import { EventDispatcher } from "./events.js"; type TestEventMap = { change: { value: number }; diff --git a/src/events.ts b/src/events.ts index aff144605..aa2e8623a 100644 --- a/src/events.ts +++ b/src/events.ts @@ -89,3 +89,44 @@ export class EventDispatcher> { return slot; } } + +/** + * Tracks which JSON-RPC methods already have a handler registered through the + * owning `App` / `AppBridge`, so a second direct `setRequestHandler` / + * `setNotificationHandler` call for the same method throws instead of + * silently replacing the first handler (the base SDK `Protocol` replaces). + * + * The `on*` setters use {@link replace} for DOM-style replace semantics, and + * `removeRequestHandler` / `removeNotificationHandler` use {@link release}. + * + * @internal + */ +export class MethodRegistry { + private readonly _methods = new Set(); + + /** Claim `method`. Throws if a handler is already registered for it. */ + claim(method: string, via: string): void { + if (this._methods.has(method)) { + throw new Error( + `Handler for "${method}" already registered (via ${via}). ` + + `Use addEventListener() to attach multiple listeners, ` + + `or the on* setter for replace semantics.`, + ); + } + this._methods.add(method); + } + + /** Claim `method` with replace semantics (never throws). */ + replace(method: string): void { + this._methods.add(method); + } + + /** Release `method` so it can be claimed again. */ + release(method: string): void { + this._methods.delete(method); + } + + has(method: string): boolean { + return this._methods.has(method); + } +} diff --git a/src/server/index.test.ts b/src/server/index.test.ts index 389cde138..489d0b0de 100644 --- a/src/server/index.test.ts +++ b/src/server/index.test.ts @@ -7,7 +7,13 @@ import { getUiCapability, EXTENSION_ID, } from "./index.js"; -import type { McpServer } from "@modelcontextprotocol/server"; +import { + InMemoryTransport, + McpServer as RealMcpServer, + type McpServer, +} from "@modelcontextprotocol/server"; +import { Client } from "@modelcontextprotocol/client"; +import { z } from "zod/v4"; describe("registerAppTool", () => { it("should pass through config to server.registerTool", () => { @@ -197,6 +203,119 @@ describe("registerAppTool", () => { }); }); +describe("registerAppTool schema forms", () => { + /** Registers tools on a fresh server, then connects a client to it. */ + async function connect(register: (server: RealMcpServer) => void) { + const server = new RealMcpServer({ name: "test", version: "0.0.0" }); + register(server); + const client = new Client({ name: "client", version: "0.0.0" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + return client; + } + + it("accepts a z.object() input schema and validates arguments", async () => { + const client = await connect((server) => + registerAppTool( + server, + "greet", + { + inputSchema: z.object({ name: z.string() }), + _meta: { ui: { resourceUri: "ui://greet/view.html" } }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `hello ${name}` }], + }), + ), + ); + + const { tools } = await client.listTools(); + expect(tools).toHaveLength(1); + expect(tools[0].inputSchema).toMatchObject({ + type: "object", + properties: { name: { type: "string" } }, + }); + expect(tools[0]._meta).toMatchObject({ + ui: { resourceUri: "ui://greet/view.html" }, + [RESOURCE_URI_META_KEY]: "ui://greet/view.html", + }); + + const result = await client.callTool({ + name: "greet", + arguments: { name: "world" }, + }); + expect(result.content).toEqual([{ type: "text", text: "hello world" }]); + + const invalid = await client.callTool({ + name: "greet", + arguments: { name: 42 }, + }); + expect(invalid.isError).toBe(true); + }); + + it("accepts a raw zod shape (deprecated form) and wraps it with z.object()", async () => { + const client = await connect((server) => + registerAppTool( + server, + "greet", + { + inputSchema: { name: z.string() }, + outputSchema: { greeting: z.string() }, + _meta: { ui: { resourceUri: "ui://greet/view.html" } }, + }, + async ({ name }) => ({ + content: [{ type: "text", text: `hello ${name}` }], + structuredContent: { greeting: `hello ${name}` }, + }), + ), + ); + + const { tools } = await client.listTools(); + expect(tools[0].inputSchema).toMatchObject({ + type: "object", + properties: { name: { type: "string" } }, + }); + expect(tools[0].outputSchema).toMatchObject({ + type: "object", + properties: { greeting: { type: "string" } }, + }); + + const result = await client.callTool({ + name: "greet", + arguments: { name: "world" }, + }); + expect(result.structuredContent).toEqual({ greeting: "hello world" }); + + const invalid = await client.callTool({ + name: "greet", + arguments: { name: 42 }, + }); + expect(invalid.isError).toBe(true); + }); + + it("accepts the raw-shape form without an input schema", async () => { + const client = await connect((server) => + registerAppTool( + server, + "ping", + { + outputSchema: { ok: z.boolean() }, + _meta: { ui: { resourceUri: "ui://ping/view.html" } }, + }, + async () => ({ + content: [{ type: "text", text: "pong" }], + structuredContent: { ok: true }, + }), + ), + ); + + const result = await client.callTool({ name: "ping", arguments: {} }); + expect(result.structuredContent).toEqual({ ok: true }); + }); +}); + describe("registerAppResource", () => { it("should register a resource with default MIME type", () => { let capturedName: string | undefined; diff --git a/src/server/index.ts b/src/server/index.ts index dc9a1f301..5fad055a0 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -39,31 +39,69 @@ import { } from "../spec.types.js"; import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js"; import type { + CallToolResult, ClientCapabilities, + InputRequiredResult, McpServer, ReadResourceResult, RegisteredTool, ResourceMetadata, + ServerContext, StandardSchemaWithJSON, ToolAnnotations, ToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, } from "@modelcontextprotocol/server"; +import type { z } from "zod/v4"; // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; export type { ResourceMetadata, ToolCallback }; +/** + * A plain `{ field: z.string() }` record accepted by the deprecated raw-shape + * form of {@link registerAppTool `registerAppTool`}; the SDK auto-wraps it with `z.object()`. + * + * @deprecated Wrap with `z.object({...})` instead. + */ +export type ZodRawShape = Record; + +/** + * {@link ToolCallback `ToolCallback`} variant used when `inputSchema` is a {@link ZodRawShape `ZodRawShape`}. + * Mirrors the callback type of the SDK's deprecated `registerTool` overload. + * + * @deprecated Wrap with `z.object({...})` instead and use {@link ToolCallback `ToolCallback`}. + */ +export type LegacyToolCallback = + Args extends ZodRawShape + ? ( + args: z.infer>, + ctx: ServerContext, + ) => + | CallToolResult + | InputRequiredResult + | Promise + : ( + ctx: ServerContext, + ) => + | CallToolResult + | InputRequiredResult + | Promise; + /** * Base tool configuration matching the standard MCP server tool options. * Extended by {@link McpUiAppToolConfig `McpUiAppToolConfig`} to add UI metadata requirements. + * + * `inputSchema`/`outputSchema` accept any Standard JSON Schema (zod v4, ArkType, + * Valibot, ...). A raw zod shape (`{ field: z.string() }`) is still accepted for + * backward compatibility but deprecated. */ export interface ToolConfig { title?: string; description?: string; - inputSchema?: StandardSchemaWithJSON; - outputSchema?: StandardSchemaWithJSON; + inputSchema?: ZodRawShape | StandardSchemaWithJSON; + outputSchema?: ZodRawShape | StandardSchemaWithJSON; annotations?: ToolAnnotations; _meta?: Record; } @@ -217,6 +255,31 @@ export function registerAppTool< outputSchema?: OutputArgs; }, cb: ToolCallback, +): RegisteredTool; +/** + * @deprecated Wrap with `z.object({...})` instead. Raw-shape form: + * `inputSchema`/`outputSchema` may be a plain `{ field: z.string() }` record; + * the SDK auto-wraps it with `z.object()`. + */ +export function registerAppTool< + InputArgs extends ZodRawShape, + OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = + undefined, +>( + server: Pick, + name: string, + config: McpUiAppToolConfig & { + inputSchema?: InputArgs; + outputSchema?: OutputArgs; + }, + cb: LegacyToolCallback, +): RegisteredTool; +export function registerAppTool( + server: Pick, + name: string, + config: McpUiAppToolConfig, + // Widest callback shape accepted by either overload above. + cb: (...args: never[]) => unknown, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -234,7 +297,16 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); + // The public overloads above guarantee `config`/`cb` match one of the SDK's + // own registerTool overloads; the union-typed implementation cannot select + // between them, so widen for the forwarding call. + return server.registerTool( + name, + { ...config, _meta: normalizedMeta } as Parameters< + McpServer["registerTool"] + >[1], + cb as ToolCallback, + ); } export type McpUiReadResourceResult = ReadResourceResult & { diff --git a/src/spec.types.ts b/src/spec.types.ts index 5d3fda63c..0c4539637 100644 --- a/src/spec.types.ts +++ b/src/spec.types.ts @@ -18,7 +18,7 @@ import type { RequestId, ResourceLink, Tool, -} from "./core-types"; +} from "@modelcontextprotocol/client"; /** * Current protocol version supported by this SDK. diff --git a/src/standard-schema.ts b/src/standard-schema.ts index 4bb85de64..a06864a5b 100644 --- a/src/standard-schema.ts +++ b/src/standard-schema.ts @@ -4,36 +4,22 @@ import type { StandardTypedV1, } from "@standard-schema/spec"; -export type { StandardJSONSchemaV1, StandardSchemaV1, StandardTypedV1 }; - -// TODO(sdk-v2): once @modelcontextprotocol/core v2 is stable, import -// StandardSchemaWithJSON / standardSchemaToJsonSchema / validateStandardSchema -// from there and delete this file. At that point decide whether to tighten -// App.registerTool to StandardSchemaWithJSON (drops zod 3 from the peer range -// and the lazy z.toJSONSchema fallback below). +import type { StandardSchemaWithJSON } from "@modelcontextprotocol/client"; /** * A schema that implements both Standard Schema (validation) and Standard JSON * Schema (serialization). Zod v4, ArkType, and Valibot (via - * `@valibot/to-json-schema`) all satisfy this. - * - * Mirrors the type of the same name in `@modelcontextprotocol/core` v2 so that - * bumping to that package later is a drop-in import swap. + * `@valibot/to-json-schema`) all satisfy this. Re-exported from the SDK so + * View authors can import it alongside {@link app!App `App`}. * * @see https://standardschema.dev/ - * @see https://github.com/modelcontextprotocol/typescript-sdk/pull/1689 */ -export interface StandardSchemaWithJSON { - readonly "~standard": StandardSchemaV1.Props & - StandardJSONSchemaV1.Props; -} - -export namespace StandardSchemaWithJSON { - export type InferInput = - StandardTypedV1.InferInput; - export type InferOutput = - StandardTypedV1.InferOutput; -} +export type { + StandardJSONSchemaV1, + StandardSchemaV1, + StandardSchemaWithJSON, + StandardTypedV1, +}; /** JSON-Schema target draft used for tool input/output schemas (matches core MCP). */ const TARGET = { target: "draft-2020-12" } as const; @@ -41,11 +27,8 @@ const TARGET = { target: "draft-2020-12" } as const; /** * Serialize a Standard Schema to JSON Schema for the given direction. * - * Uses `~standard.jsonSchema` when present (zod v4, ArkType, Valibot, …). - * Falls back to a lazy `zod/v4` import for zod v3.25.x — which implements - * `~standard.validate` but not yet `~standard.jsonSchema` — so the existing - * `^3.25.0 || ^4.0.0` peer range keeps working. Non-zod schemas without - * `jsonSchema` throw. + * Requires `~standard.jsonSchema` (zod v4, ArkType, Valibot, …); schemas + * without it throw. */ export async function standardSchemaToJsonSchema( schema: StandardSchemaV1, @@ -57,11 +40,6 @@ export async function standardSchemaToJsonSchema( if (std.jsonSchema) { return std.jsonSchema[io](TARGET); } - if (std.vendor === "zod") { - const { z } = await import("zod/v4"); - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridging StandardSchemaV1 → zod's $ZodType for the v3.25 fallback - return z.toJSONSchema(schema as any, { io }); - } throw new Error( `Schema (vendor: ${std.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). ` + `Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`, diff --git a/src/wire-compat.test.ts b/src/wire-compat.test.ts new file mode 100644 index 000000000..762ac77a9 --- /dev/null +++ b/src/wire-compat.test.ts @@ -0,0 +1,576 @@ +/** + * Cross-version wire compatibility. + * + * Replays JSON-RPC messages captured from ext-apps 1.7.x (built on + * `@modelcontextprotocol/sdk` 1.x) through the 2.x `App` and `AppBridge` over + * a raw in-memory transport, and asserts what 2.x emits back. The MCP Apps + * wire protocol is meant to be unchanged across the major bump; the few + * host-side error deltas are pinned here so they stay deliberate. + */ +import { describe, it, expect, afterEach } from "bun:test"; +import { + Client, + InMemoryTransport, + ProtocolError, + type JSONRPCMessage, + type Transport, +} from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { z } from "zod/v4"; + +import { App } from "./app.js"; +import { + AppBridge, + type McpUiHostCapabilities, + type McpUiHostContext, +} from "./app-bridge.js"; +import { LATEST_PROTOCOL_VERSION } from "./types.js"; + +/** Wait for pending microtasks/timers to complete. */ +const flush = () => new Promise((resolve) => setTimeout(resolve, 5)); + +/** + * One end of a raw in-memory channel. The 2.x side gets `transport`; the test + * plays the role of the 1.x peer by calling `inject()` with captured JSON and + * reading `sent` to see exactly what 2.x put on the wire. + */ +function createRawChannel() { + const sent: JSONRPCMessage[] = []; + let closed = false; + const transport: Transport = { + async start() {}, + async send(message) { + // Serialize like a real transport would so we assert on plain JSON. + sent.push(JSON.parse(JSON.stringify(message))); + }, + async close() { + if (closed) return; + closed = true; + transport.onclose?.(); + }, + }; + return { + transport, + sent, + /** Deliver a raw message (as the 1.x peer would have sent it). */ + inject(message: unknown) { + transport.onmessage?.(message as JSONRPCMessage); + }, + /** Messages sent after the given index. */ + since(index: number) { + return sent.slice(index); + }, + }; +} + +const hostInfo = { name: "TestHost", version: "9.9.9" }; +const hostCapabilities: McpUiHostCapabilities = { + openLinks: {}, + serverTools: {}, + serverResources: {}, + logging: {}, + updateModelContext: {}, +}; +const hostContext: McpUiHostContext = { + theme: "dark", + displayMode: "inline", + locale: "en-US", +}; + +/** Shaped like ext-apps 1.7.x View traffic (sdk-1 key order, 0-based numeric ids); not a recorded capture. */ +const v1View = { + initialize: { + method: "ui/initialize", + params: { + appCapabilities: { tools: { listChanged: true } }, + appInfo: { name: "TestView", version: "1.2.3" }, + protocolVersion: "2026-01-26", + }, + jsonrpc: "2.0", + id: 0, + }, + initialized: { method: "ui/notifications/initialized", jsonrpc: "2.0" }, + callTool: { + method: "tools/call", + params: { name: "echo", arguments: { a: 1 }, _meta: { progressToken: 6 } }, + jsonrpc: "2.0", + id: 6, + }, + readResource: { + method: "resources/read", + params: { uri: "test://x" }, + jsonrpc: "2.0", + id: 12, + }, + toolsListResult: (id: number) => ({ + result: { + tools: [ + { + name: "viewtool", + description: "view tool", + inputSchema: { + type: "object", + properties: { x: { type: "number" } }, + }, + }, + ], + }, + jsonrpc: "2.0", + id, + }), +}; + +/** Shaped like ext-apps 1.7.x AppBridge traffic (sdk 1.x); not a recorded capture. */ +const v1Host = { + initializeResult: (id: number) => ({ + result: { + protocolVersion: "2026-01-26", + hostCapabilities, + hostInfo, + hostContext, + }, + jsonrpc: "2.0", + id, + }), + toolInput: { + method: "ui/notifications/tool-input", + params: { arguments: { location: "NYC" } }, + jsonrpc: "2.0", + }, + callToolResult: (id: number) => ({ + result: { content: [{ type: "text", text: "echo 1" }] }, + jsonrpc: "2.0", + id, + }), + /** McpServer 1.x reports an unknown tool as an isError result, not an error. */ + callToolUnknownResult: (id: number) => ({ + result: { + content: [ + { type: "text", text: "MCP error -32602: Tool nope not found" }, + ], + isError: true, + }, + jsonrpc: "2.0", + id, + }), + resourceNotFoundError: (id: number) => ({ + jsonrpc: "2.0", + id, + error: { + code: -32002, + message: "MCP error -32002: Resource not found (host-thrown -32002)", + }, + }), + toolsList: { method: "tools/list", jsonrpc: "2.0", id: 0 }, + callViewTool: { + method: "tools/call", + params: { name: "viewtool", arguments: { x: 7 } }, + jsonrpc: "2.0", + id: 1, + }, +}; + +describe("wire compatibility: 2.x AppBridge with a 1.x View", () => { + let bridge: AppBridge; + + afterEach(async () => { + await bridge?.close().catch(() => {}); + }); + + async function connectBridge() { + const channel = createRawChannel(); + bridge = new AppBridge(null, hostInfo, hostCapabilities, { hostContext }); + await bridge.connect(channel.transport); + return channel; + } + + async function handshake() { + const channel = await connectBridge(); + let initialized = 0; + bridge.oninitialized = () => { + initialized++; + }; + channel.inject(v1View.initialize); + await flush(); + channel.inject(v1View.initialized); + await flush(); + return { channel, initialized: () => initialized }; + } + + it("sends nothing on connect: no MCP initialize toward the View", async () => { + const channel = await connectBridge(); + await flush(); + expect(channel.sent).toEqual([]); + }); + + it("answers a 1.x ui/initialize with the 1.x result shape and same id", async () => { + const channel = await connectBridge(); + channel.inject(v1View.initialize); + await flush(); + + expect(channel.sent).toEqual([ + { + jsonrpc: "2.0", + id: 0, + result: { + protocolVersion: LATEST_PROTOCOL_VERSION, + hostCapabilities, + hostInfo, + hostContext, + }, + }, + ]); + expect(bridge.getAppCapabilities()).toEqual({ + tools: { listChanged: true }, + }); + expect(bridge.getAppVersion()).toEqual({ + name: "TestView", + version: "1.2.3", + }); + }); + + it("accepts ui/notifications/initialized without params", async () => { + const { channel, initialized } = await handshake(); + expect(initialized()).toBe(1); + // Only the initialize response went out; the notification gets no reply + // and the bridge never starts an MCP handshake of its own. + expect(channel.sent).toHaveLength(1); + expect(channel.sent.some((m) => "method" in m)).toBe(false); + }); + + it("answers a 1.x tools/call request through oncalltool", async () => { + const { channel } = await handshake(); + bridge.oncalltool = async (params) => ({ + content: [{ type: "text", text: `echo ${params.arguments?.a}` }], + }); + const mark = channel.sent.length; + channel.inject(v1View.callTool); + await flush(); + + expect(channel.since(mark)).toEqual([ + { + jsonrpc: "2.0", + id: 6, + result: { content: [{ type: "text", text: "echo 1" }] }, + }, + ]); + }); + + it("re-encodes a handler-thrown -32002 as -32602 without the MCP error prefix", async () => { + // Documented host-side delta: SDK 2.x never emits -32002 on the wire. + const { channel } = await handshake(); + bridge.onreadresource = async () => { + throw new ProtocolError( + -32002, + "Resource not found (host-thrown -32002)", + ); + }; + const mark = channel.sent.length; + channel.inject(v1View.readResource); + await flush(); + + const [reply] = channel.since(mark); + expect(reply).toMatchObject({ + jsonrpc: "2.0", + id: 12, + error: { code: -32602 }, + }); + const message = (reply as { error: { message: string } }).error.message; + expect(message).toBe("Resource not found (host-thrown -32002)"); + expect(message).not.toMatch(/^MCP error/); + }); + + it("rejects invalid params on a ui/* request with -32602", async () => { + const { channel } = await handshake(); + bridge.onopenlink = async () => ({}); + const mark = channel.sent.length; + channel.inject({ + method: "ui/open-link", + params: { url: 42 }, + jsonrpc: "2.0", + id: 14, + }); + await flush(); + + expect(channel.since(mark)).toHaveLength(1); + expect(channel.since(mark)[0]).toMatchObject({ + jsonrpc: "2.0", + id: 14, + error: { code: -32602 }, + }); + }); + + it("lists and calls View tools with 1.x-shaped responses", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const listPromise = bridge.listTools({}); + await flush(); + + const [listRequest] = channel.since(mark) as Array<{ + id: number; + method: string; + }>; + expect(listRequest).toMatchObject({ jsonrpc: "2.0", method: "tools/list" }); + channel.inject(v1View.toolsListResult(listRequest.id)); + const list = await listPromise; + expect(list.tools.map((t) => t.name)).toEqual(["viewtool"]); + + const callPromise = bridge.callTool({ + name: "viewtool", + arguments: { x: 7 }, + }); + await flush(); + const [callRequest] = channel.since(mark + 1) as Array<{ id: number }>; + expect(callRequest).toMatchObject({ + jsonrpc: "2.0", + method: "tools/call", + params: { name: "viewtool", arguments: { x: 7 } }, + }); + channel.inject({ + result: { content: [{ type: "text", text: "view got 7" }] }, + jsonrpc: "2.0", + id: callRequest.id, + }); + expect(await callPromise).toEqual({ + content: [{ type: "text", text: "view got 7" }], + }); + }); + + it("only ever emits jsonrpc 2.0 messages, never an initialize request", async () => { + const { channel } = await handshake(); + bridge.oncalltool = async () => ({ content: [] }); + channel.inject(v1View.callTool); + bridge.sendToolInput({ arguments: { q: "x" } }); + bridge.sendToolResult({ content: [] }); + await flush(); + + for (const message of channel.sent) { + expect(message.jsonrpc).toBe("2.0"); + } + const methods = channel.sent + .filter((m): m is JSONRPCMessage & { method: string } => "method" in m) + .map((m) => m.method); + expect(methods).toEqual([ + "ui/notifications/tool-input", + "ui/notifications/tool-result", + ]); + expect(methods).not.toContain("initialize"); + expect(methods).not.toContain("notifications/initialized"); + }); +}); + +describe("wire compatibility: 2.x App with a 1.x host", () => { + let app: App; + + afterEach(async () => { + await app?.close().catch(() => {}); + }); + + /** Connect the App and answer its ui/initialize the way a 1.x host did. */ + async function handshake( + capabilities: ConstructorParameters[1] = {}, + setup: (app: App) => void = () => {}, + ) { + const channel = createRawChannel(); + app = new App({ name: "TestView", version: "1.2.3" }, capabilities, { + autoResize: false, + }); + setup(app); + const connected = app.connect(channel.transport); + await flush(); + + const [initialize] = channel.sent as Array<{ id: number }>; + channel.inject(v1Host.initializeResult(initialize.id)); + await connected; + await flush(); + return { channel, initialize }; + } + + it("sends a 1.x-compatible ui/initialize and ui/notifications/initialized", async () => { + const { channel } = await handshake({ tools: { listChanged: true } }); + + expect(channel.sent).toEqual([ + { + jsonrpc: "2.0", + id: 0, + method: "ui/initialize", + params: { + protocolVersion: LATEST_PROTOCOL_VERSION, + appInfo: { name: "TestView", version: "1.2.3" }, + appCapabilities: { tools: { listChanged: true } }, + }, + }, + { jsonrpc: "2.0", method: "ui/notifications/initialized" }, + ]); + expect(app.getHostContext()).toEqual(hostContext); + expect(app.getHostCapabilities()).toEqual(hostCapabilities); + expect(app.getHostVersion()).toEqual(hostInfo); + }); + + it("delivers a 1.x notification with sdk-1 key order to ontoolinput", async () => { + const received: unknown[] = []; + const { channel } = await handshake({}, (app) => { + app.ontoolinput = (params) => { + received.push(params); + }; + }); + channel.inject(v1Host.toolInput); + await flush(); + expect(received).toEqual([{ arguments: { location: "NYC" } }]); + }); + + it("resolves callServerTool from a 1.x tools/call response", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.callServerTool({ name: "echo", arguments: { a: 1 } }); + await flush(); + + const [request] = channel.since(mark) as Array<{ id: number }>; + expect(request).toMatchObject({ + jsonrpc: "2.0", + method: "tools/call", + params: { + name: "echo", + arguments: { a: 1 }, + _meta: { progressToken: request.id }, + }, + }); + channel.inject(v1Host.callToolResult(request.id)); + expect(await promise).toEqual({ + content: [{ type: "text", text: "echo 1" }], + }); + }); + + it("passes through a 1.x isError tool result unchanged", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.callServerTool({ name: "nope", arguments: {} }); + await flush(); + const [request] = channel.since(mark) as Array<{ id: number }>; + channel.inject(v1Host.callToolUnknownResult(request.id)); + expect(await promise).toEqual({ + content: [ + { type: "text", text: "MCP error -32602: Tool nope not found" }, + ], + isError: true, + }); + }); + + it("surfaces a 1.x -32002 error as a ProtocolError with code -32002", async () => { + const { channel } = await handshake(); + const mark = channel.sent.length; + const promise = app.readServerResource({ uri: "test://x" }); + await flush(); + const [request] = channel.since(mark) as Array<{ id: number }>; + channel.inject(v1Host.resourceNotFoundError(request.id)); + + const error = await promise.then( + () => undefined, + (e: unknown) => e, + ); + expect(error).toBeInstanceOf(ProtocolError); + expect((error as ProtocolError).code).toBe(-32002); + expect((error as ProtocolError).message).toContain( + "Resource not found (host-thrown -32002)", + ); + }); + + it("answers 1.x tools/list (no params) and tools/call requests from the host", async () => { + const { channel } = await handshake({ tools: { listChanged: true } }); + app.registerTool( + "viewtool", + { description: "view tool", inputSchema: z.object({ x: z.number() }) }, + async ({ x }) => ({ + content: [{ type: "text", text: `view got ${x}` }], + }), + ); + await flush(); + const mark = channel.sent.length; + + channel.inject(v1Host.toolsList); + await flush(); + const [listReply] = channel.since(mark) as unknown as Array<{ + result: { tools: Array<{ name: string; inputSchema: unknown }> }; + }>; + expect(listReply).toMatchObject({ jsonrpc: "2.0", id: 0 }); + expect(listReply.result.tools).toHaveLength(1); + expect(listReply.result.tools[0]).toMatchObject({ + name: "viewtool", + description: "view tool", + inputSchema: { + type: "object", + properties: { x: { type: "number" } }, + required: ["x"], + }, + }); + + channel.inject(v1Host.callViewTool); + await flush(); + expect(channel.since(mark + 1)).toEqual([ + { + jsonrpc: "2.0", + id: 1, + result: { content: [{ type: "text", text: "view got 7" }] }, + }, + ]); + }); +}); + +describe("AppBridge proxy over a default (modern era) 2.x Client/Server", () => { + let client: Client; + let server: McpServer; + let app: App; + let bridge: AppBridge; + + afterEach(async () => { + await app?.close().catch(() => {}); + await bridge?.close().catch(() => {}); + await client?.close().catch(() => {}); + await server?.close().catch(() => {}); + }); + + it("forwards tools/call and tools/list_changed with default negotiation", async () => { + server = new McpServer({ name: "ActualServer", version: "1.0.0" }); + server.registerTool( + "echo", + { inputSchema: z.object({ a: z.number() }) }, + async ({ a }) => ({ content: [{ type: "text", text: `echo ${a}` }] }), + ); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + + client = new Client({ name: "HostOuterClient", version: "1.0.0" }); + await client.connect(clientTransport); + expect(client.getServerCapabilities()?.tools).toEqual({ + listChanged: true, + }); + + bridge = new AppBridge(client, hostInfo, hostCapabilities, { + hostContext, + }); + app = new App( + { name: "TestView", version: "1.2.3" }, + {}, + { + autoResize: false, + }, + ); + const listChanged: string[] = []; + app.setNotificationHandler("notifications/tools/list_changed", () => { + listChanged.push("tools"); + }); + const [appTransport, bridgeTransport] = + InMemoryTransport.createLinkedPair(); + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + expect( + await app.callServerTool({ name: "echo", arguments: { a: 1 } }), + ).toMatchObject({ content: [{ type: "text", text: "echo 1" }] }); + + server.sendToolListChanged(); + await flush(); + expect(listChanged).toEqual(["tools"]); + }); +}); diff --git a/typedoc.config.mjs b/typedoc.config.mjs index 1531bdc69..c9b947602 100644 --- a/typedoc.config.mjs +++ b/typedoc.config.mjs @@ -19,6 +19,7 @@ const config = { "docs/authorization.md", "docs/csp-cors.md", "docs/migrate_from_openai_apps.md", + "docs/migrate-to-2.md", ], entryPoints: [ "src/server/index.ts", @@ -43,6 +44,7 @@ const config = { ResponseCacheStore: BASE_SDK_DOCS, SdkError: BASE_SDK_DOCS, "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS, + StandardJSONSchemaV1: BASE_SDK_DOCS, "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS, "__type.enforceStrictCapabilities": BASE_SDK_DOCS, },