From d88ef47f2bfc8ad9d7c63f9f9c353270357faa34 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 14:56:31 +0000 Subject: [PATCH 1/2] feat: port to MCP SDK v2 (minimum-diff via compat layer) Ports ext-apps to @modelcontextprotocol/sdk ^2.0.0-alpha.2 using the v2 backwards-compat layer. Source delta is 4 files +44/-31; the compat layer keeps v1 deep-import paths and the ZodSchema overloads working. Unavoidable changes: - Protocol -> Protocol generic-arity (events.ts) - assertCapability* override params widened to string (app.ts, app-bridge.ts) - registerAppTool callback unifies LegacyToolCallback | ToolCallback to cover both raw-shape and StandardSchema args (server/index.ts) CI will fail until @modelcontextprotocol/sdk@2.0.0-alpha.2 is published; see typescript-sdk milestone v2.0.0-bc. package-lock.json intentionally not updated in this commit. --- package.json | 4 ++-- src/app-bridge.ts | 4 ++-- src/app.ts | 4 ++-- src/events.ts | 48 ++++++++++++++++++++++----------------------- src/server/index.ts | 19 ++++++++++++++++-- 5 files changed, 46 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 34b2c6a46..6475c5ce2 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "author": "Olivier Chafik", "devDependencies": { "@boneskull/typedoc-plugin-mermaid": "^0.2.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^2.0.0-alpha.2", "@playwright/test": "1.57.0", "@types/bun": "^1.3.2", "@types/node": "20.19.27", @@ -107,7 +107,7 @@ "zod": "^4.1.13" }, "peerDependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^2.0.0-alpha.2", "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" diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 77125872c..185015376 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1325,7 +1325,7 @@ export class AppBridge extends ProtocolWithEvents< * Verify that the guest supports the capability required for the given request method. * @internal */ - assertCapabilityForMethod(method: AppRequest["method"]): void { + assertCapabilityForMethod(method: string): void { // TODO } @@ -1341,7 +1341,7 @@ export class AppBridge extends ProtocolWithEvents< * Verify that the host supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(method: AppNotification["method"]): void { + assertNotificationCapability(method: string): void { // TODO } diff --git a/src/app.ts b/src/app.ts index 4f429e829..00cd83c34 100644 --- a/src/app.ts +++ b/src/app.ts @@ -762,7 +762,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 { + assertCapabilityForMethod(method: string): void { // TODO } @@ -792,7 +792,7 @@ export class App extends ProtocolWithEvents< * Verify that the app supports the capability required for the given notification method. * @internal */ - assertNotificationCapability(method: AppNotification["method"]): void { + assertNotificationCapability(method: string): void { // TODO } diff --git a/src/events.ts b/src/events.ts index 4d17ca4f2..9c8c3e7e8 100644 --- a/src/events.ts +++ b/src/events.ts @@ -3,7 +3,11 @@ import { Request, Notification, Result, + type BaseContext, + type LegacyContextFields, } from "@modelcontextprotocol/sdk/types.js"; + +type AppContext = BaseContext & LegacyContextFields; import { ZodLiteral, ZodObject } from "zod/v4"; type MethodSchema = ZodObject<{ method: ZodLiteral }>; @@ -61,7 +65,10 @@ export abstract class ProtocolWithEvents< SendNotificationT extends Notification, SendResultT extends Result, EventMap extends Record, -> extends Protocol { +> extends Protocol { + protected buildContext(ctx: AppContext): AppContext { + return ctx; + } private _registeredMethods = new Set(); private _eventSlots = new Map(); @@ -209,14 +216,11 @@ export abstract class ProtocolWithEvents< * * @throws {Error} if a handler for this method is already registered. */ - override setRequestHandler: Protocol< - SendRequestT, - SendNotificationT, - SendResultT - >["setRequestHandler"] = (schema, handler) => { - this._assertMethodNotRegistered(schema, "setRequestHandler"); - super.setRequestHandler(schema, handler); - }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setRequestHandler = ((...args: any) => { + this._assertMethodNotRegistered(args[0], "setRequestHandler"); + super.setRequestHandler(...(args as [MethodSchema, () => Result])); + }) as Protocol["setRequestHandler"]; /** * Registers a notification handler. Throws if a handler for the same @@ -225,14 +229,11 @@ export abstract class ProtocolWithEvents< * * @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); - }; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + override setNotificationHandler = ((...args: any) => { + this._assertMethodNotRegistered(args[0], "setNotificationHandler"); + super.setNotificationHandler(...(args as [MethodSchema, () => void])); + }) as Protocol["setNotificationHandler"]; /** * Warn if a request handler `on*` setter is replacing a previously-set @@ -255,15 +256,12 @@ export abstract class ProtocolWithEvents< * 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + protected replaceRequestHandler = ((...args: any) => { + const method = (args[0] as MethodSchema).shape.method.value; this._registeredMethods.add(method); - super.setRequestHandler(schema, handler); - }; + super.setRequestHandler(...(args as [MethodSchema, () => Result])); + }) as Protocol["setRequestHandler"]; private _assertMethodNotRegistered(schema: unknown, via: string): void { const method = (schema as MethodSchema).shape.method.value; diff --git a/src/server/index.ts b/src/server/index.ts index 87209ef39..0febe3a54 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -41,17 +41,27 @@ import { } from "../app.js"; import type { BaseToolCallback, + LegacyToolCallback, McpServer, RegisteredTool, ResourceMetadata, - ToolCallback, + ToolCallback as SchemaToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { AnySchema, ZodRawShapeCompat, + StandardSchemaWithJSON, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; +import type { ZodRawShape } from "@modelcontextprotocol/sdk"; + +type ToolCallback = + Args extends ZodRawShape + ? LegacyToolCallback + : Args extends StandardSchemaWithJSON + ? SchemaToolCallback + : SchemaToolCallback; import type { ClientCapabilities, ReadResourceResult, @@ -241,7 +251,12 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); + return (server.registerTool as McpServer["registerTool"])( + name, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + { ...config, _meta: normalizedMeta } as any, + cb as SchemaToolCallback, + ); } export type McpUiReadResourceResult = ReadResourceResult & { From 3871100d2f87cc590c8b260f0ca167d6dd0e1eb3 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Fri, 17 Apr 2026 15:18:35 +0000 Subject: [PATCH 2/2] refactor: drop type assertions in v2 port events.ts: convert property-override arrow functions to method overrides with declared overload signatures. The overrides now expose only the ZodSchema form (the only one this package uses); a guard skips tracking during super() construction (Protocol's ctor registers built-in handlers before our fields initialize). Adds _methodOf() helper, removes the MethodSchema local type and the eslint-disable directives. server/index.ts: split registerAppTool into two public overloads (StandardSchemaWithJSON and ZodRawShape forms) matching registerTool's, removing the union-args casts. The impl signature still loses the overload pairing, so the forwarding call into registerTool uses one @ts-expect-error in place of three casts. Re-export LegacyToolCallback alongside ToolCallback. --- src/events.ts | 95 ++++++++++++++++++++++++++++++++------------- src/server/index.ts | 45 +++++++++++++-------- 2 files changed, 95 insertions(+), 45 deletions(-) diff --git a/src/events.ts b/src/events.ts index 9c8c3e7e8..00e4045b0 100644 --- a/src/events.ts +++ b/src/events.ts @@ -5,12 +5,10 @@ import { Result, type BaseContext, type LegacyContextFields, + type ZodLikeRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; type AppContext = BaseContext & LegacyContextFields; -import { ZodLiteral, ZodObject } from "zod/v4"; - -type MethodSchema = ZodObject<{ method: ZodLiteral }>; /** * Per-event state: a singular `on*` handler (replace semantics) plus a @@ -78,7 +76,7 @@ export abstract class ProtocolWithEvents< * schema on first use. */ protected abstract readonly eventSchemas: { - [K in keyof EventMap]: MethodSchema; + [K in keyof EventMap]: ZodLikeRequestSchema; }; /** @@ -202,12 +200,15 @@ export abstract class ProtocolWithEvents< // ── 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(). + // These overrides are prototype methods, so Protocol's constructor (which + // registers built-in ping/cancelled/progress handlers via + // `this.setRequestHandler` during super()) dispatches here before our own + // fields have initialized. The `_registeredMethods === undefined` guard + // skips tracking during that window. + // + // The base method has four overloads in v2; we expose only the Zod-schema + // form here since that is all this package uses. Subclasses retain the + // typed (request, ctx) handler signature. /** * Registers a request handler. Throws if a handler for the same method @@ -216,11 +217,24 @@ export abstract class ProtocolWithEvents< * * @throws {Error} if a handler for this method is already registered. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override setRequestHandler = ((...args: any) => { - this._assertMethodNotRegistered(args[0], "setRequestHandler"); - super.setRequestHandler(...(args as [MethodSchema, () => Result])); - }) as Protocol["setRequestHandler"]; + override setRequestHandler( + requestSchema: T, + handler: ( + request: ReturnType, + ctx: AppContext, + ) => Result | Promise, + ): void; + override setRequestHandler( + schema: ZodLikeRequestSchema, + handler: (request: unknown, ctx: AppContext) => Result | Promise, + ): void { + if (this._registeredMethods === undefined) { + super.setRequestHandler(schema, handler); + return; + } + this._assertMethodNotRegistered(schema, "setRequestHandler"); + super.setRequestHandler(schema, handler); + } /** * Registers a notification handler. Throws if a handler for the same @@ -229,11 +243,21 @@ export abstract class ProtocolWithEvents< * * @throws {Error} if a handler for this method is already registered. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - override setNotificationHandler = ((...args: any) => { - this._assertMethodNotRegistered(args[0], "setNotificationHandler"); - super.setNotificationHandler(...(args as [MethodSchema, () => void])); - }) as Protocol["setNotificationHandler"]; + override setNotificationHandler( + notificationSchema: T, + handler: (notification: ReturnType) => void | Promise, + ): void; + override setNotificationHandler( + schema: ZodLikeRequestSchema, + handler: (notification: unknown) => void | Promise, + ): void { + if (this._registeredMethods === undefined) { + super.setNotificationHandler(schema, handler); + return; + } + this._assertMethodNotRegistered(schema, "setNotificationHandler"); + super.setNotificationHandler(schema, handler); + } /** * Warn if a request handler `on*` setter is replacing a previously-set @@ -256,15 +280,30 @@ export abstract class ProtocolWithEvents< * Replace a request handler, bypassing double-set protection. Used by * `on*` request-handler setters that need replace semantics. */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - protected replaceRequestHandler = ((...args: any) => { - const method = (args[0] as MethodSchema).shape.method.value; - this._registeredMethods.add(method); - super.setRequestHandler(...(args as [MethodSchema, () => Result])); - }) as Protocol["setRequestHandler"]; + protected replaceRequestHandler( + requestSchema: T, + handler: ( + request: ReturnType, + ctx: AppContext, + ) => Result | Promise, + ): void; + protected replaceRequestHandler( + schema: ZodLikeRequestSchema, + handler: (request: unknown, ctx: AppContext) => Result | Promise, + ): void { + this._registeredMethods.add(this._methodOf(schema)); + super.setRequestHandler(schema, handler); + } - private _assertMethodNotRegistered(schema: unknown, via: string): void { - const method = (schema as MethodSchema).shape.method.value; + private _methodOf(arg: ZodLikeRequestSchema | string): string { + return typeof arg === "string" ? arg : arg.shape.method.value; + } + + private _assertMethodNotRegistered( + schema: ZodLikeRequestSchema | string, + via: string, + ): void { + const method = this._methodOf(schema); if (this._registeredMethods.has(method)) { throw new Error( `Handler for "${method}" already registered (via ${via}). ` + diff --git a/src/server/index.ts b/src/server/index.ts index 0febe3a54..ffabe1e51 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -45,7 +45,7 @@ import type { McpServer, RegisteredTool, ResourceMetadata, - ToolCallback as SchemaToolCallback, + ToolCallback, ReadResourceCallback as _ReadResourceCallback, RegisteredResource, } from "@modelcontextprotocol/sdk/server/mcp.js"; @@ -55,13 +55,6 @@ import type { StandardSchemaWithJSON, } from "@modelcontextprotocol/sdk/server/zod-compat.js"; import type { ZodRawShape } from "@modelcontextprotocol/sdk"; - -type ToolCallback = - Args extends ZodRawShape - ? LegacyToolCallback - : Args extends StandardSchemaWithJSON - ? SchemaToolCallback - : SchemaToolCallback; import type { ClientCapabilities, ReadResourceResult, @@ -70,7 +63,7 @@ import type { // Re-exports for convenience export { RESOURCE_URI_META_KEY, RESOURCE_MIME_TYPE }; -export type { ResourceMetadata, ToolCallback }; +export type { ResourceMetadata, ToolCallback, LegacyToolCallback }; /** * Base tool configuration matching the standard MCP server tool options. @@ -224,8 +217,8 @@ export interface McpUiAppResourceConfig extends ResourceMetadata { * @see {@link registerAppResource `registerAppResource`} to register the HTML resource referenced by the tool */ export function registerAppTool< - OutputArgs extends ZodRawShapeCompat | AnySchema, - InputArgs extends undefined | ZodRawShapeCompat | AnySchema = undefined, + OutputArgs extends StandardSchemaWithJSON, + InputArgs extends StandardSchemaWithJSON | undefined = undefined, >( server: Pick, name: string, @@ -234,6 +227,25 @@ export function registerAppTool< outputSchema?: OutputArgs; }, cb: ToolCallback, +): RegisteredTool; +/** Raw-shape form: `inputSchema` may be a plain `{ field: z.string() }` record. */ +export function registerAppTool< + InputArgs extends ZodRawShape, + OutputArgs extends ZodRawShape | StandardSchemaWithJSON | undefined = undefined, +>( + server: Pick, + name: string, + config: McpUiAppToolConfig & { + inputSchema: InputArgs; + outputSchema?: OutputArgs; + }, + cb: LegacyToolCallback, +): RegisteredTool; +export function registerAppTool( + server: Pick, + name: string, + config: McpUiAppToolConfig, + cb: (...args: never) => ReturnType>, ): RegisteredTool { // Normalize metadata for backward compatibility: // - If _meta.ui.resourceUri is set, also set the legacy flat key @@ -251,12 +263,11 @@ export function registerAppTool< normalizedMeta = { ...meta, ui: { ...uiMeta, resourceUri: legacyUri } }; } - return (server.registerTool as McpServer["registerTool"])( - name, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - { ...config, _meta: normalizedMeta } as any, - cb as SchemaToolCallback, - ); + // The two public overloads above guarantee (config.inputSchema, cb) match one + // of registerTool's overloads. The impl signature loses that pairing, so this + // forward needs a single suppression rather than three casts. + // @ts-expect-error -- forwarding overload-paired args through one impl signature + return server.registerTool(name, { ...config, _meta: normalizedMeta }, cb); } export type McpUiReadResourceResult = ReadResourceResult & {