From 3623553e266d98f021f5978f8dde5c4051ed77bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:19:22 +0000 Subject: [PATCH 1/4] build(deps): bump @modelcontextprotocol/ext-apps from 1.7.5 to 2.0.0 Bumps [@modelcontextprotocol/ext-apps](https://github.com/modelcontextprotocol/ext-apps) from 1.7.5 to 2.0.0. - [Release notes](https://github.com/modelcontextprotocol/ext-apps/releases) - [Changelog](https://github.com/modelcontextprotocol/ext-apps/blob/main/RELEASES.md) - [Commits](https://github.com/modelcontextprotocol/ext-apps/compare/v1.7.5...v2.0.0) --- updated-dependencies: - dependency-name: "@modelcontextprotocol/ext-apps" dependency-version: 2.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- examples/rsc-agent-runtime/package.json | 2 +- packages/workbench/package.json | 2 +- pnpm-lock.yaml | 26 +++++++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/examples/rsc-agent-runtime/package.json b/examples/rsc-agent-runtime/package.json index e91004d26..d779f3f47 100644 --- a/examples/rsc-agent-runtime/package.json +++ b/examples/rsc-agent-runtime/package.json @@ -16,7 +16,7 @@ }, "dependencies": { "@agent-bundle/runtime": "workspace:*", - "@modelcontextprotocol/ext-apps": "1.7.5", + "@modelcontextprotocol/ext-apps": "2.0.0", "@modelcontextprotocol/sdk": "1.30.0", "express": "5.2.1", "react": "19.3.0", diff --git a/packages/workbench/package.json b/packages/workbench/package.json index 468b637b1..44b4cbbac 100644 --- a/packages/workbench/package.json +++ b/packages/workbench/package.json @@ -15,7 +15,7 @@ "dependencies": { "@effect/atom-react": "4.0.0-rc.112", "@modelcontextprotocol/client": "2.0.0", - "@modelcontextprotocol/ext-apps": "1.7.5", + "@modelcontextprotocol/ext-apps": "2.0.0", "@modelcontextprotocol/sdk": "1.30.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a885750a3..b9c219bb8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -172,8 +172,8 @@ importers: specifier: workspace:* version: link:../../packages/rsc-runtime '@modelcontextprotocol/ext-apps': - specifier: 1.7.5 - version: 1.7.5(@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@4.5.4))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) + specifier: 2.0.0 + version: 2.0.0(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/server@2.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) '@modelcontextprotocol/sdk': specifier: 1.30.0 version: 1.30.0(supports-color@7.2.0)(zod@4.5.4) @@ -452,8 +452,8 @@ importers: specifier: 2.0.0 version: 2.0.0 '@modelcontextprotocol/ext-apps': - specifier: 1.7.5 - version: 1.7.5(@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@4.5.4))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) + specifier: 2.0.0 + version: 2.0.0(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/server@2.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) '@modelcontextprotocol/sdk': specifier: 1.30.0 version: 1.30.0(supports-color@7.2.0)(zod@4.5.4) @@ -876,15 +876,19 @@ packages: resolution: {integrity: sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==} engines: {node: '>=20'} - '@modelcontextprotocol/ext-apps@1.7.5': - resolution: {integrity: sha512-TjPH2S2y5UEGKhmI6+XGFuqfqOV4ppe1x6DA3txnUaEWkgtA4G5vo14jGKFZmegdkZ1H4QMLyujLvoU1BEdnAg==} + '@modelcontextprotocol/ext-apps@2.0.0': + resolution: {integrity: sha512-a6tXzFcIbIIdnqumQ7W8Oxd8W/KAPkAKYpoxpD9nDgxZ24ywgxG5pK/8G9W5pOFUygS7gWHQhPv8cwRB44/8yg==} 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 react-dom: @@ -3721,12 +3725,14 @@ snapshots: dependencies: zod: 4.5.4 - '@modelcontextprotocol/ext-apps@1.7.5(@modelcontextprotocol/sdk@1.30.0(supports-color@7.2.0)(zod@4.5.4))(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4)': + '@modelcontextprotocol/ext-apps@2.0.0(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/server@2.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4)': dependencies: - '@modelcontextprotocol/sdk': 1.30.0(supports-color@7.2.0)(zod@4.5.4) + '@modelcontextprotocol/client': 2.0.0 + '@modelcontextprotocol/core': 2.0.0 '@standard-schema/spec': 1.1.0 zod: 4.5.4 optionalDependencies: + '@modelcontextprotocol/server': 2.0.0 react: 19.3.0 react-dom: 19.3.0(react@19.3.0) From cbc59d7e55f8ca0775757cb27a46eb8ca6e6cbe7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 00:19:07 +0000 Subject: [PATCH 2/4] build(deps): migrate Workbench bridge and rsc-agent-runtime example to ext-apps 2.0 --- .changeset/workbench-ext-apps-2.md | 5 + docs/diagnostics.md | 10 +- examples/rsc-agent-runtime/package.json | 7 +- .../src/dev/generation-materializer.ts | 2 +- .../src/mcp/create-server.ts | 8 +- .../rsc-agent-runtime/src/mcp/handlers.ts | 18 ++-- examples/rsc-agent-runtime/src/mcp/http.ts | 98 +++++++++++++------ .../src/mcp/resolve-state.ts | 17 ++-- examples/rsc-agent-runtime/src/mcp/stdio.ts | 2 +- .../src/types/mcp-ext-apps-react.d.ts | 19 ---- .../tests/generation-materializer.test.ts | 4 +- .../tests/host-artifacts.test.ts | 4 +- .../tests/mcp-transports.integration.test.ts | 20 +++- .../tests/micro-eval.spot.test.ts | 4 +- .../src/build/mcp-app-diagnostics.ts | 7 +- packages/workbench/package.json | 1 - .../workbench/src/mcp/runtime-app-bridge.ts | 35 ++++--- .../tests/runtime-app-bridge.test.ts | 56 +++++++++-- pnpm-lock.yaml | 92 +++-------------- website/docs/en/guide/authoring/mcp.mdx | 8 +- website/docs/zh/guide/authoring/mcp.mdx | 7 +- 21 files changed, 220 insertions(+), 204 deletions(-) create mode 100644 .changeset/workbench-ext-apps-2.md delete mode 100644 examples/rsc-agent-runtime/src/types/mcp-ext-apps-react.d.ts diff --git a/.changeset/workbench-ext-apps-2.md b/.changeset/workbench-ext-apps-2.md new file mode 100644 index 000000000..a563aa079 --- /dev/null +++ b/.changeset/workbench-ext-apps-2.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Host MCP App previews in the `agent-bundle dev` Workbench on `@modelcontextprotocol/ext-apps` 2.0.0. The MCP Apps wire protocol is unchanged, so views built on ext-apps 1.x or 2.x both render, and cancelling a View request still aborts a pending consent prompt or open-link, download, and display-mode action. Error responses a View receives follow ext-apps 2.0: invalid params on `ui/*` requests report `-32602` instead of `-32603`, and messages drop the `MCP error N:` prefix. The `AB4772` size guidance now gives the ext-apps 2.x view floor, about 249 kB (65 kB gzip). (#815) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index d4b05c964..373414f77 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -553,10 +553,12 @@ self-contained HTML and their gzip size, what a compressing transport would carry. `AB4772` is the size advisory, one **warning** per App. A view that reaches its host through `agent-bundle/app` carries the framework's client and nothing else from the protocol stack; one that imports -`@modelcontextprotocol/ext-apps` instead starts at about 437 kB (104 kB gzip) -— `zod` v3 and v4, `@modelcontextprotocol/sdk`, `zod-to-json-schema`, and -`ext-apps` itself — so the advisory bound of 1 MiB (1,048,576 bytes) sits at -roughly 2.4× that floor and at half the 2 MiB (2,097,152 bytes) bound above +`@modelcontextprotocol/ext-apps` 2.x instead starts at about 249 kB (65 kB +gzip) — `zod` 4, `@modelcontextprotocol/client`, `@modelcontextprotocol/core`, +and `ext-apps` itself — and a 1.x view, which also carries `zod` 3, +`@modelcontextprotocol/sdk`, and `zod-to-json-schema`, at about 437 kB (104 kB +gzip). The advisory bound of 1 MiB (1,048,576 bytes) sits at roughly 2.4× the +1.x floor and at half the 2 MiB (2,097,152 bytes) bound above which the Workbench and `serve-app` hosts refuse the resource and the Rstest browser harness refuses to mount it. The advisory fires when a production build emits 1 MiB or more, and in either compile mode when the document diff --git a/examples/rsc-agent-runtime/package.json b/examples/rsc-agent-runtime/package.json index d779f3f47..731eab861 100644 --- a/examples/rsc-agent-runtime/package.json +++ b/examples/rsc-agent-runtime/package.json @@ -16,9 +16,11 @@ }, "dependencies": { "@agent-bundle/runtime": "workspace:*", + "@modelcontextprotocol/client": "2.0.0", + "@modelcontextprotocol/core": "2.0.0", "@modelcontextprotocol/ext-apps": "2.0.0", - "@modelcontextprotocol/sdk": "1.30.0", - "express": "5.2.1", + "@modelcontextprotocol/node": "2.0.0", + "@modelcontextprotocol/server": "2.0.0", "react": "19.3.0", "react-dom": "19.3.0", "react-server-dom-rspack": "0.1.0", @@ -28,7 +30,6 @@ "@rsbuild/core": "2.2.5", "@rsbuild/plugin-react": "2.1.0", "@rstest/core": "0.11.12", - "@types/express": "5.0.6", "@types/react": "19.3.0", "@types/react-dom": "19.3.0", "agent-bundle": "workspace:*", diff --git a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts index 3d2624e3a..2b47b48b6 100644 --- a/examples/rsc-agent-runtime/src/dev/generation-materializer.ts +++ b/examples/rsc-agent-runtime/src/dev/generation-materializer.ts @@ -40,7 +40,7 @@ const requiredEntries = Object.freeze([ 'mcp/stdio', 'rsc/index', ] as const); -const executableAsyncEntries = Object.freeze(['mcp/http', 'mcp/stdio'] as const); +const executableAsyncEntries = Object.freeze(['mcp/stdio'] as const); const maximumDefinitionStdout = 1024 * 1024; const maximumDefinitionStderr = 64 * 1024; const definitionTimeoutMs = 5_000; diff --git a/examples/rsc-agent-runtime/src/mcp/create-server.ts b/examples/rsc-agent-runtime/src/mcp/create-server.ts index 5fb801bfc..011c2a79b 100644 --- a/examples/rsc-agent-runtime/src/mcp/create-server.ts +++ b/examples/rsc-agent-runtime/src/mcp/create-server.ts @@ -2,13 +2,13 @@ import { readFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import { RESOURCE_MIME_TYPE, registerAppResource, registerAppTool } from '@modelcontextprotocol/ext-apps/server'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { McpServer, type ServerContext } from '@modelcontextprotocol/server'; import { runtimeDefinition } from '../definition.js'; import { projectName, projectVersion } from '../project-identity.js'; import { createMcpHandlers } from './handlers.js'; import { resourceMetadata } from './host-metadata.js'; -import type { McpRequestExtra, ResolveStateOptions } from './resolve-state.js'; +import type { ResolveStateOptions } from './resolve-state.js'; export interface CreateRuntimeMcpServerOptions extends ResolveStateOptions { publicMcpUrl?: string; @@ -30,12 +30,12 @@ export const createRuntimeMcpServer = (options: CreateRuntimeMcpServerOptions = throw new Error(`No MCP handler registered for ${tool.handlerId}`); } - const callback = (input: unknown, extra: McpRequestExtra) => + const callback = (input: unknown, ctx: ServerContext) => handler( input !== null && typeof input === 'object' && typeof (input as { limit?: unknown }).limit === 'number' ? { limit: (input as { limit: number }).limit } : {}, - extra, + ctx, ); const config = { _meta: tool._meta, diff --git a/examples/rsc-agent-runtime/src/mcp/handlers.ts b/examples/rsc-agent-runtime/src/mcp/handlers.ts index 4d973695c..a12a30751 100644 --- a/examples/rsc-agent-runtime/src/mcp/handlers.ts +++ b/examples/rsc-agent-runtime/src/mcp/handlers.ts @@ -1,13 +1,13 @@ import { documentToCallToolResult } from '@agent-bundle/runtime'; -import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import type { CallToolResult, ServerContext } from '@modelcontextprotocol/server'; import { createFileRuntimeKernel } from '../runtime/state-file.js'; import { requestAgentDocument } from '../flight/request-render.js'; -import { resolveStateFile, type McpRequestExtra, type ResolveStateOptions } from './resolve-state.js'; +import { resolveStateFile, type ResolveStateOptions } from './resolve-state.js'; type ToolInput = { limit?: number }; -type McpToolHandler = (input: ToolInput, extra: McpRequestExtra) => Promise; +type McpToolHandler = (input: ToolInput, ctx: ServerContext) => Promise; const textSnapshot = (snapshot: { edits: unknown[]; stateVersion: number }): CallToolResult => ({ content: [{ text: JSON.stringify(snapshot), type: 'text' }], @@ -15,20 +15,20 @@ const textSnapshot = (snapshot: { edits: unknown[]; stateVersion: number }): Cal }); export const createMcpHandlers = (options: ResolveStateOptions): Record => ({ - recent_edits: async (input, extra) => { - const stateFile = await resolveStateFile(options, extra); + recent_edits: async (input, ctx) => { + const stateFile = await resolveStateFile(options, ctx); const snapshot = await createFileRuntimeKernel({ stateFile }).readSnapshot({ limit: input.limit }); return textSnapshot(snapshot); }, - render_edit_timeline: async (input, extra) => { - const stateFile = await resolveStateFile(options, extra); + render_edit_timeline: async (input, ctx) => { + const stateFile = await resolveStateFile(options, ctx); const snapshot = await createFileRuntimeKernel({ stateFile }).readSnapshot({ limit: input.limit }); return documentToCallToolResult( await requestAgentDocument({ snapshot, stateFile, type: 'mcp/render-timeline' }), ); }, - runtime_status: async (_input, extra) => { - const stateFile = await resolveStateFile(options, extra); + runtime_status: async (_input, ctx) => { + const stateFile = await resolveStateFile(options, ctx); return documentToCallToolResult(await requestAgentDocument({ stateFile, type: 'mcp/runtime-status' })); }, }); diff --git a/examples/rsc-agent-runtime/src/mcp/http.ts b/examples/rsc-agent-runtime/src/mcp/http.ts index 87359817d..a75cddb74 100644 --- a/examples/rsc-agent-runtime/src/mcp/http.ts +++ b/examples/rsc-agent-runtime/src/mcp/http.ts @@ -1,53 +1,95 @@ -import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js'; -import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; +import { createServer, type IncomingMessage, type ServerResponse } from 'node:http'; + +import { hostHeaderValidation, toNodeHandler } from '@modelcontextprotocol/node'; +import { createMcpHandler, isJsonContentType } from '@modelcontextprotocol/server'; import { createRuntimeMcpServer } from './create-server.js'; import { allowsOrigin, resolveHttpSecurityConfig } from './http-security.js'; const port = Number.parseInt(process.env.PORT ?? '3000', 10); const security = resolveHttpSecurityConfig(); -const app = createMcpExpressApp({ allowedHosts: security.allowedHosts }); +const allowsHost = hostHeaderValidation(security.allowedHosts); +const mcpHandler = createMcpHandler( + () => createRuntimeMcpServer({ publicMcpUrl: process.env.AGENT_RUNTIME_PUBLIC_MCP_URL }), + { legacy: 'stateless' }, +); +const handleMcp = toNodeHandler(mcpHandler); + +const maximumRequestBodyBytes = 100 * 1024; + +const writeJson = (response: ServerResponse, status: number, body: unknown): void => { + response.writeHead(status, { 'Content-Type': 'application/json' }); + response.end(JSON.stringify(body)); +}; + +const writeJsonRpcError = (response: ServerResponse, status: number, code: number, message: string): void => { + writeJson(response, status, { error: { code, message }, id: null, jsonrpc: '2.0' }); +}; -app.use((request, response, next) => { - if (allowsOrigin(security, request.get('host'), request.get('origin'))) { - next(); +const readRequestBody = async (request: IncomingMessage): Promise => { + const chunks: Buffer[] = []; + let size = 0; + for await (const chunk of request as AsyncIterable) { + size += chunk.length; + if (size <= maximumRequestBodyBytes) { + chunks.push(chunk); + } + } + return size > maximumRequestBodyBytes ? undefined : Buffer.concat(chunks); +}; + +const serveMcp = async (request: IncomingMessage, response: ServerResponse): Promise => { + if (request.method !== 'POST') { + await handleMcp(request, response); return; } - response.status(403).json({ - error: { code: -32000, message: `Invalid Origin header: ${request.get('origin')}` }, - id: null, - jsonrpc: '2.0', - }); -}); + const body = await readRequestBody(request); + if (body === undefined) { + writeJsonRpcError(response, 413, -32000, 'Request body too large'); + return; + } -app.get('/health', (_request, response) => { - response.json({ ok: true, transport: 'streamable-http' }); -}); + let parsedBody: unknown; + if (isJsonContentType(request.headers['content-type'])) { + try { + parsedBody = JSON.parse(body.toString('utf8')); + } catch { + writeJsonRpcError(response, 400, -32700, 'Parse error'); + return; + } + } + await handleMcp(request, response, parsedBody); +}; -app.post('/mcp', async (request, response) => { - const server = createRuntimeMcpServer({ publicMcpUrl: process.env.AGENT_RUNTIME_PUBLIC_MCP_URL }); - const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined }); +const httpServer = createServer((request, response) => { + if (!allowsHost(request, response)) { + return; + } - try { - await server.connect(transport); - await transport.handleRequest(request, response, request.body); - } catch (error) { - if (!response.headersSent) { - response.status(500).json({ error: error instanceof Error ? error.message : String(error) }); - } - } finally { - await server.close(); + if (!allowsOrigin(security, request.headers.host, request.headers.origin)) { + writeJsonRpcError(response, 403, -32000, `Invalid Origin header: ${request.headers.origin}`); + return; + } + + const { pathname } = new URL(request.url ?? '/', 'http://localhost'); + if (request.method === 'GET' && pathname === '/health') { + writeJson(response, 200, { ok: true, transport: 'streamable-http' }); + } else if (pathname === '/mcp') { + serveMcp(request, response).catch(() => response.destroy()); + } else { + writeJson(response, 404, { error: 'Not found' }); } }); -const httpServer = app.listen(port, '127.0.0.1', () => { +httpServer.listen(port, '127.0.0.1', () => { const address = httpServer.address(); const actualPort = typeof address === 'object' && address !== null ? address.port : port; process.stderr.write(`${JSON.stringify({ port: actualPort, transport: 'streamable-http' })}\n`); }); const close = (): void => { + void mcpHandler.close(); httpServer.close(() => process.exit(0)); }; diff --git a/examples/rsc-agent-runtime/src/mcp/resolve-state.ts b/examples/rsc-agent-runtime/src/mcp/resolve-state.ts index ed65653e2..52cafb18b 100644 --- a/examples/rsc-agent-runtime/src/mcp/resolve-state.ts +++ b/examples/rsc-agent-runtime/src/mcp/resolve-state.ts @@ -1,24 +1,21 @@ import { fileURLToPath } from 'node:url'; import { resolve } from 'node:path'; -import { ListRootsResultSchema, type ServerNotification, type ServerRequest } from '@modelcontextprotocol/sdk/types.js'; -import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; +import type { ServerContext } from '@modelcontextprotocol/server'; import { resolveImplicitRuntimeStateFile } from '../runtime/state-file.js'; -export type McpRequestExtra = RequestHandlerExtra; - export interface ResolveStateOptions { stateFile?: string; - resolveStateFile?: (extra: McpRequestExtra) => string | undefined | Promise; + resolveStateFile?: (ctx: ServerContext) => string | undefined | Promise; } const usablePath = (value: string | undefined): string | undefined => value === undefined || value.trim() === '' ? undefined : resolve(value); -const stateFileFromRoots = async (extra: McpRequestExtra): Promise => { +const stateFileFromRoots = async (ctx: ServerContext): Promise => { try { - const result = await extra.sendRequest({ method: 'roots/list' }, ListRootsResultSchema); + const result = await ctx.mcpReq.send({ method: 'roots/list' }); const root = result.roots[0]; if (root === undefined) { return undefined; @@ -30,8 +27,8 @@ const stateFileFromRoots = async (extra: McpRequestExtra): Promise => { - const resolvedByOption = options.resolveStateFile === undefined ? undefined : await options.resolveStateFile(extra); +export const resolveStateFile = async (options: ResolveStateOptions, ctx: ServerContext): Promise => { + const resolvedByOption = options.resolveStateFile === undefined ? undefined : await options.resolveStateFile(ctx); const explicit = usablePath(resolvedByOption) ?? usablePath(options.stateFile); if (explicit !== undefined) { return explicit; @@ -42,7 +39,7 @@ export const resolveStateFile = async (options: ResolveStateOptions, extra: McpR return fromEnvironment; } - const fromRoots = await stateFileFromRoots(extra); + const fromRoots = await stateFileFromRoots(ctx); if (fromRoots !== undefined) { return fromRoots; } diff --git a/examples/rsc-agent-runtime/src/mcp/stdio.ts b/examples/rsc-agent-runtime/src/mcp/stdio.ts index caef9e655..e29cbf14a 100644 --- a/examples/rsc-agent-runtime/src/mcp/stdio.ts +++ b/examples/rsc-agent-runtime/src/mcp/stdio.ts @@ -8,7 +8,7 @@ const main = async (): Promise => { const guard = redirectConsoleToStderr(); const { createRuntimeMcpServer } = await import('./create-server.js'); const server = createRuntimeMcpServer(); - const { StdioServerTransport } = await import('@modelcontextprotocol/sdk/server/stdio.js'); + const { StdioServerTransport } = await import('@modelcontextprotocol/server/stdio'); guard.restoreProtocolStdout(); await runStdioServer({ server, diff --git a/examples/rsc-agent-runtime/src/types/mcp-ext-apps-react.d.ts b/examples/rsc-agent-runtime/src/types/mcp-ext-apps-react.d.ts deleted file mode 100644 index 7c6b7371c..000000000 --- a/examples/rsc-agent-runtime/src/types/mcp-ext-apps-react.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -declare module '@modelcontextprotocol/ext-apps/react' { - import type { Implementation } from '@modelcontextprotocol/sdk/types.js'; - import type { App, McpUiAppCapabilities, McpUiHostContext } from '@modelcontextprotocol/ext-apps'; - - export type UseAppOptions = { - appInfo: Implementation; - capabilities: McpUiAppCapabilities; - onAppCreated?: (app: App) => void; - }; - - export type AppState = { - app: App | null; - error: Error | null; - isConnected: boolean; - }; - - export function useApp(options: UseAppOptions): AppState; - export function useHostStyles(app: App | null, initialContext?: McpUiHostContext | null): void; -} diff --git a/examples/rsc-agent-runtime/tests/generation-materializer.test.ts b/examples/rsc-agent-runtime/tests/generation-materializer.test.ts index 82b4e0352..0d203bd69 100644 --- a/examples/rsc-agent-runtime/tests/generation-materializer.test.ts +++ b/examples/rsc-agent-runtime/tests/generation-materializer.test.ts @@ -813,7 +813,7 @@ test('recaptures an identical cohort from its immutable checkpoints after an enq expect(failed).toHaveLength(1); }); -test('requires every executable entry to declare its async cohort assets', async () => { +test('requires the lazily loading stdio entry to declare its async cohort assets', async () => { const storageRoot = await mkdtemp(join(tmpdir(), 'rsc-agent-runtime-generations-')); const compilerRoot = join(storageRoot, 'compiler'); const store = createStore(storageRoot); @@ -821,7 +821,7 @@ test('requires every executable entry to declare its async cohort assets', async await writeCompilerCohort(compilerRoot); const manifestPath = join(compilerRoot, 'rsc', 'runtime-assets.json'); const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as { entries: Record }; - delete manifest.entries['mcp/http']?.async; + delete manifest.entries['mcp/stdio']?.async; await writeFile(manifestPath, JSON.stringify(manifest), 'utf8'); const candidate = await store.begin({ id: 'missing-async', sourceRevision: 'source-missing-async' }); const snapshot = await captureCompilerCohort({ diff --git a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts index 1ae4de953..39d01be20 100644 --- a/examples/rsc-agent-runtime/tests/host-artifacts.test.ts +++ b/examples/rsc-agent-runtime/tests/host-artifacts.test.ts @@ -7,8 +7,8 @@ import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import type { Readable } from 'node:stream'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Client } from '@modelcontextprotocol/client'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { expect, test } from '@rstest/core'; import { ensureExampleBuilt } from './support/ensure-built.js'; diff --git a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts index 7e05be97f..6308e0f3d 100644 --- a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts @@ -8,9 +8,8 @@ import type { Readable } from 'node:stream'; import { pathToFileURL } from 'node:url'; import { createRsbuild } from '@rsbuild/core'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; -import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import { Client, StreamableHTTPClientTransport } from '@modelcontextprotocol/client'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { expect, test } from '@rstest/core'; import { createFileRuntimeKernel } from '../src/runtime/state-file.js'; @@ -40,21 +39,24 @@ const createClient = (): Client => new Client({ name: 'rsc-agent-runtime-test', version: '1.0.0' }); const requestStatus = ({ + body, headers, path, port, }: { + body?: string; headers: Record; path: string; port: number; }): Promise => new Promise((resolve, reject) => { - const request = httpRequest({ headers, hostname: '127.0.0.1', method: 'GET', path, port }, (response) => { + const method = body === undefined ? 'GET' : 'POST'; + const request = httpRequest({ headers, hostname: '127.0.0.1', method, path, port }, (response) => { response.resume(); response.once('end', () => resolve(response.statusCode ?? 0)); }); request.once('error', reject); - request.end(); + request.end(body); }); const expectStaticSurface = async (client: Client) => { @@ -315,6 +317,14 @@ test('built Streamable HTTP MCP reports its one JSON startup line and closes cle requestStatus({ headers: { Host: localHost, Origin: 'https://attacker.example' }, path, port: startup.port }), ).resolves.toBe(403); } + await expect( + requestStatus({ + body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'ping', params: { padding: 'x'.repeat(200 * 1024) } }), + headers: { 'Content-Type': 'application/json', Host: localHost }, + path: '/mcp', + port: startup.port, + }), + ).resolves.toBe(413); } finally { await client.close(); child.kill('SIGTERM'); diff --git a/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts b/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts index ef1507efc..f8fe3cab6 100644 --- a/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts +++ b/examples/rsc-agent-runtime/tests/micro-eval.spot.test.ts @@ -4,8 +4,8 @@ import { mkdtemp, rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { Client } from '@modelcontextprotocol/sdk/client/index.js'; -import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Client } from '@modelcontextprotocol/client'; +import { StdioClientTransport } from '@modelcontextprotocol/client/stdio'; import { expect, test } from '@rstest/core'; import { createFileRuntimeKernel } from '../src/runtime/state-file.js'; diff --git a/packages/agent-bundle/src/build/mcp-app-diagnostics.ts b/packages/agent-bundle/src/build/mcp-app-diagnostics.ts index 4ecab3a28..032ada4a9 100644 --- a/packages/agent-bundle/src/build/mcp-app-diagnostics.ts +++ b/packages/agent-bundle/src/build/mcp-app-diagnostics.ts @@ -41,9 +41,10 @@ export interface McpAppOutputSize { /** * Emitted bytes at which a production view draws the `AB4772` advisory. The - * framework floor for any view using `@modelcontextprotocol/ext-apps` (its - * SDK, both `zod` generations, `zod-to-json-schema`) measured 437 kB; this is - * roughly 2.4× that floor and half the {@link MAX_APP_HTML_BYTES} host bound. + * framework floor for a view using `@modelcontextprotocol/ext-apps` measured + * 249 kB on 2.x and 437 kB on 1.x (its SDK, both `zod` generations, + * `zod-to-json-schema`); this is roughly 2.4× the 1.x floor and half the + * {@link MAX_APP_HTML_BYTES} host bound. */ export const MCP_APP_HTML_ADVISORY_BYTES = 1_048_576; diff --git a/packages/workbench/package.json b/packages/workbench/package.json index 44b4cbbac..e3ec5f346 100644 --- a/packages/workbench/package.json +++ b/packages/workbench/package.json @@ -16,7 +16,6 @@ "@effect/atom-react": "4.0.0-rc.112", "@modelcontextprotocol/client": "2.0.0", "@modelcontextprotocol/ext-apps": "2.0.0", - "@modelcontextprotocol/sdk": "1.30.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", "effect": "4.0.0-rc.112", diff --git a/packages/workbench/src/mcp/runtime-app-bridge.ts b/packages/workbench/src/mcp/runtime-app-bridge.ts index 83f47bf0a..6e6f7fa29 100644 --- a/packages/workbench/src/mcp/runtime-app-bridge.ts +++ b/packages/workbench/src/mcp/runtime-app-bridge.ts @@ -1,5 +1,4 @@ -import { AppBridge, PostMessageTransport } from '@modelcontextprotocol/ext-apps/app-bridge'; -import { ListToolsRequestSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'; +import { AppBridge, PostMessageTransport, type McpUiResourceCsp } from '@modelcontextprotocol/ext-apps/app-bridge'; import type { McpAppBridgeMessage, @@ -77,7 +76,7 @@ interface PostMessageTarget { } interface AppBridgeHandlerExtra { - readonly signal: AbortSignal; + readonly mcpReq: Readonly<{ readonly signal: AbortSignal }>; } type DisplayHandler = ( @@ -85,7 +84,7 @@ type DisplayHandler = ( extra?: AppBridgeHandlerExtra, ) => Promise>; -/** The ext-apps 1.7.5 declaration omits inherited runtime members. */ +/** The official AppBridge surface this factory drives and hands to AppRenderer. */ interface RuntimeAppBridge extends AppRendererBridge { addEventListener(type: 'initialized', listener: () => void): void; addEventListener( @@ -102,8 +101,8 @@ interface RuntimeAppBridge extends AppRendererBridge { onrequestdisplaymode: DisplayHandler | undefined; onupdatemodelcontext: ((params: Readonly<{ readonly structuredContent?: McpAppJsonValue }>, extra: AppBridgeHandlerExtra) => Promise>>) | undefined; setRequestHandler( - schema: typeof ListToolsRequestSchema, - handler: (request: Readonly<{ readonly params: Readonly> }>, extra: AppBridgeHandlerExtra) => Promise, + method: 'tools/list', + handler: (request: Readonly<{ readonly params?: Readonly<{ readonly cursor?: string }> }>, extra: AppBridgeHandlerExtra) => Promise, ): void; teardownResource( params: Readonly>, @@ -320,7 +319,8 @@ const bridgeCapabilities = ( ...(options.installedHandlers.downloadFile === undefined ? {} : { downloadFile: Object.freeze({}) }), logging: Object.freeze({}), sandbox: Object.freeze({ - ...(preview.resource.csp === undefined ? {} : { csp: preview.resource.csp }), + // AppBridge only serializes host capabilities into the ui/initialize result. + ...(preview.resource.csp === undefined ? {} : { csp: preview.resource.csp as McpUiResourceCsp }), permissions: preview.documentPolicy.approvedPermissions, }), ...(server?.tools === undefined ? {} : { serverTools: Object.freeze({ ...(options.listChanged.tools ? { listChanged: true } : {}) }) }), @@ -543,8 +543,8 @@ export const createRuntimeAppBridgeFactory = (options: RuntimeAppBridgeOptions): }, source: target, }); - // This is the one intentionally isolated compatibility cast: ext-apps - // 1.x consumes the SDK-v1 Client shape, while the controller owns v2. + // This is the one intentionally isolated cast: AppBridge reads only these + // members of the controller-owned client, and the facade exposes nothing else. const officialClient = Object.freeze({ getServerCapabilities: () => { const capabilities = attached.client.getServerCapabilities(); @@ -576,10 +576,9 @@ export const createRuntimeAppBridgeFactory = (options: RuntimeAppBridgeOptions): writable: false, }); if (serverCapabilities?.tools !== undefined) { - bridge.setRequestHandler(ListToolsRequestSchema, async (request, extra) => officialClient.request( + bridge.setRequestHandler('tools/list', async (request, extra) => officialClient.request( { method: 'tools/list', params: request.params }, - ListToolsResultSchema, - { signal: extra.signal }, + { signal: extra.mcpReq.signal }, )); } bridge.addEventListener('loggingmessage', (entry) => { @@ -597,18 +596,18 @@ export const createRuntimeAppBridgeFactory = (options: RuntimeAppBridgeOptions): bridge.onopenlink = async ({ url }, extra) => { const candidate = validateMcpAppExternalUrl(url); if (candidate === undefined || options.installedHandlers.openExternalLink === undefined) return { isError: true }; - const approved = await sideEffectConsent(options, preview, 'open-external-link', Object.freeze({ url: candidate }), 'Open MCP App link', extra.signal); + const approved = await sideEffectConsent(options, preview, 'open-external-link', Object.freeze({ url: candidate }), 'Open MCP App link', extra.mcpReq.signal); if (!approved) return { isError: true }; - throwIfAborted(extra.signal); + throwIfAborted(extra.mcpReq.signal); await options.installedHandlers.openExternalLink(candidate); return {}; }; bridge.ondownloadfile = async ({ contents }, extra) => { const candidate = validateMcpAppDownloadContents(contents); if (candidate === undefined || options.installedHandlers.downloadFile === undefined) return { isError: true }; - const approved = await sideEffectConsent(options, preview, 'download-file', candidate.contents, 'Download MCP App file', extra.signal); + const approved = await sideEffectConsent(options, preview, 'download-file', candidate.contents, 'Download MCP App file', extra.mcpReq.signal); if (!approved) return { isError: true }; - throwIfAborted(extra.signal); + throwIfAborted(extra.mcpReq.signal); await options.installedHandlers.downloadFile(candidate); return {}; }; @@ -618,9 +617,9 @@ export const createRuntimeAppBridgeFactory = (options: RuntimeAppBridgeOptions): : 'inline'; const protectedDisplayHandler: DisplayHandler = async ({ mode }, extra) => { if (!validDisplayMode(mode) || options.installedHandlers.requestDisplayMode === undefined) return { mode: safeDisplayMode }; - const approved = await sideEffectConsent(options, preview, 'request-display-mode', Object.freeze({ mode }), 'Change MCP App display mode', extra?.signal); + const approved = await sideEffectConsent(options, preview, 'request-display-mode', Object.freeze({ mode }), 'Change MCP App display mode', extra?.mcpReq.signal); if (!approved) return { mode: safeDisplayMode }; - throwIfAborted(extra?.signal); + throwIfAborted(extra?.mcpReq.signal); const applied = await options.installedHandlers.requestDisplayMode(mode); if (!validDisplayMode(applied)) return { mode: safeDisplayMode }; const rendered = displayFallback === undefined diff --git a/packages/workbench/tests/runtime-app-bridge.test.ts b/packages/workbench/tests/runtime-app-bridge.test.ts index e3626247a..637429484 100644 --- a/packages/workbench/tests/runtime-app-bridge.test.ts +++ b/packages/workbench/tests/runtime-app-bridge.test.ts @@ -231,6 +231,46 @@ it('forwards an initialized App tools/call through the controller-owned client t }); }); +it('passes the official AppBridge request signal to consent and forwarded tools/list', async () => { + await withBrowser(async (browser) => { + const consentSignals: Array = []; + const forwarded: Array> = []; + const factory = runtimeFactory(async () => Object.freeze({ + ...appAccess(async () => undefined), + client: Object.freeze({ + getServerCapabilities: () => Object.freeze({ tools: Object.freeze({}) }), + request: async (request: Readonly<{ readonly method: string }>, options?: Readonly<{ readonly signal?: AbortSignal }>) => { + forwarded.push(Object.freeze({ method: request.method, signal: options?.signal })); + return Object.freeze({ tools: Object.freeze([]) }); + }, + setNotificationHandler: () => undefined, + }), + }), { + installedHandlers: Object.freeze({ openExternalLink: async () => undefined }), + requestConsent: async (_challenge, signal?: AbortSignal) => { + consentSignals.push(signal); + return 'deny'; + }, + }); + const bridge = await invokeBridgeFactory(factory, browser); + const emit = (data: unknown) => browser.emit({ data, origin: 'https://apps.example.test', source: browser.target }); + const response = (id: string) => browser.posts.find((entry) => (entry.message as { readonly id?: unknown } | null)?.id === id)?.message; + + emit({ id: 'initialize', jsonrpc: '2.0', method: 'ui/initialize', params: { appCapabilities: {}, appInfo: { name: 'app', version: '1' }, protocolVersion: '2026-01-26' } }); + await eventually(() => response('initialize') !== undefined); + emit({ jsonrpc: '2.0', method: 'ui/notifications/initialized', params: {} }); + emit({ id: 'open-link', jsonrpc: '2.0', method: 'ui/open-link', params: { url: 'https://weather.example/forecast' } }); + emit({ id: 'list-tools', jsonrpc: '2.0', method: 'tools/list', params: {} }); + + await eventually(() => response('open-link') !== undefined && response('list-tools') !== undefined); + expect(consentSignals).toEqual([expect.any(AbortSignal)]); + expect(forwarded).toEqual([{ method: 'tools/list', signal: expect.any(AbortSignal) }]); + expect(response('open-link')).toEqual({ id: 'open-link', jsonrpc: '2.0', result: { isError: true } }); + expect(response('list-tools')).toEqual({ id: 'list-tools', jsonrpc: '2.0', result: { tools: [] } }); + await bridge.close(); + }); +}); + it('rejects noncanonical links and noncanonical bounded downloads before consent or host actions', async () => { await withBrowser(async (browser) => { const opened: string[] = []; @@ -335,9 +375,9 @@ it('aborts valid runtime App side effects before a late consent decision or host }, ); const bridge = await invokeBridgeFactory(factory, browser) as unknown as Readonly<{ - readonly ondownloadfile?: (params: Readonly<{ readonly contents: unknown }>, extra: Readonly<{ readonly signal: AbortSignal }>) => Promise>; - readonly onopenlink?: (params: Readonly<{ readonly url: unknown }>, extra: Readonly<{ readonly signal: AbortSignal }>) => Promise>; - readonly onrequestdisplaymode?: (params: Readonly<{ readonly mode: 'inline' | 'fullscreen' | 'pip' }>, extra: Readonly<{ readonly signal: AbortSignal }>) => Promise>; + readonly ondownloadfile?: (params: Readonly<{ readonly contents: unknown }>, extra: Readonly<{ readonly mcpReq: Readonly<{ readonly signal: AbortSignal }> }>) => Promise>; + readonly onopenlink?: (params: Readonly<{ readonly url: unknown }>, extra: Readonly<{ readonly mcpReq: Readonly<{ readonly signal: AbortSignal }> }>) => Promise>; + readonly onrequestdisplaymode?: (params: Readonly<{ readonly mode: 'inline' | 'fullscreen' | 'pip' }>, extra: Readonly<{ readonly mcpReq: Readonly<{ readonly signal: AbortSignal }> }>) => Promise>; }>; if (bridge.onopenlink === undefined || bridge.ondownloadfile === undefined || bridge.onrequestdisplaymode === undefined) { throw new Error('Expected side-effect handlers.'); @@ -347,10 +387,10 @@ it('aborts valid runtime App side effects before a late consent decision or host const abort = new AbortController(); prompt = deferred<'allow-once' | 'deny'>(); const pending = index === 0 - ? bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ signal: abort.signal })) + ? bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ mcpReq: Object.freeze({ signal: abort.signal }) })) : index === 1 - ? bridge.ondownloadfile({ contents: [{ text: 'forecast', type: 'text' }] }, Object.freeze({ signal: abort.signal })) - : bridge.onrequestdisplaymode({ mode: 'fullscreen' }, Object.freeze({ signal: abort.signal })); + ? bridge.ondownloadfile({ contents: [{ text: 'forecast', type: 'text' }] }, Object.freeze({ mcpReq: Object.freeze({ signal: abort.signal }) })) + : bridge.onrequestdisplaymode({ mode: 'fullscreen' }, Object.freeze({ mcpReq: Object.freeze({ signal: abort.signal }) })); await eventually(() => promptSignals.at(-1) === abort.signal); const reason = new DOMException(`Cancelled side effect ${index}.`, 'AbortError'); abort.abort(reason); @@ -368,7 +408,7 @@ it('aborts valid runtime App side effects before a late consent decision or host holdCreate = true; const createAbort = new AbortController(); - const heldCreate = bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ signal: createAbort.signal })); + const heldCreate = bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ mcpReq: Object.freeze({ signal: createAbort.signal }) })); await eventually(() => createSignals.at(-1) === createAbort.signal); const createReason = new DOMException('Cancelled creation.', 'AbortError'); createAbort.abort(createReason); @@ -381,7 +421,7 @@ it('aborts valid runtime App side effects before a late consent decision or host prompt = deferred<'allow-once' | 'deny'>(); prompt.resolve('allow-once'); const decisionAbort = new AbortController(); - const heldDecision = bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ signal: decisionAbort.signal })); + const heldDecision = bridge.onopenlink({ url: 'https://weather.example/forecast' }, Object.freeze({ mcpReq: Object.freeze({ signal: decisionAbort.signal }) })); await eventually(() => decisionSignals.at(-1) === decisionAbort.signal); const decisionReason = new DOMException('Cancelled decision.', 'AbortError'); decisionAbort.abort(decisionReason); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9c219bb8..8062bfe3a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -38,7 +38,7 @@ importers: version: 1.2.0-beta.15 '@modelcontextprotocol/conformance': specifier: 0.1.16 - version: 0.1.16 + version: 0.1.16(supports-color@7.2.0) '@modelcontextprotocol/server': specifier: 2.0.0 version: 2.0.0 @@ -171,15 +171,21 @@ importers: '@agent-bundle/runtime': specifier: workspace:* version: link:../../packages/rsc-runtime + '@modelcontextprotocol/client': + specifier: 2.0.0 + version: 2.0.0 + '@modelcontextprotocol/core': + specifier: 2.0.0 + version: 2.0.0 '@modelcontextprotocol/ext-apps': specifier: 2.0.0 version: 2.0.0(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/server@2.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) - '@modelcontextprotocol/sdk': - specifier: 1.30.0 - version: 1.30.0(supports-color@7.2.0)(zod@4.5.4) - express: - specifier: 5.2.1 - version: 5.2.1(supports-color@7.2.0) + '@modelcontextprotocol/node': + specifier: 2.0.0 + version: 2.0.0(@modelcontextprotocol/server@2.0.0)(hono@4.13.3) + '@modelcontextprotocol/server': + specifier: 2.0.0 + version: 2.0.0 react: specifier: 19.3.0 version: 19.3.0 @@ -202,9 +208,6 @@ importers: '@rstest/core': specifier: 0.11.12 version: 0.11.12 - '@types/express': - specifier: 5.0.6 - version: 5.0.6 '@types/react': specifier: 19.3.0 version: 19.3.0 @@ -454,9 +457,6 @@ importers: '@modelcontextprotocol/ext-apps': specifier: 2.0.0 version: 2.0.0(@modelcontextprotocol/client@2.0.0)(@modelcontextprotocol/core@2.0.0)(@modelcontextprotocol/server@2.0.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)(zod@4.5.4) - '@modelcontextprotocol/sdk': - specifier: 1.30.0 - version: 1.30.0(supports-color@7.2.0)(zod@4.5.4) '@xterm/addon-fit': specifier: 0.11.0 version: 0.11.0 @@ -1412,15 +1412,9 @@ packages: '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/body-parser@1.19.6': - resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - '@types/debug@4.1.13': resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} @@ -1433,18 +1427,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - '@types/express-serve-static-core@5.1.3': - resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} - - '@types/express@5.0.6': - resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} - '@types/hast@3.0.5': resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} - '@types/http-errors@2.0.5': - resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1460,12 +1445,6 @@ packages: '@types/npm-package-arg@6.1.4': resolution: {integrity: sha512-vDgdbMy2QXHnAruzlv68pUtXCjmqUk3WrBAsRboRovsOmxbfn/WiYCjmecyKjGztnMps5dWp4Uq2prp+Ilo17Q==} - '@types/qs@6.15.1': - resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} - - '@types/range-parser@1.2.7': - resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} - '@types/react-dom@19.3.0': resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==} peerDependencies: @@ -1474,12 +1453,6 @@ packages: '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} - '@types/send@1.2.1': - resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} - - '@types/serve-static@2.2.0': - resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} - '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -3706,7 +3679,7 @@ snapshots: pkce-challenge: 5.0.1 zod: 4.5.4 - '@modelcontextprotocol/conformance@0.1.16': + '@modelcontextprotocol/conformance@0.1.16(supports-color@7.2.0)': dependencies: '@modelcontextprotocol/sdk': 1.30.0(supports-color@7.2.0)(zod@4.5.4) '@octokit/rest': 22.0.1 @@ -4277,20 +4250,11 @@ snapshots: tslib: 2.8.1 optional: true - '@types/body-parser@1.19.6': - dependencies: - '@types/connect': 3.4.38 - '@types/node': 26.5.1 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 - '@types/connect@3.4.38': - dependencies: - '@types/node': 26.5.1 - '@types/debug@4.1.13': dependencies: '@types/ms': 2.1.0 @@ -4303,25 +4267,10 @@ snapshots: '@types/estree@1.0.9': {} - '@types/express-serve-static-core@5.1.3': - dependencies: - '@types/node': 26.5.1 - '@types/qs': 6.15.1 - '@types/range-parser': 1.2.7 - '@types/send': 1.2.1 - - '@types/express@5.0.6': - dependencies: - '@types/body-parser': 1.19.6 - '@types/express-serve-static-core': 5.1.3 - '@types/serve-static': 2.2.0 - '@types/hast@3.0.5': dependencies: '@types/unist': 3.0.3 - '@types/http-errors@2.0.5': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -4336,10 +4285,6 @@ snapshots: '@types/npm-package-arg@6.1.4': {} - '@types/qs@6.15.1': {} - - '@types/range-parser@1.2.7': {} - '@types/react-dom@19.3.0(@types/react@19.3.0)': dependencies: '@types/react': 19.3.0 @@ -4348,15 +4293,6 @@ snapshots: dependencies: csstype: 3.2.3 - '@types/send@1.2.1': - dependencies: - '@types/node': 26.5.1 - - '@types/serve-static@2.2.0': - dependencies: - '@types/http-errors': 2.0.5 - '@types/node': 26.5.1 - '@types/unist@2.0.11': {} '@types/unist@3.0.3': {} diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index fc33f9696..9ba5bd895 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -1039,9 +1039,11 @@ MCP App (): mcp-apps/.html ( gzip) The figures are that build's own measurement, not a contract. A view that reaches its host through `agent-bundle/app` carries the framework's client and nothing else from the protocol -stack; one that imports `@modelcontextprotocol/ext-apps` instead carries `zod` (v3 and v4), -`@modelcontextprotocol/sdk`, `zod-to-json-schema`, and `ext-apps` itself — about 437 kB (104 kB -gzip) before the first line of your own code — and even that view sits well under 1 MiB. A +stack; one that imports `@modelcontextprotocol/ext-apps` 2.x instead carries `zod` 4, +`@modelcontextprotocol/client`, `@modelcontextprotocol/core`, and `ext-apps` itself — about +249 kB (65 kB gzip) before the first line of your own code. An `ext-apps` 1.x view also carries +`zod` 3, `@modelcontextprotocol/sdk`, and `zod-to-json-schema`, about 437 kB (104 kB gzip), and +even that view sits well under 1 MiB. A production build that emits 1 MiB or more reports the `AB4772` advisory naming the five largest modules; any build over 2 MiB reports it too, because 2 MiB is the bound above which the Workbench and `serve-app` hosts refuse the resource and the view stops diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index 1fe70e059..07ef56f45 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -902,9 +902,10 @@ MCP App (): mcp-apps/.html ( gzip) 数字来自当次实际 输出;不要把某次示例构建的体积当作固定契约。 -任何导入 `@modelcontextprotocol/ext-apps` 的视图都会带上 `zod`(v3 与 v4)、 -`@modelcontextprotocol/sdk`、`zod-to-json-schema` 以及 `ext-apps` 本身——在你写下第一行自己的代码 -之前就约有 437 kB(gzip 后 104 kB)——因此一个健康的视图远在 1 MiB 以下。生产构建的输出达到或超过 +任何导入 `@modelcontextprotocol/ext-apps` 2.x 的视图都会带上 `zod` 4、`@modelcontextprotocol/client`、 +`@modelcontextprotocol/core` 以及 `ext-apps` 本身——在你写下第一行自己的代码之前就约有 249 kB +(gzip 后 65 kB)。`ext-apps` 1.x 视图还会带上 `zod` 3、`@modelcontextprotocol/sdk` 与 +`zod-to-json-schema`,约 437 kB(gzip 后 104 kB),即便如此也远在 1 MiB 以下。生产构建的输出达到或超过 1 MiB 时会报告 `AB4772` 建议性诊断并点名最大的五个模块;任何超过 2 MiB 的构建也会报告它,因为 2 MiB 是 Workbench 与 `serve-app` 宿主拒绝该资源的上限,超过后视图在那里将不再渲染。阈值是固定的; 没有配置键可以改变它们。 From 6beeda15fd081426c4f8c93c94cad20e80fdeef1 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 00:23:27 +0000 Subject: [PATCH 3/4] test(workbench): type the signal-forwarding bridge fixture --- packages/workbench/tests/runtime-app-bridge.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/workbench/tests/runtime-app-bridge.test.ts b/packages/workbench/tests/runtime-app-bridge.test.ts index 637429484..357676dd3 100644 --- a/packages/workbench/tests/runtime-app-bridge.test.ts +++ b/packages/workbench/tests/runtime-app-bridge.test.ts @@ -245,7 +245,7 @@ it('passes the official AppBridge request signal to consent and forwarded tools/ }, setNotificationHandler: () => undefined, }), - }), { + }) as never, { installedHandlers: Object.freeze({ openExternalLink: async () => undefined }), requestConsent: async (_challenge, signal?: AbortSignal) => { consentSignals.push(signal); From 20307ad1ddf03a402a2819d8bb23c6dd0d56b0da Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 25 Sep 2026 00:52:24 +0000 Subject: [PATCH 4/4] fix: drop dormant-bridge changeset, reject encoded MCP bodies, align ext-apps 1.x size figures --- .changeset/workbench-ext-apps-2.md | 5 ----- docs/diagnostics.md | 4 ++-- examples/rsc-agent-runtime/src/mcp/http.ts | 7 +++++++ .../tests/mcp-transports.integration.test.ts | 8 ++++++++ packages/agent-bundle/src/build/mcp-app-diagnostics.ts | 2 +- website/docs/en/guide/authoring/mcp.mdx | 2 +- website/docs/zh/guide/authoring/mcp.mdx | 2 +- 7 files changed, 20 insertions(+), 10 deletions(-) delete mode 100644 .changeset/workbench-ext-apps-2.md diff --git a/.changeset/workbench-ext-apps-2.md b/.changeset/workbench-ext-apps-2.md deleted file mode 100644 index a563aa079..000000000 --- a/.changeset/workbench-ext-apps-2.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"agent-bundle": patch ---- - -Host MCP App previews in the `agent-bundle dev` Workbench on `@modelcontextprotocol/ext-apps` 2.0.0. The MCP Apps wire protocol is unchanged, so views built on ext-apps 1.x or 2.x both render, and cancelling a View request still aborts a pending consent prompt or open-link, download, and display-mode action. Error responses a View receives follow ext-apps 2.0: invalid params on `ui/*` requests report `-32602` instead of `-32603`, and messages drop the `MCP error N:` prefix. The `AB4772` size guidance now gives the ext-apps 2.x view floor, about 249 kB (65 kB gzip). (#815) diff --git a/docs/diagnostics.md b/docs/diagnostics.md index 2da52d266..1bee6a784 100644 --- a/docs/diagnostics.md +++ b/docs/diagnostics.md @@ -556,8 +556,8 @@ and nothing else from the protocol stack; one that imports `@modelcontextprotocol/ext-apps` 2.x instead starts at about 249 kB (65 kB gzip): `zod` 4, `@modelcontextprotocol/client`, `@modelcontextprotocol/core`, and `ext-apps` itself. A 1.x view, which also carries `zod` 3, -`@modelcontextprotocol/sdk`, and `zod-to-json-schema`, starts at about 437 kB -(104 kB gzip). The advisory bound of 1 MiB (1,048,576 bytes) sits at roughly +`@modelcontextprotocol/sdk`, and `zod-to-json-schema`, starts at about 445 kB +(106 kB gzip). The advisory bound of 1 MiB (1,048,576 bytes) sits at roughly 2.4× the 1.x floor and at half the 2 MiB (2,097,152 bytes) bound above which the Workbench and `serve-app` hosts refuse the resource and the Rstest browser harness refuses to mount it. The advisory fires when a production diff --git a/examples/rsc-agent-runtime/src/mcp/http.ts b/examples/rsc-agent-runtime/src/mcp/http.ts index a75cddb74..d87ae70a7 100644 --- a/examples/rsc-agent-runtime/src/mcp/http.ts +++ b/examples/rsc-agent-runtime/src/mcp/http.ts @@ -44,6 +44,13 @@ const serveMcp = async (request: IncomingMessage, response: ServerResponse): Pro return; } + const encoding = request.headers['content-encoding']; + if (encoding !== undefined && encoding.toLowerCase() !== 'identity') { + request.resume(); + writeJsonRpcError(response, 415, -32000, `Unsupported Content-Encoding: ${encoding}`); + return; + } + const body = await readRequestBody(request); if (body === undefined) { writeJsonRpcError(response, 413, -32000, 'Request body too large'); diff --git a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts index 6308e0f3d..8475bbabc 100644 --- a/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/mcp-transports.integration.test.ts @@ -325,6 +325,14 @@ test('built Streamable HTTP MCP reports its one JSON startup line and closes cle port: startup.port, }), ).resolves.toBe(413); + await expect( + requestStatus({ + body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'ping' }), + headers: { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json', Host: localHost }, + path: '/mcp', + port: startup.port, + }), + ).resolves.toBe(415); } finally { await client.close(); child.kill('SIGTERM'); diff --git a/packages/agent-bundle/src/build/mcp-app-diagnostics.ts b/packages/agent-bundle/src/build/mcp-app-diagnostics.ts index 032ada4a9..a54d1e94b 100644 --- a/packages/agent-bundle/src/build/mcp-app-diagnostics.ts +++ b/packages/agent-bundle/src/build/mcp-app-diagnostics.ts @@ -42,7 +42,7 @@ export interface McpAppOutputSize { /** * Emitted bytes at which a production view draws the `AB4772` advisory. The * framework floor for a view using `@modelcontextprotocol/ext-apps` measured - * 249 kB on 2.x and 437 kB on 1.x (its SDK, both `zod` generations, + * 249 kB on 2.x and 445 kB on 1.x (its SDK, both `zod` generations, * `zod-to-json-schema`); this is roughly 2.4× the 1.x floor and half the * {@link MAX_APP_HTML_BYTES} host bound. */ diff --git a/website/docs/en/guide/authoring/mcp.mdx b/website/docs/en/guide/authoring/mcp.mdx index 23b26688c..64b80d923 100644 --- a/website/docs/en/guide/authoring/mcp.mdx +++ b/website/docs/en/guide/authoring/mcp.mdx @@ -1042,7 +1042,7 @@ through `agent-bundle/app` carries the framework's client and nothing else from stack; one that imports `@modelcontextprotocol/ext-apps` 2.x instead carries `zod` 4, `@modelcontextprotocol/client`, `@modelcontextprotocol/core`, and `ext-apps` itself, about 249 kB (65 kB gzip) before the first line of your own code. An `ext-apps` 1.x view also carries -`zod` 3, `@modelcontextprotocol/sdk`, and `zod-to-json-schema`, about 437 kB (104 kB gzip), and +`zod` 3, `@modelcontextprotocol/sdk`, and `zod-to-json-schema`, about 445 kB (106 kB gzip), and even that view sits well under 1 MiB. A production build that emits 1 MiB or more reports the `AB4772` advisory naming the five largest modules; any build over 2 MiB reports it too, because 2 MiB is diff --git a/website/docs/zh/guide/authoring/mcp.mdx b/website/docs/zh/guide/authoring/mcp.mdx index 5438d81c7..241733dda 100644 --- a/website/docs/zh/guide/authoring/mcp.mdx +++ b/website/docs/zh/guide/authoring/mcp.mdx @@ -905,7 +905,7 @@ MCP App (): mcp-apps/.html ( gzip) 任何导入 `@modelcontextprotocol/ext-apps` 2.x 的视图都会带上 `zod` 4、`@modelcontextprotocol/client`、 `@modelcontextprotocol/core` 以及 `ext-apps` 本身,在你写下第一行自己的代码之前就约有 249 kB (gzip 后 65 kB)。`ext-apps` 1.x 视图还会带上 `zod` 3、`@modelcontextprotocol/sdk` 与 -`zod-to-json-schema`,约 437 kB(gzip 后 104 kB),即便如此也远在 1 MiB 以下。生产构建的输出达到或超过 +`zod-to-json-schema`,约 445 kB(gzip 后 106 kB),即便如此也远在 1 MiB 以下。生产构建的输出达到或超过 1 MiB 时会报告 `AB4772` 建议性诊断并点名最大的五个模块;任何超过 2 MiB 的构建也会报告它,因为 2 MiB 是 Workbench 与 `serve-app` 宿主拒绝该资源的上限,超过后视图在那里将不再渲染。阈值是固定的; 没有配置键可以改变它们。