From 6d083cd69f7b8214f236b75cc0ce52fe18617a7a Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Tue, 8 Sep 2026 13:09:13 +0100 Subject: [PATCH] Address review: MCPB build, guard the SDK's own handlers, doc fixes - pdf-server build-mcpb.mjs packs the ext-apps build from this checkout into the staging dir instead of asking the registry for ^2.0.0, which is not published yet; CI's "Build MCPB bundle" step failed on ETARGET. - Claim notifications/cancelled and notifications/progress in the MethodRegistry right after super() so a direct setNotificationHandler cannot silently replace the base Protocol's own handlers. - Migration guide and add-app-to-server skill: custom methods use the three-argument setRequestHandler("m", { params }, handler) form (the two-argument form is only for spec methods); zod schemas live in @modelcontextprotocol/core, not client/server; it is toolInfo.tool.outputSchema that loosened in schema.json, and the recursive JSON-value def is nested under three definitions; core is a required peer that client already depends on. - app-bridge.ts: fix "forthe"/"forresult" typos in @see links, update three JSDoc examples to the method-string setNotificationHandler form, move the UntypedHandlerSetter type below the import block (same in app.ts). - wire-compat.test.ts: say the fixtures are 1.x-shaped, not captures. --- README.md | 2 +- docs/migrate-to-2.md | 24 +++++++---- examples/pdf-server/build-mcpb.mjs | 15 +++++++ .../skills/add-app-to-server/SKILL.md | 5 ++- src/app-bridge.ts | 42 ++++++++++--------- src/app.ts | 7 +++- src/wire-compat.test.ts | 4 +- 7 files changed, 65 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 4b92f0c..acfc661 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,7 @@ resources: ## Getting Started Requires Node.js 20+. The base MCP SDK packages are `^2.0.0` peers of -`ext-apps` (`@modelcontextprotocol/core` is pulled in transitively). +`ext-apps` (`@modelcontextprotocol/core` is a required peer that `client` already depends on, so npm installs it for you). For a View or host: diff --git a/docs/migrate-to-2.md b/docs/migrate-to-2.md index 3f6db7b..2164b83 100644 --- a/docs/migrate-to-2.md +++ b/docs/migrate-to-2.md @@ -27,7 +27,7 @@ host-side deltas are in error responses (see below). | 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 installed transitively; +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. @@ -48,8 +48,10 @@ its `Protocol` class); `@modelcontextprotocol/core` is installed transitively; `extra.mcpReq.id`. - **`setRequestHandler` / `setNotificationHandler` take method names.** `app.setRequestHandler(SomeRequestSchema, handler)` becomes - `app.setRequestHandler("some/method", handler)` (pass the params schema as the - optional extra argument for validation). + `app.setRequestHandler("some/method", { params: SomeParamsSchema }, (params, ctx) => …)` + for custom methods (the handler receives the parsed params); the two-argument + `setRequestHandler("tools/call", handler)` form exists only for spec-defined + method names. - **Errors.** Remote JSON-RPC errors are `ProtocolError` (numeric `code`); local conditions are `SdkError` with a string `code`: request timeout → `"REQUEST_TIMEOUT"`, connection closed → `"CONNECTION_CLOSED"`. Cancelling a @@ -74,10 +76,15 @@ The published `./schema.json` export is regenerated from the 2.x core schemas: - `McpUiToolResultNotification.params.structuredContent` is any JSON value (was `type: "object"`). -- Result `_meta` documents `io.modelcontextprotocol/serverInfo` and no longer - lists `progressToken` / `related-task` (both still pass through). -- `McpUiHostContext.toolInfo.tool.inputSchema` is a loose object. -- A recursive `$defs.__schema0` JSON-value definition is added. +- `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 @@ -87,7 +94,8 @@ The published `./schema.json` export is regenerated from the 2.x core schemas: `@modelcontextprotocol/server`, `sdk/server/streamableHttp.js` → `NodeStreamableHTTPServerTransport` from `@modelcontextprotocol/node`, `sdk/server/stdio.js` → `@modelcontextprotocol/server/stdio`, `sdk/types.js` - → `@modelcontextprotocol/client` or `server`). + → `@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. 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/plugins/mcp-apps/skills/add-app-to-server/SKILL.md b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md index df7d931..5a473f0 100644 --- a/plugins/mcp-apps/skills/add-app-to-server/SKILL.md +++ b/plugins/mcp-apps/skills/add-app-to-server/SKILL.md @@ -94,10 +94,11 @@ handler schemas before adding the App integration: | `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, schemas) | `@modelcontextprotocol/client` or `@modelcontextprotocol/server` | +| `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", handler)` | +| `setRequestHandler(SomeRequestSchema, handler)` | `setRequestHandler("some/method", { params: ParamsSchema }, handler)` (the 2-arg form is only for spec methods such as `"tools/call"`) | ## Step 3: Set Up the Build Pipeline diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 04ccb4f..3d145d4 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -42,8 +42,6 @@ type MethodSchema = ZodObject<{ params: ZodType; }>; -type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; - import { type McpUiSandboxResourceReadyNotification, type McpUiSizeChangedNotification, @@ -93,6 +91,8 @@ import { export * from "./types.js"; export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE } from "./constants.js"; import { RESOURCE_URI_META_KEY } from "./constants.js"; + +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; export { PostMessageTransport } from "./message-transport.js"; /** @@ -527,6 +527,8 @@ export class AppBridge extends Protocol { options?: HostOptions, ) { super(options); + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); this._ensureEventSlot("initialized"); @@ -1213,8 +1215,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CallToolRequest` from @modelcontextprotocol/client forthe request type - * @see `CallToolResult` from @modelcontextprotocol/client forthe 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"], @@ -1267,8 +1269,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `CreateMessageRequest` from @modelcontextprotocol/client forthe request type - * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client forresult types + * @see `CreateMessageRequest` from @modelcontextprotocol/client for the request type + * @see `CreateMessageResult` / `CreateMessageResultWithTools` from @modelcontextprotocol/client for result types */ private _oncreatesamplingmessage?: ( params: CreateMessageRequest["params"], @@ -1314,12 +1316,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ToolListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/tools/list_changed", () => { * bridge.sendToolListChanged(); * }); * ``` * - * @see `ToolListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `ToolListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendToolListChanged(params: ToolListChangedNotification["params"] = {}) { return this.notification({ @@ -1350,8 +1352,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourcesRequest` from @modelcontextprotocol/client forthe request type - * @see `ListResourcesResult` from @modelcontextprotocol/client forthe 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"], @@ -1403,8 +1405,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListResourceTemplatesRequest` from @modelcontextprotocol/client forthe request type - * @see `ListResourceTemplatesResult` from @modelcontextprotocol/client forthe 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"], @@ -1459,8 +1461,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ReadResourceRequest` from @modelcontextprotocol/client forthe request type - * @see `ReadResourceResult` from @modelcontextprotocol/client forthe 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"], @@ -1502,12 +1504,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(ResourceListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/resources/list_changed", () => { * bridge.sendResourceListChanged(); * }); * ``` * - * @see `ResourceListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `ResourceListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendResourceListChanged( params: ResourceListChangedNotification["params"] = {}, @@ -1540,8 +1542,8 @@ export class AppBridge extends Protocol { * }; * ``` * - * @see `ListPromptsRequest` from @modelcontextprotocol/client forthe request type - * @see `ListPromptsResult` from @modelcontextprotocol/client forthe 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"], @@ -1582,12 +1584,12 @@ export class AppBridge extends Protocol { * @example * ```typescript * // In your MCP client notification handler: - * mcpClient.setNotificationHandler(PromptListChangedNotificationSchema, () => { + * mcpClient.setNotificationHandler("notifications/prompts/list_changed", () => { * bridge.sendPromptListChanged(); * }); * ``` * - * @see `PromptListChangedNotification` from @modelcontextprotocol/client forthe notification type + * @see `PromptListChangedNotification` from @modelcontextprotocol/client for the notification type */ sendPromptListChanged(params: PromptListChangedNotification["params"] = {}) { return this.notification({ diff --git a/src/app.ts b/src/app.ts index fd6bbc7..58ea9da 100644 --- a/src/app.ts +++ b/src/app.ts @@ -26,7 +26,6 @@ export { RESOURCE_MIME_TYPE, RESOURCE_URI_META_KEY } from "./constants.js"; import { EventDispatcher, MethodRegistry } from "./events.js"; export { EventDispatcher } from "./events.js"; -type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; import { PostMessageTransport } from "./message-transport.js"; import { LATEST_PROTOCOL_VERSION, @@ -69,6 +68,8 @@ import { } from "./standard-schema.js"; import { z, type ZodLiteral, type ZodObject, type ZodType } from "zod/v4"; +type UntypedHandlerSetter = (this: unknown, ...args: unknown[]) => void; + type MethodSchema = ZodObject<{ method: ZodLiteral; params: ZodType; @@ -530,6 +531,10 @@ export class App extends Protocol { private options: AppOptions = { autoResize: true }, ) { super(options); + // Claim the handlers the base Protocol constructor installed so a direct + // setNotificationHandler cannot silently replace them either. + this._methods.replace("notifications/cancelled"); + this._methods.replace("notifications/progress"); if (!options.allowUnsafeEval) { z.config({ jitless: true }); diff --git a/src/wire-compat.test.ts b/src/wire-compat.test.ts index 5fdca8b..762ac77 100644 --- a/src/wire-compat.test.ts +++ b/src/wire-compat.test.ts @@ -77,7 +77,7 @@ const hostContext: McpUiHostContext = { locale: "en-US", }; -/** Captured from ext-apps 1.7.x View (sdk 1.x key order, numeric ids from 0). */ +/** 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", @@ -120,7 +120,7 @@ const v1View = { }), }; -/** Captured from ext-apps 1.7.x AppBridge (sdk 1.x). */ +/** Shaped like ext-apps 1.7.x AppBridge traffic (sdk 1.x); not a recorded capture. */ const v1Host = { initializeResult: (id: number) => ({ result: {