diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fb41938..6848e4c 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:
@@ -132,8 +147,10 @@ jobs:
- run: npm ci
+ # Headless runs without a channel use chromium-headless-shell, so the
+ # full Chromium build (about 165 MB) would only be downloaded, never run.
- name: Install Playwright browsers
- run: npx playwright install --with-deps chromium
+ run: npx playwright install --with-deps --only-shell chromium
- name: Run E2E tests
run: npx playwright test --reporter=list
@@ -165,9 +182,14 @@ jobs:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt-get install -y nodejs
+ # The checkout lives on the Windows drive, which WSL reaches over 9P;
+ # writing node_modules there is what makes this job slow (npm ci alone
+ # took about 3 minutes). Copy the tree onto the distro's ext4 filesystem
+ # first, as Microsoft's WSL guidance recommends for anything I/O heavy.
- name: Build and test in WSL
shell: wsl-bash {0}
run: |
+ cp -r "$(pwd)" ~/ext-apps && cd ~/ext-apps
npm ci
npm run build
npm run examples:build
diff --git a/AGENTS.md b/AGENTS.md
index 6bfc8cd..219b947 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -60,8 +60,8 @@ rm -fR package-lock.json node_modules && \
### Key Source Files
-- `src/app.ts` - `App` class extends MCP Protocol, handles guest initialization, tool calls, messaging
-- `src/app-bridge.ts` - `AppBridge` class for hosts, proxies MCP requests, sends tool input/results to guests
+- `src/app.ts` - `App` subclasses `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
@@ -70,12 +70,12 @@ rm -fR package-lock.json node_modules && \
### Protocol Flow
```
-View (App) <--PostMessageTransport--> Host (AppBridge) <--MCP Client--> MCP Server
+View (App/Protocol) <--PostMessageTransport--> Host (AppBridge/Protocol) <--separate MCP Client--> MCP Server
```
1. Host creates iframe with view HTML
2. View creates `App` instance and calls `connect()` with `PostMessageTransport`
-3. View sends `ui/initialize` request, receives host capabilities and context
+3. View sends `ui/initialize` (the only handshake on the iframe channel) and receives authoritative Apps capabilities and context
4. Host sends `sendToolInput()` with tool arguments after initialization
5. View can call server tools via `app.callServerTool()` or send messages via `app.sendMessage()`
6. Host sends `sendToolResult()` when tool execution completes
diff --git a/README.md b/README.md
index 2fd89ff..ab4004d 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
Why
·
- Quickstart
+ Quickstart
·
API Docs
·
@@ -90,6 +90,7 @@ it will guide you through the rest.
+
> [!NOTE]
@@ -121,12 +122,36 @@ 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
+npm install -S @modelcontextprotocol/ext-apps \
+ @modelcontextprotocol/client@^2.0.0 \
+ zod@^4.2.0
```
+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/client@^2.0.0 \
+ @modelcontextprotocol/server@^2.0.0 \
+ @modelcontextprotocol/node@^2.0.0 \
+ @modelcontextprotocol/express@^2.0.0 \
+ zod@^4.2.0
+```
+
+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)
+[Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
to build your first MCP App.
## Using the SDK
@@ -247,7 +272,7 @@ point your client at a local build:
## Resources
-- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/Quickstart.html)
+- [Quickstart Guide](https://apps.extensions.modelcontextprotocol.io/api/documents/quickstart.html)
- [API Documentation](https://apps.extensions.modelcontextprotocol.io/api/)
- [Specification (2026-01-26)](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx)
([Draft](https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx))
diff --git a/build.bun.ts b/build.bun.ts
index a129d5d..61361e6 100644
--- a/build.bun.ts
+++ b/build.bun.ts
@@ -1,6 +1,9 @@
#!/usr/bin/env bun
import { $ } from "bun";
-import { cpSync, mkdirSync } from "node:fs";
+import { cpSync, mkdirSync, rmSync } from "node:fs";
+
+// Avoid publishing artifacts left behind by an earlier build or branch.
+rmSync("dist", { recursive: true, force: true });
// Run TypeScript compiler for type declarations
await $`tsc`;
@@ -31,9 +34,15 @@ function buildJs(
});
}
-// zod is a peerDependency — keep it external so consumers share a single
-// zod instance (instanceof ZodError / schema.extend() break with duplicate copies).
-const PEER_EXTERNALS = ["@modelcontextprotocol/sdk", "zod"];
+// Peer dependencies stay external in the standard entry points so consumers
+// share one base MCP SDK and Zod instance. The *-with-deps entry points keep
+// bundling these dependencies for standalone browser use.
+const PEER_EXTERNALS = [
+ "@modelcontextprotocol/client",
+ "@modelcontextprotocol/core",
+ "@modelcontextprotocol/server",
+ "zod",
+];
await Promise.all([
buildJs("src/app.ts", {
diff --git a/docs/authorization.md b/docs/authorization.md
index 59fc26c..df7adc6 100644
--- a/docs/authorization.md
+++ b/docs/authorization.md
@@ -150,7 +150,7 @@ registerAppTool(
"get_account_balance",
{
description: "Get account balance",
- inputSchema: { accountId: z.string() },
+ inputSchema: z.object({ accountId: z.string() }),
},
async ({ accountId }) => {
if (!authInfo) {
diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md
new file mode 100644
index 0000000..f9341d0
--- /dev/null
+++ b/docs/migrate-to-2.md
@@ -0,0 +1,130 @@
+---
+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).
+
+## What stays the same
+
+`App` / `AppBridge` constructors, the `on*` setters, `addEventListener`,
+`callServerTool`, `readServerResource`, `registerAppResource`, the React hooks
+and the `_meta.ui.resourceUri` handling are unchanged. Published `.d.ts` files
+now resolve under `moduleResolution: NodeNext` / `Node16` without workarounds
+(#705).
+
+## 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`; for HTTP transports also `@modelcontextprotocol/node@^2.0.0` (`NodeStreamableHTTPServerTransport`) and `@modelcontextprotocol/express@^2.0.0` (`createMcpExpressApp`, which peers on `express`) |
+| 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 share no classes or types (a 1.x `McpError` or
+ `Client` cannot be passed to 2.x code), even though they speak the same
+ wire protocol.
+- **zod 3 dropped.** The peer range is `zod@^4.2.0`. Tool schemas must
+ implement Standard JSON Schema (`~standard.jsonSchema`): zod 4.2+, ArkType,
+ Valibot. zod 4.0 and 4.1 do not expose `~standard.jsonSchema` and are
+ rejected too. 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` is gone; use the SDK's `Protocol`. The `AppRequest`,
+ `AppNotification` and `AppResult` unions are kept as deprecated type
+ aliases (nothing in 2.x consumes them) and will be removed in 3.0.
+- **Handler context.** Custom request handlers, `registerAppTool` callbacks
+ (server) and `app.registerTool` callbacks (View) receive the SDK 2.x
+ context: `extra.signal` → `extra.mcpReq.signal`, `extra.requestId` →
+ `extra.mcpReq.id`; `extra.sessionId` is unchanged and `extra.authInfo`
+ moved to `extra.http?.authInfo`.
+- **`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. The 1.x `(Schema, handler)` form still works on `App` and
+ `AppBridge` as a deprecated overload: it logs a one-time warning, hands the
+ handler the whole `{ method, params }` message as before, and gives request
+ handlers a 1.x-shaped `extra` (`signal`, `requestId`, `sessionId`, `_meta`,
+ `sendRequest`, `sendNotification`, `authInfo`; exported as
+ `LegacyRequestHandlerExtra`). It will be removed in 3.0.
+- **Double-registration guard covers the SDK's own handlers.** Registering a
+ handler for `notifications/progress` or `notifications/cancelled` (which
+ the base `Protocol` already owns) now throws `already registered`, as
+ `ping` and the `on*`-owned methods did in 1.x. Use `addEventListener` or
+ the SDK's progress callbacks instead of replacing these.
+- **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.
+ A resource-not-found reply that carries `data.uri` arrives as
+ `ResourceNotFoundError` (a `ProtocolError` subclass, `code` `-32602`, with a
+ `.uri` getter).
+
+## 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`, hand-written
+ Express routing → `createMcpExpressApp` from `@modelcontextprotocol/express`,
+ `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/migrate_from_openai_apps.md b/docs/migrate_from_openai_apps.md
index d91281b..22d5d9f 100644
--- a/docs/migrate_from_openai_apps.md
+++ b/docs/migrate_from_openai_apps.md
@@ -66,7 +66,7 @@ The server-side changes involve updating metadata structure and using helper fun
#### Before (OpenAI)
```typescript
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
import { z } from "zod";
function createServer() {
@@ -78,7 +78,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
- inputSchema: { userId: z.string() },
+ inputSchema: z.object({ userId: z.string() }),
annotations: { readOnlyHint: true },
_meta: {
"openai/outputTemplate": "ui://view/cart.html",
@@ -126,7 +126,7 @@ function createServer() {
#### After (MCP Apps)
```typescript
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
import {
registerAppTool,
registerAppResource,
@@ -144,7 +144,7 @@ function createServer() {
{
title: "Shopping Cart",
description: "Display the user's shopping cart",
- inputSchema: { userId: z.string() },
+ inputSchema: z.object({ userId: z.string() }),
annotations: { readOnlyHint: true },
_meta: { ui: { resourceUri: "ui://view/cart.html" } },
},
diff --git a/docs/overview.md b/docs/overview.md
index 28f5b23..8eda95d 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -56,7 +56,11 @@ flowchart LR
- **Host** — The chat client (e.g., Claude Desktop) that connects to servers, embeds Views in iframes, and proxies communication between them.
- **View** — The UI running inside a sandboxed iframe. It receives tool data from the Host and can call server tools or send messages back to the chat.
-The View acts as an MCP client, the Host acts as a proxy, and the Server is a standard MCP server.
+The View's `App` 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.
## Lifecycle
@@ -93,7 +97,7 @@ sequenceDiagram
```
1. **Discovery** — The Host learns about tools and their UI resources when connecting to the server.
-2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` and receives host context (theme, capabilities, container dimensions). This handshake ensures the View is ready before receiving data.
+2. **Initialization** — When a UI tool is called, the Host renders the iframe. The View sends `ui/initialize` (the only handshake on the iframe channel) and receives host context (theme, capabilities, container dimensions). `ui/initialize` is authoritative for Apps capabilities and identity, and `ui/notifications/initialized` is the public View-ready signal.
3. **Data delivery** — The Host sends tool arguments and, once available, tool results to the View. Results include both `content` (text for the model's context) and optionally `structuredContent` (data optimized for UI rendering). This separation lets servers provide rich data to the UI without bloating the model's context.
4. **Interactive phase** — The user interacts with the View. The View can call tools, send messages, or update context.
5. **Teardown** — Before unmounting, the Host notifies the View so it can save state or release resources.
diff --git a/docs/patterns.md b/docs/patterns.md
index 87a6d82..884de26 100644
--- a/docs/patterns.md
+++ b/docs/patterns.md
@@ -19,7 +19,7 @@ registerAppTool(
"update-quantity",
{
description: "Update item quantity in cart",
- inputSchema: { itemId: z.string(), quantity: z.number() },
+ inputSchema: z.object({ itemId: z.string(), quantity: z.number() }),
_meta: {
ui: {
resourceUri: "ui://shop/cart.html",
@@ -127,14 +127,14 @@ registerAppTool(
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
- inputSchema: {
+ inputSchema: z.object({
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- },
+ }),
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
@@ -281,10 +281,7 @@ server.registerResource(
```ts source="./patterns.tsx#binaryBlobResourceClient"
-const result = await app.request(
- { method: "resources/read", params: { uri: `video://${videoId}` } },
- ReadResourceResultSchema,
-);
+const result = await app.readServerResource({ uri: `video://${videoId}` });
const content = result.contents[0];
if (!content || !("blob" in content)) {
diff --git a/docs/patterns.tsx b/docs/patterns.tsx
index 6b87692..bb9eaa4 100644
--- a/docs/patterns.tsx
+++ b/docs/patterns.tsx
@@ -17,16 +17,12 @@ import { randomUUID } from "node:crypto";
import type {
CallToolResult,
ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
-import { ReadResourceResultSchema } from "@modelcontextprotocol/sdk/types.js";
+} from "@modelcontextprotocol/server";
import type { McpUiHostContext } from "../src/types.js";
import { useEffect, useState } from "react";
import { useApp } from "../src/react/index.js";
import { registerAppTool } from "../src/server/index.js";
-import {
- McpServer,
- ResourceTemplate,
-} from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer, ResourceTemplate } from "@modelcontextprotocol/server";
import { z } from "zod";
/**
@@ -117,14 +113,14 @@ function chunkedDataServer(server: McpServer) {
{
title: "Read Data Bytes",
description: "Load binary data in chunks",
- inputSchema: {
+ inputSchema: z.object({
id: z.string().describe("Resource identifier"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
.number()
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- },
+ }),
outputSchema: DataChunkSchema,
// Hidden from model - only callable by the App
_meta: { ui: { visibility: ["app"] } },
@@ -252,10 +248,7 @@ function binaryBlobResourceServer(
*/
async function binaryBlobResourceClient(app: App, videoId: string) {
//#region binaryBlobResourceClient
- const result = await app.request(
- { method: "resources/read", params: { uri: `video://${videoId}` } },
- ReadResourceResultSchema,
- );
+ const result = await app.readServerResource({ uri: `video://${videoId}` });
const content = result.contents[0];
if (!content || !("blob" in content)) {
diff --git a/docs/quickstart.md b/docs/quickstart.md
index 3e00796..a021912 100644
--- a/docs/quickstart.md
+++ b/docs/quickstart.md
@@ -21,7 +21,7 @@ This tutorial assumes you've built an MCP server before and are comfortable with
We'll use the [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) to build the server.
-You'll also need Node.js 18+.
+You'll also need Node.js 20+.
## 1. Set up the project
@@ -37,7 +37,7 @@ Install the dependencies you'll need:
```bash
npm init -y
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk express cors
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @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
```
@@ -59,6 +59,7 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.json"
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
@@ -87,6 +88,7 @@ npm pkg set scripts.start='concurrently --raw "cross-env NODE_ENV=development IN
```json source="../examples/quickstart/tsconfig.server.json"
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
@@ -178,9 +180,10 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
+import { z } from "zod";
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -205,7 +208,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
@@ -240,10 +243,10 @@ export function createServer(): McpServer {
```ts source="../examples/quickstart/main.ts"
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -263,7 +266,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/docs/testing-mcp-apps.md b/docs/testing-mcp-apps.md
index df1396c..a8eb333 100644
--- a/docs/testing-mcp-apps.md
+++ b/docs/testing-mcp-apps.md
@@ -28,6 +28,10 @@ The [`basic-host`](https://github.com/modelcontextprotocol/ext-apps/tree/main/ex
cd examples/basic-host
```
+ The root install runs the package build, including type-checking the
+ documentation snippets against the 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:
```bash
diff --git a/examples/basic-host/package.json b/examples/basic-host/package.json
index f54b237..cefe9d5 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/sdk": "^1.29.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-host/src/implementation.ts b/examples/basic-host/src/implementation.ts
index 31e3698..5f680ff 100644
--- a/examples/basic-host/src/implementation.ts
+++ b/examples/basic-host/src/implementation.ts
@@ -1,8 +1,12 @@
import { RESOURCE_MIME_TYPE, getToolUiResourceUri, type McpUiSandboxProxyReadyNotification, AppBridge, PostMessageTransport, type McpUiResourceCsp, type McpUiResourcePermissions, buildAllowAttribute, type McpUiUpdateModelContextRequest, type McpUiMessageRequest } from "@modelcontextprotocol/ext-apps/app-bridge";
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
-import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
-import type { CallToolResult, Resource, Tool } from "@modelcontextprotocol/sdk/types.js";
+import {
+ Client,
+ SSEClientTransport,
+ StreamableHTTPClientTransport,
+ type CallToolResult,
+ type Resource,
+ type Tool,
+} from "@modelcontextprotocol/client";
import { getTheme, onThemeChange } from "./theme";
import { HOST_STYLE_VARIABLES } from "./host-styles";
diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx
index 3d488a7..00c0d52 100644
--- a/examples/basic-host/src/index.tsx
+++ b/examples/basic-host/src/index.tsx
@@ -1,5 +1,5 @@
import { getToolUiResourceUri, McpUiToolMetaSchema } from "@modelcontextprotocol/ext-apps/app-bridge";
-import type { Tool } from "@modelcontextprotocol/sdk/types.js";
+import type { Tool } from "@modelcontextprotocol/client";
import { Component, type ErrorInfo, type ReactNode, StrictMode, Suspense, use, useEffect, useMemo, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { callTool, connectToServer, hasAppHtml, initializeApp, loadSandboxProxy, log, newAppBridge, type ServerInfo, type ToolCallInfo, type ModelContext, type AppMessage } from "./implementation";
diff --git a/examples/basic-host/tsconfig.json b/examples/basic-host/tsconfig.json
index 9d7c3bc..3c143ca 100644
--- a/examples/basic-host/tsconfig.json
+++ b/examples/basic-host/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/main.ts b/examples/basic-server-preact/main.ts
index 7642632..ffdff52 100644
--- a/examples/basic-server-preact/main.ts
+++ b/examples/basic-server-preact/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-preact/package.json b/examples/basic-server-preact/package.json
index 81ed0de..60aa40d 100644
--- a/examples/basic-server-preact/package.json
+++ b/examples/basic-server-preact/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Basic MCP App Server example using Preact",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-preact"
},
"license": "MIT",
@@ -24,12 +24,15 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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-preact/server.ts b/examples/basic-server-preact/server.ts
index bf14ea5..dfec94a 100644
--- a/examples/basic-server-preact/server.ts
+++ b/examples/basic-server-preact/server.ts
@@ -1,8 +1,12 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -28,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-preact/src/mcp-app.tsx b/examples/basic-server-preact/src/mcp-app.tsx
index a8bdc68..351a8ec 100644
--- a/examples/basic-server-preact/src/mcp-app.tsx
+++ b/examples/basic-server-preact/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { useCallback, useEffect, useState } from "preact/hooks";
import { render } from "preact";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-preact/tsconfig.json b/examples/basic-server-preact/tsconfig.json
index 32182d9..9bf9389 100644
--- a/examples/basic-server-preact/tsconfig.json
+++ b/examples/basic-server-preact/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-preact/tsconfig.server.json b/examples/basic-server-preact/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-preact/tsconfig.server.json
+++ b/examples/basic-server-preact/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-react/main.ts b/examples/basic-server-react/main.ts
index ec187b6..21c5e18 100644
--- a/examples/basic-server-react/main.ts
+++ b/examples/basic-server-react/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-react/package.json b/examples/basic-server-react/package.json
index 5ae783e..ea51a8a 100644
--- a/examples/basic-server-react/package.json
+++ b/examples/basic-server-react/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Basic MCP App Server example using React",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-react"
},
"license": "MIT",
@@ -34,13 +34,16 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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-react/server.ts b/examples/basic-server-react/server.ts
index 23f6dda..d77c908 100644
--- a/examples/basic-server-react/server.ts
+++ b/examples/basic-server-react/server.ts
@@ -1,8 +1,12 @@
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
@@ -29,7 +33,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-react/src/mcp-app.tsx b/examples/basic-server-react/src/mcp-app.tsx
index 6bb5540..2b5fbef 100644
--- a/examples/basic-server-react/src/mcp-app.tsx
+++ b/examples/basic-server-react/src/mcp-app.tsx
@@ -3,7 +3,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-react/tsconfig.json b/examples/basic-server-react/tsconfig.json
index fc3c210..3ecbdad 100644
--- a/examples/basic-server-react/tsconfig.json
+++ b/examples/basic-server-react/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-react/tsconfig.server.json b/examples/basic-server-react/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-react/tsconfig.server.json
+++ b/examples/basic-server-react/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-solid/main.ts b/examples/basic-server-solid/main.ts
index c8d9de2..095ef8d 100644
--- a/examples/basic-server-solid/main.ts
+++ b/examples/basic-server-solid/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-solid/package.json b/examples/basic-server-solid/package.json
index c2950d9..b3ca909 100644
--- a/examples/basic-server-solid/package.json
+++ b/examples/basic-server-solid/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Basic MCP App Server example using Solid",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-solid"
},
"license": "MIT",
@@ -24,12 +24,15 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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-solid/server.ts b/examples/basic-server-solid/server.ts
index 2ed2d93..addb522 100644
--- a/examples/basic-server-solid/server.ts
+++ b/examples/basic-server-solid/server.ts
@@ -1,8 +1,12 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -28,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-solid/src/mcp-app.tsx b/examples/basic-server-solid/src/mcp-app.tsx
index 66d47f3..2d78b7b 100644
--- a/examples/basic-server-solid/src/mcp-app.tsx
+++ b/examples/basic-server-solid/src/mcp-app.tsx
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { createEffect, createSignal, onMount, Show } from "solid-js";
import { render } from "solid-js/web";
import styles from "./mcp-app.module.css";
diff --git a/examples/basic-server-solid/tsconfig.json b/examples/basic-server-solid/tsconfig.json
index 84f0efe..bb13a01 100644
--- a/examples/basic-server-solid/tsconfig.json
+++ b/examples/basic-server-solid/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-solid/tsconfig.server.json b/examples/basic-server-solid/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-solid/tsconfig.server.json
+++ b/examples/basic-server-solid/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-svelte/main.ts b/examples/basic-server-svelte/main.ts
index 6c50a25..734ec0c 100644
--- a/examples/basic-server-svelte/main.ts
+++ b/examples/basic-server-svelte/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-svelte/package.json b/examples/basic-server-svelte/package.json
index 8238feb..19fe4c9 100644
--- a/examples/basic-server-svelte/package.json
+++ b/examples/basic-server-svelte/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Basic MCP App Server example using Svelte",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-svelte"
},
"license": "MIT",
@@ -24,12 +24,15 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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-svelte/server.ts b/examples/basic-server-svelte/server.ts
index da603ca..9414285 100644
--- a/examples/basic-server-svelte/server.ts
+++ b/examples/basic-server-svelte/server.ts
@@ -1,8 +1,12 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -28,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-svelte/src/App.svelte b/examples/basic-server-svelte/src/App.svelte
index fde128d..cabcc31 100644
--- a/examples/basic-server-svelte/src/App.svelte
+++ b/examples/basic-server-svelte/src/App.svelte
@@ -7,7 +7,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
diff --git a/examples/basic-server-svelte/tsconfig.json b/examples/basic-server-svelte/tsconfig.json
index 5edb66a..4fd3cd7 100644
--- a/examples/basic-server-svelte/tsconfig.json
+++ b/examples/basic-server-svelte/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-svelte/tsconfig.server.json b/examples/basic-server-svelte/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-svelte/tsconfig.server.json
+++ b/examples/basic-server-svelte/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-vanillajs/main.ts b/examples/basic-server-vanillajs/main.ts
index 286fa34..4de7331 100644
--- a/examples/basic-server-vanillajs/main.ts
+++ b/examples/basic-server-vanillajs/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-vanillajs/package.json b/examples/basic-server-vanillajs/package.json
index 8ca330e..ebfebbb 100644
--- a/examples/basic-server-vanillajs/package.json
+++ b/examples/basic-server-vanillajs/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-vanillajs"
},
"license": "MIT",
@@ -24,11 +24,14 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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-vanillajs/server.ts b/examples/basic-server-vanillajs/server.ts
index 5b2daf7..693d861 100644
--- a/examples/basic-server-vanillajs/server.ts
+++ b/examples/basic-server-vanillajs/server.ts
@@ -1,6 +1,9 @@
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -29,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
outputSchema: z.object({
time: z.string(),
}),
diff --git a/examples/basic-server-vanillajs/src/mcp-app.ts b/examples/basic-server-vanillajs/src/mcp-app.ts
index c3168c2..6b5084a 100644
--- a/examples/basic-server-vanillajs/src/mcp-app.ts
+++ b/examples/basic-server-vanillajs/src/mcp-app.ts
@@ -8,7 +8,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import "./global.css";
import "./mcp-app.css";
diff --git a/examples/basic-server-vanillajs/tsconfig.json b/examples/basic-server-vanillajs/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/basic-server-vanillajs/tsconfig.json
+++ b/examples/basic-server-vanillajs/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-vanillajs/tsconfig.server.json b/examples/basic-server-vanillajs/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-vanillajs/tsconfig.server.json
+++ b/examples/basic-server-vanillajs/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/basic-server-vue/main.ts b/examples/basic-server-vue/main.ts
index 669a071..d774269 100644
--- a/examples/basic-server-vue/main.ts
+++ b/examples/basic-server-vue/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/basic-server-vue/package.json b/examples/basic-server-vue/package.json
index 9c0eb7b..858dd89 100644
--- a/examples/basic-server-vue/package.json
+++ b/examples/basic-server-vue/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Basic MCP App Server example using Vue",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/basic-server-vue"
},
"license": "MIT",
@@ -24,12 +24,15 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/basic-server-vue/server.ts b/examples/basic-server-vue/server.ts
index 5aa72de..9adb911 100644
--- a/examples/basic-server-vue/server.ts
+++ b/examples/basic-server-vue/server.ts
@@ -1,8 +1,12 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { CallToolResult, ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -28,7 +32,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time as an ISO 8601 string.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async (): Promise => {
diff --git a/examples/basic-server-vue/src/App.vue b/examples/basic-server-vue/src/App.vue
index 196ad64..a45c262 100644
--- a/examples/basic-server-vue/src/App.vue
+++ b/examples/basic-server-vue/src/App.vue
@@ -7,7 +7,7 @@ import {
applyHostStyleVariables,
type McpUiHostContext,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
function extractTime(result: CallToolResult): string {
const { text } = result.content?.find((c) => c.type === "text")!;
diff --git a/examples/basic-server-vue/tsconfig.json b/examples/basic-server-vue/tsconfig.json
index 5edb66a..4fd3cd7 100644
--- a/examples/basic-server-vue/tsconfig.json
+++ b/examples/basic-server-vue/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/basic-server-vue/tsconfig.server.json b/examples/basic-server-vue/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/basic-server-vue/tsconfig.server.json
+++ b/examples/basic-server-vue/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/budget-allocator-server/main.ts b/examples/budget-allocator-server/main.ts
index cc488f0..02c03cb 100644
--- a/examples/budget-allocator-server/main.ts
+++ b/examples/budget-allocator-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/budget-allocator-server/package.json b/examples/budget-allocator-server/package.json
index 0cd94d0..6e41622 100644
--- a/examples/budget-allocator-server/package.json
+++ b/examples/budget-allocator-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/budget-allocator-server"
},
"license": "MIT",
@@ -26,12 +26,14 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/budget-allocator-server/server.ts b/examples/budget-allocator-server/server.ts
index 04fba11..01f5dbf 100755
--- a/examples/budget-allocator-server/server.ts
+++ b/examples/budget-allocator-server/server.ts
@@ -4,11 +4,11 @@
* Provides budget configuration, 24 months of historical allocation data,
* and industry benchmarks by company stage.
*/
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -270,7 +270,7 @@ export function createServer(): McpServer {
title: "Get Budget Data",
description:
"Returns budget configuration with 24 months of historical allocations and industry benchmarks by company stage. The widget is interactive and exposes tools for reading/modifying allocations, adjusting budgets, and comparing against industry benchmarks.",
- inputSchema: {},
+ inputSchema: z.object({}),
outputSchema: BudgetDataResponseSchema,
_meta: { ui: { resourceUri } },
},
diff --git a/examples/budget-allocator-server/tsconfig.json b/examples/budget-allocator-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/budget-allocator-server/tsconfig.json
+++ b/examples/budget-allocator-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/budget-allocator-server/tsconfig.server.json b/examples/budget-allocator-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/budget-allocator-server/tsconfig.server.json
+++ b/examples/budget-allocator-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/cohort-heatmap-server/main.ts b/examples/cohort-heatmap-server/main.ts
index d59903b..8b33603 100644
--- a/examples/cohort-heatmap-server/main.ts
+++ b/examples/cohort-heatmap-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/cohort-heatmap-server/package.json b/examples/cohort-heatmap-server/package.json
index 9988679..7e2d2e2 100644
--- a/examples/cohort-heatmap-server/package.json
+++ b/examples/cohort-heatmap-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/cohort-heatmap-server"
},
"license": "MIT",
@@ -26,13 +26,15 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/cohort-heatmap-server/server.ts b/examples/cohort-heatmap-server/server.ts
index 6d07144..129e2dc 100644
--- a/examples/cohort-heatmap-server/server.ts
+++ b/examples/cohort-heatmap-server/server.ts
@@ -1,5 +1,7 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -178,8 +180,8 @@ export function createServer(): McpServer {
title: "Get Cohort Retention Data",
description:
"Returns cohort retention heatmap data showing customer retention over time by signup month",
- inputSchema: GetCohortDataInputSchema.shape,
- outputSchema: CohortDataSchema.shape,
+ inputSchema: GetCohortDataInputSchema,
+ outputSchema: CohortDataSchema,
_meta: { ui: { resourceUri } },
},
async ({ metric, periodType, cohortCount, maxPeriods }) => {
diff --git a/examples/cohort-heatmap-server/tsconfig.json b/examples/cohort-heatmap-server/tsconfig.json
index fc3c210..3ecbdad 100644
--- a/examples/cohort-heatmap-server/tsconfig.json
+++ b/examples/cohort-heatmap-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/cohort-heatmap-server/tsconfig.server.json b/examples/cohort-heatmap-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/cohort-heatmap-server/tsconfig.server.json
+++ b/examples/cohort-heatmap-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/customer-segmentation-server/main.ts b/examples/customer-segmentation-server/main.ts
index 130076c..c788757 100644
--- a/examples/customer-segmentation-server/main.ts
+++ b/examples/customer-segmentation-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/customer-segmentation-server/package.json b/examples/customer-segmentation-server/package.json
index 2fc85f9..bac3b89 100644
--- a/examples/customer-segmentation-server/package.json
+++ b/examples/customer-segmentation-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Customer segmentation MCP App Server with filtering",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/customer-segmentation-server"
},
"license": "MIT",
@@ -26,12 +26,14 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/customer-segmentation-server/server.ts b/examples/customer-segmentation-server/server.ts
index 244415d..7cc1c1f 100644
--- a/examples/customer-segmentation-server/server.ts
+++ b/examples/customer-segmentation-server/server.ts
@@ -1,8 +1,8 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -100,8 +100,8 @@ export function createServer(): McpServer {
title: "Get Customer Data",
description:
"Returns customer data with segment information for visualization. Optionally filter by segment.",
- inputSchema: GetCustomerDataInputSchema.shape,
- outputSchema: GetCustomerDataOutputSchema.shape,
+ inputSchema: GetCustomerDataInputSchema,
+ outputSchema: GetCustomerDataOutputSchema,
_meta: { ui: { resourceUri } },
},
async ({ segment }): Promise => {
diff --git a/examples/customer-segmentation-server/tsconfig.json b/examples/customer-segmentation-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/customer-segmentation-server/tsconfig.json
+++ b/examples/customer-segmentation-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/customer-segmentation-server/tsconfig.server.json b/examples/customer-segmentation-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/customer-segmentation-server/tsconfig.server.json
+++ b/examples/customer-segmentation-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/debug-server/main.ts b/examples/debug-server/main.ts
index 3c89a97..9fb0825 100644
--- a/examples/debug-server/main.ts
+++ b/examples/debug-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/debug-server/package.json b/examples/debug-server/package.json
index 16d2f4c..e28f4d3 100644
--- a/examples/debug-server/package.json
+++ b/examples/debug-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/debug-server"
},
"license": "MIT",
@@ -34,9 +34,11 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
- "zod": "^4.1.13"
+ "@modelcontextprotocol/express": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^2.0.0",
+ "@modelcontextprotocol/node": "2.0.0",
+ "@modelcontextprotocol/server": "2.0.0",
+ "zod": "^4.2.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/debug-server/server.ts b/examples/debug-server/server.ts
index fd3452a..068f94d 100644
--- a/examples/debug-server/server.ts
+++ b/examples/debug-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import { appendFileSync } from "node:fs";
import path from "node:path";
diff --git a/examples/debug-server/tsconfig.json b/examples/debug-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/debug-server/tsconfig.json
+++ b/examples/debug-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/debug-server/tsconfig.server.json b/examples/debug-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/debug-server/tsconfig.server.json
+++ b/examples/debug-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/integration-server/main.ts b/examples/integration-server/main.ts
index c45787e..9a42842 100644
--- a/examples/integration-server/main.ts
+++ b/examples/integration-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/integration-server/package.json b/examples/integration-server/package.json
index a8b5421..7e04f4a 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": {
@@ -15,13 +15,16 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/integration-server/server.ts b/examples/integration-server/server.ts
index e44689d..346f4f7 100644
--- a/examples/integration-server/server.ts
+++ b/examples/integration-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -33,7 +33,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: {},
+ inputSchema: z.object({}),
outputSchema: z.object({
time: z.string(),
}),
@@ -72,7 +72,7 @@ export function createServer(): McpServer {
);
// Sample downloadable resource — used to demo ResourceLink in ui/download-file
- server.resource(
+ server.registerResource(
SAMPLE_DOWNLOAD_URI,
SAMPLE_DOWNLOAD_URI,
{
diff --git a/examples/integration-server/src/mcp-app.tsx b/examples/integration-server/src/mcp-app.tsx
index 7f0b18c..281a356 100644
--- a/examples/integration-server/src/mcp-app.tsx
+++ b/examples/integration-server/src/mcp-app.tsx
@@ -3,7 +3,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { StrictMode, useCallback, useEffect, useState } from "react";
import { createRoot } from "react-dom/client";
import styles from "./mcp-app.module.css";
diff --git a/examples/integration-server/tsconfig.json b/examples/integration-server/tsconfig.json
index fc3c210..3ecbdad 100644
--- a/examples/integration-server/tsconfig.json
+++ b/examples/integration-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/integration-server/tsconfig.server.json b/examples/integration-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/integration-server/tsconfig.server.json
+++ b/examples/integration-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/lazy-auth-server/package.json b/examples/lazy-auth-server/package.json
index 8e96906..dbd4601 100644
--- a/examples/lazy-auth-server/package.json
+++ b/examples/lazy-auth-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/lazy-auth-server"
},
"license": "MIT",
@@ -22,11 +22,13 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/lazy-auth-server/server.ts b/examples/lazy-auth-server/server.ts
index 6670b3e..5240008 100644
--- a/examples/lazy-auth-server/server.ts
+++ b/examples/lazy-auth-server/server.ts
@@ -21,13 +21,13 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
-import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import {
+ McpServer,
+ UrlElicitationRequiredError,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import cors from "cors";
import express, { type Express, type Request, type Response } from "express";
import { SignJWT, jwtVerify } from "jose";
@@ -35,6 +35,7 @@ import crypto from "node:crypto";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js). Derived
// from import.meta.url rather than import.meta.filename/dirname, which are
@@ -621,7 +622,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Show Auth Button",
description:
"Public tool: shows a button that triggers the protected get_secret tool.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri: buttonUri } },
},
async (): Promise => ({
@@ -654,7 +655,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Get Secret",
description:
"Protected tool: returns secret data. Requires authentication.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri: secretUri } },
},
async (): Promise => {
@@ -698,7 +699,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Revoke Auth Token",
description:
"Protected tool: revokes the caller's entire auth session (access + refresh token).",
- inputSchema: {},
+ inputSchema: z.object({}),
},
async (): Promise => {
if (!authInfo) {
@@ -736,7 +737,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Elicit URL (session-callback)",
description:
"URL elicitation via elicitInput. Blocks until the client accepts; sends ElicitCompleteNotification before returning.",
- inputSchema: {},
+ inputSchema: z.object({}),
},
async (_args, extra): Promise => {
const eid = crypto.randomUUID();
@@ -751,7 +752,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
message: "Please open this URL to continue.",
elicitationId: eid,
},
- { relatedRequestId: extra.requestId, timeout: 300_000 },
+ { relatedRequestId: extra.mcpReq.id, timeout: 300_000 },
);
// Send completion notification before returning — lands on the same SSE
// stream as this tool response. For Streamable HTTP this is the ONLY
@@ -783,7 +784,7 @@ export function createServer(authInfo?: AuthInfo, req?: Request): McpServer {
title: "Elicit by -32042 Error",
description:
"Throws UrlElicitationRequiredError (-32042). Client must open the URL, then retry. On retry, if the last-issued elicitationId was completed, sends ElicitCompleteNotification and succeeds.",
- inputSchema: {},
+ inputSchema: z.object({}),
},
async (): Promise => {
// Retry path: check if the user completed the elicitation we last issued.
@@ -1047,7 +1048,7 @@ export function createApp(): Express {
}
const server = createServer(authInfo, req);
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
diff --git a/examples/lazy-auth-server/tsconfig.json b/examples/lazy-auth-server/tsconfig.json
index 6c553b5..d6a42ce 100644
--- a/examples/lazy-auth-server/tsconfig.json
+++ b/examples/lazy-auth-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/lazy-auth-server/tsconfig.server.json b/examples/lazy-auth-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/lazy-auth-server/tsconfig.server.json
+++ b/examples/lazy-auth-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/map-server/main.ts b/examples/map-server/main.ts
index f91f57b..8038394 100644
--- a/examples/map-server/main.ts
+++ b/examples/map-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/map-server/package.json b/examples/map-server/package.json
index 5de1754..cc4585f 100644
--- a/examples/map-server/package.json
+++ b/examples/map-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/map-server"
},
"license": "MIT",
@@ -26,11 +26,14 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/map-server/server.ts b/examples/map-server/server.ts
index 10125a6..5767d46 100644
--- a/examples/map-server/server.ts
+++ b/examples/map-server/server.ts
@@ -5,11 +5,11 @@
* - geocode: Search for places using OpenStreetMap Nominatim
* - show-map: Display an interactive 3D globe at a given location
*/
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -148,7 +148,7 @@ export function createServer(): McpServer {
title: "Show Map",
description:
"Display an interactive world map zoomed to a specific bounding box. Use the GeoCode tool to find the bounding box of a location. The widget is interactive and exposes tools for navigation (fly to locations) and querying the current view.",
- inputSchema: {
+ inputSchema: z.object({
west: z
.number()
.optional()
@@ -173,7 +173,7 @@ export function createServer(): McpServer {
.string()
.optional()
.describe("Optional label to display on the map"),
- },
+ }),
_meta: { ui: { resourceUri } },
},
async ({ west, south, east, north, label }): Promise => ({
@@ -196,13 +196,13 @@ export function createServer(): McpServer {
title: "Geocode",
description:
"Search for places using OpenStreetMap. Returns coordinates and bounding boxes for up to 5 matches.",
- inputSchema: {
+ inputSchema: z.object({
query: z
.string()
.describe(
"Place name or address to search for (e.g., 'Paris', 'Golden Gate Bridge', '1600 Pennsylvania Ave')",
),
- },
+ }),
},
async ({ query }): Promise => {
try {
diff --git a/examples/map-server/src/mcp-app.ts b/examples/map-server/src/mcp-app.ts
index 39d7c2a..d3090c7 100644
--- a/examples/map-server/src/mcp-app.ts
+++ b/examples/map-server/src/mcp-app.ts
@@ -6,7 +6,7 @@
* a navigate-to tool for the host to control navigation.
*/
import { App } from "@modelcontextprotocol/ext-apps";
-import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js";
+import type { ContentBlock } from "@modelcontextprotocol/client";
import { z } from "zod";
// TypeScript declaration for Cesium loaded from CDN
diff --git a/examples/map-server/tsconfig.json b/examples/map-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/map-server/tsconfig.json
+++ b/examples/map-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/map-server/tsconfig.server.json b/examples/map-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/map-server/tsconfig.server.json
+++ b/examples/map-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/pdf-server/build-mcpb.mjs b/examples/pdf-server/build-mcpb.mjs
index c1c3469..2917334 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/main.ts b/examples/pdf-server/main.ts
index 1ff848a..bb7b57f 100644
--- a/examples/pdf-server/main.ts
+++ b/examples/pdf-server/main.ts
@@ -6,10 +6,10 @@
import fs from "node:fs";
import path from "node:path";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import {
@@ -39,7 +39,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/pdf-server/package.json b/examples/pdf-server/package.json
index 8942a90..c8d9cf8 100644
--- a/examples/pdf-server/package.json
+++ b/examples/pdf-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/pdf-server"
},
"license": "MIT",
@@ -26,12 +26,15 @@
},
"dependencies": {
"@cantoo/pdf-lib": "^2.6.5",
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/pdf-server/server.test.ts b/examples/pdf-server/server.test.ts
index 33a03f1..0b2a5eb 100644
--- a/examples/pdf-server/server.test.ts
+++ b/examples/pdf-server/server.test.ts
@@ -2,8 +2,7 @@ import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
+import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { getDocument } from "pdfjs-dist/legacy/build/pdf.mjs";
import { PDFDocument } from "@cantoo/pdf-lib";
import { makeRandomJpeg } from "../../tests/helpers/range-counting-server";
diff --git a/examples/pdf-server/server.ts b/examples/pdf-server/server.ts
index 93dedfd..e67cd47 100644
--- a/examples/pdf-server/server.ts
+++ b/examples/pdf-server/server.ts
@@ -13,18 +13,19 @@
import { randomUUID } from "crypto";
import fs from "node:fs";
import path from "node:path";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
registerAppResource,
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
import {
- RootsListChangedNotificationSchema,
+ McpServer,
type CallToolResult,
+ type ElicitResult,
+ type PrimitiveSchemaDefinition,
type ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+ type Server,
+} from "@modelcontextprotocol/server";
// Stub DOMMatrix/ImageData/Path2D before pdfjs-dist loads — its legacy
// build instantiates DOMMatrix at module scope and the @napi-rs/canvas
// polyfill is unreliable under npx. See ./pdfjs-polyfill.ts for details.
@@ -62,10 +63,6 @@ class FetchStandardFontDataFactory {
return new Uint8Array(await res.arrayBuffer());
}
}
-import type {
- PrimitiveSchemaDefinition,
- ElicitResult,
-} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
// =============================================================================
@@ -1282,7 +1279,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
refreshRoots(server.server);
};
server.server.setNotificationHandler(
- RootsListChangedNotificationSchema,
+ "notifications/roots/list_changed",
async () => {
await refreshRoots(server.server);
},
@@ -1292,10 +1289,12 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
const { readPdfRange } = sharedPdfCache;
// Tool: list_pdfs - List available PDFs
- server.tool(
+ server.registerTool(
"list_pdfs",
- "List available PDFs that can be displayed",
- {},
+ {
+ description: "List available PDFs that can be displayed",
+ inputSchema: z.object({}),
+ },
async (): Promise => {
const seen = new Set();
const localFiles: string[] = [];
@@ -1377,7 +1376,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
title: "Read PDF Bytes",
description:
"Read a range of bytes from a PDF (max 512KB per request). The model should NOT call this tool directly.",
- inputSchema: {
+ inputSchema: z.object({
url: z.string().describe("PDF URL or local file path"),
offset: z.number().min(0).default(0).describe("Byte offset"),
byteCount: z
@@ -1386,7 +1385,7 @@ export function createServer(options: CreateServerOptions = {}): McpServer {
.max(MAX_CHUNK_BYTES)
.default(MAX_CHUNK_BYTES)
.describe("Bytes to read"),
- },
+ }),
outputSchema: z.object({
url: z.string(),
bytes: z.string().describe("Base64 encoded bytes"),
@@ -1473,7 +1472,7 @@ Returns a viewUUID in structuredContent. Pass it to \`interact\`:
Accepts local files (use list_pdfs), client MCP root directories, or any HTTPS URL.
Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before display.`,
- inputSchema: {
+ inputSchema: z.object({
url: z
.string()
.default(DEFAULT_PDF)
@@ -1489,7 +1488,7 @@ Set \`elicit_form_inputs\` to true to prompt the user to fill form fields before
"If true and the PDF has form fields, prompt the user to fill them before displaying",
),
}),
- },
+ }),
outputSchema: z.object({
viewUUID: z
.string()
@@ -2426,7 +2425,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
**FORMS** — fill_form: fill fields with \`fields\` array of {name, value}.
**SAVE** — save_as: write the annotated PDF (annotations + form values) to a file. Pass \`path\` (absolute path or file://) for a new location, or omit \`path\` to overwrite the original. Set \`overwrite: true\` to replace an existing file (always required when omitting \`path\`).`,
- inputSchema: {
+ inputSchema: z.object({
viewUUID: z
.string()
.describe(
@@ -2523,7 +2522,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.describe(
"Array of commands to execute sequentially. More efficient than separate calls. Tip: end with get_pages+getScreenshots to verify changes.",
),
- },
+ }),
},
async (
{
@@ -2600,7 +2599,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
const result = await processInteractCommand(
uuid,
commandList[i],
- extra.signal,
+ extra.mcpReq.signal,
);
if (result.isError) {
const errText = result.content
@@ -2657,7 +2656,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Page Data",
description:
"Submit rendered page data for a get_pages request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: {
+ inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the get_pages command"),
@@ -2670,7 +2669,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
}),
)
.describe("Page data entries"),
- },
+ }),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, pages }): Promise => {
@@ -2700,7 +2699,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Save Data",
description:
"Submit annotated PDF bytes for a save_as request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: {
+ inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the save_as command"),
@@ -2709,7 +2708,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.string()
.optional()
.describe("Error message if the viewer failed to build bytes"),
- },
+ }),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, data, error }): Promise => {
@@ -2739,7 +2738,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Submit Viewer State",
description:
"Submit a viewer-state snapshot for a get_viewer_state request (used by viewer). The model should NOT call this tool directly.",
- inputSchema: {
+ inputSchema: z.object({
requestId: z
.string()
.describe("The request ID from the get_viewer_state command"),
@@ -2751,7 +2750,7 @@ Example — add a signature image and a stamp, then screenshot to verify:
.string()
.optional()
.describe("Error message if the viewer failed to read state"),
- },
+ }),
_meta: { ui: { visibility: ["app"] } },
},
async ({ requestId, state, error }): Promise => {
@@ -2781,9 +2780,9 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Poll PDF Commands",
description:
"Poll for pending commands for a PDF viewer. The model should NOT call this tool directly.",
- inputSchema: {
+ inputSchema: z.object({
viewUUID: z.string().describe("The viewUUID of the PDF viewer"),
- },
+ }),
_meta: { ui: { visibility: ["app"] } },
},
async ({ viewUUID: uuid }): Promise => {
@@ -2828,10 +2827,10 @@ Example — add a signature image and a stamp, then screenshot to verify:
title: "Save PDF",
description:
"Save annotated PDF bytes back to a local file. The model should NOT call this tool directly — use interact with action: save_as instead.",
- inputSchema: {
+ inputSchema: z.object({
url: z.string().describe("Original PDF URL or local file path"),
data: z.string().describe("Base64-encoded PDF bytes"),
- },
+ }),
outputSchema: z.object({
filePath: z.string(),
mtimeMs: z.number(),
diff --git a/examples/pdf-server/src/mcp-app.ts b/examples/pdf-server/src/mcp-app.ts
index a953e24..45e0c74 100644
--- a/examples/pdf-server/src/mcp-app.ts
+++ b/examples/pdf-server/src/mcp-app.ts
@@ -12,8 +12,10 @@ import {
applyDocumentTheme,
applyHostStyleVariables,
} from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
-import type { ContentBlock } from "@modelcontextprotocol/sdk/spec.types.js";
+import type {
+ CallToolResult,
+ ContentBlock,
+} from "@modelcontextprotocol/client";
import { z } from "zod";
import * as pdfjsLib from "pdfjs-dist";
import { AnnotationLayer, AnnotationMode, TextLayer } from "pdfjs-dist";
diff --git a/examples/pdf-server/tsconfig.json b/examples/pdf-server/tsconfig.json
index 0501827..2fe7e62 100644
--- a/examples/pdf-server/tsconfig.json
+++ b/examples/pdf-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client", "bun"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/pdf-server/tsconfig.server.json b/examples/pdf-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/pdf-server/tsconfig.server.json
+++ b/examples/pdf-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/qr-server/package.json b/examples/qr-server/package.json
index 8152866..425728e 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,7 +8,6 @@
"build": "echo 'No build step needed for Python server'"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0"
+ "@modelcontextprotocol/ext-apps": "^2.0.0"
}
}
diff --git a/examples/quickstart/main.ts b/examples/quickstart/main.ts
index 06895a4..7f370a8 100644
--- a/examples/quickstart/main.ts
+++ b/examples/quickstart/main.ts
@@ -1,7 +1,7 @@
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -21,7 +21,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/quickstart/package.json b/examples/quickstart/package.json
index e1dcf2d..57f8444 100644
--- a/examples/quickstart/package.json
+++ b/examples/quickstart/package.json
@@ -1,12 +1,12 @@
{
"name": "@modelcontextprotocol/quickstart",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"private": true,
"description": "Quickstart MCP App Server example",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/quickstart"
},
"license": "MIT",
@@ -15,10 +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/sdk": "^1.29.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"
+ "express": "^5.1.0",
+ "zod": "^4.2.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/quickstart/server.ts b/examples/quickstart/server.ts
index eb3c3b1..611d0f0 100644
--- a/examples/quickstart/server.ts
+++ b/examples/quickstart/server.ts
@@ -3,9 +3,10 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { McpServer } from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
+import { z } from "zod";
const DIST_DIR = path.join(import.meta.dirname, "dist");
@@ -30,7 +31,7 @@ export function createServer(): McpServer {
{
title: "Get Time",
description: "Returns the current server time.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } }, // Links this tool to its UI resource
},
async () => {
diff --git a/examples/quickstart/tsconfig.json b/examples/quickstart/tsconfig.json
index 6c553b5..d6a42ce 100644
--- a/examples/quickstart/tsconfig.json
+++ b/examples/quickstart/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/quickstart/tsconfig.server.json b/examples/quickstart/tsconfig.server.json
index 7e65f5f..72552e2 100644
--- a/examples/quickstart/tsconfig.server.json
+++ b/examples/quickstart/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/say-server/package.json b/examples/say-server/package.json
index d51dabd..cc12249 100644
--- a/examples/say-server/package.json
+++ b/examples/say-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/say-server"
},
"license": "MIT",
@@ -15,7 +15,6 @@
"build": "echo 'No build step needed for Python server'"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0"
+ "@modelcontextprotocol/ext-apps": "^2.0.0"
}
}
diff --git a/examples/scenario-modeler-server/main.ts b/examples/scenario-modeler-server/main.ts
index dbd3ab6..cc20e63 100644
--- a/examples/scenario-modeler-server/main.ts
+++ b/examples/scenario-modeler-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/scenario-modeler-server/package.json b/examples/scenario-modeler-server/package.json
index 716c7b5..e6f37c0 100644
--- a/examples/scenario-modeler-server/package.json
+++ b/examples/scenario-modeler-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-scenario-modeler",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Financial scenario modeling MCP App Server",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/scenario-modeler-server"
},
"license": "MIT",
@@ -26,14 +26,16 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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",
"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/scenario-modeler-server/server.ts b/examples/scenario-modeler-server/server.ts
index b59851c..c5fa05d 100644
--- a/examples/scenario-modeler-server/server.ts
+++ b/examples/scenario-modeler-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -305,8 +305,8 @@ export function createServer(): McpServer {
title: "Get Scenario Data",
description:
"Returns SaaS scenario templates and optionally computes custom projections for given inputs",
- inputSchema: GetScenarioDataInputSchema.shape,
- outputSchema: GetScenarioDataOutputSchema.shape,
+ inputSchema: GetScenarioDataInputSchema,
+ outputSchema: GetScenarioDataOutputSchema,
_meta: { ui: { resourceUri } },
},
async (args: {
diff --git a/examples/scenario-modeler-server/tsconfig.json b/examples/scenario-modeler-server/tsconfig.json
index fc3c210..3ecbdad 100644
--- a/examples/scenario-modeler-server/tsconfig.json
+++ b/examples/scenario-modeler-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/scenario-modeler-server/tsconfig.server.json b/examples/scenario-modeler-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/scenario-modeler-server/tsconfig.server.json
+++ b/examples/scenario-modeler-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/shadertoy-server/main.ts b/examples/shadertoy-server/main.ts
index ac8f6ad..7e388f0 100644
--- a/examples/shadertoy-server/main.ts
+++ b/examples/shadertoy-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/shadertoy-server/package.json b/examples/shadertoy-server/package.json
index 0505f47..aec56c6 100644
--- a/examples/shadertoy-server/package.json
+++ b/examples/shadertoy-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/shadertoy-server"
},
"license": "MIT",
@@ -24,11 +24,13 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/shadertoy-server/server.ts b/examples/shadertoy-server/server.ts
index 5238c40..50c27bc 100644
--- a/examples/shadertoy-server/server.ts
+++ b/examples/shadertoy-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppTool,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
diff --git a/examples/shadertoy-server/tsconfig.json b/examples/shadertoy-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/shadertoy-server/tsconfig.json
+++ b/examples/shadertoy-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/shadertoy-server/tsconfig.server.json b/examples/shadertoy-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/shadertoy-server/tsconfig.server.json
+++ b/examples/shadertoy-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/sheet-music-server/main.ts b/examples/sheet-music-server/main.ts
index 8b143a7..d7bf3a0 100644
--- a/examples/sheet-music-server/main.ts
+++ b/examples/sheet-music-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/sheet-music-server/package.json b/examples/sheet-music-server/package.json
index e9e39cc..85ca6bb 100644
--- a/examples/sheet-music-server/package.json
+++ b/examples/sheet-music-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/sheet-music-server"
},
"license": "MIT",
@@ -24,12 +24,14 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/sheet-music-server/server.ts b/examples/sheet-music-server/server.ts
index cfd319b..be7214a 100644
--- a/examples/sheet-music-server/server.ts
+++ b/examples/sheet-music-server/server.ts
@@ -1,8 +1,8 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
diff --git a/examples/sheet-music-server/tsconfig.json b/examples/sheet-music-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/sheet-music-server/tsconfig.json
+++ b/examples/sheet-music-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/sheet-music-server/tsconfig.server.json b/examples/sheet-music-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/sheet-music-server/tsconfig.server.json
+++ b/examples/sheet-music-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/system-monitor-server/main.ts b/examples/system-monitor-server/main.ts
index 365a560..c55c799 100644
--- a/examples/system-monitor-server/main.ts
+++ b/examples/system-monitor-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/system-monitor-server/package.json b/examples/system-monitor-server/package.json
index 9506c61..2dc3ef1 100644
--- a/examples/system-monitor-server/package.json
+++ b/examples/system-monitor-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/system-monitor-server"
},
"license": "MIT",
@@ -26,13 +26,15 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/system-monitor-server/server.ts b/examples/system-monitor-server/server.ts
index 8fac5f2..a17bcf6 100644
--- a/examples/system-monitor-server/server.ts
+++ b/examples/system-monitor-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
@@ -135,8 +135,8 @@ export function createServer(): McpServer {
title: "Get System Info",
description:
"Returns system information, including hostname, platform, CPU info, and memory.",
- inputSchema: {},
- outputSchema: SystemInfoSchema.shape,
+ inputSchema: z.object({}),
+ outputSchema: SystemInfoSchema,
_meta: { ui: { resourceUri } },
},
(): CallToolResult => {
@@ -156,8 +156,8 @@ export function createServer(): McpServer {
title: "Poll System Stats",
description:
"Returns dynamic system metrics for polling: per-core CPU timing, memory usage, and uptime. App-only.",
- inputSchema: {},
- outputSchema: PollStatsSchema.shape,
+ inputSchema: z.object({}),
+ outputSchema: PollStatsSchema,
_meta: { ui: { visibility: ["app"] } },
},
async (): Promise => {
diff --git a/examples/system-monitor-server/tsconfig.json b/examples/system-monitor-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/system-monitor-server/tsconfig.json
+++ b/examples/system-monitor-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/system-monitor-server/tsconfig.server.json b/examples/system-monitor-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/system-monitor-server/tsconfig.server.json
+++ b/examples/system-monitor-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/threejs-server/main.ts b/examples/threejs-server/main.ts
index 13a2084..ef16e26 100644
--- a/examples/threejs-server/main.ts
+++ b/examples/threejs-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/threejs-server/package.json b/examples/threejs-server/package.json
index 2121460..68864d9 100644
--- a/examples/threejs-server/package.json
+++ b/examples/threejs-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "Three.js 3D visualization MCP App Server",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/threejs-server"
},
"license": "MIT",
@@ -26,14 +26,17 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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",
"three": "^0.181.0",
- "zod": "^4.1.13"
+ "zod": "^4.2.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
diff --git a/examples/threejs-server/server.ts b/examples/threejs-server/server.ts
index ae320df..0a13c19 100644
--- a/examples/threejs-server/server.ts
+++ b/examples/threejs-server/server.ts
@@ -8,8 +8,10 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type { ReadResourceResult } from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -161,7 +163,7 @@ export function createServer(): McpServer {
title: "Show Three.js Scene",
description:
"Render an interactive 3D scene with custom Three.js code. Supports transparent backgrounds (alpha: true) for seamless host UI integration. Available globals: THREE, OrbitControls, EffectComposer, RenderPass, UnrealBloomPass, canvas, width, height.",
- inputSchema: {
+ inputSchema: z.object({
code: z
.string()
.default(DEFAULT_THREEJS_CODE)
@@ -172,7 +174,7 @@ export function createServer(): McpServer {
.positive()
.default(400)
.describe("Height in pixels"),
- },
+ }),
outputSchema: z.object({
success: z.boolean(),
}),
@@ -192,7 +194,7 @@ export function createServer(): McpServer {
{
title: "Learn Three.js",
description: "Get documentation and examples for using the Three.js View",
- inputSchema: {},
+ inputSchema: z.object({}),
},
async () => {
return {
diff --git a/examples/threejs-server/src/mcp-app-wrapper.tsx b/examples/threejs-server/src/mcp-app-wrapper.tsx
index 34194b1..44fee0c 100644
--- a/examples/threejs-server/src/mcp-app-wrapper.tsx
+++ b/examples/threejs-server/src/mcp-app-wrapper.tsx
@@ -6,7 +6,7 @@
*/
import type { App, McpUiHostContext } from "@modelcontextprotocol/ext-apps";
import { useApp } from "@modelcontextprotocol/ext-apps/react";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { StrictMode, useState, useCallback, useEffect, useRef } from "react";
import { createRoot } from "react-dom/client";
import { z } from "zod";
diff --git a/examples/threejs-server/tsconfig.json b/examples/threejs-server/tsconfig.json
index fc3c210..3ecbdad 100644
--- a/examples/threejs-server/tsconfig.json
+++ b/examples/threejs-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/threejs-server/tsconfig.server.json b/examples/threejs-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/threejs-server/tsconfig.server.json
+++ b/examples/threejs-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/transcript-server/README.md b/examples/transcript-server/README.md
index 3b3fa42..559ffad 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/main.ts b/examples/transcript-server/main.ts
index 68db8dc..e86ad7c 100644
--- a/examples/transcript-server/main.ts
+++ b/examples/transcript-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -25,7 +25,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/transcript-server/package.json b/examples/transcript-server/package.json
index 8e9d495..46a8f98 100644
--- a/examples/transcript-server/package.json
+++ b/examples/transcript-server/package.json
@@ -1,11 +1,11 @@
{
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.7.5",
+ "version": "2.0.0",
"type": "module",
"description": "MCP App Server for live speech transcription",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/transcript-server"
},
"license": "MIT",
@@ -24,11 +24,13 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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/transcript-server/server.ts b/examples/transcript-server/server.ts
index 56bf88d..863eb29 100644
--- a/examples/transcript-server/server.ts
+++ b/examples/transcript-server/server.ts
@@ -1,8 +1,8 @@
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import {
@@ -10,6 +10,7 @@ import {
registerAppResource,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server";
+import { z } from "zod";
// Works both from source (server.ts) and compiled (dist/server.js)
const DIST_DIR = import.meta.filename.endsWith(".ts")
? path.join(import.meta.dirname, "dist")
@@ -33,7 +34,7 @@ export function createServer(): McpServer {
title: "Transcribe Speech",
description:
"Opens a live speech transcription interface using the Web Speech API.",
- inputSchema: {},
+ inputSchema: z.object({}),
_meta: { ui: { resourceUri } },
},
async (): Promise => {
diff --git a/examples/transcript-server/tsconfig.json b/examples/transcript-server/tsconfig.json
index 535267b..00e4801 100644
--- a/examples/transcript-server/tsconfig.json
+++ b/examples/transcript-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client", "dom-speech-recognition"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/transcript-server/tsconfig.server.json b/examples/transcript-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/transcript-server/tsconfig.server.json
+++ b/examples/transcript-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/video-resource-server/main.ts b/examples/video-resource-server/main.ts
index 082fa5a..1983373 100644
--- a/examples/video-resource-server/main.ts
+++ b/examples/video-resource-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/video-resource-server/package.json b/examples/video-resource-server/package.json
index 8d8b8ec..4a7a61f 100644
--- a/examples/video-resource-server/package.json
+++ b/examples/video-resource-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/video-resource-server"
},
"license": "MIT",
@@ -24,11 +24,14 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/server.ts b/examples/video-resource-server/server.ts
index 2cb317e..8ae6240 100644
--- a/examples/video-resource-server/server.ts
+++ b/examples/video-resource-server/server.ts
@@ -12,11 +12,9 @@ import {
import {
McpServer,
ResourceTemplate,
-} from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import fs from "node:fs/promises";
import path from "node:path";
import { z } from "zod";
@@ -135,14 +133,14 @@ Available videos:
${Object.entries(VIDEO_LIBRARY)
.map(([id, v]) => `- ${id}: ${v.description}`)
.join("\n")}`,
- inputSchema: {
+ inputSchema: z.object({
videoId: z
.enum(Object.keys(VIDEO_LIBRARY) as [string, ...string[]])
.default("bunny-1mb")
.describe(
`Video ID to play. Available: ${Object.keys(VIDEO_LIBRARY).join(", ")}`,
),
- },
+ }),
outputSchema: z.object({
videoUri: z.string(),
description: z.string(),
diff --git a/examples/video-resource-server/src/mcp-app.ts b/examples/video-resource-server/src/mcp-app.ts
index 88ed913..2a3f8f9 100644
--- a/examples/video-resource-server/src/mcp-app.ts
+++ b/examples/video-resource-server/src/mcp-app.ts
@@ -5,7 +5,7 @@
* The video is served as a base64 blob and converted to a data URI for playback.
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import "./global.css";
import "./mcp-app.css";
diff --git a/examples/video-resource-server/tsconfig.json b/examples/video-resource-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/video-resource-server/tsconfig.json
+++ b/examples/video-resource-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/video-resource-server/tsconfig.server.json b/examples/video-resource-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/video-resource-server/tsconfig.server.json
+++ b/examples/video-resource-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/examples/wiki-explorer-server/main.ts b/examples/wiki-explorer-server/main.ts
index cc7b3a2..bfeeffb 100644
--- a/examples/wiki-explorer-server/main.ts
+++ b/examples/wiki-explorer-server/main.ts
@@ -4,10 +4,10 @@
* Or: node dist/index.js [--stdio]
*/
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
-import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
+import { createMcpExpressApp } from "@modelcontextprotocol/express";
+import type { McpServer } from "@modelcontextprotocol/server";
+import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
import cors from "cors";
import type { Request, Response } from "express";
import { createServer } from "./server.js";
@@ -27,7 +27,7 @@ export async function startStreamableHTTPServer(
app.all("/mcp", async (req: Request, res: Response) => {
const server = createServer();
- const transport = new StreamableHTTPServerTransport({
+ const transport = new NodeStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
diff --git a/examples/wiki-explorer-server/package.json b/examples/wiki-explorer-server/package.json
index af02eae..52304cf 100644
--- a/examples/wiki-explorer-server/package.json
+++ b/examples/wiki-explorer-server/package.json
@@ -1,11 +1,11 @@
{
"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": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps",
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git",
"directory": "examples/wiki-explorer-server"
},
"license": "MIT",
@@ -26,12 +26,15 @@
"serve": "bun --watch main.ts"
},
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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/examples/wiki-explorer-server/server.ts b/examples/wiki-explorer-server/server.ts
index 59d5e18..8bc4eb7 100644
--- a/examples/wiki-explorer-server/server.ts
+++ b/examples/wiki-explorer-server/server.ts
@@ -3,11 +3,11 @@ import {
registerAppResource,
registerAppTool,
} from "@modelcontextprotocol/ext-apps/server";
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- CallToolResult,
- ReadResourceResult,
-} from "@modelcontextprotocol/sdk/types.js";
+import {
+ McpServer,
+ type CallToolResult,
+ type ReadResourceResult,
+} from "@modelcontextprotocol/server";
import * as cheerio from "cheerio";
import fs from "node:fs/promises";
import path from "node:path";
diff --git a/examples/wiki-explorer-server/src/mcp-app.ts b/examples/wiki-explorer-server/src/mcp-app.ts
index 291292d..9833d9c 100644
--- a/examples/wiki-explorer-server/src/mcp-app.ts
+++ b/examples/wiki-explorer-server/src/mcp-app.ts
@@ -2,7 +2,7 @@
* Wiki Explorer - Force-directed graph visualization of Wikipedia link networks
*/
import { App, type McpUiHostContext } from "@modelcontextprotocol/ext-apps";
-import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
+import type { CallToolResult } from "@modelcontextprotocol/client";
import { z } from "zod";
import {
forceCenter,
diff --git a/examples/wiki-explorer-server/tsconfig.json b/examples/wiki-explorer-server/tsconfig.json
index 535267b..b028ff5 100644
--- a/examples/wiki-explorer-server/tsconfig.json
+++ b/examples/wiki-explorer-server/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "vite/client"],
"target": "ESNext",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"module": "ESNext",
diff --git a/examples/wiki-explorer-server/tsconfig.server.json b/examples/wiki-explorer-server/tsconfig.server.json
index 05ddd8e..27eb28f 100644
--- a/examples/wiki-explorer-server/tsconfig.server.json
+++ b/examples/wiki-explorer-server/tsconfig.server.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node"],
"target": "ES2022",
"lib": ["ES2022"],
"module": "NodeNext",
diff --git a/package-lock.json b/package-lock.json
index 9dc76e7..0189ca2 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/*"
@@ -16,7 +16,10 @@
},
"devDependencies": {
"@boneskull/typedoc-plugin-mermaid": "^0.2.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.0",
+ "@modelcontextprotocol/core": "2.0.0",
+ "@modelcontextprotocol/sdk": "1.30.0",
+ "@modelcontextprotocol/server": "2.0.0",
"@playwright/test": "1.57.0",
"@types/bun": "^1.3.2",
"@types/node": "20.19.27",
@@ -31,6 +34,7 @@
"electron-to-chromium": "1.5.267",
"esbuild": "^0.25.12",
"express": "^5.1.0",
+ "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5",
"husky": "^9.1.7",
"nodemon": "^3.1.0",
"playwright": "1.57.0",
@@ -44,18 +48,23 @@
"typedoc": "^0.28.14",
"typedoc-github-theme": "^0.4.0",
"typescript": "^5.9.3",
- "zod": "^4.1.13"
+ "zod": "^4.2.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "^2.0.0",
+ "@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": "^3.25.0 || ^4.0.0"
+ "zod": "^4.2.0"
},
"peerDependenciesMeta": {
+ "@modelcontextprotocol/server": {
+ "optional": true
+ },
"react": {
"optional": true
},
@@ -66,13 +75,13 @@
},
"examples/basic-host": {
"name": "@modelcontextprotocol/ext-apps-basic-host",
- "version": "1.7.5",
+ "version": "2.0.0",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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",
@@ -108,15 +117,18 @@
},
"examples/basic-server-preact": {
"name": "@modelcontextprotocol/server-basic-preact",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-preact": "dist/index.js"
@@ -152,16 +164,19 @@
},
"examples/basic-server-react": {
"name": "@modelcontextprotocol/server-basic-react",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-react": "dist/index.js"
@@ -199,15 +214,18 @@
},
"examples/basic-server-solid": {
"name": "@modelcontextprotocol/server-basic-solid",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-solid": "dist/index.js"
@@ -243,15 +261,18 @@
},
"examples/basic-server-svelte": {
"name": "@modelcontextprotocol/server-basic-svelte",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-svelte": "dist/index.js"
@@ -287,14 +308,17 @@
},
"examples/basic-server-vanillajs": {
"name": "@modelcontextprotocol/server-basic-vanillajs",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-vanillajs": "dist/index.js"
@@ -329,15 +353,18 @@
},
"examples/basic-server-vue": {
"name": "@modelcontextprotocol/server-basic-vue",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-server-basic-vue": "dist/index.js"
@@ -373,15 +400,17 @@
},
"examples/budget-allocator-server": {
"name": "@modelcontextprotocol/server-budget-allocator",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-budget-allocator-server": "dist/index.js"
@@ -416,16 +445,18 @@
},
"examples/cohort-heatmap-server": {
"name": "@modelcontextprotocol/server-cohort-heatmap",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-cohort-heatmap-server": "dist/index.js"
@@ -463,15 +494,17 @@
},
"examples/customer-segmentation-server": {
"name": "@modelcontextprotocol/server-customer-segmentation",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-customer-segmentation-server": "dist/index.js"
@@ -506,12 +539,14 @@
},
"examples/debug-server": {
"name": "@modelcontextprotocol/server-debug",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
- "zod": "^4.1.13"
+ "@modelcontextprotocol/express": "2.0.0",
+ "@modelcontextprotocol/ext-apps": "^2.0.0",
+ "@modelcontextprotocol/node": "2.0.0",
+ "@modelcontextprotocol/server": "2.0.0",
+ "zod": "^4.2.0"
},
"bin": {
"mcp-server-debug": "dist/index.js"
@@ -547,15 +582,18 @@
"license": "MIT"
},
"examples/integration-server": {
- "version": "1.7.5",
+ "version": "2.0.0",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-integration-server": "dist/index.js"
@@ -592,14 +630,16 @@
},
"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/sdk": "^1.29.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"
+ "jose": "^6.0.0",
+ "zod": "^4.2.0"
},
"bin": {
"mcp-server-lazy-auth": "dist/index.js"
@@ -634,14 +674,17 @@
},
"examples/map-server": {
"name": "@modelcontextprotocol/server-map",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-map-server": "dist/index.js"
@@ -676,16 +719,19 @@
},
"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/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-pdf-server": "dist/index.js"
@@ -720,21 +766,23 @@
},
"examples/qr-server": {
"name": "@modelcontextprotocol/server-qr",
- "version": "1.7.5",
+ "version": "2.0.0",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0"
+ "@modelcontextprotocol/ext-apps": "^2.0.0"
}
},
"examples/quickstart": {
"name": "@modelcontextprotocol/quickstart",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
+ "express": "^5.1.0",
+ "zod": "^4.2.0"
},
"devDependencies": {
"@types/cors": "^2.8.19",
@@ -760,26 +808,27 @@
},
"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/sdk": "^1.29.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/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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",
"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"
@@ -817,14 +866,16 @@
},
"examples/shadertoy-server": {
"name": "@modelcontextprotocol/server-shadertoy",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-shadertoy-server": "dist/index.js"
@@ -859,15 +910,17 @@
},
"examples/sheet-music-server": {
"name": "@modelcontextprotocol/server-sheet-music",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-sheet-music-server": "dist/index.js"
@@ -902,16 +955,18 @@
},
"examples/system-monitor-server": {
"name": "@modelcontextprotocol/server-system-monitor",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-system-monitor-server": "dist/index.js"
@@ -946,17 +1001,20 @@
},
"examples/threejs-server": {
"name": "@modelcontextprotocol/server-threejs",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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",
"three": "^0.181.0",
- "zod": "^4.1.13"
+ "zod": "^4.2.0"
},
"bin": {
"mcp-threejs-server": "dist/index.js"
@@ -995,14 +1053,16 @@
},
"examples/transcript-server": {
"name": "@modelcontextprotocol/server-transcript",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.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"
},
"bin": {
"mcp-transcript-server": "dist/index.js"
@@ -1038,14 +1098,17 @@
},
"examples/video-resource-server": {
"name": "@modelcontextprotocol/server-video-resource",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-video-resource-server": "dist/index.js"
@@ -1080,15 +1143,18 @@
},
"examples/wiki-explorer-server": {
"name": "@modelcontextprotocol/server-wiki-explorer",
- "version": "1.7.5",
+ "version": "2.0.0",
"license": "MIT",
"dependencies": {
- "@modelcontextprotocol/ext-apps": "^1.7.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.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"
},
"bin": {
"mcp-wiki-explorer-server": "dist/index.js"
@@ -2613,50 +2679,93 @@
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
- "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==",
+ "node_modules/@modelcontextprotocol/client": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz",
+ "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==",
"license": "MIT",
- "workspaces": [
- "examples/*"
- ],
"dependencies": {
- "@standard-schema/spec": "^1.1.0"
+ "@modelcontextprotocol/core": "2.0.0",
+ "cross-spawn": "^7.0.5",
+ "eventsource": "^3.0.2",
+ "eventsource-parser": "^3.0.0",
+ "jose": "^6.1.3",
+ "pkce-challenge": "^5.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==",
+ "license": "MIT",
+ "dependencies": {
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@modelcontextprotocol/express": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/express/-/express-2.0.0.tgz",
+ "integrity": "sha512-Snlr8j9FR9LcVvEJPF7qJ7d5zTL4Bes2dk7RcacN9eSZ7OLohwxqhEvWu1+UxELyScgLbLLOdeVEGdwKI1iwVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "cors": "^2.8.5"
},
"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
- }
+ "@modelcontextprotocol/server": "^2.0.0",
+ "express": "^4.18.0 || ^5.0.0"
}
},
+ "node_modules/@modelcontextprotocol/ext-apps": {
+ "resolved": "",
+ "link": true
+ },
"node_modules/@modelcontextprotocol/ext-apps-basic-host": {
"resolved": "examples/basic-host",
"link": true
},
+ "node_modules/@modelcontextprotocol/node": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/node/-/node-2.0.0.tgz",
+ "integrity": "sha512-Y4hAC2XdGDUdDOCbLDOCA4+aL3NUldjsOWlDL/YwpAxrPhRm1xHd7lZ+mLacvZ9t3PaH28wgNoaLQGrIk1P2pg==",
+ "license": "MIT",
+ "dependencies": {
+ "@hono/node-server": "^1.19.9"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "peerDependencies": {
+ "@modelcontextprotocol/server": "^2.0.0",
+ "hono": "^4.11.4"
+ },
+ "peerDependenciesMeta": {
+ "hono": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@modelcontextprotocol/quickstart": {
"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==",
+ "version": "1.30.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz",
+ "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "@hono/node-server": "^1.19.9",
+ "@hono/node-server": "^1.19.9 || ^2.0.5",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"content-type": "^1.0.5",
@@ -2690,6 +2799,19 @@
}
}
},
+ "node_modules/@modelcontextprotocol/server": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/server/-/server-2.0.0.tgz",
+ "integrity": "sha512-YhHWdHfpFMQfd0prsEnxKeS3Qz3ytIGmsS0sth4KDjnacIT7hxk6hXHkJ9KysxlkvTM+WZAtQbbcUhdoP4Hvtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@modelcontextprotocol/core": "2.0.0",
+ "zod": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
"node_modules/@modelcontextprotocol/server-basic-preact": {
"resolved": "examples/basic-server-preact",
"link": true
@@ -4297,9 +4419,10 @@
}
},
"node_modules/ajv": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
+ "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -4316,6 +4439,7 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
"integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@@ -5892,11 +6016,13 @@
}
},
"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==",
+ "version": "8.7.0",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz",
+ "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
+ "debug": "^4.4.3",
"ip-address": "^10.2.0"
},
"engines": {
@@ -5909,16 +6035,49 @@
"express": ">= 4.11"
}
},
+ "node_modules/ext-apps-v1": {
+ "name": "@modelcontextprotocol/ext-apps",
+ "version": "1.7.5",
+ "resolved": "https://registry.npmjs.org/@modelcontextprotocol/ext-apps/-/ext-apps-1.7.5.tgz",
+ "integrity": "sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==",
+ "dev": true,
+ "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
+ }
+ }
+ },
"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==",
+ "dev": true,
"license": "MIT"
},
"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==",
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz",
+ "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==",
+ "dev": true,
"funding": [
{
"type": "github",
@@ -6393,9 +6552,10 @@
}
},
"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==",
+ "version": "10.7.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.0.tgz",
+ "integrity": "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -6592,12 +6752,14 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "dev": true,
"license": "MIT"
},
"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==",
+ "dev": true,
"license": "BSD-2-Clause"
},
"node_modules/json5": {
@@ -7596,6 +7758,7 @@
"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==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -9741,12 +9904,13 @@
}
},
"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==",
+ "version": "3.25.2",
+ "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz",
+ "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==",
+ "dev": true,
"license": "ISC",
"peerDependencies": {
- "zod": "^3.25 || ^4"
+ "zod": "^3.25.28 || ^4"
}
}
}
diff --git a/package.json b/package.json
index 15ef5d7..c86efa9 100644
--- a/package.json
+++ b/package.json
@@ -2,10 +2,10 @@
"name": "@modelcontextprotocol/ext-apps",
"repository": {
"type": "git",
- "url": "https://github.com/modelcontextprotocol/ext-apps"
+ "url": "git+https://github.com/modelcontextprotocol/ext-apps.git"
},
"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",
@@ -54,6 +54,7 @@
"prepack": "npm run build",
"build:all": "npm run examples:build",
"test": "bun test src examples",
+ "test:dependency-isolation": "npm run build && node scripts/check-dependency-isolation.mjs",
"test:e2e": "playwright test",
"test:e2e:update": "playwright test --update-snapshots",
"test:e2e:ui": "playwright test --ui",
@@ -75,9 +76,13 @@
"update-lock:docker": "rm -rf node_modules package-lock.json examples/*/node_modules && docker run --rm --platform linux/amd64 -v $(pwd):/work -w /work -e HOME=/tmp node:latest npm i --registry=https://registry.npmjs.org/ --ignore-scripts && rm -rf node_modules examples/*/node_modules && npm i --registry=https://registry.npmjs.org/"
},
"author": "Olivier Chafik",
+ "//interop": "The @modelcontextprotocol/sdk 1.x and ext-apps-v1 devDependencies exist only for src/interop-v1.test.ts, which runs the published 1.7.5 package against this build. The library does not use them.",
"devDependencies": {
"@boneskull/typedoc-plugin-mermaid": "^0.2.0",
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "2.0.0",
+ "@modelcontextprotocol/core": "2.0.0",
+ "@modelcontextprotocol/sdk": "1.30.0",
+ "@modelcontextprotocol/server": "2.0.0",
"@playwright/test": "1.57.0",
"@types/bun": "^1.3.2",
"@types/node": "20.19.27",
@@ -92,6 +97,7 @@
"electron-to-chromium": "1.5.267",
"esbuild": "^0.25.12",
"express": "^5.1.0",
+ "ext-apps-v1": "npm:@modelcontextprotocol/ext-apps@1.7.5",
"husky": "^9.1.7",
"nodemon": "^3.1.0",
"playwright": "1.57.0",
@@ -105,15 +111,20 @@
"typedoc": "^0.28.14",
"typedoc-github-theme": "^0.4.0",
"typescript": "^5.9.3",
- "zod": "^4.1.13"
+ "zod": "^4.2.0"
},
"peerDependencies": {
- "@modelcontextprotocol/sdk": "^1.29.0",
+ "@modelcontextprotocol/client": "^2.0.0",
+ "@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": "^3.25.0 || ^4.0.0"
+ "zod": "^4.2.0"
},
"peerDependenciesMeta": {
+ "@modelcontextprotocol/server": {
+ "optional": true
+ },
"react": {
"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 02d09a4..5a473f0 100644
--- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
+++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md
@@ -77,13 +77,28 @@ Before writing any code, analyze the server's existing tools and determine which
## Step 2: Add Dependencies
```bash
-npm install @modelcontextprotocol/ext-apps
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @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).
+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.
-Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
+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
@@ -146,10 +161,14 @@ Transform plain MCP tools into App tools with UI.
**Before** (plain MCP tool):
```typescript
-server.tool("my-tool", { param: z.string() }, async (args) => {
- const data = await fetchData(args.param);
- return { content: [{ type: "text", text: JSON.stringify(data) }] };
-});
+server.registerTool(
+ "my-tool",
+ { inputSchema: z.object({ param: z.string() }) },
+ async ({ param }) => {
+ const data = await fetchData(param);
+ return { content: [{ type: "text", text: JSON.stringify(data) }] };
+ },
+);
```
**After** (App tool with UI):
@@ -158,17 +177,22 @@ import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@model
const resourceUri = "ui://my-tool/mcp-app.html";
-registerAppTool(server, "my-tool", {
- description: "Shows data with an interactive UI",
- inputSchema: { param: z.string() },
- _meta: { ui: { resourceUri } },
-}, async (args) => {
- const data = await fetchData(args.param);
- return {
- content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
- structuredContent: { data }, // structured data for the UI
- };
-});
+registerAppTool(
+ server,
+ "my-tool",
+ {
+ description: "Shows data with an interactive UI",
+ inputSchema: z.object({ param: z.string() }),
+ _meta: { ui: { resourceUri } },
+ },
+ async ({ param }) => {
+ const data = await fetchData(param);
+ return {
+ content: [{ type: "text", text: JSON.stringify(data) }], // text fallback for non-UI hosts
+ structuredContent: { data }, // structured data for the UI
+ };
+ },
+);
```
Key guidance:
@@ -187,17 +211,21 @@ import path from "node:path";
const resourceUri = "ui://my-tool/mcp-app.html";
-registerAppResource(server, {
- uri: resourceUri,
- name: "My Tool UI",
- mimeType: RESOURCE_MIME_TYPE,
-}, async () => {
- const html = await fs.readFile(
- path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
- "utf-8",
- );
- return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
-});
+registerAppResource(
+ server,
+ "My Tool UI",
+ resourceUri,
+ {},
+ async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return {
+ contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }],
+ };
+ },
+);
```
If multiple tools share the same UI, they can reference the same `resourceUri` and the same resource registration.
@@ -271,25 +299,32 @@ registerAppTool(server, "poll-data", {
});
```
-The UI calls these via `app.callServerTool("poll-data", {})`.
+The UI calls these via
+`app.callServerTool({ name: "poll-data", arguments: {} })`.
### CSP Configuration
If the UI needs to load external resources (fonts, APIs, CDNs), declare the domains:
```typescript
-registerAppResource(server, {
- uri: resourceUri,
- name: "My Tool UI",
- mimeType: RESOURCE_MIME_TYPE,
- _meta: {
- ui: {
- connectDomains: ["api.example.com"], // fetch/XHR targets
- resourceDomains: ["cdn.example.com"], // scripts, styles, images
- frameDomains: ["embed.example.com"], // nested iframes
+registerAppResource(server, "My Tool UI", resourceUri, {}, async () => ({
+ contents: [
+ {
+ uri: resourceUri,
+ mimeType: RESOURCE_MIME_TYPE,
+ text: html,
+ _meta: {
+ ui: {
+ csp: {
+ connectDomains: ["https://api.example.com"], // fetch/XHR targets
+ resourceDomains: ["https://cdn.example.com"], // scripts, styles, images
+ frameDomains: ["https://embed.example.com"], // nested iframes
+ },
+ },
+ },
},
- },
-}, async () => { /* ... */ });
+ ],
+}));
```
### Streaming Partial Input
@@ -326,7 +361,14 @@ server.server.oninitialized = () => {
}, appToolHandler);
} else {
// Text-only client — register plain tool
- server.tool("my-tool", "Shows data", { param: z.string() }, plainToolHandler);
+ server.registerTool(
+ "my-tool",
+ {
+ description: "Shows data",
+ inputSchema: z.object({ param: z.string() }),
+ },
+ plainToolHandler,
+ );
}
};
```
diff --git a/plugins/mcp-apps/skills/convert-web-app/SKILL.md b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
index 5db2d94..8fb7fa0 100644
--- a/plugins/mcp-apps/skills/convert-web-app/SKILL.md
+++ b/plugins/mcp-apps/skills/convert-web-app/SKILL.md
@@ -115,19 +115,22 @@ Create a new MCP server with tool and resource registration. This wraps the exis
### Dependencies
```bash
-npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/sdk zod
+npm install @modelcontextprotocol/ext-apps @modelcontextprotocol/client@^2.0.0 @modelcontextprotocol/server@^2.0.0 zod@^4.2.0
npm install -D tsx vite vite-plugin-singlefile
```
-Use `npm install` to add dependencies rather than manually writing version numbers. This lets npm resolve the latest compatible versions. Never specify version numbers from memory.
+Use `npm install` 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
Create `server.ts`:
```typescript
-import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
-import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
+import { McpServer } from "@modelcontextprotocol/server";
+import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import fs from "node:fs/promises";
import path from "node:path";
@@ -140,7 +143,9 @@ const resourceUri = "ui://my-app/mcp-app.html";
// Register the tool — inputSchema maps to the app's data sources
registerAppTool(server, "show-app", {
description: "Displays the app with the given parameters",
- inputSchema: { query: z.string().describe("The search query") },
+ inputSchema: z.object({
+ query: z.string().describe("The search query"),
+ }),
_meta: { ui: { resourceUri } },
}, async (args) => {
// Process args server-side if needed
@@ -151,19 +156,29 @@ registerAppTool(server, "show-app", {
});
// Register the HTML resource
-registerAppResource(server, {
- uri: resourceUri,
- name: "My App UI",
- mimeType: RESOURCE_MIME_TYPE,
- // Add CSP domains from Step 2 if needed:
- // _meta: { ui: { connectDomains: ["api.example.com"], resourceDomains: ["cdn.example.com"] } },
-}, async () => {
- const html = await fs.readFile(
- path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
- "utf-8",
- );
- return { contents: [{ uri: resourceUri, mimeType: RESOURCE_MIME_TYPE, text: html }] };
-});
+registerAppResource(
+ server,
+ "My App UI",
+ resourceUri,
+ {},
+ async () => {
+ const html = await fs.readFile(
+ path.resolve(import.meta.dirname, "dist", "mcp-app.html"),
+ "utf-8",
+ );
+ return {
+ contents: [
+ {
+ uri: resourceUri,
+ mimeType: RESOURCE_MIME_TYPE,
+ text: html,
+ // Add CSP domains from Step 2 if needed:
+ // _meta: { ui: { csp: { connectDomains: ["https://api.example.com"], resourceDomains: ["https://cdn.example.com"] } } },
+ },
+ ],
+ };
+ },
+);
// Start the server
const transport = new StdioServerTransport();
@@ -310,7 +325,10 @@ const data = await fetch("/api/data").then(r => r.json());
// After (hybrid):
async function fetchData(): Promise {
if (isMcpApp) {
- const result = await app.callServerTool("fetch-data", {});
+ const result = await app.callServerTool({
+ name: "fetch-data",
+ arguments: {},
+ });
return result.structuredContent;
}
return fetch("/api/data").then(r => r.json());
@@ -437,7 +455,8 @@ registerAppTool(server, "refresh-data", {
});
```
-The UI calls these via `app.callServerTool("refresh-data", {})`.
+The UI calls these via
+`app.callServerTool({ name: "refresh-data", arguments: {} })`.
### Streaming Partial Input
diff --git a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
index 5bd1219..d73dc90 100644
--- a/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
+++ b/plugins/mcp-apps/skills/create-mcp-app/SKILL.md
@@ -110,11 +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/sdk zod 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
```
-This lets npm resolve the latest compatible versions. **Never** specify version numbers from memory.
+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
new file mode 100644
index 0000000..3809224
--- /dev/null
+++ b/scripts/check-dependency-isolation.mjs
@@ -0,0 +1,209 @@
+import { execFileSync } from "node:child_process";
+import {
+ mkdtempSync,
+ mkdirSync,
+ readdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from "node:fs";
+import { tmpdir } from "node:os";
+import { join, resolve } from "node:path";
+
+const root = resolve(import.meta.dirname, "..");
+const packageJson = JSON.parse(
+ readFileSync(join(root, "package.json"), "utf8"),
+);
+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?.[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 = {
+ PATH: process.env.PATH ?? process.env.Path,
+ HOME: process.env.HOME ?? process.env.USERPROFILE,
+ npm_config_cache: join(temporaryRoot, "npm-cache"),
+ };
+ 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"),
+ );
+ 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": 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"; 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": exactDevDependency("@types/node"),
+ "@modelcontextprotocol/ext-apps": `file:${tarball}`,
+ [server]: exactDevDependency(server),
+ },
+ absent: client,
+ mustNotInstall: false,
+ entry:
+ 'import { registerAppTool } from "@modelcontextprotocol/ext-apps/server"; console.log(registerAppTool);',
+ },
+ ];
+
+ for (const consumer of consumers) {
+ const directory = join(temporaryRoot, consumer.name);
+ mkdirSync(directory);
+ writeFileSync(
+ join(directory, "package.json"),
+ JSON.stringify({
+ private: true,
+ type: "module",
+ dependencies: consumer.dependencies,
+ }),
+ );
+ execFileSync(
+ "npm",
+ [
+ "install",
+ "--ignore-scripts",
+ "--package-lock=false",
+ "--no-audit",
+ "--no-fund",
+ ],
+ { cwd: directory, stdio: "inherit", env: npmEnvironment },
+ );
+
+ if (consumer.mustNotInstall) {
+ const absentPath = join(
+ directory,
+ "node_modules",
+ ...consumer.absent.split("/"),
+ "package.json",
+ );
+ 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);
+ writeFileSync(
+ join(directory, "tsconfig.json"),
+ JSON.stringify({
+ compilerOptions: {
+ jsx: "react-jsx",
+ lib: ["ES2020", "DOM"],
+ module: "ESNext",
+ moduleResolution: "bundler",
+ noEmit: true,
+ skipLibCheck: false,
+ strict: true,
+ target: "ES2020",
+ },
+ include: ["entry.ts"],
+ }),
+ );
+ execFileSync(
+ process.execPath,
+ [join(root, "node_modules", "typescript", "bin", "tsc")],
+ { cwd: directory, stdio: "inherit" },
+ );
+
+ const metafile = join(directory, "bundle-meta.json");
+ execFileSync(
+ join(root, "node_modules", ".bin", "esbuild"),
+ [
+ "entry.ts",
+ "--bundle",
+ "--platform=browser",
+ "--outfile=bundle.js",
+ `--metafile=${metafile}`,
+ ],
+ { cwd: directory, stdio: "inherit" },
+ );
+ const bundleInputs = Object.keys(
+ JSON.parse(readFileSync(metafile, "utf8")).inputs,
+ ).join("\n");
+ if (bundleInputs.includes(`/node_modules/${consumer.absent}/`)) {
+ throw new Error(
+ `${consumer.name} unexpectedly bundled ${consumer.absent}`,
+ );
+ }
+ }
+} finally {
+ rmSync(temporaryRoot, { recursive: true, force: true });
+}
+
+console.log("Role peer dependency isolation checks passed.");
diff --git a/scripts/generate-schemas.ts b/scripts/generate-schemas.ts
index 109f352..0ff9f04 100644
--- a/scripts/generate-schemas.ts
+++ b/scripts/generate-schemas.ts
@@ -12,7 +12,7 @@
*
* ts-to-zod generates `import { z } from "zod"`. We rewrite this to
* `import { z } from "zod/v4"` because the generated schemas compose with
- * schemas imported from `@modelcontextprotocol/sdk/types.js`, which the SDK
+ * schemas imported from `@modelcontextprotocol/core`, which the SDK
* constructs via `zod/v4`. Mixing schemas from the v3 and v4 APIs at runtime
* fails with errors like `keyValidator._parse is not a function` (v3 internals
* calling into v4 objects, or vice versa). The `zod/v4` subpath is exported by
@@ -22,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/sdk`, 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()`)
*
@@ -193,7 +195,7 @@ function postProcess(content: string): string {
`import { z } from "zod/v4";
import {
${mcpImports},
-} from "@modelcontextprotocol/sdk/types.js";`,
+} from "@modelcontextprotocol/core";`,
);
// 2. Remove z.any() placeholders for external types (now imported from MCP SDK)
diff --git a/scripts/typedoc-plugin-base-sdk-links.mjs b/scripts/typedoc-plugin-base-sdk-links.mjs
new file mode 100644
index 0000000..6098918
--- /dev/null
+++ b/scripts/typedoc-plugin-base-sdk-links.mjs
@@ -0,0 +1,16 @@
+/**
+ * Resolve module-local links inherited from the base MCP SDK declarations.
+ *
+ * TypeDoc's externalSymbolLinkMappings handles package-qualified symbols, but
+ * the base SDK writes this particular reference as `index.inputRequired`.
+ */
+
+const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/";
+
+/** @param {import("typedoc").Application} app */
+export function load(app) {
+ app.converter.addUnknownSymbolResolver((ref) => {
+ const name = ref.symbolReference?.path?.map((part) => part.path).join(".");
+ if (name === "index.inputRequired") return BASE_SDK_DOCS;
+ });
+}
diff --git a/src/app-bridge.examples.ts b/src/app-bridge.examples.ts
index d5dc5bc..47ad623 100644
--- a/src/app-bridge.examples.ts
+++ b/src/app-bridge.examples.ts
@@ -7,17 +7,11 @@
* @module
*/
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
-import {
- CallToolResult,
- CallToolResultSchema,
+import { Client, type Transport } from "@modelcontextprotocol/client";
+import type {
CreateMessageRequest,
CreateMessageResult,
- ListResourcesResultSchema,
- ReadResourceResultSchema,
- ListPromptsResultSchema,
-} from "@modelcontextprotocol/sdk/types.js";
+} from "@modelcontextprotocol/server";
import { AppBridge, PostMessageTransport } from "./app-bridge.js";
import type { McpUiDisplayMode } from "./types.js";
@@ -223,8 +217,7 @@ function AppBridge_oncalltool_forwardToServer(
bridge.oncalltool = async (params, extra) => {
return mcpClient.request(
{ method: "tools/call", params },
- CallToolResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
//#endregion AppBridge_oncalltool_forwardToServer
@@ -245,7 +238,9 @@ function AppBridge_oncreatesamplingmessage_forwardToLlm(
//#region AppBridge_oncreatesamplingmessage_forwardToLlm
bridge.oncreatesamplingmessage = async (params, extra) => {
// Apply rate limiting, user approval, cost controls here
- return await myLlmProvider.complete(params, { signal: extra.signal });
+ return await myLlmProvider.complete(params, {
+ signal: extra.mcpReq.signal,
+ });
};
//#endregion AppBridge_oncreatesamplingmessage_forwardToLlm
}
@@ -261,8 +256,7 @@ function AppBridge_onlistresources_returnResources(
bridge.onlistresources = async (params, extra) => {
return mcpClient.request(
{ method: "resources/list", params },
- ListResourcesResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
//#endregion AppBridge_onlistresources_returnResources
@@ -279,8 +273,7 @@ function AppBridge_onreadresource_returnResource(
bridge.onreadresource = async (params, extra) => {
return mcpClient.request(
{ method: "resources/read", params },
- ReadResourceResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
//#endregion AppBridge_onreadresource_returnResource
@@ -297,8 +290,7 @@ function AppBridge_onlistprompts_returnPrompts(
bridge.onlistprompts = async (params, extra) => {
return mcpClient.request(
{ method: "prompts/list", params },
- ListPromptsResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
//#endregion AppBridge_onlistprompts_returnPrompts
@@ -444,10 +436,10 @@ async function AppBridge_sendToolResult_afterExecution(
args: Record,
) {
//#region AppBridge_sendToolResult_afterExecution
- const result = await mcpClient.request(
- { method: "tools/call", params: { name: "get_weather", arguments: args } },
- CallToolResultSchema,
- );
+ const result = await mcpClient.request({
+ method: "tools/call",
+ params: { name: "get_weather", arguments: args },
+ });
bridge.sendToolResult(result);
//#endregion AppBridge_sendToolResult_afterExecution
}
diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts
index d327c95..a87b0e7 100644
--- a/src/app-bridge.test.ts
+++ b/src/app-bridge.test.ts
@@ -1,29 +1,19 @@
import { describe, it, expect, beforeEach, afterEach, spyOn } from "bun:test";
-import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
-import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import type { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
-import {
- EmptyResultSchema,
- ListPromptsResultSchema,
- ListResourcesResultSchema,
- ListResourceTemplatesResultSchema,
- PromptListChangedNotificationSchema,
- ReadResourceResultSchema,
- ResourceListChangedNotificationSchema,
- ToolListChangedNotificationSchema,
-} from "@modelcontextprotocol/sdk/types.js";
+import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
+import { Server, type ServerCapabilities } from "@modelcontextprotocol/server";
import { z } from "zod/v4";
-import { App } from "./app";
-import { LATEST_PROTOCOL_VERSION } from "./types";
+import { App } from "./app.js";
+import { LATEST_PROTOCOL_VERSION } from "./types.js";
import {
AppBridge,
buildAllowAttribute,
getToolUiResourceUri,
isToolVisibilityModelOnly,
isToolVisibilityAppOnly,
+ McpUiOpenLinkResultSchema,
type McpUiHostCapabilities,
-} from "./app-bridge";
+} from "./app-bridge.js";
/** Wait for pending microtasks to complete */
const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
@@ -34,11 +24,18 @@ const flush = () => new Promise((resolve) => setTimeout(resolve, 0));
*/
function createMockClient(
serverCapabilities: ServerCapabilities = {},
-): Pick {
+): Pick<
+ Client,
+ | "getServerCapabilities"
+ | "request"
+ | "notification"
+ | "setNotificationHandler"
+> {
return {
getServerCapabilities: () => serverCapabilities,
request: async () => ({}) as never,
notification: async () => {},
+ setNotificationHandler: () => {},
};
}
@@ -88,6 +85,53 @@ describe("App <-> AppBridge integration", () => {
expect(initializedFired).toBe(true);
});
+ it("sends only the Apps handshake on the wire and gates the Apps-ready callback", async () => {
+ const methods: string[] = [];
+ let releaseAppsInitialized!: () => void;
+ let reachedAppsInitialized!: () => void;
+ const appsInitializedGate = new Promise((resolve) => {
+ releaseAppsInitialized = resolve;
+ });
+ const reachedGate = new Promise((resolve) => {
+ reachedAppsInitialized = resolve;
+ });
+ const originalSend = appTransport.send.bind(appTransport);
+ appTransport.send = async (message, options) => {
+ if ("method" in message) methods.push(message.method);
+ if (
+ "method" in message &&
+ message.method === "ui/notifications/initialized"
+ ) {
+ reachedAppsInitialized();
+ await appsInitializedGate;
+ }
+ return originalSend(message, options);
+ };
+
+ let singularCalls = 0;
+ let listenerCalls = 0;
+ bridge.oninitialized = () => singularCalls++;
+ bridge.addEventListener("initialized", () => listenerCalls++);
+
+ await bridge.connect(bridgeTransport);
+ const connecting = app.connect(appTransport);
+ await reachedGate;
+
+ expect(methods).toEqual([
+ "ui/initialize",
+ "ui/notifications/initialized",
+ ]);
+ expect(bridge.getAppVersion()).toEqual(testAppInfo);
+ expect(singularCalls).toBe(0);
+ expect(listenerCalls).toBe(0);
+
+ releaseAppsInitialized();
+ await connecting;
+
+ expect(singularCalls).toBe(1);
+ expect(listenerCalls).toBe(1);
+ });
+
it("App receives host info and capabilities after connect", async () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -97,6 +141,7 @@ describe("App <-> AppBridge integration", () => {
const hostCaps = app.getHostCapabilities();
expect(hostCaps).toEqual(testHostCapabilities);
+ expect(bridge.getCapabilities()).toEqual(testHostCapabilities);
});
it("Bridge receives app info and capabilities after initialization", async () => {
@@ -622,6 +667,30 @@ describe("App <-> AppBridge integration", () => {
expect(result.isError).toBe(true);
});
+
+ it("rejects invalid params for a custom ui request", async () => {
+ bridge.onopenlink = async () => ({});
+ await app.connect(appTransport);
+
+ await expect(
+ app.request(
+ {
+ method: "ui/open-link",
+ params: { url: 42 },
+ },
+ McpUiOpenLinkResultSchema,
+ ),
+ ).rejects.toThrow();
+ });
+
+ it("rejects invalid results from a custom ui handler", async () => {
+ bridge.onopenlink = async () => ({ isError: "yes" }) as never;
+ await app.connect(appTransport);
+
+ await expect(
+ app.openLink({ url: "https://example.com" }),
+ ).rejects.toThrow();
+ });
});
describe("deprecated method aliases", () => {
@@ -691,10 +760,7 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport);
// Bridge can send ping via the protocol's request method
- const result = await bridge.request(
- { method: "ping", params: {} },
- EmptyResultSchema,
- );
+ const result = await bridge.request({ method: "ping", params: {} });
expect(result).toEqual({});
});
@@ -809,7 +875,7 @@ describe("App <-> AppBridge integration", () => {
});
await bridge.callTool({ name: "noargs", arguments: {} });
expect(receivedExtra).toBeDefined();
- expect(receivedExtra.signal).toBeInstanceOf(AbortSignal);
+ expect(receivedExtra.mcpReq.signal).toBeInstanceOf(AbortSignal);
});
it("isError result skips output schema validation", async () => {
@@ -1485,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",
{
@@ -1532,6 +1567,8 @@ describe("App <-> AppBridge integration", () => {
},
async () => ({ content: [] }),
);
+ await app.connect(appTransport);
+
expect(bridge.listTools({})).rejects.toThrow(
/does not implement Standard JSON Schema/,
);
@@ -2191,10 +2228,10 @@ describe("App <-> AppBridge integration", () => {
await app.connect(appTransport);
// App sends resources/list request via the protocol's request method
- const result = await app.request(
- { method: "resources/list", params: requestParams },
- ListResourcesResultSchema,
- );
+ const result = await app.request({
+ method: "resources/list",
+ params: requestParams,
+ });
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2235,10 +2272,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request(
- { method: "resources/read", params: requestParams },
- ReadResourceResultSchema,
- );
+ const result = await app.request({
+ method: "resources/read",
+ params: requestParams,
+ });
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2286,10 +2323,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request(
- { method: "resources/templates/list", params: requestParams },
- ListResourceTemplatesResultSchema,
- );
+ const result = await app.request({
+ method: "resources/templates/list",
+ params: requestParams,
+ });
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2309,10 +2346,10 @@ describe("App <-> AppBridge integration", () => {
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
- const result = await app.request(
- { method: "prompts/list", params: requestParams },
- ListPromptsResultSchema,
- );
+ const result = await app.request({
+ method: "prompts/list",
+ params: requestParams,
+ });
expect(receivedRequests).toHaveLength(1);
expect(receivedRequests[0]).toMatchObject(requestParams);
@@ -2321,9 +2358,13 @@ describe("App <-> AppBridge integration", () => {
it("sendToolListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- app.setNotificationHandler(ToolListChangedNotificationSchema, (n) => {
- receivedNotifications.push(n.params);
- });
+ bridge.oncalltool = async () => ({ content: [] });
+ app.setNotificationHandler(
+ "notifications/tools/list_changed",
+ (notification) => {
+ receivedNotifications.push(notification.params);
+ },
+ );
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2336,9 +2377,13 @@ describe("App <-> AppBridge integration", () => {
it("sendResourceListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- app.setNotificationHandler(ResourceListChangedNotificationSchema, (n) => {
- receivedNotifications.push(n.params);
- });
+ bridge.onlistresources = async () => ({ resources: [] });
+ app.setNotificationHandler(
+ "notifications/resources/list_changed",
+ (notification) => {
+ receivedNotifications.push(notification.params);
+ },
+ );
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2351,9 +2396,13 @@ describe("App <-> AppBridge integration", () => {
it("sendPromptListChanged sends notification to app", async () => {
const receivedNotifications: unknown[] = [];
- app.setNotificationHandler(PromptListChangedNotificationSchema, (n) => {
- receivedNotifications.push(n.params);
- });
+ bridge.onlistprompts = async () => ({ prompts: [] });
+ app.setNotificationHandler(
+ "notifications/prompts/list_changed",
+ (notification) => {
+ receivedNotifications.push(notification.params);
+ },
+ );
await bridge.connect(bridgeTransport);
await app.connect(appTransport);
@@ -2366,6 +2415,184 @@ describe("App <-> AppBridge integration", () => {
});
});
+describe("AppBridge outer v2 Client proxy", () => {
+ let outerClient: Client;
+ let outerServer: Server;
+ let outerClientTransport: InMemoryTransport;
+ let outerServerTransport: InMemoryTransport;
+ let outerInitializeCount: number;
+
+ beforeEach(async () => {
+ [outerClientTransport, outerServerTransport] =
+ InMemoryTransport.createLinkedPair();
+ outerInitializeCount = 0;
+ const originalSend = outerClientTransport.send.bind(outerClientTransport);
+ outerClientTransport.send = async (message, options) => {
+ if ("method" in message && message.method === "initialize") {
+ outerInitializeCount++;
+ }
+ return originalSend(message, options);
+ };
+
+ outerServer = new Server(
+ { name: "ActualServer", version: "1.0.0" },
+ {
+ capabilities: {
+ tools: { listChanged: true },
+ resources: { listChanged: true },
+ prompts: { listChanged: true },
+ },
+ supportedProtocolVersions: ["2025-11-25"],
+ },
+ );
+ outerServer.setRequestHandler("tools/call", async (request) => ({
+ content: [
+ {
+ type: "text",
+ text: `outer:${request.params.name}`,
+ },
+ ],
+ }));
+
+ outerClient = new Client(
+ { name: "HostOuterClient", version: "1.0.0" },
+ {
+ capabilities: {},
+ versionNegotiation: { mode: "legacy" },
+ supportedProtocolVersions: ["2025-11-25"],
+ },
+ );
+ await outerServer.connect(outerServerTransport);
+ await outerClient.connect(outerClientTransport);
+ });
+
+ afterEach(async () => {
+ await outerClient.close().catch(() => {});
+ await outerServer.close().catch(() => {});
+ });
+
+ it("proxies through the outer Client without reconnecting it", async () => {
+ const bridge = new AppBridge(
+ outerClient,
+ testHostInfo,
+ testHostCapabilities,
+ );
+ const app = new App(testAppInfo, {}, { autoResize: false });
+ let [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
+
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+ expect(
+ await app.callServerTool({ name: "first", arguments: {} }),
+ ).toMatchObject({
+ content: [{ type: "text", text: "outer:first" }],
+ });
+ expect(outerInitializeCount).toBe(1);
+
+ await app.close();
+ await bridge.close();
+ [appTransport, bridgeTransport] = InMemoryTransport.createLinkedPair();
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+
+ expect(
+ await app.callServerTool({ name: "second", arguments: {} }),
+ ).toMatchObject({
+ content: [{ type: "text", text: "outer:second" }],
+ });
+ expect(outerInitializeCount).toBe(1);
+ expect(outerClient.getServerVersion()).toMatchObject({
+ name: "ActualServer",
+ version: "1.0.0",
+ });
+
+ await app.close();
+ await bridge.close();
+ });
+
+ it("propagates View cancellation to the actual outer Server", async () => {
+ let started!: () => void;
+ let aborted!: () => void;
+ const startedPromise = new Promise((resolve) => {
+ started = resolve;
+ });
+ const abortedPromise = new Promise((resolve) => {
+ aborted = resolve;
+ });
+ outerServer.setRequestHandler("tools/call", async (_request, context) => {
+ started();
+ await new Promise((resolve) => {
+ context.mcpReq.signal.addEventListener(
+ "abort",
+ () => {
+ aborted();
+ resolve();
+ },
+ { once: true },
+ );
+ });
+ return { content: [] };
+ });
+
+ const bridge = new AppBridge(
+ outerClient,
+ testHostInfo,
+ testHostCapabilities,
+ );
+ const app = new App(testAppInfo, {}, { autoResize: false });
+ const [appTransport, bridgeTransport] =
+ InMemoryTransport.createLinkedPair();
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+
+ const controller = new AbortController();
+ const result = app.callServerTool(
+ { name: "cancel-me", arguments: {} },
+ { signal: controller.signal },
+ );
+ await startedPromise;
+ controller.abort("test cancellation");
+ await abortedPromise;
+ await expect(result).rejects.toThrow();
+
+ await app.close();
+ await bridge.close();
+ });
+
+ it("forwards outer list-changed notifications to the App", async () => {
+ const bridge = new AppBridge(
+ outerClient,
+ testHostInfo,
+ testHostCapabilities,
+ );
+ const app = new App(testAppInfo, {}, { autoResize: false });
+ const [appTransport, bridgeTransport] =
+ InMemoryTransport.createLinkedPair();
+ const received: string[] = [];
+ app.setNotificationHandler("notifications/tools/list_changed", () => {
+ received.push("tools");
+ });
+ app.setNotificationHandler("notifications/resources/list_changed", () => {
+ received.push("resources");
+ });
+ app.setNotificationHandler("notifications/prompts/list_changed", () => {
+ received.push("prompts");
+ });
+
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+ await outerServer.sendToolListChanged();
+ await outerServer.sendResourceListChanged();
+ await outerServer.sendPromptListChanged();
+ await flush();
+
+ expect(received).toEqual(["tools", "resources", "prompts"]);
+
+ await app.close();
+ await bridge.close();
+ });
+});
+
describe("getToolUiResourceUri", () => {
describe("new nested format (_meta.ui.resourceUri)", () => {
it("extracts resourceUri from _meta.ui.resourceUri", () => {
@@ -2721,7 +2948,7 @@ describe("isToolVisibilityAppOnly", () => {
expect(a).toEqual([]);
});
- it("App.onEventDispatch merges hostcontext before listeners fire", async () => {
+ it("App merges hostcontext before listeners fire", async () => {
let seen: unknown;
app.addEventListener("hostcontextchanged", () => {
seen = app.getHostContext();
@@ -2766,33 +2993,90 @@ describe("isToolVisibilityAppOnly", () => {
testHostInfo,
testHostCapabilities,
);
- bridge2.setRequestHandler(
- // @ts-expect-error — exercising throw path with raw schema
- { shape: { method: { value: "test/method" } } },
- () => ({}),
+ 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.onopenlink = async () => ({});
expect(() => {
bridge2.setRequestHandler(
- // @ts-expect-error — exercising throw path with raw schema
- { shape: { method: { value: "test/method" } } },
+ "ui/open-link",
+ { params: z.object({}) },
() => ({}),
);
}).toThrow(/already registered/);
});
it("direct setNotificationHandler throws for event-mapped methods", () => {
- const app2 = new App(testAppInfo, {}, { autoResize: false });
- app2.addEventListener("toolinput", () => {});
+ const bridge2 = new AppBridge(
+ createMockClient() as Client,
+ testHostInfo,
+ testHostCapabilities,
+ );
+ bridge2.onsizechange = () => {};
expect(() => {
- app2.setNotificationHandler(
- // @ts-expect-error — exercising throw path with raw schema
- {
- shape: { method: { value: "ui/notifications/tool-input" } },
- },
+ 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 23383c4..1ac4db4 100644
--- a/src/app-bridge.ts
+++ b/src/app-bridge.ts
@@ -1,58 +1,53 @@
-import { Client } from "@modelcontextprotocol/sdk/client/index.js";
-import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
-import {
- CallToolRequest,
- CallToolRequestSchema,
- CallToolResult,
- CallToolResultSchema,
- CreateMessageRequest,
- CreateMessageRequestSchema,
- CreateMessageResult,
- CreateMessageResultWithTools,
- EmptyResult,
- Implementation,
- ListPromptsRequest,
- ListPromptsRequestSchema,
- ListPromptsResult,
- ListPromptsResultSchema,
- ListResourcesRequest,
- ListResourcesRequestSchema,
- ListResourcesResult,
- ListResourcesResultSchema,
- ListResourceTemplatesRequest,
- ListResourceTemplatesRequestSchema,
- ListResourceTemplatesResult,
- ListResourceTemplatesResultSchema,
- ListToolsRequest,
- ListToolsRequestSchema,
- ListToolsResultSchema,
- LoggingMessageNotification,
- LoggingMessageNotificationSchema,
- PingRequest,
- PingRequestSchema,
- PromptListChangedNotification,
- PromptListChangedNotificationSchema,
- ReadResourceRequest,
- ReadResourceRequestSchema,
- ReadResourceResult,
- ReadResourceResultSchema,
- ResourceListChangedNotification,
- ResourceListChangedNotificationSchema,
- Tool,
- ToolListChangedNotification,
- ToolListChangedNotificationSchema,
-} from "@modelcontextprotocol/sdk/types.js";
import {
Protocol,
- ProtocolOptions,
- RequestOptions,
-} from "@modelcontextprotocol/sdk/shared/protocol.js";
-import { ProtocolWithEvents } from "./events";
+ type BaseContext,
+ type Client,
+ type ProtocolOptions,
+ type CallToolRequest,
+ type CallToolResult,
+ type CreateMessageRequest,
+ type CreateMessageResult,
+ type CreateMessageResultWithTools,
+ type EmptyResult,
+ type Implementation,
+ type JSONRPCRequest,
+ type ListPromptsRequest,
+ type ListPromptsResult,
+ type ListResourcesRequest,
+ type ListResourcesResult,
+ type ListResourceTemplatesRequest,
+ type ListResourceTemplatesResult,
+ type ListToolsRequest,
+ type LoggingMessageNotification,
+ type PingRequest,
+ type PromptListChangedNotification,
+ type ReadResourceRequest,
+ type ReadResourceResult,
+ type RequestOptions,
+ type ResourceListChangedNotification,
+ type Result,
+ type Tool,
+ type ToolListChangedNotification,
+ type Transport,
+} from "@modelcontextprotocol/client";
+import {
+ EmptyResultSchema,
+ LoggingMessageNotificationSchema,
+} from "@modelcontextprotocol/core";
+import { EventDispatcher, MethodRegistry } from "./events.js";
+import {
+ toModernArgs,
+ type LegacyNotificationHandlerSetter,
+ type LegacyRequestHandlerSetter,
+} from "./legacy-handlers.js";
+import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
+
+type MethodSchema = ZodObject<{
+ method: ZodLiteral;
+ params: ZodType;
+}>;
import {
- type AppNotification,
- type AppRequest,
- type AppResult,
type McpUiSandboxResourceReadyNotification,
type McpUiSizeChangedNotification,
type McpUiToolCancelledNotification,
@@ -71,15 +66,19 @@ import {
McpUiInitializeRequest,
McpUiInitializeRequestSchema,
McpUiInitializeResult,
+ McpUiInitializeResultSchema,
McpUiMessageRequest,
McpUiMessageRequestSchema,
McpUiMessageResult,
+ McpUiMessageResultSchema,
McpUiOpenLinkRequest,
McpUiOpenLinkRequestSchema,
McpUiOpenLinkResult,
+ McpUiOpenLinkResultSchema,
McpUiDownloadFileRequest,
McpUiDownloadFileRequestSchema,
McpUiDownloadFileResult,
+ McpUiDownloadFileResultSchema,
McpUiResourceTeardownRequest,
McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification,
@@ -90,13 +89,16 @@ import {
McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeRequestSchema,
McpUiRequestDisplayModeResult,
+ McpUiRequestDisplayModeResultSchema,
McpUiResourcePermissions,
McpUiToolMeta,
-} from "./types";
-export * from "./types";
-export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./app";
-import { RESOURCE_URI_META_KEY } from "./app";
-export { PostMessageTransport } from "./message-transport";
+} from "./types.js";
+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";
/**
* Extract UI resource URI from tool metadata.
@@ -203,10 +205,10 @@ export function buildAllowAttribute(
*
* @property hostContext - Optional initial host context to provide to the view
*
- * @see `ProtocolOptions` from @modelcontextprotocol/sdk for available options
+ * @see `ProtocolOptions` from @modelcontextprotocol/client for available options
* @see {@link McpUiHostContext `McpUiHostContext`} for the hostContext structure
*/
-export type HostOptions = ProtocolOptions & {
+export type HostOptions = Omit & {
hostContext?: McpUiHostContext;
};
@@ -221,15 +223,12 @@ export const SUPPORTED_PROTOCOL_VERSIONS = [LATEST_PROTOCOL_VERSION];
/**
* Extra metadata passed to request handlers.
*
- * This type represents the additional context provided by the `Protocol` class
- * when handling requests, including abort signals and session information.
- * It is extracted from the MCP SDK's request handler signature.
+ * This type represents the additional context (`BaseContext`) provided by the
+ * base MCP SDK `Protocol` when handling requests.
*
* @internal
*/
-type RequestHandlerExtra = Parameters<
- Parameters[1]
->[1];
+type RequestHandlerExtra = BaseContext;
/**
* Maps DOM-style event names to their notification `params` types.
@@ -249,11 +248,11 @@ export type AppBridgeEventMap = {
/**
* Host-side bridge for communicating with a single View ({@link app!App `App`}).
*
- * `AppBridge` extends the MCP SDK's `Protocol` class and acts as a proxy between
- * the host application and a view running in an iframe. When an MCP client
- * is provided to the constructor, it automatically forwards MCP server capabilities
- * (tools, resources, prompts) to the view. It also handles the initialization
- * handshake.
+ * `AppBridge` extends the base MCP SDK's public `Protocol` class and acts as a
+ * proxy between the host application and a view running in an iframe. When a
+ * separate MCP client is provided to the constructor, it automatically forwards
+ * MCP server capabilities (tools, resources, prompts) to the view. It also
+ * handles the Apps initialization handshake.
*
* ## Architecture
*
@@ -303,47 +302,125 @@ export type AppBridgeEventMap = {
* await bridge.connect(transport);
* ```
*/
-export class AppBridge extends ProtocolWithEvents<
- AppRequest,
- AppNotification,
- AppResult,
- AppBridgeEventMap
-> {
+export class AppBridge extends Protocol {
private _appCapabilities?: McpUiAppCapabilities;
private _hostContext: McpUiHostContext = {};
private _appInfo?: Implementation;
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.
/**
- * Wrap every handler registered via `replaceRequestHandler` with a check
- * that the View has sent `ui/notifications/initialized`. Warns (never
- * throws) so lenient hosts keep working while still surfacing the
- * misordering that leaves strict hosts with a permanently hidden iframe.
- * `ui/initialize` and `ping` use `setRequestHandler` directly and are
- * intentionally exempt.
+ * 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: LegacyRequestHandlerSetter<
+ Protocol["setRequestHandler"]
+ > = (...args: unknown[]) => {
+ const [method, ...rest] = toModernArgs("request", args) ?? args;
+ this._methods.claim(method as string, "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: LegacyNotificationHandlerSetter<
+ Protocol["setNotificationHandler"]
+ > = (...args: unknown[]) => {
+ const [method, ...rest] = toModernArgs("notification", args) ?? args;
+ this._methods.claim(method as string, "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
+ * handler registration. AppBridge uses it to preserve the existing warning
+ * when a View calls host methods before `ui/notifications/initialized`,
+ * without adding overload-forwarding adapters around `setRequestHandler`.
+ * `ui/initialize` and `ping` are exempt because they are valid before the
+ * Apps-ready gate. This wraps registered callbacks, not JSON-RPC dispatch.
*
* @see {@link https://github.com/anthropics/claude-ai-mcp/issues/149 claude-ai-mcp#149}
*/
- private _baseReplaceRequestHandler = this.replaceRequestHandler;
- protected override replaceRequestHandler: Protocol<
- AppRequest,
- AppNotification,
- AppResult
- >["setRequestHandler"] = (schema, handler) => {
- this._baseReplaceRequestHandler(schema, (request, extra) => {
+ protected override _wrapHandler(
+ method: string,
+ handler: (request: JSONRPCRequest, context: BaseContext) => Promise,
+ ): (request: JSONRPCRequest, context: BaseContext) => Promise {
+ const wrapped = super._wrapHandler(method, handler);
+ if (method === "ui/initialize" || method === "ping") {
+ return wrapped;
+ }
+ return async (request, context) => {
if (!this._initializedReceived) {
console.warn(
- `[ext-apps] AppBridge received '${request.method}' before ` +
+ `[ext-apps] AppBridge received '${method}' before ` +
`ui/notifications/initialized. The View is calling host ` +
`methods before completing the handshake; it should await ` +
`app.connect() first.`,
);
}
- return handler(request, extra);
- });
- };
+ return wrapped(request, context);
+ };
+ }
- protected readonly eventSchemas = {
+ private readonly eventSchemas: {
+ [K in keyof AppBridgeEventMap]: MethodSchema;
+ } = {
sizechange: McpUiSizeChangedNotificationSchema,
sandboxready: McpUiSandboxProxyReadyNotificationSchema,
initialized: McpUiInitializedNotificationSchema,
@@ -351,6 +428,72 @@ export class AppBridge extends ProtocolWithEvents<
loggingmessage: LoggingMessageNotificationSchema,
};
+ protected setEventHandler(
+ event: K,
+ handler: ((params: AppBridgeEventMap[K]) => void) | undefined,
+ ): void {
+ this._ensureEventSlot(event);
+ this._events.setHandler(event, handler);
+ }
+
+ protected getEventHandler(
+ event: K,
+ ): ((params: AppBridgeEventMap[K]) => void) | undefined {
+ return this._events.getHandler(event);
+ }
+
+ addEventListener(
+ event: K,
+ handler: (params: AppBridgeEventMap[K]) => void,
+ ): void {
+ this._ensureEventSlot(event);
+ this._events.addEventListener(event, handler);
+ }
+
+ removeEventListener(
+ event: K,
+ handler: (params: AppBridgeEventMap[K]) => void,
+ ): void {
+ this._events.removeEventListener(event, handler);
+ }
+
+ private _ensureEventSlot(event: K): void {
+ if (this._registeredEvents.has(event)) return;
+ const schema = this.eventSchemas[event];
+ if (!schema) throw new Error(`Unknown event: ${String(event)}`);
+
+ const dispatch = (params: AppBridgeEventMap[K]) => {
+ if (event === "initialized") this._initializedReceived = true;
+ this._events.dispatch(event, params);
+ };
+
+ if (event === "loggingmessage") {
+ this.setNotificationHandler("notifications/message", (notification) => {
+ dispatch(notification.params as AppBridgeEventMap[K]);
+ });
+ } else {
+ this.setNotificationHandler(
+ schema.shape.method.value,
+ { params: schema.shape.params },
+ (params) => dispatch(params as AppBridgeEventMap[K]),
+ );
+ }
+ this._registeredEvents.add(event);
+ }
+
+ protected warnIfRequestHandlerReplaced(
+ name: string,
+ previous: unknown,
+ next: unknown,
+ ): void {
+ if (previous && next) {
+ console.warn(
+ `[MCP Apps] ${name} handler replaced. ` +
+ `Previous handler will no longer be called.`,
+ );
+ }
+ }
+
/**
* Create a new AppBridge instance.
*
@@ -359,8 +502,8 @@ export class AppBridge extends ProtocolWithEvents<
* between the View and the server. When `null`, you must register handlers
* manually using the {@link oncalltool `oncalltool`}, {@link onlistresources `onlistresources`}, etc. setters.
* @param _hostInfo - Host application identification (name and version)
- * @param _capabilities - Features and capabilities the host supports
- * @param options - Configuration options (inherited from Protocol)
+ * @param _hostCapabilities - Features and capabilities the host supports
+ * @param options - Configuration options (`ProtocolOptions` from the base MCP SDK plus `hostContext`)
*
* @example With MCP client (automatic forwarding)
* ```ts source="./app-bridge.examples.ts#AppBridge_constructor_withMcpClient"
@@ -387,22 +530,27 @@ export class AppBridge extends ProtocolWithEvents<
constructor(
private _client: Client | null,
private _hostInfo: Implementation,
- private _capabilities: McpUiHostCapabilities,
+ private _hostCapabilities: McpUiHostCapabilities,
options?: HostOptions,
) {
super(options);
+ this._methods.replace("notifications/cancelled");
+ this._methods.replace("notifications/progress");
- this.addEventListener("initialized", () => {
- this._initializedReceived = true;
- });
+ this._ensureEventSlot("initialized");
this._hostContext = options?.hostContext || {};
- this.setRequestHandler(McpUiInitializeRequestSchema, (request) =>
- this._oninitialize(request),
+ this.replaceRequestHandler(
+ "ui/initialize",
+ {
+ params: McpUiInitializeRequestSchema.shape.params,
+ result: McpUiInitializeResultSchema,
+ },
+ (params) => this._onAppsInitialize(params),
);
- this.setRequestHandler(PingRequestSchema, (request, extra) => {
+ this.replaceRequestHandler("ping", (request, extra) => {
this.onping?.(request.params, extra);
return {};
});
@@ -410,14 +558,47 @@ export class AppBridge extends ProtocolWithEvents<
// Default handler for requestDisplayMode - returns current mode from host context.
// Hosts can override this by setting bridge.onrequestdisplaymode = ...
this.replaceRequestHandler(
- McpUiRequestDisplayModeRequestSchema,
- (request) => {
+ "ui/request-display-mode",
+ {
+ params: McpUiRequestDisplayModeRequestSchema.shape.params,
+ result: McpUiRequestDisplayModeResultSchema,
+ },
+ () => {
const currentMode = this._hostContext.displayMode ?? "inline";
return { mode: currentMode };
},
);
}
+ /** @internal */
+ protected buildContext(ctx: BaseContext): BaseContext {
+ return ctx;
+ }
+
+ /**
+ * Verify that the view supports the capability required for the given request method.
+ * @internal
+ */
+ protected assertCapabilityForMethod(_method: string): void {
+ // Host-to-view requests are not capability-gated.
+ }
+
+ /**
+ * Verify that a request handler is registered and supported for the given method.
+ * @internal
+ */
+ protected assertRequestHandlerCapability(_method: string): void {
+ // Host handler registration is not capability-gated.
+ }
+
+ /**
+ * Verify that the host supports the capability required for the given notification method.
+ * @internal
+ */
+ protected assertNotificationCapability(_method: string): void {
+ // Host notifications are not capability-gated.
+ }
+
/**
* Get the view's capabilities discovered during initialization.
*
@@ -541,8 +722,7 @@ export class AppBridge extends ProtocolWithEvents<
* ```typescript
* bridge.onsandboxready = async () => {
* const resource = await mcpClient.request(
- * { method: "resources/read", params: { uri: "ui://my-app" } },
- * ReadResourceResultSchema
+ * { method: "resources/read", params: { uri: "ui://my-app" } }
* );
*
* bridge.sendSandboxResourceReady({
@@ -589,13 +769,13 @@ export class AppBridge extends ProtocolWithEvents<
* @deprecated Use {@link addEventListener `addEventListener("initialized", handler)`} instead — it composes with other listeners and supports cleanup via {@link removeEventListener `removeEventListener`}.
*/
get oninitialized():
- | ((params: McpUiInitializedNotification["params"]) => void)
+ | ((params?: McpUiInitializedNotification["params"]) => void)
| undefined {
return this.getEventHandler("initialized");
}
set oninitialized(
callback:
- | ((params: McpUiInitializedNotification["params"]) => void)
+ | ((params?: McpUiInitializedNotification["params"]) => void)
| undefined,
) {
this.setEventHandler("initialized", callback);
@@ -653,10 +833,14 @@ export class AppBridge extends ProtocolWithEvents<
this.warnIfRequestHandlerReplaced("onmessage", this._onmessage, callback);
this._onmessage = callback;
this.replaceRequestHandler(
- McpUiMessageRequestSchema,
- async (request, extra) => {
+ "ui/message",
+ {
+ params: McpUiMessageRequestSchema.shape.params,
+ result: McpUiMessageResultSchema,
+ },
+ async (params, extra) => {
if (!this._onmessage) throw new Error("No onmessage handler set");
- return this._onmessage(request.params, extra);
+ return this._onmessage(params, extra);
},
);
}
@@ -722,10 +906,14 @@ export class AppBridge extends ProtocolWithEvents<
this.warnIfRequestHandlerReplaced("onopenlink", this._onopenlink, callback);
this._onopenlink = callback;
this.replaceRequestHandler(
- McpUiOpenLinkRequestSchema,
- async (request, extra) => {
+ "ui/open-link",
+ {
+ params: McpUiOpenLinkRequestSchema.shape.params,
+ result: McpUiOpenLinkResultSchema,
+ },
+ async (params, extra) => {
if (!this._onopenlink) throw new Error("No onopenlink handler set");
- return this._onopenlink(request.params, extra);
+ return this._onopenlink(params, extra);
},
);
}
@@ -794,11 +982,15 @@ export class AppBridge extends ProtocolWithEvents<
);
this._ondownloadfile = callback;
this.replaceRequestHandler(
- McpUiDownloadFileRequestSchema,
- async (request, extra) => {
+ "ui/download-file",
+ {
+ params: McpUiDownloadFileRequestSchema.shape.params,
+ result: McpUiDownloadFileResultSchema,
+ },
+ async (params, extra) => {
if (!this._ondownloadfile)
throw new Error("No ondownloadfile handler set");
- return this._ondownloadfile(request.params, extra);
+ return this._ondownloadfile(params, extra);
},
);
}
@@ -894,11 +1086,15 @@ export class AppBridge extends ProtocolWithEvents<
);
this._onrequestdisplaymode = callback;
this.replaceRequestHandler(
- McpUiRequestDisplayModeRequestSchema,
- async (request, extra) => {
+ "ui/request-display-mode",
+ {
+ params: McpUiRequestDisplayModeRequestSchema.shape.params,
+ result: McpUiRequestDisplayModeResultSchema,
+ },
+ async (params, extra) => {
if (!this._onrequestdisplaymode)
throw new Error("No onrequestdisplaymode handler set");
- return this._onrequestdisplaymode(request.params, extra);
+ return this._onrequestdisplaymode(params, extra);
},
);
}
@@ -991,11 +1187,15 @@ export class AppBridge extends ProtocolWithEvents<
);
this._onupdatemodelcontext = callback;
this.replaceRequestHandler(
- McpUiUpdateModelContextRequestSchema,
- async (request, extra) => {
+ "ui/update-model-context",
+ {
+ params: McpUiUpdateModelContextRequestSchema.shape.params,
+ result: EmptyResultSchema,
+ },
+ async (params, extra) => {
if (!this._onupdatemodelcontext)
throw new Error("No onupdatemodelcontext handler set");
- return this._onupdatemodelcontext(request.params, extra);
+ return this._onupdatemodelcontext(params, extra);
},
);
}
@@ -1017,14 +1217,13 @@ export class AppBridge extends ProtocolWithEvents<
* bridge.oncalltool = async (params, extra) => {
* return mcpClient.request(
* { method: "tools/call", params },
- * CallToolResultSchema,
- * { signal: extra.signal },
+ * { signal: extra.mcpReq.signal },
* );
* };
* ```
*
- * @see `CallToolRequest` from @modelcontextprotocol/sdk for the request type
- * @see `CallToolResult` from @modelcontextprotocol/sdk for the result type
+ * @see `CallToolRequest` from @modelcontextprotocol/client for the request type
+ * @see `CallToolResult` from @modelcontextprotocol/client for the result type
*/
private _oncalltool?: (
params: CallToolRequest["params"],
@@ -1043,13 +1242,10 @@ export class AppBridge extends ProtocolWithEvents<
) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback;
- this.replaceRequestHandler(
- CallToolRequestSchema,
- async (request, extra) => {
- if (!this._oncalltool) throw new Error("No oncalltool handler set");
- return this._oncalltool(request.params, extra);
- },
- );
+ this.replaceRequestHandler("tools/call", async (request, extra) => {
+ if (!this._oncalltool) throw new Error("No oncalltool handler set");
+ return this._oncalltool(request.params, extra);
+ });
}
/**
@@ -1074,23 +1270,43 @@ export class AppBridge extends ProtocolWithEvents<
* ```ts source="./app-bridge.examples.ts#AppBridge_oncreatesamplingmessage_forwardToLlm"
* bridge.oncreatesamplingmessage = async (params, extra) => {
* // Apply rate limiting, user approval, cost controls here
- * return await myLlmProvider.complete(params, { signal: extra.signal });
+ * return await myLlmProvider.complete(params, {
+ * signal: extra.mcpReq.signal,
+ * });
* };
* ```
*
- * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type
- * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types
+ * @see `CreateMessageRequest` from @modelcontextprotocol/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(
- CreateMessageRequestSchema,
+ this.warnIfRequestHandlerReplaced(
+ "oncreatesamplingmessage",
+ this._oncreatesamplingmessage,
+ callback,
+ );
+ this._oncreatesamplingmessage = callback;
+ this.replaceRequestHandler(
+ "sampling/createMessage",
async (request, extra) => {
- return callback(request.params, extra);
+ if (!this._oncreatesamplingmessage) {
+ throw new Error("No oncreatesamplingmessage handler set");
+ }
+ return this._oncreatesamplingmessage(request.params, extra);
},
);
}
@@ -1107,12 +1323,12 @@ export class AppBridge extends ProtocolWithEvents<
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => {
+ * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => {
* bridge.sendToolListChanged();
* });
* ```
*
- * @see `ToolListChangedNotification` from @modelcontextprotocol/sdk for the notification type
+ * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type
*/
sendToolListChanged(params: ToolListChangedNotification["params"] = {}) {
return this.notification({
@@ -1138,14 +1354,13 @@ export class AppBridge extends ProtocolWithEvents<
* bridge.onlistresources = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/list", params },
- * ListResourcesResultSchema,
- * { signal: extra.signal },
+ * { signal: extra.mcpReq.signal },
* );
* };
* ```
*
- * @see `ListResourcesRequest` from @modelcontextprotocol/sdk for the request type
- * @see `ListResourcesResult` from @modelcontextprotocol/sdk for the result type
+ * @see `ListResourcesRequest` from @modelcontextprotocol/client for the request type
+ * @see `ListResourcesResult` from @modelcontextprotocol/client for the result type
*/
private _onlistresources?: (
params: ListResourcesRequest["params"],
@@ -1168,14 +1383,11 @@ export class AppBridge extends ProtocolWithEvents<
callback,
);
this._onlistresources = callback;
- this.replaceRequestHandler(
- ListResourcesRequestSchema,
- async (request, extra) => {
- if (!this._onlistresources)
- throw new Error("No onlistresources handler set");
- return this._onlistresources(request.params, extra);
- },
- );
+ this.replaceRequestHandler("resources/list", async (request, extra) => {
+ if (!this._onlistresources)
+ throw new Error("No onlistresources handler set");
+ return this._onlistresources(request.params, extra);
+ });
}
/**
@@ -1195,14 +1407,13 @@ export class AppBridge extends ProtocolWithEvents<
* bridge.onlistresourcetemplates = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/templates/list", params },
- * ListResourceTemplatesResultSchema,
- * { signal: extra.signal }
+ * { signal: extra.mcpReq.signal }
* );
* };
* ```
*
- * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/sdk for the request type
- * @see `ListResourceTemplatesResult` from @modelcontextprotocol/sdk for the result type
+ * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client for the request type
+ * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client for the result type
*/
private _onlistresourcetemplates?: (
params: ListResourceTemplatesRequest["params"],
@@ -1226,7 +1437,7 @@ export class AppBridge extends ProtocolWithEvents<
);
this._onlistresourcetemplates = callback;
this.replaceRequestHandler(
- ListResourceTemplatesRequestSchema,
+ "resources/templates/list",
async (request, extra) => {
if (!this._onlistresourcetemplates)
throw new Error("No onlistresourcetemplates handler set");
@@ -1252,14 +1463,13 @@ export class AppBridge extends ProtocolWithEvents<
* bridge.onreadresource = async (params, extra) => {
* return mcpClient.request(
* { method: "resources/read", params },
- * ReadResourceResultSchema,
- * { signal: extra.signal },
+ * { signal: extra.mcpReq.signal },
* );
* };
* ```
*
- * @see `ReadResourceRequest` from @modelcontextprotocol/sdk for the request type
- * @see `ReadResourceResult` from @modelcontextprotocol/sdk for the result type
+ * @see `ReadResourceRequest` from @modelcontextprotocol/client for the request type
+ * @see `ReadResourceResult` from @modelcontextprotocol/client for the result type
*/
private _onreadresource?: (
params: ReadResourceRequest["params"],
@@ -1282,14 +1492,11 @@ export class AppBridge extends ProtocolWithEvents<
callback,
);
this._onreadresource = callback;
- this.replaceRequestHandler(
- ReadResourceRequestSchema,
- async (request, extra) => {
- if (!this._onreadresource)
- throw new Error("No onreadresource handler set");
- return this._onreadresource(request.params, extra);
- },
- );
+ this.replaceRequestHandler("resources/read", async (request, extra) => {
+ if (!this._onreadresource)
+ throw new Error("No onreadresource handler set");
+ return this._onreadresource(request.params, extra);
+ });
}
/**
@@ -1304,12 +1511,12 @@ export class AppBridge extends ProtocolWithEvents<
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => {
+ * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => {
* bridge.sendResourceListChanged();
* });
* ```
*
- * @see `ResourceListChangedNotification` from @modelcontextprotocol/sdk for the notification type
+ * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type
*/
sendResourceListChanged(
params: ResourceListChangedNotification["params"] = {},
@@ -1337,14 +1544,13 @@ export class AppBridge extends ProtocolWithEvents<
* bridge.onlistprompts = async (params, extra) => {
* return mcpClient.request(
* { method: "prompts/list", params },
- * ListPromptsResultSchema,
- * { signal: extra.signal },
+ * { signal: extra.mcpReq.signal },
* );
* };
* ```
*
- * @see `ListPromptsRequest` from @modelcontextprotocol/sdk for the request type
- * @see `ListPromptsResult` from @modelcontextprotocol/sdk for the result type
+ * @see `ListPromptsRequest` from @modelcontextprotocol/client for the request type
+ * @see `ListPromptsResult` from @modelcontextprotocol/client for the result type
*/
private _onlistprompts?: (
params: ListPromptsRequest["params"],
@@ -1367,14 +1573,10 @@ export class AppBridge extends ProtocolWithEvents<
callback,
);
this._onlistprompts = callback;
- this.replaceRequestHandler(
- ListPromptsRequestSchema,
- async (request, extra) => {
- if (!this._onlistprompts)
- throw new Error("No onlistprompts handler set");
- return this._onlistprompts(request.params, extra);
- },
- );
+ this.replaceRequestHandler("prompts/list", async (request, extra) => {
+ if (!this._onlistprompts) throw new Error("No onlistprompts handler set");
+ return this._onlistprompts(request.params, extra);
+ });
}
/**
@@ -1389,12 +1591,12 @@ export class AppBridge extends ProtocolWithEvents<
* @example
* ```typescript
* // In your MCP client notification handler:
- * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => {
+ * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => {
* bridge.sendPromptListChanged();
* });
* ```
*
- * @see `PromptListChangedNotification` from @modelcontextprotocol/sdk for the notification type
+ * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type
*/
sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) {
return this.notification({
@@ -1403,46 +1605,6 @@ export class AppBridge extends ProtocolWithEvents<
});
}
- /**
- * Verify that the guest supports the capability required for the given request method.
- * @internal
- */
- assertCapabilityForMethod(method: AppRequest["method"]): void {
- // TODO
- }
-
- /**
- * Verify that a request handler is registered and supported for the given method.
- * @internal
- */
- assertRequestHandlerCapability(method: AppRequest["method"]): void {
- // TODO
- }
-
- /**
- * Verify that the host supports the capability required for the given notification method.
- * @internal
- */
- assertNotificationCapability(method: AppNotification["method"]): void {
- // TODO
- }
-
- /**
- * Verify that task creation is supported for the given request method.
- * @internal
- */
- protected assertTaskCapability(_method: string): void {
- throw new Error("Tasks are not supported in MCP Apps");
- }
-
- /**
- * Verify that task handler is supported for the given method.
- * @internal
- */
- protected assertTaskHandlerCapability(_method: string): void {
- throw new Error("Task handlers are not supported in MCP Apps");
- }
-
/**
* Get the host capabilities passed to the constructor.
*
@@ -1451,17 +1613,17 @@ export class AppBridge extends ProtocolWithEvents<
* @see {@link McpUiHostCapabilities `McpUiHostCapabilities`} for the capabilities structure
*/
getCapabilities(): McpUiHostCapabilities {
- return this._capabilities;
+ return this._hostCapabilities;
}
/**
* Handle the ui/initialize request from the guest.
* @internal
*/
- private async _oninitialize(
- request: McpUiInitializeRequest,
+ private async _onAppsInitialize(
+ params: McpUiInitializeRequest["params"],
): Promise {
- const requestedVersion = request.params.protocolVersion;
+ const requestedVersion = params.protocolVersion;
if (this._appInfo !== undefined) {
console.warn(
@@ -1472,8 +1634,8 @@ export class AppBridge extends ProtocolWithEvents<
);
}
- this._appCapabilities = request.params.appCapabilities;
- this._appInfo = request.params.appInfo;
+ this._appCapabilities = params.appCapabilities;
+ this._appInfo = params.appInfo;
const protocolVersion = SUPPORTED_PROTOCOL_VERSIONS.includes(
requestedVersion,
@@ -1636,10 +1798,10 @@ export class AppBridge extends ProtocolWithEvents<
*
* @example
* ```ts source="./app-bridge.examples.ts#AppBridge_sendToolResult_afterExecution"
- * const result = await mcpClient.request(
- * { method: "tools/call", params: { name: "get_weather", arguments: args } },
- * CallToolResultSchema,
- * );
+ * const result = await mcpClient.request({
+ * method: "tools/call",
+ * params: { name: "get_weather", arguments: args },
+ * });
* bridge.sendToolResult(result);
* ```
*
@@ -1765,11 +1927,7 @@ export class AppBridge extends ProtocolWithEvents<
* @returns Promise resolving to the tool call result
*/
callTool(params: CallToolRequest["params"], options?: RequestOptions) {
- return this.request(
- { method: "tools/call", params },
- CallToolResultSchema,
- options,
- );
+ return this.request({ method: "tools/call", params }, options);
}
/**
@@ -1782,11 +1940,7 @@ export class AppBridge extends ProtocolWithEvents<
* @returns Promise resolving to the list of tools
*/
listTools(params: ListToolsRequest["params"], options?: RequestOptions) {
- return this.request(
- { method: "tools/list", params },
- ListToolsResultSchema,
- options,
- );
+ return this.request({ method: "tools/list", params }, options);
}
/**
@@ -1862,13 +2016,12 @@ export class AppBridge extends ProtocolWithEvents<
this.oncalltool = async (params, extra) => {
return this._client!.request(
{ method: "tools/call", params },
- CallToolResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
if (serverCapabilities.tools.listChanged) {
this._client.setNotificationHandler(
- ToolListChangedNotificationSchema,
+ "notifications/tools/list_changed",
(n) => this.sendToolListChanged(n.params),
);
}
@@ -1877,27 +2030,24 @@ export class AppBridge extends ProtocolWithEvents<
this.onlistresources = async (params, extra) => {
return this._client!.request(
{ method: "resources/list", params },
- ListResourcesResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
this.onlistresourcetemplates = async (params, extra) => {
return this._client!.request(
{ method: "resources/templates/list", params },
- ListResourceTemplatesResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
this.onreadresource = async (params, extra) => {
return this._client!.request(
{ method: "resources/read", params },
- ReadResourceResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
if (serverCapabilities.resources.listChanged) {
this._client.setNotificationHandler(
- ResourceListChangedNotificationSchema,
+ "notifications/resources/list_changed",
(n) => this.sendResourceListChanged(n.params),
);
}
@@ -1906,23 +2056,21 @@ export class AppBridge extends ProtocolWithEvents<
this.onlistprompts = async (params, extra) => {
return this._client!.request(
{ method: "prompts/list", params },
- ListPromptsResultSchema,
- { signal: extra.signal },
+ { signal: extra.mcpReq.signal },
);
};
if (serverCapabilities.prompts.listChanged) {
this._client.setNotificationHandler(
- PromptListChangedNotificationSchema,
+ "notifications/prompts/list_changed",
(n) => this.sendPromptListChanged(n.params),
);
}
}
}
- // MCP-UI specific handlers are registered by the host component
- // after the proxy is created. The standard MCP initialization
- // (via oninitialized callback set in constructor) handles the ready signal.
-
+ // Protocol.connect only wires the transport — no MCP handshake happens on
+ // the iframe channel. The View drives ui/initialize;
+ // ui/notifications/initialized remains the public View-ready signal.
return super.connect(transport);
}
}
diff --git a/src/app.examples.ts b/src/app.examples.ts
index 81bdd44..7d475ef 100644
--- a/src/app.examples.ts
+++ b/src/app.examples.ts
@@ -7,11 +7,8 @@
* @module
*/
-import type { Tool } from "@modelcontextprotocol/sdk/types.js";
-import type {
- McpServer,
- ToolCallback,
-} from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { SamplingMessage, Tool } from "@modelcontextprotocol/client";
+import type { McpServer, ToolCallback } from "@modelcontextprotocol/server";
import {
App,
PostMessageTransport,
@@ -369,7 +366,7 @@ async function App_createSamplingMessage_simple(app: App) {
*/
async function App_createSamplingMessage_withTools(
app: App,
- messages: import("@modelcontextprotocol/sdk/types.js").SamplingMessage[],
+ messages: SamplingMessage[],
) {
//#region App_createSamplingMessage_withTools
if (!app.getHostCapabilities()?.sampling?.tools) return;
diff --git a/src/app.test.ts b/src/app.test.ts
new file mode 100644
index 0000000..8420e81
--- /dev/null
+++ b/src/app.test.ts
@@ -0,0 +1,381 @@
+import { afterEach, describe, expect, it } from "bun:test";
+import {
+ InMemoryTransport,
+ Server,
+ type JSONRPCMessage,
+} from "@modelcontextprotocol/server";
+import { z } from "zod/v4";
+
+import { App } from "./app.js";
+import {
+ LATEST_PROTOCOL_VERSION,
+ McpUiInitializeRequestSchema,
+ McpUiInitializeResultSchema,
+ McpUiInitializedNotificationSchema,
+} from "./types.js";
+
+type ConnectedPair = {
+ app: App;
+ server: Server;
+ sentByApp: JSONRPCMessage[];
+ appsInitialized: Promise;
+};
+
+async function connectPair(
+ app = new App(
+ { name: "view-app", version: "1.0.0" },
+ { tools: { listChanged: true } },
+ { autoResize: false },
+ ),
+): Promise {
+ const server = new Server(
+ { name: "inner-mcp-server", version: "0.0.0" },
+ {
+ capabilities: { resources: {}, tools: {} },
+ },
+ );
+ const [appTransport, serverTransport] = InMemoryTransport.createLinkedPair();
+ const sentByApp: JSONRPCMessage[] = [];
+ const send = appTransport.send.bind(appTransport);
+ appTransport.send = async (message, options) => {
+ sentByApp.push(message);
+ await send(message, options);
+ };
+
+ let resolveAppsInitialized!: () => void;
+ const appsInitialized = new Promise((resolve) => {
+ resolveAppsInitialized = resolve;
+ });
+
+ server.setRequestHandler(
+ "ui/initialize",
+ {
+ params: McpUiInitializeRequestSchema.shape.params,
+ result: McpUiInitializeResultSchema,
+ },
+ (params) => {
+ expect(params.protocolVersion).toBe(LATEST_PROTOCOL_VERSION);
+ return {
+ protocolVersion: LATEST_PROTOCOL_VERSION,
+ hostCapabilities: { openLinks: {} },
+ hostInfo: { name: "apps-host", version: "2.0.0" },
+ hostContext: { theme: "dark" as const, locale: "en-US" },
+ };
+ },
+ );
+ server.setNotificationHandler(
+ "ui/notifications/initialized",
+ { params: McpUiInitializedNotificationSchema.shape.params },
+ () => resolveAppsInitialized(),
+ );
+ server.setRequestHandler("tools/call", (request) => ({
+ content: [{ type: "text", text: `called ${request.params.name}` }],
+ }));
+ server.setRequestHandler("resources/read", (request) => ({
+ contents: [{ uri: request.params.uri, text: "resource body" }],
+ }));
+ server.setRequestHandler("resources/list", () => ({
+ resources: [{ uri: "test://resource", name: "Test resource" }],
+ }));
+ server.setRequestHandler("sampling/createMessage", () => ({
+ model: "test-model",
+ role: "assistant",
+ content: { type: "text", text: "sampled" },
+ }));
+
+ await server.connect(serverTransport);
+ await app.connect(appTransport);
+ await appsInitialized;
+
+ return { app, server, sentByApp, appsInitialized };
+}
+
+const connected: Array> = [];
+
+afterEach(async () => {
+ await Promise.all(
+ connected
+ .splice(0)
+ .flatMap(({ app, server }) => [app.close(), server.close()]),
+ );
+});
+
+describe("App base MCP SDK v2 Protocol migration", () => {
+ it("performs only the Apps handshake — no MCP initialize on the wire", async () => {
+ const pair = await connectPair();
+ connected.push(pair);
+
+ const lifecycleMethods = pair.sentByApp
+ .filter(
+ (message): message is JSONRPCMessage & { method: string } =>
+ "method" in message,
+ )
+ .map((message) => message.method)
+ .filter((method) =>
+ [
+ "initialize",
+ "notifications/initialized",
+ "ui/initialize",
+ "ui/notifications/initialized",
+ ].includes(method),
+ );
+
+ // The filter above includes the core MCP lifecycle methods, so this
+ // equality also proves no `initialize`/`notifications/initialized` was sent
+ // — the wire contract deployed v1 hosts depend on.
+ expect(lifecycleMethods).toEqual([
+ "ui/initialize",
+ "ui/notifications/initialized",
+ ]);
+ });
+
+ it("uses ui/initialize as the authority for Apps host state", async () => {
+ const pair = await connectPair();
+ connected.push(pair);
+
+ expect(pair.app.getHostVersion()).toEqual({
+ name: "apps-host",
+ version: "2.0.0",
+ });
+ expect(pair.app.getHostCapabilities()).toEqual({ openLinks: {} });
+ expect(pair.app.getHostContext()).toEqual({
+ theme: "dark",
+ locale: "en-US",
+ });
+ });
+
+ it("preserves registered App tools with the base MCP SDK v2 Client handler context", async () => {
+ const pair = await connectPair();
+ connected.push(pair);
+ let receivedSignal: AbortSignal | undefined;
+
+ pair.app.registerTool(
+ "greet",
+ { description: "Greets a person" },
+ async (extra) => {
+ receivedSignal = extra.mcpReq.signal;
+ return { content: [{ type: "text", text: "hello" }] };
+ },
+ );
+
+ const listed = await pair.server.request({
+ method: "tools/list",
+ params: {},
+ });
+ const result = await pair.server.request({
+ method: "tools/call",
+ params: { name: "greet", arguments: {} },
+ });
+
+ expect(listed.tools.map((tool) => tool.name)).toEqual(["greet"]);
+ expect(result.content).toEqual([{ type: "text", text: "hello" }]);
+ expect(receivedSignal).toBeInstanceOf(AbortSignal);
+ });
+
+ it("uses base MCP SDK v2 method-keyed sends for standard MCP requests", async () => {
+ const pair = await connectPair();
+ connected.push(pair);
+
+ const toolResult = await pair.app.callServerTool({
+ name: "refresh",
+ arguments: {},
+ });
+ const resource = await pair.app.readServerResource({
+ uri: "test://resource",
+ });
+ const resources = await pair.app.listServerResources();
+ const sample = await pair.app.createSamplingMessage({
+ messages: [{ role: "user", content: { type: "text", text: "hello" } }],
+ maxTokens: 20,
+ });
+
+ expect(toolResult.content).toEqual([
+ { type: "text", text: "called refresh" },
+ ]);
+ expect(resource.contents[0]).toEqual({
+ uri: "test://resource",
+ text: "resource body",
+ });
+ expect(resources.resources.map(({ uri }) => uri)).toEqual([
+ "test://resource",
+ ]);
+ expect(sample.content).toEqual({ type: "text", text: "sampled" });
+ });
+
+ it("preserves reconnect behavior after close", async () => {
+ const first = await connectPair();
+ await first.app.close();
+ await first.server.close();
+
+ const second = await connectPair(first.app);
+ connected.push(second);
+
+ expect(second.app.getHostVersion()).toEqual({
+ name: "apps-host",
+ version: "2.0.0",
+ });
+ });
+
+ it("preserves auto-resize setup after Apps initialization", async () => {
+ const app = new App(
+ { name: "view-app", version: "1.0.0" },
+ {},
+ { autoResize: true },
+ );
+ let setupCalls = 0;
+ app.setupSizeChangedNotifications = () => {
+ setupCalls += 1;
+ return () => {};
+ };
+
+ const pair = await connectPair(app);
+ connected.push(pair);
+
+ expect(setupCalls).toBe(1);
+ });
+
+ it("preserves strict late-handler timing errors", async () => {
+ const app = new App(
+ { name: "view-app", version: "1.0.0" },
+ {},
+ { autoResize: false, strict: true },
+ );
+ const pair = await connectPair(app);
+ connected.push(pair);
+
+ expect(() => {
+ app.ontoolinput = () => {};
+ }).toThrow(/handler registered after connect/);
+ });
+
+ it("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("test/method", { params }, () => {
+ handledBy = 1;
+ return {};
+ });
+ 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: "test/method", params: {} },
+ z.object({}),
+ );
+ expect(handledBy).toBe(2);
+ });
+
+ it("direct setNotificationHandler throws when a handler is already registered", async () => {
+ const pair = await connectPair();
+ connected.push(pair);
+ const calls: number[] = [];
+ const params = z.object({});
+
+ pair.app.setNotificationHandler("test/notification", { params }, () => {
+ calls.push(1);
+ });
+ expect(() => {
+ pair.app.setNotificationHandler("test/notification", { params }, () => {
+ calls.push(2);
+ });
+ }).toThrow(/already registered/);
+ await pair.server.notification({
+ method: "test/notification",
+ params: {},
+ });
+
+ 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 adfad5c..89c6c35 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -1,40 +1,45 @@
import {
+ Protocol,
+ type BaseContext,
+ type ProtocolOptions,
type RequestOptions,
- mergeCapabilities,
- ProtocolOptions,
-} from "@modelcontextprotocol/sdk/shared/protocol.js";
-
+ type CallToolRequest,
+ type CallToolResult,
+ type CreateMessageRequest,
+ type CreateMessageResult,
+ type CreateMessageResultWithTools,
+ type Implementation,
+ type ListResourcesRequest,
+ type ListResourcesResult,
+ type ListToolsRequest,
+ type ListToolsResult,
+ type LoggingMessageNotification,
+ type ReadResourceRequest,
+ type ReadResourceResult,
+ type Tool,
+ type ToolAnnotations,
+ type ToolListChangedNotification,
+ type Transport,
+} from "@modelcontextprotocol/client";
+import { EmptyResultSchema } from "@modelcontextprotocol/core";
+export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js";
+import { EventDispatcher, MethodRegistry } from "./events.js";
import {
- CallToolRequest,
- CallToolRequestSchema,
- CallToolResult,
- CallToolResultSchema,
- CreateMessageRequest,
- CreateMessageResult,
- CreateMessageResultSchema,
- CreateMessageResultWithTools,
- CreateMessageResultWithToolsSchema,
- EmptyResultSchema,
- Implementation,
- ListResourcesRequest,
- ListResourcesResult,
- ListResourcesResultSchema,
- ListToolsRequest,
- ListToolsRequestSchema,
- ListToolsResult,
- LoggingMessageNotification,
- PingRequestSchema,
- ReadResourceRequest,
- ReadResourceResult,
- ReadResourceResultSchema,
- Tool,
- ToolAnnotations,
- ToolListChangedNotification,
-} from "@modelcontextprotocol/sdk/types.js";
-import { AppNotification, AppRequest, AppResult } from "./types";
-import { ProtocolWithEvents } from "./events";
-export { ProtocolWithEvents };
-import { PostMessageTransport } from "./message-transport";
+ toModernArgs,
+ type LegacyNotificationHandlerSetter,
+ type LegacyRequestHandlerSetter,
+} from "./legacy-handlers.js";
+export type {
+ LegacyMethodSchema,
+ LegacyNotificationHandler,
+ LegacyNotificationHandlerSetter,
+ LegacyRequestHandler,
+ LegacyRequestHandlerExtra,
+ LegacyRequestHandlerSetter,
+} from "./legacy-handlers.js";
+export { EventDispatcher } from "./events.js";
+
+import { PostMessageTransport } from "./message-transport.js";
import {
LATEST_PROTOCOL_VERSION,
McpUiAppCapabilities,
@@ -55,6 +60,7 @@ import {
McpUiResourceTeardownRequest,
McpUiResourceTeardownRequestSchema,
McpUiResourceTeardownResult,
+ McpUiResourceTeardownResultSchema,
McpUiRequestTeardownNotification,
McpUiSizeChangedNotification,
McpUiToolCancelledNotification,
@@ -67,104 +73,60 @@ import {
McpUiToolResultNotificationSchema,
McpUiRequestDisplayModeRequest,
McpUiRequestDisplayModeResultSchema,
-} from "./types";
-import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
+} from "./types.js";
import {
StandardSchemaV1,
standardSchemaToJsonSchema,
validateStandardSchema,
-} from "./standard-schema";
-import { z } from "zod/v4";
+} 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;
+}>;
+
+function mergeAppCapabilities(
+ base: McpUiAppCapabilities,
+ additional: McpUiAppCapabilities,
+): McpUiAppCapabilities {
+ const result = { ...base, ...additional };
+ if (additional.experimental !== undefined) {
+ result.experimental = {
+ ...base.experimental,
+ ...additional.experimental,
+ };
+ }
+ if (additional.tools !== undefined) {
+ result.tools = { ...base.tools, ...additional.tools };
+ }
+ return result;
+}
export type {
StandardSchemaV1,
StandardSchemaWithJSON,
-} from "./standard-schema";
+} from "./standard-schema.js";
-export { PostMessageTransport } from "./message-transport";
-export * from "./types";
+export { PostMessageTransport } from "./message-transport.js";
+export * from "./types.js";
export {
applyHostStyleVariables,
applyHostFonts,
getDocumentTheme,
applyDocumentTheme,
-} from "./styles";
-
-/**
- * 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
- * }
- * ```
- */
-export const RESOURCE_URI_META_KEY = "ui/resourceUri";
-
-/**
- * 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";
+} from "./styles.js";
/**
* Options for configuring {@link App `App`} behavior.
*
- * Extends `ProtocolOptions` from the MCP SDK with `App`-specific configuration.
+ * Extends `ProtocolOptions` from the base MCP SDK with `App`-specific configuration.
*
- * @see `ProtocolOptions` from @modelcontextprotocol/sdk for inherited options
+ * @see `ProtocolOptions` from @modelcontextprotocol/client for inherited options
*/
-export type AppOptions = ProtocolOptions & {
+export type AppOptions = Omit & {
/**
* Automatically report size changes to the host using `ResizeObserver`.
*
@@ -204,9 +166,7 @@ export type AppOptions = ProtocolOptions & {
allowUnsafeEval?: boolean;
};
-type RequestHandlerExtra = Parameters<
- Parameters[1]
->[1];
+type RequestHandlerExtra = BaseContext;
/**
* Result of an app-registered tool callback. When `Out` is provided,
@@ -228,7 +188,7 @@ export type AppToolResult<
* validated/parsed input; when `In` is `undefined`, the callback receives only
* `extra`. When `Out` is provided, the return's `structuredContent` is typed.
*
- * Mirrors `ToolCallback` from `@modelcontextprotocol/sdk/server/mcp.js` but is
+ * Mirrors `ToolCallback` from `@modelcontextprotocol/server` but is
* parameterized over {@link StandardSchemaV1} instead of zod, so any
* Standard-Schema-compatible library (Zod, ArkType, Valibot, …) can be used.
*/
@@ -246,7 +206,7 @@ export type AppToolCallback<
/**
* Handle returned by {@link App.registerTool}. Mirrors `RegisteredTool` from
- * `@modelcontextprotocol/sdk/server/mcp.js` but stores
+ * `@modelcontextprotocol/server` but stores
* {@link StandardSchemaV1} schemas.
*/
export type RegisteredAppTool = {
@@ -271,9 +231,9 @@ export type RegisteredAppTool = {
/**
* Maps DOM-style event names to their notification `params` types.
*
- * Used by {@link App `App`} (which extends {@link ProtocolWithEvents `ProtocolWithEvents`})
- * to provide type-safe `addEventListener` / `removeEventListener` and
- * singular `on*` handler support.
+ * Used by {@link App `App`} and its composed {@link EventDispatcher} to
+ * provide type-safe `addEventListener` / `removeEventListener` and singular
+ * `on*` handler support.
*/
export type AppEventMap = {
toolinput: McpUiToolInputNotification["params"];
@@ -287,9 +247,9 @@ export type AppEventMap = {
* Main class for MCP Apps to communicate with their host.
*
* The `App` class provides a framework-agnostic way to build interactive MCP Apps
- * that run inside host applications. It extends the MCP SDK's `Protocol` class and
- * handles the connection lifecycle, initialization handshake, and bidirectional
- * communication with the host.
+ * that run inside host applications. It extends the base MCP SDK's public
+ * `Protocol` class, composes an {@link EventDispatcher}, and adds the Apps
+ * handshake, lifecycle, and high-level APIs.
*
* ## Architecture
*
@@ -304,16 +264,16 @@ export type AppEventMap = {
* 3. **Interactive**: Send requests, receive notifications, call tools
* 4. **Teardown**: Host sends teardown request before unmounting
*
- * ## Inherited Methods
+ * ## Protocol and event methods
*
- * As a subclass of {@link ProtocolWithEvents `ProtocolWithEvents`}, `App` inherits:
+ * From the base MCP SDK's `Protocol`, `App` provides:
* - `setRequestHandler()` - Register handlers for requests from host
* - `setNotificationHandler()` - Register handlers for notifications from host
+ *
+ * From its composed {@link EventDispatcher}, `App` also provides:
* - `addEventListener()` - Append a listener for a notification event (multi-listener)
* - `removeEventListener()` - Remove a previously added listener
*
- * @see {@link ProtocolWithEvents `ProtocolWithEvents`} for the DOM-model event system
- *
* ## Notification Setters (DOM-model `on*` handlers)
*
* For common notifications, the `App` class provides getter/setter properties
@@ -342,17 +302,90 @@ export type AppEventMap = {
* await app.connect();
* ```
*/
-export class App extends ProtocolWithEvents<
- AppRequest,
- AppNotification,
- AppResult,
- AppEventMap
-> {
+export class App extends Protocol {
private _hostCapabilities?: McpUiHostCapabilities;
private _hostInfo?: Implementation;
private _hostContext?: McpUiHostContext;
private _registeredTools: { [name: string]: RegisteredAppTool } = {};
private _initializedSent = false;
+ private readonly _registeredEvents = new Set();
+ private readonly _events = new EventDispatcher();
+ 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: LegacyRequestHandlerSetter<
+ Protocol["setRequestHandler"]
+ > = (...args: unknown[]) => {
+ const [method, ...rest] = toModernArgs("request", args) ?? args;
+ this._methods.claim(method as string, "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: LegacyNotificationHandlerSetter<
+ Protocol["setNotificationHandler"]
+ > = (...args: unknown[]) => {
+ const [method, ...rest] = toModernArgs("notification", args) ?? args;
+ this._methods.claim(method as string, "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
@@ -378,7 +411,9 @@ export class App extends ProtocolWithEvents<
console.warn(`${msg}. This will throw in a future release.`);
}
- protected readonly eventSchemas = {
+ private readonly eventSchemas: {
+ [K in keyof AppEventMap]: MethodSchema;
+ } = {
toolinput: McpUiToolInputNotificationSchema,
toolinputpartial: McpUiToolInputPartialNotificationSchema,
toolresult: McpUiToolResultNotificationSchema,
@@ -427,28 +462,65 @@ export class App extends ProtocolWithEvents<
console.warn(msg);
}
- protected override setEventHandler(
+ protected setEventHandler(
event: K,
handler: ((params: AppEventMap[K]) => void) | undefined,
): void {
if (handler) this._assertHandlerTiming(event);
- super.setEventHandler(event, handler);
+ this._ensureEventSlot(event);
+ this._events.setHandler(event, handler);
}
- override addEventListener(
+ addEventListener(
event: K,
handler: (params: AppEventMap[K]) => void,
): void {
this._assertHandlerTiming(event);
- super.addEventListener(event, handler);
+ this._ensureEventSlot(event);
+ this._events.addEventListener(event, handler);
}
- protected override onEventDispatch(
+ removeEventListener(
event: K,
- params: AppEventMap[K],
+ handler: (params: AppEventMap[K]) => void,
): void {
- if (event === "hostcontextchanged") {
- this._hostContext = { ...this._hostContext, ...params };
+ this._events.removeEventListener(event, handler);
+ }
+
+ private _ensureEventSlot(event: K): void {
+ if (this._registeredEvents.has(event)) return;
+ const schema = this.eventSchemas[event];
+ if (!schema) throw new Error(`Unknown event: ${String(event)}`);
+ this.setNotificationHandler(
+ schema.shape.method.value,
+ { params: schema.shape.params },
+ (params) => {
+ const eventParams = params as AppEventMap[K];
+ if (event === "hostcontextchanged") {
+ this._hostContext = { ...this._hostContext, ...eventParams };
+ }
+ this._events.dispatch(event, eventParams);
+ },
+ );
+ this._registeredEvents.add(event);
+ }
+
+ protected getEventHandler(
+ event: K,
+ ): ((params: AppEventMap[K]) => void) | undefined {
+ return this._events.getHandler(event);
+ }
+
+ protected warnIfRequestHandlerReplaced(
+ name: string,
+ previous: unknown,
+ next: unknown,
+ ): void {
+ if (previous && next) {
+ console.warn(
+ `[MCP Apps] ${name} handler replaced. ` +
+ `Previous handler will no longer be called.`,
+ );
}
}
@@ -456,7 +528,7 @@ export class App extends ProtocolWithEvents<
* Create a new MCP App instance.
*
* @param _appInfo - App identification (name and version)
- * @param _capabilities - Features and capabilities this app provides
+ * @param _appCapabilities - Features and capabilities this app provides
* @param options - Configuration options including `autoResize` behavior
*
* @example
@@ -470,33 +542,44 @@ export class App extends ProtocolWithEvents<
*/
constructor(
private _appInfo: Implementation,
- private _capabilities: McpUiAppCapabilities = {},
+ private _appCapabilities: McpUiAppCapabilities = {},
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(PingRequestSchema, (request) => {
+ this.replaceRequestHandler("ping", (request) => {
console.log("Received ping:", request.params);
return {};
});
- // Eagerly register the hostcontextchanged event slot so that
- // onEventDispatch (which merges into _hostContext) fires even if the
- // user never assigns onhostcontextchanged or calls addEventListener.
+ // Eagerly register hostcontextchanged so the owner-side context merge runs
+ // even if the user never assigns a handler or adds a listener.
this.setEventHandler("hostcontextchanged", undefined);
}
- private registerCapabilities(capabilities: McpUiAppCapabilities): void {
+ /** @internal */
+ protected buildContext(ctx: BaseContext): BaseContext {
+ return ctx;
+ }
+
+ private registerAppCapabilities(capabilities: McpUiAppCapabilities): void {
if (this.transport) {
throw new Error(
"Cannot register capabilities after transport is established",
);
}
- this._capabilities = mergeCapabilities(this._capabilities, capabilities);
+ this._appCapabilities = mergeAppCapabilities(
+ this._appCapabilities,
+ capabilities,
+ );
}
registerTool<
@@ -519,7 +602,7 @@ export class App extends ProtocolWithEvents<
}
const app = this;
const notify = () => {
- if (app._initializedSent && app._capabilities.tools?.listChanged) {
+ if (app._initializedSent && app._appCapabilities.tools?.listChanged) {
void app.sendToolListChanged();
}
};
@@ -591,8 +674,8 @@ export class App extends ProtocolWithEvents<
// to declare { tools: {} } in the constructor just to use registerTool.
// Only do this pre-connect; post-connect the capability was already
// advertised (or wasn't) and can't change.
- if (!this._capabilities.tools && !this.transport) {
- this.registerCapabilities({ tools: { listChanged: true } });
+ if (!this._appCapabilities.tools && !this.transport) {
+ this.registerAppCapabilities({ tools: { listChanged: true } });
}
this.ensureToolHandlersInitialized();
@@ -924,9 +1007,9 @@ export class App extends ProtocolWithEvents<
* without replacing.
*
* Notification params are automatically merged into the internal host context
- * via {@link onEventDispatch `onEventDispatch`} before any handler or listener
- * fires. This means {@link getHostContext `getHostContext`} will return the
- * updated values even before your callback runs.
+ * before any handler or listener fires. This means
+ * {@link getHostContext `getHostContext`} will return the updated values even
+ * before your callback runs.
*
* Register handlers before calling {@link connect `connect`} to avoid missing notifications.
*
@@ -1005,10 +1088,14 @@ export class App extends ProtocolWithEvents<
this.warnIfRequestHandlerReplaced("onteardown", this._onteardown, callback);
this._onteardown = callback;
this.replaceRequestHandler(
- McpUiResourceTeardownRequestSchema,
- (request, extra) => {
+ "ui/resource-teardown",
+ {
+ params: McpUiResourceTeardownRequestSchema.shape.params,
+ result: McpUiResourceTeardownResultSchema,
+ },
+ (params, extra) => {
if (!this._onteardown) throw new Error("No onteardown handler set");
- return this._onteardown(request.params, extra);
+ return this._onteardown(params, extra);
},
);
}
@@ -1058,7 +1145,7 @@ export class App extends ProtocolWithEvents<
) {
this.warnIfRequestHandlerReplaced("oncalltool", this._oncalltool, callback);
this._oncalltool = callback;
- this.replaceRequestHandler(CallToolRequestSchema, (request, extra) => {
+ this.replaceRequestHandler("tools/call", (request, extra) => {
if (!this._oncalltool) throw new Error("No oncalltool handler set");
return this._oncalltool(request.params, extra);
});
@@ -1129,7 +1216,7 @@ export class App extends ProtocolWithEvents<
callback,
);
this._onlisttools = callback;
- this.replaceRequestHandler(ListToolsRequestSchema, (request, extra) => {
+ this.replaceRequestHandler("tools/list", (request, extra) => {
if (!this._onlisttools) throw new Error("No onlisttools handler set");
return this._onlisttools(request.params, extra);
});
@@ -1139,7 +1226,7 @@ export class App extends ProtocolWithEvents<
* Verify that the host supports the capability required for the given request method.
* @internal
*/
- assertCapabilityForMethod(method: AppRequest["method"]): void {
+ protected override assertCapabilityForMethod(method: string): void {
switch (method) {
case "sampling/createMessage":
if (!this._hostCapabilities?.sampling) {
@@ -1155,21 +1242,21 @@ export class App extends ProtocolWithEvents<
* Verify that the app declared the capability required for the given request method.
* @internal
*/
- assertRequestHandlerCapability(method: AppRequest["method"]): void {
+ protected override assertRequestHandlerCapability(method: string): void {
switch (method) {
case "tools/call":
case "tools/list":
- if (!this._capabilities.tools) {
+ if (!this._appCapabilities.tools) {
throw new Error(
`Client does not support tool capability (required for ${method})`,
);
}
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;
}
}
@@ -1177,26 +1264,10 @@ export class App extends ProtocolWithEvents<
* Verify that the app supports the capability required for the given notification method.
* @internal
*/
- assertNotificationCapability(_method: AppNotification["method"]): void {
+ protected override assertNotificationCapability(_method: string): void {
// TODO
}
- /**
- * Verify that task creation is supported for the given request method.
- * @internal
- */
- protected assertTaskCapability(_method: string): void {
- throw new Error("Tasks are not supported in MCP Apps");
- }
-
- /**
- * Verify that task handler is supported for the given method.
- * @internal
- */
- protected assertTaskHandlerCapability(_method: string): void {
- throw new Error("Task handlers are not supported in MCP Apps");
- }
-
/**
* Call a tool on the originating MCP server (proxied through the host).
*
@@ -1245,7 +1316,6 @@ export class App extends ProtocolWithEvents<
}
return await this.request(
{ method: "tools/call", params },
- CallToolResultSchema,
{
// Hosts may interpose long-running or user-interactive steps before the
// tool result arrives. Opting in here lets a host heartbeat keep the
@@ -1302,11 +1372,7 @@ export class App extends ProtocolWithEvents<
options?: RequestOptions,
): Promise {
this._assertInitialized("readServerResource");
- return await this.request(
- { method: "resources/read", params },
- ReadResourceResultSchema,
- options,
- );
+ return await this.request({ method: "resources/read", params }, options);
}
/**
@@ -1350,11 +1416,7 @@ export class App extends ProtocolWithEvents<
options?: RequestOptions,
): Promise {
this._assertInitialized("listServerResources");
- return await this.request(
- { method: "resources/list", params },
- ListResourcesResultSchema,
- options,
- );
+ return await this.request({ method: "resources/list", params }, options);
}
/**
@@ -1415,8 +1477,8 @@ export class App extends ProtocolWithEvents<
* }
* ```
*
- * @see `CreateMessageRequest` from @modelcontextprotocol/sdk for the request type
- * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/sdk for result types
+ * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type
+ * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types
*/
async createSamplingMessage(
params: CreateMessageRequest["params"] & { tools?: undefined },
@@ -1431,12 +1493,8 @@ export class App extends ProtocolWithEvents<
options?: RequestOptions,
): Promise {
this._assertInitialized("createSamplingMessage");
- const resultSchema = params.tools
- ? CreateMessageResultWithToolsSchema
- : CreateMessageResultSchema;
return await this.request(
{ method: "sampling/createMessage", params },
- resultSchema,
options,
);
}
@@ -1953,14 +2011,18 @@ export class App extends ProtocolWithEvents<
);
}
this._initializedSent = false;
- await super.connect(transport);
try {
+ // Protocol.connect only wires the transport — no MCP handshake. The
+ // ui/initialize request below is the sole negotiation on this channel,
+ // matching the v1 wire behavior deployed hosts expect.
+ await super.connect(transport);
+
const result = await this.request(
{
method: "ui/initialize",
params: {
- appCapabilities: this._capabilities,
+ appCapabilities: this._appCapabilities,
appInfo: this._appInfo,
protocolVersion: LATEST_PROTOCOL_VERSION,
},
diff --git a/src/constants.ts b/src/constants.ts
new file mode 100644
index 0000000..c879a03
--- /dev/null
+++ b/src/constants.ts
@@ -0,0 +1,66 @@
+/**
+ * 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 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/events.test.ts b/src/events.test.ts
new file mode 100644
index 0000000..60e7c35
--- /dev/null
+++ b/src/events.test.ts
@@ -0,0 +1,82 @@
+import { describe, expect, it, spyOn } from "bun:test";
+
+import { EventDispatcher } from "./events.js";
+
+type TestEventMap = {
+ change: { value: number };
+};
+
+describe("EventDispatcher", () => {
+ it("replaces the singular handler", () => {
+ const dispatcher = new EventDispatcher();
+ const calls: string[] = [];
+ const first = () => calls.push("first");
+ const second = () => calls.push("second");
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
+
+ try {
+ dispatcher.setHandler("change", first);
+ dispatcher.setHandler("change", second);
+
+ expect(dispatcher.getHandler("change")).toBe(second);
+ dispatcher.dispatch("change", { value: 1 });
+ expect(calls).toEqual(["second"]);
+ } finally {
+ warn.mockRestore();
+ }
+ });
+
+ it("dispatches the singular handler before listeners in insertion order", () => {
+ const dispatcher = new EventDispatcher();
+ const calls: string[] = [];
+
+ dispatcher.setHandler("change", () => calls.push("handler"));
+ dispatcher.addEventListener("change", () => calls.push("listener-1"));
+ dispatcher.addEventListener("change", () => calls.push("listener-2"));
+
+ dispatcher.dispatch("change", { value: 1 });
+
+ expect(calls).toEqual(["handler", "listener-1", "listener-2"]);
+ });
+
+ it("uses a listener snapshot when a listener is removed during dispatch", () => {
+ const dispatcher = new EventDispatcher();
+ const calls: string[] = [];
+ const second = () => calls.push("second");
+ const first = () => {
+ calls.push("first");
+ dispatcher.removeEventListener("change", second);
+ };
+
+ dispatcher.addEventListener("change", first);
+ dispatcher.addEventListener("change", second);
+
+ dispatcher.dispatch("change", { value: 1 });
+ dispatcher.dispatch("change", { value: 2 });
+
+ expect(calls).toEqual(["first", "second", "first"]);
+ });
+
+ it("warns only when one singular handler replaces another", () => {
+ const dispatcher = new EventDispatcher();
+ const warn = spyOn(console, "warn").mockImplementation(() => {});
+ const first = () => {};
+ const second = () => {};
+
+ try {
+ dispatcher.setHandler("change", first);
+ dispatcher.setHandler("change", undefined);
+ dispatcher.setHandler("change", first);
+ expect(warn).not.toHaveBeenCalled();
+
+ dispatcher.setHandler("change", second);
+
+ expect(warn).toHaveBeenCalledTimes(1);
+ expect(String(warn.mock.calls[0]?.[0])).toContain(
+ "onchange handler replaced",
+ );
+ } finally {
+ warn.mockRestore();
+ }
+ });
+});
diff --git a/src/events.ts b/src/events.ts
index 4d17ca4..aa2e862 100644
--- a/src/events.ts
+++ b/src/events.ts
@@ -1,17 +1,6 @@
-import { Protocol } from "@modelcontextprotocol/sdk/shared/protocol.js";
-import {
- Request,
- Notification,
- Result,
-} from "@modelcontextprotocol/sdk/types.js";
-import { ZodLiteral, ZodObject } from "zod/v4";
-
-type MethodSchema = ZodObject<{ method: ZodLiteral }>;
-
/**
- * Per-event state: a singular `on*` handler (replace semantics) plus a
- * listener array (`addEventListener` semantics), mirroring the DOM model
- * where `el.onclick` and `el.addEventListener("click", …)` coexist.
+ * Per-event state: a singular `on*` handler (replace semantics) plus an
+ * ordered listener array (`addEventListener` semantics).
*/
interface EventSlot {
onHandler?: ((params: T) => void) | undefined;
@@ -19,118 +8,19 @@ interface EventSlot {
}
/**
- * Intermediate base class that adds DOM-style event support on top of the
- * MCP SDK's `Protocol`.
+ * Protocol-independent DOM-style event fan-out.
*
- * The base `Protocol` class stores one handler per method:
- * `setRequestHandler()` and `setNotificationHandler()` replace any existing
- * handler for the same method silently. This class introduces a two-channel
- * event model inspired by the DOM:
+ * Owners perform event-specific state updates before calling
+ * {@link dispatch}. Dispatch then invokes the singular handler followed by a
+ * snapshot of listeners in insertion order.
*
- * ### Singular `on*` handler (like `el.onclick`)
- *
- * Subclasses expose `get`/`set` pairs that delegate to
- * {@link setEventHandler `setEventHandler`} /
- * {@link getEventHandler `getEventHandler`}. Assigning replaces the previous
- * handler; assigning `undefined` clears it. `addEventListener` listeners are
- * unaffected.
- *
- * ### Multi-listener (`addEventListener` / `removeEventListener`)
- *
- * Append to a per-event listener array. Listeners fire in insertion order
- * after the singular `on*` handler.
- *
- * ### Dispatch order
- *
- * When a notification arrives for a mapped event:
- * 1. {@link onEventDispatch `onEventDispatch`} (subclass side-effects)
- * 2. The singular `on*` handler (if set)
- * 3. All `addEventListener` listeners in insertion order
- *
- * ### Double-set protection
- *
- * Direct calls to {@link setRequestHandler `setRequestHandler`} /
- * {@link setNotificationHandler `setNotificationHandler`} throw if a handler
- * for the same method has already been registered (through any path), so
- * accidental overwrites surface as errors instead of silent bugs.
- *
- * @typeParam EventMap - Maps event names to the listener's `params` type.
+ * @typeParam EventMap - Maps event names to their listener parameter types.
*/
-export abstract class ProtocolWithEvents<
- SendRequestT extends Request,
- SendNotificationT extends Notification,
- SendResultT extends Result,
- EventMap extends Record,
-> extends Protocol {
- private _registeredMethods = new Set();
- private _eventSlots = new Map();
+export class EventDispatcher> {
+ private readonly _eventSlots = new Map();
- /**
- * Event name → notification schema. Subclasses populate this so that
- * the event system can lazily register a dispatcher with the correct
- * schema on first use.
- */
- protected abstract readonly eventSchemas: {
- [K in keyof EventMap]: MethodSchema;
- };
-
- /**
- * Called once per incoming notification, before any handlers or listeners
- * fire. Subclasses may override to perform side effects such as merging
- * notification params into cached state.
- */
- protected onEventDispatch(
- _event: K,
- _params: EventMap[K],
- ): void {}
-
- // ── Event system (DOM model) ────────────────────────────────────────
-
- /**
- * Lazily create the event slot and register a single dispatcher with the
- * base `Protocol`. The dispatcher fans out to the `on*` handler and all
- * `addEventListener` listeners.
- */
- private _ensureEventSlot(
- event: K,
- ): EventSlot {
- let slot = this._eventSlots.get(event) as
- | EventSlot
- | undefined;
- if (!slot) {
- const schema = this.eventSchemas[event];
- if (!schema) {
- throw new Error(`Unknown event: ${String(event)}`);
- }
- slot = { listeners: [] };
- this._eventSlots.set(event, slot as EventSlot);
-
- // Claim this method so direct setNotificationHandler calls throw.
- const method = schema.shape.method.value;
- this._registeredMethods.add(method);
-
- const s = slot; // stable reference for the closure
- super.setNotificationHandler(schema, (n) => {
- const params = (n as { params: EventMap[K] }).params;
- this.onEventDispatch(event, params);
- // 1. Singular on* handler
- s.onHandler?.(params);
- // 2. addEventListener listeners — snapshot to tolerate removal during
- // dispatch (e.g., a listener that calls removeEventListener on itself)
- for (const l of [...s.listeners]) l(params);
- });
- }
- return slot;
- }
-
- /**
- * Set or clear the singular `on*` handler for an event.
- *
- * Replace semantics — like the DOM's `el.onclick = fn`. Assigning
- * `undefined` clears the handler without affecting `addEventListener`
- * listeners.
- */
- protected setEventHandler(
+ /** Set, replace, or clear the singular handler for an event. */
+ setHandler(
event: K,
handler: ((params: EventMap[K]) => void) | undefined,
): void {
@@ -144,31 +34,15 @@ export abstract class ProtocolWithEvents<
slot.onHandler = handler;
}
- /**
- * Get the singular `on*` handler for an event, or `undefined` if none is
- * set. `addEventListener` listeners are not reflected here.
- */
- protected getEventHandler(
+ /** Get the singular handler for an event, if one is set. */
+ getHandler(
event: K,
): ((params: EventMap[K]) => void) | undefined {
return (this._eventSlots.get(event) as EventSlot | undefined)
?.onHandler;
}
- /**
- * Add a listener for a notification event.
- *
- * Unlike the singular `on*` handler, calling this multiple times appends
- * listeners rather than replacing them. All registered listeners fire in
- * insertion order after the `on*` handler when the notification arrives.
- *
- * Registration is lazy: the first call (for a given event, from either
- * this method or the `on*` setter) registers a dispatcher with the base
- * `Protocol`.
- *
- * @param event - Event name (a key of the `EventMap` type parameter).
- * @param handler - Listener invoked with the notification `params`.
- */
+ /** Add a listener. Listeners run in insertion order after the handler. */
addEventListener(
event: K,
handler: (params: EventMap[K]) => void,
@@ -176,11 +50,7 @@ export abstract class ProtocolWithEvents<
this._ensureEventSlot(event).listeners.push(handler);
}
- /**
- * Remove a previously registered event listener. The dispatcher stays
- * registered even if the listener array becomes empty; future
- * notifications simply have no listeners to call.
- */
+ /** Remove the first matching listener, if present. */
removeEventListener(
event: K,
handler: (params: EventMap[K]) => void,
@@ -193,87 +63,70 @@ export abstract class ProtocolWithEvents<
if (idx !== -1) slot.listeners.splice(idx, 1);
}
- // ── Handler registration with double-set protection ─────────────────
-
- // The two overrides below are arrow-function class fields rather than
- // prototype methods so that Protocol's constructor — which registers its
- // own ping/cancelled/progress handlers via `this.setRequestHandler`
- // before our fields initialize — hits the base implementation and skips
- // tracking. Converting these to proper methods would crash with
- // `_registeredMethods` undefined during super().
-
/**
- * Registers a request handler. Throws if a handler for the same method
- * has already been registered — use the `on*` setter (replace semantics)
- * or `addEventListener` (multi-listener) for notification events.
- *
- * @throws {Error} if a handler for this method is already registered.
+ * Dispatch an event to its singular handler and a listener snapshot.
+ * Listener mutations during dispatch take effect on the next dispatch.
*/
- override setRequestHandler: Protocol<
- SendRequestT,
- SendNotificationT,
- SendResultT
- >["setRequestHandler"] = (schema, handler) => {
- this._assertMethodNotRegistered(schema, "setRequestHandler");
- super.setRequestHandler(schema, handler);
- };
-
- /**
- * Registers a notification handler. Throws if a handler for the same
- * method has already been registered — use the `on*` setter (replace
- * semantics) or `addEventListener` (multi-listener) for mapped events.
- *
- * @throws {Error} if a handler for this method is already registered.
- */
- override setNotificationHandler: Protocol<
- SendRequestT,
- SendNotificationT,
- SendResultT
- >["setNotificationHandler"] = (schema, handler) => {
- this._assertMethodNotRegistered(schema, "setNotificationHandler");
- super.setNotificationHandler(schema, handler);
- };
-
- /**
- * Warn if a request handler `on*` setter is replacing a previously-set
- * handler. Call from each request setter before updating the backing field.
- */
- protected warnIfRequestHandlerReplaced(
- name: string,
- previous: unknown,
- next: unknown,
- ): void {
- if (previous && next) {
- console.warn(
- `[MCP Apps] ${name} handler replaced. ` +
- `Previous handler will no longer be called.`,
- );
- }
+ dispatch(event: K, params: EventMap[K]): void {
+ const slot = this._eventSlots.get(event) as
+ | EventSlot
+ | undefined;
+ if (!slot) return;
+ slot.onHandler?.(params);
+ for (const l of [...slot.listeners]) l(params);
}
- /**
- * Replace a request handler, bypassing double-set protection. Used by
- * `on*` request-handler setters that need replace semantics.
- */
- protected replaceRequestHandler: Protocol<
- SendRequestT,
- SendNotificationT,
- SendResultT
- >["setRequestHandler"] = (schema, handler) => {
- const method = (schema as MethodSchema).shape.method.value;
- this._registeredMethods.add(method);
- super.setRequestHandler(schema, handler);
- };
+ private _ensureEventSlot(
+ event: K,
+ ): EventSlot {
+ let slot = this._eventSlots.get(event) as
+ | EventSlot
+ | undefined;
+ if (!slot) {
+ slot = { listeners: [] };
+ this._eventSlots.set(event, slot as EventSlot);
+ }
+ return slot;
+ }
+}
- private _assertMethodNotRegistered(schema: unknown, via: string): void {
- const method = (schema as MethodSchema).shape.method.value;
- if (this._registeredMethods.has(method)) {
+/**
+ * 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._registeredMethods.add(method);
+ 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/generated/schema.json b/src/generated/schema.json
index 6b66482..246ed7d 100644
--- a/src/generated/schema.json
+++ b/src/generated/schema.json
@@ -614,7 +614,9 @@
"propertyNames": {
"type": "string"
},
- "additionalProperties": {}
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
},
"required": {
"type": "array",
@@ -629,25 +631,10 @@
"outputSchema": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "const": "object"
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
- "required": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$schema": {
+ "type": "string"
}
},
- "required": ["type"],
"additionalProperties": {}
},
"annotations": {
@@ -1304,7 +1291,40 @@
}
},
"required": ["method", "params"],
- "additionalProperties": false
+ "additionalProperties": false,
+ "$defs": {
+ "__schema0": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/__schema0"
+ }
+ }
+ ]
+ }
+ }
},
"McpUiHostContext": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -1377,7 +1397,9 @@
"propertyNames": {
"type": "string"
},
- "additionalProperties": {}
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
},
"required": {
"type": "array",
@@ -1392,25 +1414,10 @@
"outputSchema": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "const": "object"
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
- "required": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$schema": {
+ "type": "string"
}
},
- "required": ["type"],
"additionalProperties": {}
},
"annotations": {
@@ -2062,7 +2069,40 @@
"additionalProperties": false
}
},
- "additionalProperties": {}
+ "additionalProperties": {},
+ "$defs": {
+ "__schema0": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/__schema0"
+ }
+ }
+ ]
+ }
+ }
},
"McpUiHostCss": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -2978,7 +3018,9 @@
"propertyNames": {
"type": "string"
},
- "additionalProperties": {}
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
},
"required": {
"type": "array",
@@ -2993,25 +3035,10 @@
"outputSchema": {
"type": "object",
"properties": {
- "type": {
- "type": "string",
- "const": "object"
- },
- "properties": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
- "required": {
- "type": "array",
- "items": {
- "type": "string"
- }
+ "$schema": {
+ "type": "string"
}
},
- "required": ["type"],
"additionalProperties": {}
},
"annotations": {
@@ -3673,7 +3700,40 @@
"hostCapabilities",
"hostContext"
],
- "additionalProperties": {}
+ "additionalProperties": {},
+ "$defs": {
+ "__schema0": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "object",
+ "propertyNames": {
+ "type": "string"
+ },
+ "additionalProperties": {
+ "$ref": "#/$defs/__schema0"
+ }
+ },
+ {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/__schema0"
+ }
+ }
+ ]
+ }
+ }
},
"McpUiInitializedNotification": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
@@ -5360,26 +5420,52 @@
"_meta": {
"type": "object",
"properties": {
- "progressToken": {
- "anyOf": [
- {
- "type": "string"
- },
- {
- "type": "integer",
- "minimum": -9007199254740991,
- "maximum": 9007199254740991
- }
- ]
- },
- "io.modelcontextprotocol/related-task": {
+ "io.modelcontextprotocol/serverInfo": {
"type": "object",
"properties": {
- "taskId": {
+ "name": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "icons": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "src": {
+ "type": "string"
+ },
+ "mimeType": {
+ "type": "string"
+ },
+ "sizes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "theme": {
+ "type": "string",
+ "enum": ["light", "dark"]
+ }
+ },
+ "required": ["src"],
+ "additionalProperties": false
+ }
+ },
+ "version": {
+ "type": "string"
+ },
+ "websiteUrl": {
+ "type": "string"
+ },
+ "description": {
"type": "string"
}
},
- "required": ["taskId"],
+ "required": ["name", "version"],
"additionalProperties": false
}
},
@@ -5705,13 +5791,7 @@
]
}
},
- "structuredContent": {
- "type": "object",
- "propertyNames": {
- "type": "string"
- },
- "additionalProperties": {}
- },
+ "structuredContent": {},
"isError": {
"type": "boolean"
}
diff --git a/src/generated/schema.ts b/src/generated/schema.ts
index a8bc1b6..214b227 100644
--- a/src/generated/schema.ts
+++ b/src/generated/schema.ts
@@ -10,7 +10,7 @@ import {
RequestIdSchema,
ResourceLinkSchema,
ToolSchema,
-} from "@modelcontextprotocol/sdk/types.js";
+} from "@modelcontextprotocol/core";
/**
* @description Color theme preference for the host environment.
diff --git a/src/interop-v1.test.ts b/src/interop-v1.test.ts
new file mode 100644
index 0000000..31dbfa1
--- /dev/null
+++ b/src/interop-v1.test.ts
@@ -0,0 +1,379 @@
+/**
+ * Cross-version interop against the *published* 1.x package.
+ *
+ * `ext-apps-v1` is an npm alias for `@modelcontextprotocol/ext-apps@1.7.5`
+ * (built on `@modelcontextprotocol/sdk` 1.x). Each pairing wires a real
+ * `App` to a real `AppBridge` of the other major over an in-memory duplex,
+ * with the bridge fronting a real MCP `Client`+`McpServer` of its own SDK
+ * generation, and asserts the observable outcomes. Complements
+ * `wire-compat.test.ts`, which replays hand-shaped 1.x JSON instead.
+ */
+import { describe, it, expect, beforeAll, afterAll } from "bun:test";
+import { readFileSync } from "node:fs";
+import {
+ Client as ClientV2,
+ InMemoryTransport as InMemoryTransportV2,
+ ProtocolError,
+ type Transport,
+} from "@modelcontextprotocol/client";
+import { EmptyResultSchema as EmptyResultSchemaV2 } from "@modelcontextprotocol/core";
+import { McpServer as McpServerV2 } from "@modelcontextprotocol/server";
+import { Client as ClientV1 } from "@modelcontextprotocol/sdk/client/index.js";
+import { InMemoryTransport as InMemoryTransportV1 } from "@modelcontextprotocol/sdk/inMemory.js";
+import { McpServer as McpServerV1 } from "@modelcontextprotocol/sdk/server/mcp.js";
+import { Protocol as ProtocolV1 } from "@modelcontextprotocol/sdk/shared/protocol.js";
+import {
+ EmptyResultSchema as EmptyResultSchemaV1,
+ McpError,
+} from "@modelcontextprotocol/sdk/types.js";
+import { App as AppV1 } from "ext-apps-v1";
+import { AppBridge as AppBridgeV1 } from "ext-apps-v1/app-bridge";
+import { z } from "zod/v4";
+
+import { App as AppV2 } from "./app.js";
+import { AppBridge as AppBridgeV2 } from "./app-bridge.js";
+
+const flush = () => new Promise((resolve) => setTimeout(resolve, 5));
+
+/** In-memory duplex. Structurally a `Transport` for both SDK generations. */
+function createDuplex(): [Transport, Transport] {
+ const make = () => {
+ let closed = false;
+ const t: Transport & { peer?: typeof t } = {
+ async start() {},
+ async send(message) {
+ // Serialize like a real transport so each side parses plain JSON.
+ t.peer?.onmessage?.(JSON.parse(JSON.stringify(message)));
+ },
+ async close() {
+ if (closed) return;
+ closed = true;
+ t.onclose?.();
+ await t.peer?.close();
+ },
+ };
+ return t;
+ };
+ const [a, b] = [make(), make()];
+ a.peer = b;
+ b.peer = a;
+ return [a, b];
+}
+
+/** One SDK generation's MCP pieces, as used by the App side or the host side. */
+const sdkV1 = {
+ Client: ClientV1,
+ McpServer: McpServerV1,
+ InMemoryTransport: InMemoryTransportV1,
+ EmptyResultSchema: EmptyResultSchemaV1,
+ error: (code: number, message: string): Error => new McpError(code, message),
+};
+const sdkV2 = {
+ Client: ClientV2,
+ McpServer: McpServerV2,
+ InMemoryTransport: InMemoryTransportV2,
+ EmptyResultSchema: EmptyResultSchemaV2,
+ error: (code: number, message: string): Error =>
+ new ProtocolError(code, message),
+};
+
+const pairings = [
+ {
+ name: "2.x App with 1.x AppBridge (sdk 1.x host)",
+ App: AppV2 as any,
+ AppBridge: AppBridgeV1 as any,
+ view: sdkV2,
+ host: sdkV1,
+ // sdk 1.x reports a Zod params failure as InternalError.
+ invalidParamsCode: -32603,
+ // sdk 1.x forwards a handler-thrown -32002 unchanged.
+ handlerThrown32002Code: -32002,
+ // McpServer 1.x answers an unknown tool with an isError result.
+ unknownToolRejects: false,
+ },
+ {
+ name: "1.x App with 2.x AppBridge (sdk 2.x host)",
+ App: AppV1 as any,
+ AppBridge: AppBridgeV2 as any,
+ view: sdkV1,
+ host: sdkV2,
+ invalidParamsCode: -32602,
+ // SDK 2.x never emits -32002 on the wire (see wire-compat.test.ts).
+ handlerThrown32002Code: -32602,
+ // McpServer 2.x answers an unknown tool with a JSON-RPC error.
+ unknownToolRejects: true,
+ },
+];
+
+const hostInfo = { name: "TestHost", version: "9.9.9" };
+const appInfo = { name: "TestView", version: "1.2.3" };
+const hostCapabilities = {
+ openLinks: {},
+ serverTools: {},
+ serverResources: {},
+ logging: {},
+ updateModelContext: {},
+};
+const hostContext = { theme: "dark", displayMode: "inline", locale: "en-US" };
+
+it("pits 2.x against the published 1.x package, not against itself", () => {
+ const v1PackageJson = JSON.parse(
+ readFileSync(
+ new URL("../../package.json", import.meta.resolve("ext-apps-v1")),
+ "utf8",
+ ),
+ );
+ expect(v1PackageJson).toMatchObject({
+ name: "@modelcontextprotocol/ext-apps",
+ version: "1.7.5",
+ });
+ // 1.x classes extend the sdk-1 Protocol; 2.x classes do not.
+ expect(AppV1.prototype).toBeInstanceOf(ProtocolV1);
+ expect(AppBridgeV1.prototype).toBeInstanceOf(ProtocolV1);
+ expect(AppV2.prototype).not.toBeInstanceOf(ProtocolV1);
+ expect(AppBridgeV2.prototype).not.toBeInstanceOf(ProtocolV1);
+});
+
+// The tests in each pairing share one connected App/AppBridge fixture and run
+// in file order (bun does not randomize); later tests assume earlier ones
+// left the pair connected.
+describe.each(pairings)("$name", (pairing) => {
+ const { view, host } = pairing;
+ let server: any, client: any, bridge: any, app: any;
+ const events: string[] = [];
+ const record = (name: string) => (params?: unknown) => {
+ events.push(`${name}:${JSON.stringify(params ?? null)}`);
+ };
+ const drain = () => events.splice(0);
+ // Captured inside oninitialized: the bridge must have processed
+ // ui/initialize before ui/notifications/initialized arrives.
+ let appVersionAtInitialized: unknown = "not-fired";
+ let hostAbortReason: unknown;
+
+ beforeAll(async () => {
+ server = new host.McpServer({ name: "TestServer", version: "1.0.0" });
+ server.registerTool(
+ "echo",
+ { inputSchema: z.object({ a: z.number() }) },
+ async ({ a }: { a: number }) => ({
+ content: [{ type: "text", text: `echo ${a}` }],
+ }),
+ );
+ server.registerTool("boom", { inputSchema: z.object({}) }, async () => {
+ throw new Error("tool exploded");
+ });
+ server.registerResource(
+ "res",
+ "test://res",
+ { mimeType: "text/plain" },
+ async (uri: URL) => ({
+ contents: [{ uri: uri.href, mimeType: "text/plain", text: "hello" }],
+ }),
+ );
+ const [clientTransport, serverTransport] =
+ host.InMemoryTransport.createLinkedPair();
+ await server.connect(serverTransport);
+ client = new host.Client({ name: "HostOuterClient", version: "1.0.0" });
+ await client.connect(clientTransport);
+
+ bridge = new pairing.AppBridge(client, hostInfo, hostCapabilities, {
+ hostContext,
+ });
+ app = new pairing.App(
+ appInfo,
+ { tools: { listChanged: true } },
+ {
+ autoResize: false,
+ },
+ );
+ bridge.oninitialized = () => {
+ appVersionAtInitialized = bridge.getAppVersion();
+ };
+ for (const name of ["onsizechange", "onloggingmessage"]) {
+ bridge[name] = record(`bridge.${name}`);
+ }
+ for (const name of ["onmessage", "onupdatemodelcontext"]) {
+ bridge[name] = async (params: unknown) => {
+ record(`bridge.${name}`)(params);
+ return {};
+ };
+ }
+ bridge.onopenlink = (params: { url: string }, extra: any) => {
+ record("bridge.onopenlink")(params);
+ if (params.url !== "https://slow") return {};
+ // Hang until the view cancels; sdk 1.x passes `extra.signal`, 2.x `extra.mcpReq.signal`.
+ const signal: AbortSignal = extra.signal ?? extra.mcpReq.signal;
+ return new Promise((_resolve, reject) => {
+ signal.addEventListener("abort", () => {
+ hostAbortReason = signal.reason;
+ reject(new Error("aborted"));
+ });
+ });
+ };
+ for (const name of [
+ "ontoolinput",
+ "ontoolresult",
+ "ontoolcancelled",
+ "onhostcontextchanged",
+ ]) {
+ app[name] = record(`app.${name}`);
+ }
+ app.onteardown = async (params: unknown) => {
+ record("app.onteardown")(params);
+ return {};
+ };
+ });
+
+ afterAll(async () => {
+ await app?.close().catch(() => {});
+ await bridge?.close().catch(() => {});
+ await client?.close().catch(() => {});
+ await server?.close().catch(() => {});
+ });
+
+ it("completes ui/initialize and delivers initialized in order", async () => {
+ const [appTransport, bridgeTransport] = createDuplex();
+ // Tap the raw ui/initialize result so an additive key on the 2.x side
+ // (which the getters below would not surface) still fails here.
+ let initializeResult: unknown;
+ const send = bridgeTransport.send.bind(bridgeTransport);
+ bridgeTransport.send = async (message, options) => {
+ if ("result" in message && "id" in message && message.id === 0) {
+ initializeResult = message.result;
+ }
+ await send(message, options);
+ };
+ await bridge.connect(bridgeTransport);
+ await app.connect(appTransport);
+ await flush();
+
+ expect(app.getHostContext()).toEqual(hostContext);
+ expect(app.getHostCapabilities()).toEqual(hostCapabilities);
+ expect(app.getHostVersion()).toEqual(hostInfo);
+ expect(bridge.getAppCapabilities()).toEqual({
+ tools: { listChanged: true },
+ });
+ expect(appVersionAtInitialized).toEqual(appInfo);
+ expect(initializeResult).toEqual({
+ protocolVersion: "2026-01-26",
+ hostCapabilities,
+ hostInfo,
+ hostContext,
+ });
+ });
+
+ it("delivers host-to-view notifications", async () => {
+ bridge.sendToolInput({ arguments: { location: "NYC" } });
+ bridge.sendToolResult({ content: [{ type: "text", text: "sunny" }] });
+ bridge.sendToolCancelled({ reason: "user" });
+ bridge.setHostContext({ theme: "light" });
+ await flush();
+
+ expect(drain()).toEqual([
+ 'app.ontoolinput:{"arguments":{"location":"NYC"}}',
+ 'app.ontoolresult:{"content":[{"type":"text","text":"sunny"}]}',
+ 'app.ontoolcancelled:{"reason":"user"}',
+ 'app.onhostcontextchanged:{"theme":"light"}',
+ ]);
+ expect(app.getHostContext()).toEqual({ ...hostContext, theme: "light" });
+ });
+
+ it("round-trips view-to-host requests and notifications", async () => {
+ const message = { role: "user", content: [{ type: "text", text: "hi" }] };
+ expect(await app.sendMessage(message)).toEqual({});
+ expect(await app.openLink({ url: "https://example.com" })).toEqual({});
+ expect(await app.requestDisplayMode({ mode: "fullscreen" })).toEqual({
+ mode: "inline",
+ });
+ bridge.onrequestdisplaymode = async (params: { mode: string }) => params;
+ expect(await app.requestDisplayMode({ mode: "fullscreen" })).toEqual({
+ mode: "fullscreen",
+ });
+ const context = { content: [{ type: "text", text: "ctx" }] };
+ expect(await app.updateModelContext(context)).toEqual({});
+ await app.sendLog({ level: "info", data: { m: "hello" } });
+ await app.sendSizeChanged({ width: 320, height: 240 });
+ await flush();
+
+ expect(drain()).toEqual([
+ `bridge.onmessage:${JSON.stringify(message)}`,
+ 'bridge.onopenlink:{"url":"https://example.com"}',
+ `bridge.onupdatemodelcontext:${JSON.stringify(context)}`,
+ 'bridge.onloggingmessage:{"level":"info","data":{"m":"hello"}}',
+ 'bridge.onsizechange:{"width":320,"height":240}',
+ ]);
+ });
+
+ it("proxies tools/call and resources/read to the real MCP server", async () => {
+ expect(
+ await app.callServerTool({ name: "echo", arguments: { a: 1 } }),
+ ).toEqual({ content: [{ type: "text", text: "echo 1" }] });
+ expect(
+ await app.callServerTool({ name: "boom", arguments: {} }),
+ ).toMatchObject({
+ isError: true,
+ content: [{ type: "text", text: "tool exploded" }],
+ });
+ const unknown = app.callServerTool({ name: "nope", arguments: {} });
+ if (pairing.unknownToolRejects) {
+ await expect(unknown).rejects.toMatchObject({ code: -32602 });
+ } else {
+ expect(await unknown).toMatchObject({ isError: true });
+ }
+ expect(await app.readServerResource({ uri: "test://res" })).toEqual({
+ contents: [{ uri: "test://res", mimeType: "text/plain", text: "hello" }],
+ });
+ });
+
+ it("propagates JSON-RPC errors with the expected codes", async () => {
+ const raw = (method: string, params: unknown) =>
+ app.request({ method, params }, view.EmptyResultSchema);
+ await expect(raw("ui/does-not-exist", {})).rejects.toMatchObject({
+ code: -32601,
+ });
+ await expect(raw("ui/open-link", { url: 42 })).rejects.toMatchObject({
+ code: pairing.invalidParamsCode,
+ });
+ bridge.onreadresource = async () => {
+ throw host.error(-32002, "Resource not found (host-thrown -32002)");
+ };
+ const notFound = app.readServerResource({ uri: "test://missing" });
+ await expect(notFound).rejects.toMatchObject({
+ code: pairing.handlerThrown32002Code,
+ message: expect.stringContaining(
+ "Resource not found (host-thrown -32002)",
+ ),
+ });
+ });
+
+ it("cancels an in-flight request via AbortSignal on both ends", async () => {
+ const controller = new AbortController();
+ const pending = app.openLink(
+ { url: "https://slow" },
+ { signal: controller.signal },
+ );
+ await flush();
+ controller.abort("user-cancelled");
+ await expect(pending).rejects.toMatchObject({
+ message: expect.stringContaining("user-cancelled"),
+ });
+ await flush();
+ expect(String(hostAbortReason)).toContain("user-cancelled");
+ drain();
+ });
+
+ it("tears down, then closes both ends", async () => {
+ expect(await bridge.teardownResource({})).toEqual({});
+ expect(drain()).toEqual(["app.onteardown:{}"]);
+
+ const closed: string[] = [];
+ app.onclose = () => closed.push("app");
+ bridge.onclose = () => closed.push("bridge");
+ await app.close();
+ await flush();
+ expect(closed.sort()).toEqual(["app", "bridge"]);
+ await expect(app.openLink({ url: "https://after-close" })).rejects.toThrow(
+ "Not connected",
+ );
+ });
+});
diff --git a/src/legacy-handlers.test.ts b/src/legacy-handlers.test.ts
new file mode 100644
index 0000000..0ef36d6
--- /dev/null
+++ b/src/legacy-handlers.test.ts
@@ -0,0 +1,145 @@
+import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
+import { InMemoryTransport, Server } from "@modelcontextprotocol/server";
+import { z } from "zod/v4";
+
+import { App } from "./app.js";
+import { AppBridge } from "./app-bridge.js";
+import {
+ McpUiInitializeRequestSchema,
+ McpUiInitializeResultSchema,
+ McpUiInitializedNotificationSchema,
+ McpUiResourceTeardownRequestSchema,
+ McpUiToolInputNotificationSchema,
+ McpUiSizeChangedNotificationSchema,
+ McpUiOpenLinkRequestSchema,
+} from "./types.js";
+
+const CustomRequestSchema = z.object({
+ method: z.literal("test/echo"),
+ params: z.object({ text: z.string() }),
+});
+
+const CustomNotificationSchema = z.object({
+ method: z.literal("test/ping"),
+ params: z.object({ n: z.number() }),
+});
+
+let warn: ReturnType;
+beforeEach(() => {
+ warn = spyOn(console, "warn").mockImplementation(() => {});
+});
+afterEach(() => warn.mockRestore());
+
+describe("1.x setRequestHandler(Schema, handler) on App", () => {
+ it("registers a custom request and hands the handler the whole message plus a 1.x extra", async () => {
+ const app = new App({ name: "a", version: "1" }, {}, { autoResize: false });
+ const server = new Server(
+ { name: "s", version: "1" },
+ { capabilities: {} },
+ );
+ server.setRequestHandler(
+ "ui/initialize",
+ {
+ params: McpUiInitializeRequestSchema.shape.params,
+ result: McpUiInitializeResultSchema,
+ },
+ () => ({
+ protocolVersion: "2026-01-26",
+ hostCapabilities: {},
+ hostInfo: { name: "h", version: "1" },
+ hostContext: {},
+ }),
+ );
+ server.setNotificationHandler(
+ "ui/notifications/initialized",
+ { params: McpUiInitializedNotificationSchema.shape.params },
+ () => {},
+ );
+
+ let seen: unknown;
+ let seenExtra: { signal: AbortSignal; requestId: unknown } | undefined;
+ app.setRequestHandler(CustomRequestSchema, (request, extra) => {
+ seen = request;
+ seenExtra = extra;
+ return { echoed: request.params.text };
+ });
+
+ const [at, st] = InMemoryTransport.createLinkedPair();
+ await server.connect(st);
+ await app.connect(at);
+
+ const result = await server.request(
+ { method: "test/echo", params: { text: "hi" } },
+ z.object({ echoed: z.string() }),
+ );
+
+ expect(result).toEqual({ echoed: "hi" });
+ expect(seen).toEqual({ method: "test/echo", params: { text: "hi" } });
+ expect(seenExtra?.signal).toBeInstanceOf(AbortSignal);
+ expect(seenExtra?.requestId).toBeDefined();
+ expect(warn).toHaveBeenCalledTimes(1);
+ expect(String(warn.mock.calls[0][0])).toContain(
+ 'setRequestHandler("test/echo", { params: Schema.shape.params }, handler)',
+ );
+
+ await app.close();
+ await server.close();
+ });
+
+ it("still throws for a method an on* setter already owns", () => {
+ const app = new App({ name: "a", version: "1" }, {}, { autoResize: false });
+ app.onteardown = async () => ({});
+ expect(() =>
+ app.setRequestHandler(
+ McpUiResourceTeardownRequestSchema,
+ async () => ({}),
+ ),
+ ).toThrow(/already registered/);
+ });
+
+ it("still throws for an event-mapped notification", () => {
+ const app = new App({ name: "a", version: "1" }, {}, { autoResize: false });
+ app.ontoolinput = () => {};
+ expect(() =>
+ app.setNotificationHandler(McpUiToolInputNotificationSchema, () => {}),
+ ).toThrow(/already registered/);
+ });
+});
+
+describe("1.x setNotificationHandler(Schema, handler) on AppBridge", () => {
+ it("registers a custom notification and hands the handler the whole message", async () => {
+ const bridge = new AppBridge(null, { name: "h", version: "1" }, {});
+ // A bare Server stands in for the View: unlike Client, connect() sends no
+ // MCP initialize, matching what a real App does on this channel.
+ const view = new Server({ name: "v", version: "1" }, { capabilities: {} });
+ let seen: unknown;
+ bridge.setNotificationHandler(CustomNotificationSchema, (n) => {
+ seen = n;
+ });
+
+ const [bt, vt] = InMemoryTransport.createLinkedPair();
+ await bridge.connect(bt);
+ await view.connect(vt);
+ await view.notification({ method: "test/ping", params: { n: 3 } });
+ await new Promise((r) => setTimeout(r, 10));
+
+ expect(seen).toEqual({ method: "test/ping", params: { n: 3 } });
+ await view.close();
+ await bridge.close();
+ });
+
+ it("still throws for a method an on* setter already owns", () => {
+ const bridge = new AppBridge(null, { name: "h", version: "1" }, {});
+ bridge.onsizechange = () => {};
+ expect(() =>
+ bridge.setNotificationHandler(
+ McpUiSizeChangedNotificationSchema,
+ () => {},
+ ),
+ ).toThrow(/already registered/);
+ bridge.onopenlink = async () => ({});
+ expect(() =>
+ bridge.setRequestHandler(McpUiOpenLinkRequestSchema, async () => ({})),
+ ).toThrow(/already registered/);
+ });
+});
diff --git a/src/legacy-handlers.ts b/src/legacy-handlers.ts
new file mode 100644
index 0000000..237b8eb
--- /dev/null
+++ b/src/legacy-handlers.ts
@@ -0,0 +1,121 @@
+import type { BaseContext, RequestId } from "@modelcontextprotocol/client";
+import type { ZodLiteral, ZodObject, ZodType } from "zod/v4";
+
+/**
+ * A whole-message Zod schema (`method` literal plus `params`), as accepted by
+ * the 1.x `setRequestHandler(Schema, handler)` forms.
+ */
+export type LegacyMethodSchema = ZodObject<{
+ method: ZodLiteral;
+ params: ZodType;
+}>;
+
+/**
+ * The `extra` argument 1.x request handlers received.
+ *
+ * @deprecated Use the 2.x `BaseContext` (`extra.mcpReq.signal`, `extra.mcpReq.id`).
+ */
+export type LegacyRequestHandlerExtra = {
+ signal: AbortSignal;
+ requestId: RequestId;
+ sessionId?: string;
+ _meta?: BaseContext["mcpReq"]["_meta"];
+ sendRequest: BaseContext["mcpReq"]["send"];
+ sendNotification: BaseContext["mcpReq"]["notify"];
+ authInfo?: NonNullable["authInfo"];
+};
+
+/** @deprecated Use `setRequestHandler("method", { params, result }, (params, ctx) => …)`. */
+export type LegacyRequestHandler = (
+ request: S["_output"],
+ extra: LegacyRequestHandlerExtra,
+) => unknown;
+
+/** @deprecated Use `setNotificationHandler("method", { params }, (params) => …)`. */
+export type LegacyNotificationHandler = (
+ notification: S["_output"],
+) => void | Promise;
+
+/**
+ * `setRequestHandler` with the 1.x `(Schema, handler)` form kept as a
+ * deprecated overload next to the 2.x forms in `Modern`.
+ */
+export type LegacyRequestHandlerSetter = Modern & {
+ /**
+ * @deprecated 1.x form. Pass the method name and `{ params }` instead;
+ * removed in 3.0.
+ */
+ (
+ schema: S,
+ handler: LegacyRequestHandler,
+ ): void;
+};
+
+/** `setNotificationHandler` counterpart of {@link LegacyRequestHandlerSetter}. */
+export type LegacyNotificationHandlerSetter = Modern & {
+ /**
+ * @deprecated 1.x form. Pass the method name and `{ params }` instead;
+ * removed in 3.0.
+ */
+ (
+ schema: S,
+ handler: LegacyNotificationHandler,
+ ): void;
+};
+
+/**
+ * Arguments for the 2.x three-argument `setRequestHandler` /
+ * `setNotificationHandler` form.
+ */
+type ModernArgs = [
+ method: string,
+ schemas: { params: ZodType },
+ handler: (params: unknown, ctx: BaseContext) => unknown,
+];
+
+const warned = new Set();
+
+/**
+ * Translate a 1.x `(Schema, handler)` registration into the 2.x
+ * `(method, { params }, handler)` form, or return `undefined` when the call
+ * is already in the 2.x form. The 1.x handler receives the reassembled
+ * `{ method, params }` message and, for requests, a 1.x-shaped `extra`.
+ */
+export function toModernArgs(
+ kind: "request" | "notification",
+ args: unknown[],
+): ModernArgs | undefined {
+ const [first, handler] = args;
+ const shape = (first as Partial | undefined)?.shape;
+ if (typeof shape?.method?.value !== "string") return undefined;
+ const method = shape.method.value;
+ const setter =
+ kind === "request" ? "setRequestHandler" : "setNotificationHandler";
+ if (!warned.has(setter)) {
+ warned.add(setter);
+ console.warn(
+ `[ext-apps] ${setter}(Schema, handler) is deprecated; use ` +
+ `${setter}("${method}", { params: Schema.shape.params }, handler).`,
+ );
+ }
+ const call = handler as (...a: unknown[]) => unknown;
+ return [
+ method,
+ { params: shape.params },
+ kind === "request"
+ ? (params, ctx) => call({ method, params }, legacyExtra(ctx))
+ : (params) => call({ method, params }),
+ ];
+}
+
+function legacyExtra(ctx: BaseContext): LegacyRequestHandlerExtra {
+ return {
+ signal: ctx.mcpReq.signal,
+ requestId: ctx.mcpReq.id,
+ sessionId: ctx.sessionId,
+ _meta: ctx.mcpReq._meta,
+ sendRequest: ctx.mcpReq.send,
+ sendNotification: ctx.mcpReq.notify,
+ authInfo: ctx.http?.authInfo,
+ };
+}
diff --git a/src/message-transport.test.ts b/src/message-transport.test.ts
index d5f5092..e2518cc 100644
--- a/src/message-transport.test.ts
+++ b/src/message-transport.test.ts
@@ -1,7 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
-import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js";
+import type { JSONRPCMessage } from "@modelcontextprotocol/client";
-import { PostMessageTransport } from "./message-transport";
+import { PostMessageTransport } from "./message-transport.js";
/**
* Minimal `window` stub for bun's DOM-less test environment.
diff --git a/src/message-transport.ts b/src/message-transport.ts
index 9d19543..afc85e9 100644
--- a/src/message-transport.ts
+++ b/src/message-transport.ts
@@ -1,13 +1,11 @@
-import {
+import { JSONRPCMessageSchema } from "@modelcontextprotocol/core";
+import type {
JSONRPCMessage,
- JSONRPCMessageSchema,
MessageExtraInfo,
-} from "@modelcontextprotocol/sdk/types.js";
-import {
Transport,
TransportSendOptions,
-} from "@modelcontextprotocol/sdk/shared/transport.js";
-import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types";
+} from "@modelcontextprotocol/client";
+import { TOOL_INPUT_PARTIAL_METHOD } from "./spec.types.js";
/**
* JSON-RPC transport using `window.postMessage` for iframe↔parent communication.
diff --git a/src/react/index.tsx b/src/react/index.tsx
index e76e197..9693fb1 100644
--- a/src/react/index.tsx
+++ b/src/react/index.tsx
@@ -31,7 +31,7 @@
* }
* ```
*/
-export * from "./useApp";
-export * from "./useAutoResize";
-export * from "./useDocumentTheme";
-export * from "./useHostStyles";
+export * from "./useApp.js";
+export * from "./useAutoResize.js";
+export * from "./useDocumentTheme.js";
+export * from "./useHostStyles.js";
diff --git a/src/react/useApp.tsx b/src/react/useApp.tsx
index 6189246..a8e1a09 100644
--- a/src/react/useApp.tsx
+++ b/src/react/useApp.tsx
@@ -1,13 +1,12 @@
import { useEffect, useState } from "react";
-import { Implementation } from "@modelcontextprotocol/sdk/types.js";
-import { Client } from "@modelcontextprotocol/sdk/client";
+import type { Implementation } from "@modelcontextprotocol/client";
import {
App,
AppOptions,
McpUiAppCapabilities,
PostMessageTransport,
-} from "../app";
-export * from "../app";
+} from "../app.js";
+export * from "../app.js";
/**
* Options for configuring the {@link useApp `useApp`} hook.
diff --git a/src/react/useAutoResize.ts b/src/react/useAutoResize.ts
index 78217d6..66af07e 100644
--- a/src/react/useAutoResize.ts
+++ b/src/react/useAutoResize.ts
@@ -1,5 +1,5 @@
import { useEffect, RefObject } from "react";
-import { App } from "../app";
+import { App } from "../app.js";
/**
* React hook that automatically reports UI size changes to the host.
diff --git a/src/react/useDocumentTheme.ts b/src/react/useDocumentTheme.ts
index 2fdfcc2..3dc851e 100644
--- a/src/react/useDocumentTheme.ts
+++ b/src/react/useDocumentTheme.ts
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
-import { getDocumentTheme } from "../styles";
-import { McpUiTheme } from "../types";
+import { getDocumentTheme } from "../styles.js";
+import { McpUiTheme } from "../types.js";
/**
* React hook that provides the current document theme reactively.
diff --git a/src/react/useHostStyles.ts b/src/react/useHostStyles.ts
index e380efa..d8376a0 100644
--- a/src/react/useHostStyles.ts
+++ b/src/react/useHostStyles.ts
@@ -1,11 +1,11 @@
import { useEffect, useRef } from "react";
-import { App } from "../app";
+import { App } from "../app.js";
import {
applyDocumentTheme,
applyHostFonts,
applyHostStyleVariables,
-} from "../styles";
-import { McpUiHostContext } from "../types";
+} from "../styles.js";
+import { McpUiHostContext } from "../types.js";
/**
* React hook that applies host style variables and theme as CSS custom properties.
diff --git a/src/server/index.examples.ts b/src/server/index.examples.ts
index d583ae8..9ceebce 100644
--- a/src/server/index.examples.ts
+++ b/src/server/index.examples.ts
@@ -9,17 +9,14 @@
import * as crypto from "node:crypto";
import * as fs from "node:fs/promises";
-import type {
- McpServer,
- ToolCallback,
- ReadResourceCallback,
-} from "@modelcontextprotocol/sdk/server/mcp.js";
+import type { McpServer, ToolCallback } from "@modelcontextprotocol/server";
import { z } from "zod";
import {
registerAppTool,
registerAppResource,
getUiCapability,
RESOURCE_MIME_TYPE,
+ type ReadResourceCallback,
} from "./index.js";
// Stubs for external functions used in examples
@@ -74,7 +71,7 @@ function registerAppTool_basicUsage(server: McpServer) {
{
title: "Get Weather",
description: "Get current weather for a location",
- inputSchema: { location: z.string() },
+ inputSchema: z.object({ location: z.string() }),
_meta: {
ui: { resourceUri: "ui://weather/view.html" },
},
@@ -122,7 +119,7 @@ function registerAppTool_appOnlyVisibility(server: McpServer) {
"update-quantity",
{
description: "Update item quantity in cart",
- inputSchema: { itemId: z.string(), quantity: z.number() },
+ inputSchema: z.object({ itemId: z.string(), quantity: z.number() }),
_meta: {
ui: {
resourceUri: "ui://shop/cart.html",
diff --git a/src/server/index.test.ts b/src/server/index.test.ts
index 1853cc0..489d0b0 100644
--- a/src/server/index.test.ts
+++ b/src/server/index.test.ts
@@ -6,8 +6,14 @@ import {
RESOURCE_MIME_TYPE,
getUiCapability,
EXTENSION_ID,
-} from "./index";
-import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
+} from "./index.js";
+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 c90514a..5fad055 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -32,46 +32,76 @@
*/
import {
- RESOURCE_URI_META_KEY,
- RESOURCE_MIME_TYPE,
McpUiResourceCsp,
McpUiResourceMeta,
McpUiToolMeta,
McpUiClientCapabilities,
-} from "../app.js";
+} from "../spec.types.js";
+import { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "../constants.js";
import type {
- BaseToolCallback,
+ CallToolResult,
+ ClientCapabilities,
+ InputRequiredResult,
McpServer,
+ ReadResourceResult,
RegisteredTool,
ResourceMetadata,
+ ServerContext,
+ StandardSchemaWithJSON,
+ ToolAnnotations,
ToolCallback,
ReadResourceCallback as _ReadResourceCallback,
RegisteredResource,
-} from "@modelcontextprotocol/sdk/server/mcp.js";
-import type {
- AnySchema,
- ZodRawShapeCompat,
-} from "@modelcontextprotocol/sdk/server/zod-compat.js";
-import type { StandardSchemaWithJSON } from "../standard-schema";
-import type {
- ClientCapabilities,
- ReadResourceResult,
- ToolAnnotations,
-} from "@modelcontextprotocol/sdk/types.js";
+} from "@modelcontextprotocol/server";
+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?: ZodRawShapeCompat | StandardSchemaWithJSON;
- outputSchema?: ZodRawShapeCompat | StandardSchemaWithJSON;
+ inputSchema?: ZodRawShape | StandardSchemaWithJSON;
+ outputSchema?: ZodRawShape | StandardSchemaWithJSON;
annotations?: ToolAnnotations;
_meta?: Record;
}
@@ -157,7 +187,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata {
* {
* title: "Get Weather",
* description: "Get current weather for a location",
- * inputSchema: { location: z.string() },
+ * inputSchema: z.object({ location: z.string() }),
* _meta: {
* ui: { resourceUri: "ui://weather/view.html" },
* },
@@ -197,7 +227,7 @@ export interface McpUiAppResourceConfig extends ResourceMetadata {
* "update-quantity",
* {
* description: "Update item quantity in cart",
- * inputSchema: { itemId: z.string(), quantity: z.number() },
+ * inputSchema: z.object({ itemId: z.string(), quantity: z.number() }),
* _meta: {
* ui: {
* resourceUri: "ui://shop/cart.html",
@@ -215,8 +245,25 @@ export interface McpUiAppResourceConfig extends ResourceMetadata {
* @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool
*/
export function registerAppTool<
- OutputArgs extends ZodRawShapeCompat | StandardSchemaWithJSON,
- InputArgs extends undefined | ZodRawShapeCompat | StandardSchemaWithJSON =
+ OutputArgs extends StandardSchemaWithJSON,
+ InputArgs extends StandardSchemaWithJSON | undefined = undefined,
+>(
+ server: Pick,
+ name: string,
+ config: McpUiAppToolConfig & {
+ inputSchema?: InputArgs;
+ 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,
@@ -225,15 +272,14 @@ export function registerAppTool<
inputSchema?: InputArgs;
outputSchema?: OutputArgs;
},
- // The widened constraint signals the v2 API shape, but NOTE: McpServer in
- // sdk@1.x still calls zod internals at runtime, so non-zod schemas will fail
- // here until we depend on sdk v2. Zod (which all current callers use) is
- // unaffected. The cast below bridges the 1.x type signature.
- cb: ToolCallback<
- InputArgs extends undefined | ZodRawShapeCompat | AnySchema
- ? InputArgs
- : AnySchema
- >,
+ cb: 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
@@ -251,13 +297,15 @@ export function registerAppTool<
normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } };
}
- // Cast bridges the widened StandardSchemaWithJSON constraint to the
- // sdk@1.x zod-typed signature. Drops once we depend on sdk v2.
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ // 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 any,
- cb as any,
+ { ...config, _meta: normalizedMeta } as Parameters<
+ McpServer["registerTool"]
+ >[1],
+ cb as ToolCallback,
);
}
@@ -418,10 +466,6 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui";
* This helper retrieves the capability object from the `extensions` field
* where MCP Apps advertises its support.
*
- * Note: The `clientCapabilities` parameter extends the SDK's `ClientCapabilities`
- * type with an `extensions` field (pending SEP-1724). Once `extensions` is added
- * to the SDK, this can use `ClientCapabilities` directly.
- *
* @param clientCapabilities - The client capabilities from the initialize response
* @returns The MCP Apps capability settings, or `undefined` if not supported
*
@@ -456,10 +500,7 @@ export const EXTENSION_ID = "io.modelcontextprotocol/ui";
* ```
*/
export function getUiCapability(
- clientCapabilities:
- | (ClientCapabilities & { extensions?: Record })
- | null
- | undefined,
+ clientCapabilities: ClientCapabilities | null | undefined,
): McpUiClientCapabilities | undefined {
if (!clientCapabilities) {
return undefined;
diff --git a/src/spec.types.ts b/src/spec.types.ts
index bedc1f5..0c45396 100644
--- a/src/spec.types.ts
+++ b/src/spec.types.ts
@@ -18,7 +18,7 @@ import type {
RequestId,
ResourceLink,
Tool,
-} from "@modelcontextprotocol/sdk/types.js";
+} from "@modelcontextprotocol/client";
/**
* Current protocol version supported by this SDK.
@@ -163,8 +163,8 @@ export interface McpUiOpenLinkResult {
/** @description True if the host failed to open the URL (e.g., due to security policy). */
isError?: boolean;
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
- * Note: The generated schema uses passthrough() to allow additional properties.
+ * Additional result properties are preserved for protocol extensibility.
+ * The generated schema uses passthrough() to allow them.
*/
[key: string]: unknown;
}
@@ -195,8 +195,8 @@ export interface McpUiDownloadFileResult {
/** @description True if the download failed (e.g., user cancelled or host denied). */
isError?: boolean;
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
- * Note: The generated schema uses passthrough() to allow additional properties.
+ * Additional result properties are preserved for protocol extensibility.
+ * The generated schema uses passthrough() to allow them.
*/
[key: string]: unknown;
}
@@ -223,8 +223,8 @@ export interface McpUiMessageResult {
/** @description True if the host rejected or failed to deliver the message. */
isError?: boolean;
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
- * Note: The generated schema uses passthrough() to allow additional properties.
+ * Additional result properties are preserved for protocol extensibility.
+ * The generated schema uses passthrough() to allow them.
*/
[key: string]: unknown;
}
@@ -454,7 +454,7 @@ export interface McpUiResourceTeardownRequest {
*/
export interface McpUiResourceTeardownResult {
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Additional result properties are preserved for protocol extensibility.
*/
[key: string]: unknown;
}
@@ -577,8 +577,8 @@ export interface McpUiInitializeResult {
/** @description Rich context about the host environment. */
hostContext: McpUiHostContext;
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
- * Note: The generated schema uses passthrough() to allow additional properties.
+ * Additional result properties are preserved for protocol extensibility.
+ * The generated schema uses passthrough() to allow them.
*/
[key: string]: unknown;
}
@@ -752,7 +752,7 @@ export interface McpUiRequestDisplayModeResult {
/** @description The display mode that was actually set. May differ from requested if not supported. */
mode: McpUiDisplayMode;
/**
- * Index signature required for MCP SDK `Protocol` class compatibility.
+ * Additional result properties are preserved for protocol extensibility.
* Note: The generated schema uses passthrough() to allow additional properties.
*/
[key: string]: unknown;
diff --git a/src/standard-schema.ts b/src/standard-schema.ts
index 4bb85de..a06864a 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/styles.ts b/src/styles.ts
index ad900e2..895073b 100644
--- a/src/styles.ts
+++ b/src/styles.ts
@@ -1,4 +1,4 @@
-import { McpUiStyles, McpUiTheme } from "./types";
+import { McpUiStyles, McpUiTheme } from "./types.js";
/**
* Get the current document theme from the root HTML element.
diff --git a/src/types.ts b/src/types.ts
index 7fc6b71..e4ed629 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -9,6 +9,55 @@
* @see `generated/schema.test.ts` for compile-time verification
*/
+import type {
+ CallToolRequest,
+ CallToolResult,
+ CreateMessageRequest,
+ CreateMessageResult,
+ CreateMessageResultWithTools,
+ EmptyResult,
+ ListPromptsRequest,
+ ListPromptsResult,
+ ListResourceTemplatesRequest,
+ ListResourceTemplatesResult,
+ ListResourcesRequest,
+ ListResourcesResult,
+ ListToolsRequest,
+ ListToolsResult,
+ LoggingMessageNotification,
+ PingRequest,
+ PromptListChangedNotification,
+ ReadResourceRequest,
+ ReadResourceResult,
+ ResourceListChangedNotification,
+ ToolListChangedNotification,
+} from "@modelcontextprotocol/client";
+import type {
+ McpUiDownloadFileRequest,
+ McpUiDownloadFileResult,
+ McpUiHostContextChangedNotification,
+ McpUiInitializeRequest,
+ McpUiInitializeResult,
+ McpUiInitializedNotification,
+ McpUiMessageRequest,
+ McpUiMessageResult,
+ McpUiOpenLinkRequest,
+ McpUiOpenLinkResult,
+ McpUiRequestDisplayModeRequest,
+ McpUiRequestDisplayModeResult,
+ McpUiRequestTeardownNotification,
+ McpUiResourceTeardownRequest,
+ McpUiResourceTeardownResult,
+ McpUiSandboxProxyReadyNotification,
+ McpUiSandboxResourceReadyNotification,
+ McpUiSizeChangedNotification,
+ McpUiToolCancelledNotification,
+ McpUiToolInputNotification,
+ McpUiToolInputPartialNotification,
+ McpUiToolResultNotification,
+ McpUiUpdateModelContextRequest,
+} from "./spec.types.js";
+
// Re-export all types from spec.types.ts
export {
LATEST_PROTOCOL_VERSION,
@@ -69,33 +118,6 @@ export {
type McpUiClientCapabilities,
} from "./spec.types.js";
-// Import types needed for protocol type unions (not re-exported, just used internally)
-import type {
- McpUiInitializeRequest,
- McpUiOpenLinkRequest,
- McpUiDownloadFileRequest,
- McpUiMessageRequest,
- McpUiUpdateModelContextRequest,
- McpUiResourceTeardownRequest,
- McpUiRequestDisplayModeRequest,
- McpUiHostContextChangedNotification,
- McpUiToolInputNotification,
- McpUiToolInputPartialNotification,
- McpUiToolResultNotification,
- McpUiToolCancelledNotification,
- McpUiSandboxResourceReadyNotification,
- McpUiInitializedNotification,
- McpUiSizeChangedNotification,
- McpUiSandboxProxyReadyNotification,
- McpUiRequestTeardownNotification,
- McpUiInitializeResult,
- McpUiOpenLinkResult,
- McpUiDownloadFileResult,
- McpUiMessageResult,
- McpUiResourceTeardownResult,
- McpUiRequestDisplayModeResult,
-} from "./spec.types.js";
-
// Re-export all schemas from generated/schema.ts (already PascalCase)
export {
McpUiThemeSchema,
@@ -136,40 +158,11 @@ export {
McpUiToolMetaSchema,
} from "./generated/schema.js";
-// Re-export SDK types used in protocol type unions
-import {
- CallToolRequest,
- CallToolResult,
- CreateMessageRequest,
- CreateMessageResult,
- CreateMessageResultWithTools,
- EmptyResult,
- ListPromptsRequest,
- ListPromptsResult,
- ListResourcesRequest,
- ListResourcesResult,
- ListResourceTemplatesRequest,
- ListResourceTemplatesResult,
- ListToolsRequest,
- ListToolsResult,
- LoggingMessageNotification,
- PingRequest,
- PromptListChangedNotification,
- ReadResourceRequest,
- ReadResourceResult,
- ResourceListChangedNotification,
- ToolListChangedNotification,
-} from "@modelcontextprotocol/sdk/types.js";
-
/**
- * All request types in the MCP Apps protocol.
+ * Union of every request an {@link app!App `App`} may send or receive.
*
- * Includes:
- * - MCP UI requests (initialize, open-link, message, resource-teardown, request-display-mode)
- * - MCP server requests forwarded from the app (tools/call, tools/list, resources/list,
- * resources/templates/list, resources/read, prompts/list)
- * - MCP client requests forwarded to the host (sampling/createMessage)
- * - Protocol requests (ping)
+ * @deprecated 1.x fed this to the SDK's `Protocol` generics; SDK 2.x derives
+ * request types from the method name, so nothing consumes it anymore.
*/
export type AppRequest =
| McpUiInitializeRequest
@@ -189,17 +182,9 @@ export type AppRequest =
| PingRequest;
/**
- * All notification types in the MCP Apps protocol.
+ * Union of every notification an {@link app!App `App`} may send or receive.
*
- * Host to app:
- * - Tool lifecycle (input, input-partial, result, cancelled)
- * - Host context changes
- * - MCP list changes (tools, resources, prompts)
- * - Sandbox resource ready
- *
- * App to host:
- * - Initialized, size-changed, sandbox-proxy-ready, request-teardown
- * - Logging messages
+ * @deprecated See {@link AppRequest}.
*/
export type AppNotification =
// Sent to app
@@ -220,7 +205,9 @@ export type AppNotification =
| LoggingMessageNotification;
/**
- * All result types in the MCP Apps protocol.
+ * Union of every result an {@link app!App `App`} may send or receive.
+ *
+ * @deprecated See {@link AppRequest}.
*/
export type AppResult =
| McpUiInitializeResult
diff --git a/src/wire-compat.test.ts b/src/wire-compat.test.ts
new file mode 100644
index 0000000..762ac77
--- /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/tsconfig.json b/tsconfig.json
index 08ff110..934b950 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,5 +1,6 @@
{
"compilerOptions": {
+ "types": ["node", "bun"],
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
diff --git a/typedoc.config.mjs b/typedoc.config.mjs
index 4c5baa3..c9b9476 100644
--- a/typedoc.config.mjs
+++ b/typedoc.config.mjs
@@ -1,5 +1,7 @@
import { OptionDefaults } from "typedoc";
+const BASE_SDK_DOCS = "https://ts.sdk.modelcontextprotocol.io/v2/";
+
/** @type {Partial} */
const config = {
name: "MCP Apps",
@@ -17,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",
@@ -28,7 +31,32 @@ const config = {
],
excludePrivate: true,
excludeInternal: false,
- intentionallyNotExported: ["MethodSchema", "RequestHandlerExtra"],
+ intentionallyNotExported: ["RequestHandlerExtra"],
+ externalSymbolLinkMappings: {
+ "@modelcontextprotocol/client": {
+ ClientOptions: BASE_SDK_DOCS,
+ ConnectOptions: BASE_SDK_DOCS,
+ DiscoverResult: BASE_SDK_DOCS,
+ "MessageExtraInfo.classification": BASE_SDK_DOCS,
+ ProtocolError: BASE_SDK_DOCS,
+ "Protocol.close": BASE_SDK_DOCS,
+ "Protocol.notification": BASE_SDK_DOCS,
+ ResponseCacheStore: BASE_SDK_DOCS,
+ SdkError: BASE_SDK_DOCS,
+ "SdkErrorCode.MethodNotSupportedByProtocolVersion": BASE_SDK_DOCS,
+ StandardJSONSchemaV1: BASE_SDK_DOCS,
+ "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS,
+ "__type.enforceStrictCapabilities": BASE_SDK_DOCS,
+ },
+ "@modelcontextprotocol/server": {
+ LoggingMessageNotification: BASE_SDK_DOCS,
+ "McpServer.registerTool": BASE_SDK_DOCS,
+ "MessageExtraInfo.classification": BASE_SDK_DOCS,
+ "Protocol.close": BASE_SDK_DOCS,
+ "Protocol.notification": BASE_SDK_DOCS,
+ "SdkErrorCode.UnsupportedResultType": BASE_SDK_DOCS,
+ },
+ },
blockTags: [...OptionDefaults.blockTags, "@description"],
jsDocCompatibility: {
exampleTag: false,
@@ -49,6 +77,7 @@ const config = {
out: "docs/api",
plugin: [
"typedoc-github-theme",
+ "./scripts/typedoc-plugin-base-sdk-links.mjs",
"./scripts/typedoc-plugin-fix-mermaid-entities.mjs",
"./scripts/typedoc-plugin-seo.mjs",
"./scripts/typedoc-plugin-mcpstyle.mjs",