fix(core-internal): let explicit-schema handlers/calls escape the era-universe gate - #2599
Conversation
…-universe gate The inbound and outbound era gates rejected any method name that ever appeared in a past protocol revision's registry but is absent from the current era's registry, even when the consumer explicitly registered a handler (or supplied a schema on send) for it. This made extension methods that reuse a historical core method name unreachable: the Tasks extension (SEP-2663) defines `tasks/get` and `tasks/cancel`, both of which the 2025-11-25 revision used for now-removed core methods, so a 2026-era server could never serve them and a 2026-era client could never send them — every attempt answered -32601 or threw MethodNotSupportedByProtocolVersion before the handler or the transport were ever consulted. Both gates now only apply to TYPED dispatch (setRequestHandler(method, handler) inbound, request(method, options) outbound) — exactly the path the SDK's own built-ins (initialize, ping, logging/setLevel) use, which correctly stays era-gated. A method registered or sent with an EXPLICIT schema (setRequestHandler(method, schemas, handler) / request(request, resultSchema, options)) is the extension-authoring path: the consumer supplied their own validation, so a historical registry collision no longer blocks it. Fixes modelcontextprotocol#2598
🦋 Changeset detectedLatest commit: c49b7ba The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Verified with on one of my MCP projects with the latest spec:
Thanks for the fix — it unblocks the Tasks extension on v2 servers. |
Summary
Fixes #2598.
The inbound and outbound era gates in
Protocolrejected any method name that ever appeared in a past protocol revision's registry but is absent from the current era's registry — even when the consumer had explicitly registered a handler for it (or supplied a schema when sending). This makes any extension that reuses a historical core method name permanently unreachable: the Tasks extension (SEP-2663) definestasks/getandtasks/cancel, both of which the 2025-11-25 revision used for now-removed core methods, so:server.setRequestHandler('tasks/get', { params, result }, handler)and the handler would never run — every inbound request answered-32601before the handler was consultedtasks/getviarequest({ method: 'tasks/get', params }, schema)either — it threwSdkError(MethodNotSupportedByProtocolVersion)locally before reaching the transport, despitedocs/migration/support-2026-07-28.mddocumenting this exact call as the recommended task-interop patternBoth gates now only apply to typed dispatch —
setRequestHandler(method, handler)inbound,request(method, options)outbound — which is exactly the path the SDK's own built-ins (initialize,ping,logging/setLevel) use, and which correctly stays fully era-gated (verified by a new/updated test:initialize/pingstill can't cross eras). A method registered or sent with an explicit schema —setRequestHandler(method, schemas, handler)/request(request, resultSchema, options)— is the extension-authoring path: the consumer supplied their own validation, so a historical registry name collision no longer blocks it.Changes
packages/core-internal/src/shared/protocol.ts: track explicit-schemasetRequestHandlerregistrations in a new_customSchemaRequestMethodsset; exempt them from the inbound era gate in_onrequest. Reorder the publicrequest()overload so the outbound era gate only runs on the typed-dispatch branch, not the explicit-schema branch.packages/core-internal/src/wire/codec.ts: update the module-doc to describe the refined (schema-aware) shadowing policy instead of the old blanket one.docs/migration/support-2026-07-28.md: correct the two spots that documented the old (broken) behavior, including the task-interop recipe that never actually worked inbound.packages/core-internal/test/wire/eraGates.test.ts: flip the test that pinned the bug (tasks/getwith a registered handler answering-32601) to assert the fix, add a case for the still-strict typed path, and add outbound coverage forrequest()with an explicit schema..changeset/era-gate-explicit-schema-handlers.md: patch bump for@modelcontextprotocol/clientand@modelcontextprotocol/server.Test plan
pnpm --filter @modelcontextprotocol/core-internal test— all passing except a pre-existing, unrelatedschemaTwinConformancefailure (reproduces identically on a cleanmaincheckout in this environment)pnpm --filter @modelcontextprotocol/server test/@modelcontextprotocol/client test— all passing except a pre-existing, unrelatedworkerdSchemaPreloadbuild-dependent failure (also reproduces on cleanmain)pnpm --filter @modelcontextprotocol/core-internal typecheck/ server / client typecheck — clean-32601, typed handlers (ping) still fully era-gated, outbound explicit-schemarequest()now reaches the transport on the deleted-method era