Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
103 KiB
@modelcontextprotocol/server
2.0.0
Minor Changes
-
#2501
1480241Thanks @felixweinberger! - Export theProtocolbase class andmergeCapabilitiesfrom the@modelcontextprotocol/clientand@modelcontextprotocol/serverpackage roots, restoring the v1 import for consumers that subclassProtocol(e.g. the MCP Apps SDK). The client and server packages each bundle their own compiled copy of the class, so import it from one package consistently within a process.The codemod now rewrites
ProtocolandmergeCapabilitiesimports fromshared/protocol.jsto the client or server package root, like the module's other symbols, instead of dropping them with an action-required marker. -
#2477
8e1d2e9Thanks @felixweinberger! - Move the schema source modules (spec schemas, OAuth schemas, protocol constants) into@modelcontextprotocol/coreand resolve them from there as a regular runtime dependency instead of bundling a private copy into each package. An application importing more than one of the packages now evaluates a single shared schema graph with shared object identity.@modelcontextprotocol/coregains a./internalsubpath (SDK-internal contract; may change in any release) and the four packages now version together. -
#2513
f413763Thanks @felixweinberger! - Align the 2026-07-28 wire with the final revision (spec PR #3002):serverInfomoves from theDiscoverResultbody to the result_meta, and the per-request envelope'sclientInfodemotes from required to SHOULD.Before this change the SDK shipped the pre-#3002 shape in both directions: the client hard-rejected a conforming server's
DiscoverResult(missing bodyserverInfofailed parse, so the probe misclassified the server as legacy and attempted aninitializehandshake against it — a hard connect failure against a modern-only server such as go-sdk v1.7.0-pre.3), and the server rejected conforming clients that omitclientInfo.Now:
- The 2026 wire schemas are the final revision exactly: no body
serverInfoonDiscoverResult, envelopeclientInfooptional (a present-but-malformed value still fails validation). - Servers stamp
_meta['io.modelcontextprotocol/serverInfo']on every 2026-era response (spec SHOULD; a handler-authored value wins, the 2025-era wire is untouched). This includes the entry-builtsubscriptions/listengraceful-close results — the spec'sSubscriptionsListenResultMetaextendsResultMetaObject. - Clients keep sending
clientInfoand read server identity from the discover result's_metaonly. A server that stamps no identity is anonymous:getServerVersion()isundefinedand the response cache partitions under a per-connection surrogate. A malformed_metaserverInfo value is treated as absent on receive (the spec marks the field self-reported, unverified, and display-only). - Breaking type changes:
DiscoverResultno longer declaresserverInfo;RequestMetaEnvelope'sclientInfois optional. New public constantSERVER_INFO_META_KEY('io.modelcontextprotocol/serverInfo').
- The 2026 wire schemas are the final revision exactly: no body
-
#2369
24be404Thanks @mattzcarey! - AllowinputRequired.elicit()to accept a Standard Schema such as a Zod object forrequestedSchema. The builder converts it to MCP's restricted form-elicitation JSON Schema, while the same schema can validate and type the response throughacceptedContent()on handler re-entry. Zod formats mapping toemail,uri,date, anddate-timeare supported. Shapes the restricted schema cannot express reject before anything is sent — nested objects,.regex()and customized zod format patterns, exclusive number bounds (.positive()/.gt()), literal unions (usez.enumorz.literal(['a', 'b'])), and non-spec root keywords likez.strictObject()'sadditionalProperties. -
#2541
470678dThanks @mattzcarey! - Add configurable SSE keep-alive comment frames to Streamable HTTP transports and applycreateMcpHandler's existingkeepAliveMsoption to every HTTP SSE stream it serves. -
#2420
7635115Thanks @felixweinberger! - Add runtime-neutral Bearer authentication to@modelcontextprotocol/server:requireBearerAuthgates web-standardfetch(request)hosts (Cloudflare Workers, Deno, Bun, Hono), built on the exportedverifyBearerTokenandbearerAuthChallengeResponsepieces, withOAuthTokenVerifiernow defined here. The Express middleware adapts the same core and is unchanged in behavior, except thatWWW-Authenticatechallenge values are now RFC 7235 quoted-string sanitized (quotes and backslashes escaped, control and non-ASCII characters replaced);@modelcontextprotocol/expressre-exportsOAuthTokenVerifieras before. -
#2422
61866d7Thanks @felixweinberger! - Add runtime-neutral OAuth discovery serving to@modelcontextprotocol/server:oauthMetadataResponseserves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents from web-standardfetch(request)hosts, built on the exportedbuildOAuthProtectedResourceMetadata, withgetOAuthProtectedResourceMetadataUrlnow defined here. The Express metadata router adapts the same core and is unchanged in behavior; the insecure-issuer escape hatch is an explicitdangerouslyAllowInsecureIssuerUrloption in the neutral core instead of a module-scope environment read. The web-standard matcher validates lazily so unmatched traffic always falls through, tolerates a trailing slash, supports HEAD, and marks reflected CORS preflights withVary. -
#2483
3f07a32Thanks @felixweinberger! - AddpreloadSchemas(), an explicit opt-in to eager wire-schema construction, and call it automatically in the Cloudflare Workers builds. The wire schemas are built lazily by default, which is the right trade on process-per-invocation runtimes — but on isolate platforms that bill request CPU while module evaluation runs during isolate warm-up, laziness moves construction into the first request each fresh isolate serves. CallingpreloadSchemas()at module scope (it is synchronous and idempotent) moves that one-time cost back to module evaluation; the packages' workerd export condition now does this automatically, while the Node and browser builds stay lazy. The server package gains a dedicated browser shim for this (itsbrowsercondition previously reused the workerd shim), so browser bundles keep lazy construction.
Patch Changes
-
#2402
a400259Thanks @felixweinberger! - First beta release of SDK v2 with support for the MCP 2026-07-28 specification revision. See the migration guides for upgrading from v1 (docs/migration/upgrade-to-v2.md) and adopting the 2026-07-28 revision (docs/migration/support-2026-07-28.md). -
#2456
44797d7Thanks @felixweinberger! - Restore the v1 parse tolerance forCallToolResult.content: an inbound legacy-eratools/callresult withoutcontentdefaults to[]instead of failing validation. Deployed servers — accepted by SDK v1 for years — returnstructuredContent-only (or otherwise content-less) results, and the strict parse turned every such call into anINVALID_RESULTerror before application code could run.The silent-empty-success hazard the strictness guarded is preserved where it matters: the 2025 era's wire-seam schema refuses to default
contentfor a body carrying another result family's vocabulary (task,inputRequests,requestState— the era is frozen, so the list is complete), and the 2026-era wire schemas stay strict — modern-revision servers have no legacy excuse. Task interop through an explicit result schema is untouched (including bodies that also stamp a foreignresultType), and the server-side authoring normalization refuses the same foreign-family vocabulary.Server-side authoring is era-independent: a handler result without
content(dynamic/JS callers — the TypeScript surface requires it) is normalized tocontent: []before era validation on every leg, reaching the wire spec-valid.Conscious call: the nested sampling
ToolResultContentSchemastays spec-strict — v1 had defaulted itscontenttoo, but it is params-side (tool results a caller authors into a sampling message), deliberately not restored. -
#2431
1b90c96Thanks @morluto! - Fix the CommonJSvalidators/ajvsubpath so reading the exportedAjvclass no longer throwsReferenceError: import_ajv is not defined. The subpath now re-exports the bundled provider's concreteAjvvalue in CJS output, matching the existing ESM behavior. -
#2405
f172626Thanks @mattzcarey! - Ship CommonJS builds alongside ESM. Each package now emits both.mjs/.d.mtsand.cjs/.d.cts(via tsdownformat: ['esm', 'cjs']), and itsexportsmap adds arequirecondition sorequire('@modelcontextprotocol/…')works from CommonJS consumers. Output extensions are normalized across all packages (@modelcontextprotocol/coremoves from.js/.d.tsto.mjs/.d.mts); the public import paths are unchanged. -
#2441
561c6d8Thanks @felixweinberger! - POSTs whoseContent-Typemedia type is notapplication/jsonare now rejected with415 Unsupported Media Type; the header is parsed instead of substring-matched. Previously any value merely containing the substring passed the check (for exampletext/plain; a=application/json), case variants were wrongly rejected, and the 2026-07-28 entry did not inspectContent-Typeat all — requests with a missing or non-JSON header that used to be served on that path now also answer 415. Values with parameters (application/json; charset=utf-8, including malformed parameter sections likeapplication/json;) continue to work. SDK clients always send the correct header and are unaffected.The new
isJsonContentType(header)helper is exported for transport and framework-adapter authors — custom entries composing the exported building blocks (classifyInboundRequest,PerRequestHTTPServerTransport) must apply it themselves. The hono adapter's JSON body pre-parse and the client's response dispatch now use the same parsed-media-type comparison. -
#2384
ce2f65dThanks @felixweinberger! -instanceofon the SDK error classes (ProtocolErrorand its typed subclasses,SdkError/SdkHttpError,OAuthError, and the client'sSseError,UnauthorizedError, and OAuth-client-flow error family —OAuthClientFlowErrorand its subclasses) now works across separately bundled copies of the SDK. The classes match by a stable brand (viaSymbol.hasInstanceand a registry symbol) instead of prototype identity, so a process that uses both@modelcontextprotocol/clientand@modelcontextprotocol/server- a gateway, host, or in-process test - can check errors constructed by either package against the class re-exported by the other. Ordinary prototype-basedinstanceofis preserved as a fallback; user-defined subclasses keep plain prototype semantics. Notes: cross-bundle matching requires both copies to be at or after this release; brands assert identity, not field shape, across versions - keep reading fields defensively. As a side effect, a foreign-bundleSdkErrorused as an abort reason is now rethrown as-is instead of being wrapped as aRequestTimeout. Branded hierarchies additionally expose an explicit static guard,X.isInstance(value), that reads the same brand and narrows in TypeScript — an alternative for codebases that prefer predicate-style checks overinstanceof. Also:UnauthorizedErrornow setserror.nameto'UnauthorizedError'(previously'Error'), and per-package conformance tests enforce that every exported error class participates in branding. Version-negotiation probing now recognizesUnauthorizedError(previously a dead name-string check) and propagates it unchanged, soconnect()on an auth-gated server rejects with the originalUnauthorizedError(previously wrapped as thecauseof anSdkError(EraNegotiationFailed)) — runfinishAuth()and reconnect, and the retry probes with the token. -
#2458
7c49b47Thanks @felixweinberger! - Construct the default Ajv validation engine lazily on first validation. Creating aClientorServerno longer pays the ajv + ajv-formats instantiation cost at startup when no JSON Schema validation ever runs. -
#2476
e0a0ab7Thanks @felixweinberger! - Build protocol-revision wire schemas lazily on first validation instead of at import. Each revision's schema set is now constructed by a module-level memoized factory, so importing the client or server package no longer pays the construction cost of both frozen wire-schema graphs up front. Method membership in the revision registries stays static, the schemas themselves are unchanged, and registry lookups keep returning reference-identical schema objects. -
#2451
7e69735Thanks @mattzcarey! - Return JSON-RPC Invalid Params with the original URI and aninvalid_urireason whenresources/readreceives a syntactically malformed URI. -
#2399
3c7ddafThanks @felixweinberger! - Return HTTP 400 for aMissingRequiredClientCapabilityError(-32021) produced after dispatch. The spec mandates400 Bad Requestfor this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — theinput_requiredgate rejecting an embedded request whose required capability the caller did not declare — surfaced in-band on HTTP 200. The JSON-RPC error body is unchanged, every other error code (including a handler relaying a downstream peer's-32020/-32022) keeps the origin-keyed in-band behavior, and the mapping only applies while the response is uncommitted: an exchange that already streamed — or one hosted withresponseMode: 'sse', which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream. -
#2425
e8de519Thanks @Sehlani042! - Stop advertising validator provider classes from the root client/server type declarations. The provider classes remain available from the explicit validator subpaths. -
#2453
0ab5d14Thanks @mattzcarey! - Strip RFC 9110 optional whitespace around inboundMCP-Protocol-Version,Mcp-Method, andMcp-Namevalues before classifying and validating modern HTTP requests. This keeps valid requests portable across Fetch runtimes that expose raw leading or trailing SP/HTAB throughHeaders.get(). -
#2534
f130e1aThanks @felixweinberger! - The default validator now honors declared 2019-09 and draft-07/06 dialects instead of rejecting them: a schema stamped"$schema": "http://json-schema.org/draft-07/schema#"(zod-to-json-schema's default output) validates with draft-07 semantics, and a 2019-09 stamp (zod-to-json-schema's2019-09/openAitargets) with 2019-09 semantics, on both the Ajv and Cloudflare Workers providers (with known engine differences documented in the migration guide). Schemas with no$schemastill validate as 2020-12, and unknown dialects still produce the typed error (now listing the supported dialects: 2020-12, 2019-09, draft-07, draft-06). -
Updated dependencies [
a400259,44797d7,f172626,8e1d2e9,f413763]:- @modelcontextprotocol/core@2.0.0
2.0.0-beta.5
Minor Changes
-
#2501
1480241Thanks @felixweinberger! - Export theProtocolbase class andmergeCapabilitiesfrom the@modelcontextprotocol/clientand@modelcontextprotocol/serverpackage roots, restoring the v1 import for consumers that subclassProtocol(e.g. the MCP Apps SDK). The client and server packages each bundle their own compiled copy of the class, so import it from one package consistently within a process.The codemod now rewrites
ProtocolandmergeCapabilitiesimports fromshared/protocol.jsto the client or server package root, like the module's other symbols, instead of dropping them with an action-required marker. -
#2513
f413763Thanks @felixweinberger! - Align the 2026-07-28 wire with the final revision (spec PR #3002):serverInfomoves from theDiscoverResultbody to the result_meta, and the per-request envelope'sclientInfodemotes from required to SHOULD.Before this change the SDK shipped the pre-#3002 shape in both directions: the client hard-rejected a conforming server's
DiscoverResult(missing bodyserverInfofailed parse, so the probe misclassified the server as legacy and attempted aninitializehandshake against it — a hard connect failure against a modern-only server such as go-sdk v1.7.0-pre.3), and the server rejected conforming clients that omitclientInfo.Now:
- The 2026 wire schemas are the final revision exactly: no body
serverInfoonDiscoverResult, envelopeclientInfooptional (a present-but-malformed value still fails validation). - Servers stamp
_meta['io.modelcontextprotocol/serverInfo']on every 2026-era response (spec SHOULD; a handler-authored value wins, the 2025-era wire is untouched). This includes the entry-builtsubscriptions/listengraceful-close results — the spec'sSubscriptionsListenResultMetaextendsResultMetaObject. - Clients keep sending
clientInfoand read server identity from the discover result's_metaonly. A server that stamps no identity is anonymous:getServerVersion()isundefinedand the response cache partitions under a per-connection surrogate. A malformed_metaserverInfo value is treated as absent on receive (the spec marks the field self-reported, unverified, and display-only). - Breaking type changes:
DiscoverResultno longer declaresserverInfo;RequestMetaEnvelope'sclientInfois optional. New public constantSERVER_INFO_META_KEY('io.modelcontextprotocol/serverInfo').
- The 2026 wire schemas are the final revision exactly: no body
Patch Changes
- Updated dependencies [
f413763]:- @modelcontextprotocol/core@2.0.0-beta.5
2.0.0-beta.4
Minor Changes
-
#2477
8e1d2e9Thanks @felixweinberger! - Move the schema source modules (spec schemas, OAuth schemas, protocol constants) into@modelcontextprotocol/coreand resolve them from there as a regular runtime dependency instead of bundling a private copy into each package. An application importing more than one of the packages now evaluates a single shared schema graph with shared object identity.@modelcontextprotocol/coregains a./internalsubpath (SDK-internal contract; may change in any release) and the four packages now version together. -
#2483
3f07a32Thanks @felixweinberger! - AddpreloadSchemas(), an explicit opt-in to eager wire-schema construction, and call it automatically in the Cloudflare Workers builds. The wire schemas are built lazily by default, which is the right trade on process-per-invocation runtimes — but on isolate platforms that bill request CPU while module evaluation runs during isolate warm-up, laziness moves construction into the first request each fresh isolate serves. CallingpreloadSchemas()at module scope (it is synchronous and idempotent) moves that one-time cost back to module evaluation; the packages' workerd export condition now does this automatically, while the Node and browser builds stay lazy. The server package gains a dedicated browser shim for this (itsbrowsercondition previously reused the workerd shim), so browser bundles keep lazy construction.
Patch Changes
-
#2458
7c49b47Thanks @felixweinberger! - Construct the default Ajv validation engine lazily on first validation. Creating aClientorServerno longer pays the ajv + ajv-formats instantiation cost at startup when no JSON Schema validation ever runs. -
#2476
e0a0ab7Thanks @felixweinberger! - Build protocol-revision wire schemas lazily on first validation instead of at import. Each revision's schema set is now constructed by a module-level memoized factory, so importing the client or server package no longer pays the construction cost of both frozen wire-schema graphs up front. Method membership in the revision registries stays static, the schemas themselves are unchanged, and registry lookups keep returning reference-identical schema objects. -
Updated dependencies [
8e1d2e9]:- @modelcontextprotocol/core@2.0.0-beta.4
2.0.0-beta.3
Minor Changes
-
#2369
24be404Thanks @mattzcarey! - AllowinputRequired.elicit()to accept a Standard Schema such as a Zod object forrequestedSchema. The builder converts it to MCP's restricted form-elicitation JSON Schema, while the same schema can validate and type the response throughacceptedContent()on handler re-entry. Zod formats mapping toemail,uri,date, anddate-timeare supported. Shapes the restricted schema cannot express reject before anything is sent — nested objects,.regex()and customized zod format patterns, exclusive number bounds (.positive()/.gt()), literal unions (usez.enumorz.literal(['a', 'b'])), and non-spec root keywords likez.strictObject()'sadditionalProperties. -
#2420
7635115Thanks @felixweinberger! - Add runtime-neutral Bearer authentication to@modelcontextprotocol/server:requireBearerAuthgates web-standardfetch(request)hosts (Cloudflare Workers, Deno, Bun, Hono), built on the exportedverifyBearerTokenandbearerAuthChallengeResponsepieces, withOAuthTokenVerifiernow defined here. The Express middleware adapts the same core and is unchanged in behavior, except thatWWW-Authenticatechallenge values are now RFC 7235 quoted-string sanitized (quotes and backslashes escaped, control and non-ASCII characters replaced);@modelcontextprotocol/expressre-exportsOAuthTokenVerifieras before. -
#2422
61866d7Thanks @felixweinberger! - Add runtime-neutral OAuth discovery serving to@modelcontextprotocol/server:oauthMetadataResponseserves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents from web-standardfetch(request)hosts, built on the exportedbuildOAuthProtectedResourceMetadata, withgetOAuthProtectedResourceMetadataUrlnow defined here. The Express metadata router adapts the same core and is unchanged in behavior; the insecure-issuer escape hatch is an explicitdangerouslyAllowInsecureIssuerUrloption in the neutral core instead of a module-scope environment read. The web-standard matcher validates lazily so unmatched traffic always falls through, tolerates a trailing slash, supports HEAD, and marks reflected CORS preflights withVary.
Patch Changes
-
#2456
44797d7Thanks @felixweinberger! - Restore the v1 parse tolerance forCallToolResult.content: an inbound legacy-eratools/callresult withoutcontentdefaults to[]instead of failing validation. Deployed servers — accepted by SDK v1 for years — returnstructuredContent-only (or otherwise content-less) results, and the strict parse turned every such call into anINVALID_RESULTerror before application code could run.The silent-empty-success hazard the strictness guarded is preserved where it matters: the 2025 era's wire-seam schema refuses to default
contentfor a body carrying another result family's vocabulary (task,inputRequests,requestState— the era is frozen, so the list is complete), and the 2026-era wire schemas stay strict — modern-revision servers have no legacy excuse. Task interop through an explicit result schema is untouched (including bodies that also stamp a foreignresultType), and the server-side authoring normalization refuses the same foreign-family vocabulary.Server-side authoring is era-independent: a handler result without
content(dynamic/JS callers — the TypeScript surface requires it) is normalized tocontent: []before era validation on every leg, reaching the wire spec-valid.Conscious call: the nested sampling
ToolResultContentSchemastays spec-strict — v1 had defaulted itscontenttoo, but it is params-side (tool results a caller authors into a sampling message), deliberately not restored. -
#2431
1b90c96Thanks @morluto! - Fix the CommonJSvalidators/ajvsubpath so reading the exportedAjvclass no longer throwsReferenceError: import_ajv is not defined. The subpath now re-exports the bundled provider's concreteAjvvalue in CJS output, matching the existing ESM behavior. -
#2441
561c6d8Thanks @felixweinberger! - POSTs whoseContent-Typemedia type is notapplication/jsonare now rejected with415 Unsupported Media Type; the header is parsed instead of substring-matched. Previously any value merely containing the substring passed the check (for exampletext/plain; a=application/json), case variants were wrongly rejected, and the 2026-07-28 entry did not inspectContent-Typeat all — requests with a missing or non-JSON header that used to be served on that path now also answer 415. Values with parameters (application/json; charset=utf-8, including malformed parameter sections likeapplication/json;) continue to work. SDK clients always send the correct header and are unaffected.The new
isJsonContentType(header)helper is exported for transport and framework-adapter authors — custom entries composing the exported building blocks (classifyInboundRequest,PerRequestHTTPServerTransport) must apply it themselves. The hono adapter's JSON body pre-parse and the client's response dispatch now use the same parsed-media-type comparison. -
#2384
ce2f65dThanks @felixweinberger! -instanceofon the SDK error classes (ProtocolErrorand its typed subclasses,SdkError/SdkHttpError,OAuthError, and the client'sSseError,UnauthorizedError, and OAuth-client-flow error family —OAuthClientFlowErrorand its subclasses) now works across separately bundled copies of the SDK. The classes match by a stable brand (viaSymbol.hasInstanceand a registry symbol) instead of prototype identity, so a process that uses both@modelcontextprotocol/clientand@modelcontextprotocol/server- a gateway, host, or in-process test - can check errors constructed by either package against the class re-exported by the other. Ordinary prototype-basedinstanceofis preserved as a fallback; user-defined subclasses keep plain prototype semantics. Notes: cross-bundle matching requires both copies to be at or after this release; brands assert identity, not field shape, across versions - keep reading fields defensively. As a side effect, a foreign-bundleSdkErrorused as an abort reason is now rethrown as-is instead of being wrapped as aRequestTimeout. Branded hierarchies additionally expose an explicit static guard,X.isInstance(value), that reads the same brand and narrows in TypeScript — an alternative for codebases that prefer predicate-style checks overinstanceof. Also:UnauthorizedErrornow setserror.nameto'UnauthorizedError'(previously'Error'), and per-package conformance tests enforce that every exported error class participates in branding. Version-negotiation probing now recognizesUnauthorizedError(previously a dead name-string check) and propagates it unchanged, soconnect()on an auth-gated server rejects with the originalUnauthorizedError(previously wrapped as thecauseof anSdkError(EraNegotiationFailed)) — runfinishAuth()and reconnect, and the retry probes with the token. -
#2451
7e69735Thanks @mattzcarey! - Return JSON-RPC Invalid Params with the original URI and aninvalid_urireason whenresources/readreceives a syntactically malformed URI. -
#2425
e8de519Thanks @Sehlani042! - Stop advertising validator provider classes from the root client/server type declarations. The provider classes remain available from the explicit validator subpaths. -
#2453
0ab5d14Thanks @mattzcarey! - Strip RFC 9110 optional whitespace around inboundMCP-Protocol-Version,Mcp-Method, andMcp-Namevalues before classifying and validating modern HTTP requests. This keeps valid requests portable across Fetch runtimes that expose raw leading or trailing SP/HTAB throughHeaders.get().
2.0.0-beta.2
Patch Changes
-
#2405
f172626Thanks @mattzcarey! - Ship CommonJS builds alongside ESM. Each package now emits both.mjs/.d.mtsand.cjs/.d.cts(via tsdownformat: ['esm', 'cjs']), and itsexportsmap adds arequirecondition sorequire('@modelcontextprotocol/…')works from CommonJS consumers. Output extensions are normalized across all packages (@modelcontextprotocol/coremoves from.js/.d.tsto.mjs/.d.mts); the public import paths are unchanged. -
#2399
3c7ddafThanks @felixweinberger! - Return HTTP 400 for aMissingRequiredClientCapabilityError(-32021) produced after dispatch. The spec mandates400 Bad Requestfor this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — theinput_requiredgate rejecting an embedded request whose required capability the caller did not declare — surfaced in-band on HTTP 200. The JSON-RPC error body is unchanged, every other error code (including a handler relaying a downstream peer's-32020/-32022) keeps the origin-keyed in-band behavior, and the mapping only applies while the response is uncommitted: an exchange that already streamed — or one hosted withresponseMode: 'sse', which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream.
2.0.0-beta.1
Patch Changes
- #2402
a400259Thanks @felixweinberger! - First beta release of SDK v2 with support for the MCP 2026-07-28 specification revision. See the migration guides for upgrading from v1 (docs/migration/upgrade-to-v2.md) and adopting the 2026-07-28 revision (docs/migration/support-2026-07-28.md).
2.0.0-alpha.4
Major Changes
-
#2286
1823aaeThanks @felixweinberger! - Split the wire layer into per-era codecs and make protocol-revision deletions physical. Deliberate wire/schema behavior changes (see docs/migration/support-2026-07-28.md "Per-era wire codecs"):resultTypeis no longer modeled by any neutral wire schema:EmptyResultSchema(strict) now rejects{resultType}bodies; on 2025-era connections a foreignresultTypeis stripped before validation instead of rejected; the member exists only inside the 2026-era codec, which requires it.CallToolResult.content/ToolResultContent.contentare required at the wire boundary (content.default([])removed): handler results withoutcontentare rejected with-32602instead of silently defaulted, and content-less wire results fail the client parse loudly.- Custom (3-arg) handlers now receive
_metaminus the reserved envelope keys instead of having it deleted before params validation. specTypeSchemasre-scoped to the neutral model: result validators no longer acceptresultType; task message-type validators andRequestMetaEnvelopeleft the public set (SpecTypeNamenarrowed).- Role aggregate types/schemas (
ClientRequest,ServerResult, …) no longer carry task vocabulary; the deprecatedTask*types remain importable unchanged. - Era-mismatched spec methods fail physically: inbound era-deleted methods get
-32601even with a handler registered; outbound sends throwSdkErrorCode.MethodNotSupportedByProtocolVersionlocally. - Value guards (
isCallToolResult, …) are documented as neutral-shape consumer checks, not wire validators.
-
#2286
1823aaeThanks @felixweinberger! -createMcpHandlernow returns a web-standards-only{ fetch, close, notify, bus }handler — the shape Workers/Bun/Deno expect fromexport default. The duck-typed.node(req, res, parsedBody?)face is removed; Node frameworks (Express, Fastify, plainnode:http) wrap the handler once with the newtoNodeHandler(handler, { onerror? })exported from@modelcontextprotocol/node, which converts the Node request to a web-standardRequest, callshandler.fetch, and writes theResponseback honoring write backpressure. The optionalonerrorreceives the adapter-level error fallback (request conversion /handler.fetchthrow) before the500response is written, restoring observability parity with the removed.nodeface.NodeIncomingMessageLikeandNodeServerResponseLikemove from@modelcontextprotocol/serverto@modelcontextprotocol/node. -
#2286
1823aaeThanks @felixweinberger! - Hide wire-only protocol members from the public surface, at the type level and at runtime.resultType(the 2026-07-28 result discrimination field) is no longer declared on any public result type — the wire schemas keep parsing it, and the client funnel now consumes it raw-first:'complete'results are stripped to the public shape and any other kind (e.g.input_required) rejects with the newSdkErrorCode.UnsupportedResultTypeinstead of masking into an empty success. The reserved_metaenvelope keys are lifted out of inbound requests and notifications before handlers run, and the multi-round-trip retry fields (inputResponses,requestState) out of inbound requests only (the spec reserves those names on client-initiated requests; notification params keep them), so handler params keep the 2025-era shape; for requests the lifted material surfaces atctx.mcpReq.envelope,ctx.mcpReq.inputResponses, andctx.mcpReq.requestState(notifications have no ctx — their lifted envelope keys are not surfaced). High-level client/server methods now return the named public result types (Promise<CallToolResult>etc.). Task wire vocabulary stays importable but is@deprecatedand excluded from the typed method maps (RequestMethod/RequestTypeMap/ResultTypeMap/NotificationTypeMap), andcallToolis typed as plainCallToolResult. See docs/migration/support-2026-07-28.md "Wire-only members hidden from public types". -
#2286
1823aaeThanks @felixweinberger! -resources/readfor an unknown URI now answers with JSON-RPC error code-32602(Invalid Params) on every protocol revision, witherror.data.uriechoing the requested URI. The 2026-07-28 specification requires-32602; the v1.x SDK already emitted-32602on earlier revisions, so v1.x peers see no change.This supersedes an interim
-32002emission that shipped in earlier v2 alphas. The era-aware encode seam (WireCodec.encodeErrorCode) maps any handler-thrown-32002to-32602on the wire; note that a-32002thrown withoutdata.uriis emitted as a bare-32602and is no longer recognizable as resource-not-found — throwResourceNotFoundError(or includedata: { uri }) to preserve the classification.ProtocolErrorCode.ResourceNotFound(-32002) remains importable as receive-tolerated vocabulary; clients should accept both-32602and-32002from peers (the specification's backwards-compatibility clause). The new typedResourceNotFoundErrorclass carriesdata.uri, andProtocolError.fromErrorreconstructs it from a-32602only whenerror.datais exactly{ uri: string }(and nothing else), and from a legacy-32002wheneverdata.uriis a string; a bare-32002withoutdata.uristays a genericProtocolError. -
#2286
1823aaeThanks @felixweinberger! - SEP-1613 / SEP-2106 (JSON Schema 2020-12 posture): the Node default JSON Schema validator is nowAjv2020(true draft 2020-12) instead of the draft-07Ajvclass —$defs/prefixItems/unevaluatedProperties/dependentRequiredare now enforced where they were previously silently ignored; to opt back, construct the draft-07 instance with the v1 defaults —const ajv = new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }); addFormats(ajv);— and passnew AjvJsonSchemaValidator(ajv). Schemas declaring a$schemaother than 2020-12 are rejected with a clear error rather than mis-validating.outputSchemamay now have a non-object root andCallToolResult.structuredContentis widened tounknown(a deliberate source-level break for typed consumers — see the migration guide for the narrowing pattern). Toward 2025-era clients McpServer wraps a non-objectoutputSchema(and the matchingstructuredContent) in a{result: …}envelope so the tool stays callable, with same-document$ref/$dynamicRefpointers rewritten to keep resolving — low-levelServerusers (those bypassingMcpServerand registering atools/callhandler directly) get the same wrap by routing the result through the newServer.projectCallToolResult(result, advertisedOutputSchema). Independently, on every era (the SEP's MUST applies regardless of client version), McpServer auto-appends aTextContentJSON serialisation when a handler returns non-objectstructuredContentwithout its own text block. ThestructuredContentpresence check is!== undefined(not falsy) on both sides. Thanks @mattzcarey (#2249).
Minor Changes
-
#2286
1823aaeThanks @felixweinberger! - AddcreateRequestStateCodec({ key, ttlSeconds?, bind? }), an opt-in HMAC-SHA256 sealing helper for the multi-round-triprequestState:mintseals a JSON-serializable payload (with TTL and optional context binding) andverifydrops directly intoServerOptions.requestState.verify. WebCrypto-based and runtime-neutral; verification is fail-closed and constant-time. TheServerOptions.requestState.verifyhook's return type is widened tounknown | Promise<unknown>so the codec'sverifyis directly assignable; the seam captures the hook's resolved value (the decoded payload) and hands it to handlers via the typedctx.mcpReq.requestState<T>()accessor. -
#2286
1823aaeThanks @felixweinberger! - Results of the cacheable 2026-07-28 operations (tools/list,prompts/list,resources/list,resources/templates/list,resources/read,server/discover) now always carry the revision's requiredttlMs/cacheScopefields when served on that revision, defaulting tottlMs: 0/cacheScope: 'private'. Servers can configure the emitted values with the newServerOptions.cacheHintsoption (per operation) and the newcacheHintmember of theregisterResourceconfig (per resource); resolution is per field, most specific author first: cache fields returned by a handler win over the per-resource hint, which wins over the per-operation hint, and configured hints are validated at construction/registration time (RangeErroron invalid values). Responses on 2025-era connections are unchanged and never carry these fields. Note for untyped callers:registerResourcenow interprets acacheHintkey in its config object — it is validated and kept out of the resource's list metadata, where it was previously passed through as ordinary metadata. -
#2286
1823aaeThanks @felixweinberger! - AddSdkErrorCode.MethodNotSupportedByProtocolVersion: a typed local error raised before anything reaches the transport when a spec method is sent toward a peer whose negotiated protocol version's wire era does not define it (for exampletasks/gettoward a 2026-07-28 peer). The protocol layer now resolves a per-era wire codec from the connection's negotiated protocol version (instance state onClient/Server, with the legacy era as the pre-negotiation default) and resolves per-method schemas at dispatch time instead of registration time; an edge classification on an inbound message is validated against that instance era, and a mismatch is rejected as an entry/routing error. Behavior on existing (2025-era) connections is unchanged. -
#2286
1823aaeThanks @felixweinberger! - RevisecreateMcpHandler's legacy handling (a behavior change to the unreleased entry). The entry now serves 2025-era (non-envelope) traffic by default through per-request stateless serving from the same factory —legacy: 'stateless'is the default rather than an opt-in — and the strict, modern-only posture is selected with the newlegacy: 'reject'value (the earlier alpha's default). The handler-valuedlegacyoption (bring-your-own legacy serving) is removed: existing legacy deployments (for example a sessionful streamable HTTP wiring) keep serving 2025 traffic by routing in user land with the newisLegacyRequest(request, parsedBody?)export, which runs the entry's own classification step — it returnstrueonly for requests with no per-request_metaenvelope claim, while malformed or incomplete modern claims are NOT legacy and must be routed to the modern handler, which answers them with the documented validation errors. The predicate classifies a clone, so the routed request body stays readable.legacyStatelessFallbackremains exported as a standalone fetch-shaped handler with the same stateless serving as the default. -
#2286
1823aaeThanks @felixweinberger! - AddcreateMcpHandler(factory, { legacy?, onerror?, responseMode? }), an HTTP entry point that serves the 2026-07-28 draft revision per request: each envelope-carrying request is classified once, served on a fresh instance from the factory bound to the claimed revision, and answered with a JSON body or a lazily-upgraded SSE stream. 2025-era serving is selected with thelegacyoption ('stateless'— the default — for per-request stateless serving via the existing streamable HTTP transport,'reject'for a modern-only strict endpoint that answers 2025-era requests with the unsupported-protocol-version error naming its supported revisions). The handler is a web-standard{ fetch, close, notify, bus }object:fetch(request, { authInfo?, parsedBody? })is the only request face (Node frameworks wrap it withtoNodeHandler(handler)from@modelcontextprotocol/node), andclose()tears down in-flight modern exchanges. Also exported:legacyStatelessFallback(the same stateless legacy serving as a standalone fetch-shaped handler), thePerRequestHTTPServerTransportsingle-exchange transport and theclassifyInboundRequestclassifier for hand-wired compositions, and the supporting types.responseMode: 'json'never streams and drops mid-call notifications (progress, logging and other related messages emitted before the result); listen-class subscription streams are always served over SSE. The entry performs no Origin/Host validation (use the middleware packages) and no token verification —authInfois pass-through and never derived from request headers. -
#2381
f0bf785Thanks @felixweinberger! - Serveinput_requiredhandlers on 2025-era connections: the legacy shim (on by default) converts each embedded request of aninput_requiredreturn into a real server→client request (elicitation/create,sampling/createMessage,roots/list) over the live session — stamped with the originating request's id for stream association — and re-enters the handler with the collectedinputResponsesuntil a final result. Handlers are written once in the 2026inputRequired(...)style and serve both eras; the previous loud-32603failure remains available viaServerOptions.inputRequired.legacyShim: false. Knobs:inputRequired.maxRounds(default 8) andinputRequired.roundTimeoutMs(default 600 000 ms per leg; legs carry a progressToken so a client reporting progress mid-leg resets the leg timeout). Semantics mirror the modern client driver exactly: per-round replacedinputResponses, byte-exactrequestStateecho with the verify hook running every round, paced requestState-only rounds, and elicitation accepted content passed through UNVALIDATED (the handler validates via the schema-awareacceptedContentoverload, exactly as on the 2026 era). URL-mode legs synthesize theelicitationIdthe 2025-11-25 wire requires. Failures surface per family (tools/call→isErrortool result;prompts/get/resources/read→ JSON-RPC error); stateless legacy HTTP degrades to a clean capability refusal; the shim emits no progress of its own (the originating progressToken is the handler's single must-increase stream — the shim never adds a second author to it).ctx.mcpReq.requestStateis now a typed accessor:ctx.mcpReq.requestState<T>()returns the payload the configuredrequestState.verifyhook resolved with (e.g.createRequestStateCodec.verify— the hook's return value is now load-bearing; verifiers that are not also decoders should resolveundefined), the raw wire string when no hook is configured, orundefinedwhen the round carried no state. Code that read the property directly becomes a call:ctx.mcpReq.requestState→ctx.mcpReq.requestState<string>(). Note the member is now always present (a function), so truthiness no longer means "has state", and it is dropped by JSON serialization of the context.New typed readers for
inputResponses, exported from@modelcontextprotocol/server: a schema-awareacceptedContent(responses, key, schema)overload (validates untrusted accepted content against any synchronous Standard Schema) andinputResponse(responses, key)(discriminatedmissing | elicit | sampling | rootsview, for decline/cancel detection and the non-elicitation kinds). Content conveniences like text extraction stay in application code as one-liners over the discriminated view. -
#2286
1823aaeThanks @felixweinberger! - AddMissingRequiredClientCapabilityError, the typed error class for the 2026-07-28-32021protocol error (processing a request requires a capability the client did not declare). Itsdata.requiredCapabilitieslists the missing capabilities andProtocolError.fromErrorrecognizes the code/data shape. The 2026-07-28 HTTP entry gains a pre-dispatch gate that refuses a request requiring an undeclared client capability with this error and HTTP status400; no method served on the 2026-07-28 registry currently carries such a requirement, so observable behavior is unchanged until methods with capability requirements exist. -
#2286
1823aaeThanks @felixweinberger! - Add the server side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). Handlers fortools/call,prompts/get, andresources/readcan return the value built byinputRequired()(exported from the server package together withacceptedContent()) to request additional client input in-band; the structured-content requirement and the tools/call result-schema validation are skipped for that return, the encode seam emits it asresultType: 'input_required', and the handler reads the responses on re-entry fromctx.mcpReq.inputResponses(with non-bare entries reported viactx.mcpReq.droppedInputResponseKeys). The seam re-checks the at-least-one rule for hand-built results, checks every embedded request against the capabilities the client declared on that request's envelope (answering the typed-32021error on violation), and fails loudly — never emitting a mis-typed result — when an input-required value is returned from any other method. Toward a 2025-era request the return is served by the default-on legacy shim (real server→client requests plus handler re-entry); the loud failure for that case remains available viaServerOptions.inputRequired.legacyShim: false. AUrlElicitationRequiredErrorescaping a handler on a 2026-era request fails as an internal error with a clear steer toinputRequired.elicitUrl(...), so the-32042error never reaches the 2026-07-28 wire; 2025-era serving keeps today's-32042behavior exactly. The typed local error raised when push-style server-to-client request APIs are used while serving a 2026-era request now steers toinputRequired(...). Tool, prompt, and resource callback types accept the new return alongside their existing result types; 2025-era wire behavior is unchanged. An optionalServerOptions.requestState.verifyhook lets a server integrity-check the echoedrequestStatebefore the handler runs — a throw answers the wire-level-32602Invalid Params error withdata.reason: 'invalid_request_state'; the SDK provides no default verification. -
#2286
1823aaeThanks @felixweinberger! - Add Origin header validation alongside the existing Host header validation. The server package gains framework-agnostic helpers (validateOriginHeader,localhostAllowedOrigins,originValidationResponse); the Express, Hono and Fastify adapters gainoriginValidation/localhostOriginValidationmiddleware and a newallowedOriginsoption on their app factories, which now arm Origin validation by default for localhost-class binds (mirroring the Host validation ladder; the 0.0.0.0-without-allowlist warning is unchanged). Requests without anOriginheader pass — non-browser MCP clients are unaffected — while a presentOriginthat is not allowed or cannot be parsed (including the opaquenullorigin) is rejected with403. The Node adapter shipshostHeaderValidation/originValidationrequest guards for plainnode:httpservers, which previously had no validation helpers. -
#2286
1823aaeThanks @felixweinberger! - SEP-2243Mcp-Param-*server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path,createMcpHandlernow validatesMcp-Param-{Name}headers against the named tool'sx-mcp-headerdeclarations and the bodyargumentsbefore dispatch: a missing header for a present body value, a header that decodes to a different value than the body, or an invalid=?base64?…?=sentinel is rejected with400 Bad Requestand JSON-RPC-32020(HeaderMismatch) — the same shape the existing standard-header cross-checks emit. Anull/absent body value passes regardless of any header (the spec's "server MUST NOT expect the header" rows).McpServer.registerToolnow warns at registration time when anx-mcp-headerdeclaration violates the spec's constraints. The 2025-era serving paths and the low-levelServerfactory shape are unchanged. -
#2286
1823aaeThanks @felixweinberger! - SEP-2243 standard-header server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path,createMcpHandlernow enforces the requiredMcp-MethodandMcp-Namestandard request headers in addition to the existingMCP-Protocol-VersionandMcp-Methodcross-checks: a modern request without anMcp-Methodheader, atools/call/prompts/get/resources/readrequest without anMcp-Nameheader, anMcp-Nameheader carrying an invalid=?base64?…?=sentinel, and anMcp-Nameheader whose (decoded) value disagrees with the body'sparams.name/params.uriare all rejected with400 Bad Requestand JSON-RPC-32020(HeaderMismatch). The 2025-era serving paths are unchanged.New public surface:
@modelcontextprotocol/server: themcpNameHeaderfield onInboundHttpRequest, and the'standard-header-validation'member ofInboundValidationRung(withclient-capabilities/param-header-validationrenumbered).
-
#2286
1823aaeThanks @felixweinberger! - AddserveStdio(factory, options?)(exported from@modelcontextprotocol/server/stdio), the connection-pinned stdio entry point for serving the 2026-07-28 draft revision on long-lived connections. The entry owns the transport and the era decision: the client's opening exchange selects the era (a 2025initializehandshake, 2026-07-28 per-request_metaenvelope traffic, or aserver/discoverprobe followed by either), and ONE instance from the factory is pinned to the connection and serves only that era — mirroring howcreateMcpHandlerclassifies each HTTP request before constructing an instance. 2025-era openings are served by default;legacy: 'reject'answers them with the unsupported-protocol-version error naming the supported modern revisions instead. Atransportoption accepts a bring-your-ownStdioServerTransport(for example over a Unix domain socket);onerrorreports out-of-band errors; the returned handle'sclose()tears the connection down.Removed:
ServerOptions.eraSupport(introduced in an earlier 2.0 alpha, never in a stable release). A hand-constructedServer/McpServerserves only the 2025-era protocol it was written for; serving the 2026-07-28 revision always goes through a serving entry. Migratenew McpServer(info, { eraSupport: 'dual-era' })+connect(new StdioServerTransport())toserveStdio(() => new McpServer(info)), anderaSupport: 'modern'toserveStdio(factory, { legacy: 'reject' }). -
#2286
1823aaeThanks @felixweinberger! - Align the 2026-07-28 protocol error codes to the spec renumber:HeaderMismatchis now-32020(was-32001),MissingRequiredClientCapabilityis now-32021(was-32003), andUnsupportedProtocolVersionis now-32022(was-32004). These codes are part of the draft 2026-07-28 protocol revision only and have never appeared on a 2025-era wire — the 2025 serving paths and the SDK-conventional-32001(Session not found) on the stateful Streamable HTTP transport are unchanged.ProtocolErrorCode.MissingRequiredClientCapability,ProtocolErrorCode.UnsupportedProtocolVersion, theHEADER_MISMATCH_ERROR_CODEconstant, and theHEADER_MISMATCH/MISSING_REQUIRED_CLIENT_CAPABILITY/UNSUPPORTED_PROTOCOL_VERSIONspec-type constants now carry the renumbered values; theUnsupportedProtocolVersionErrorandMissingRequiredClientCapabilityErrorclasses (andProtocolError.fromErrorrecognition) follow. The client probe classifier recognizes-32022for the corrective continuation and the SEP-2243 one-refresh-on-miss retry triggers on-32020. -
#2286
1823aaeThanks @felixweinberger! -subscriptions/listengraceful close: per spec PR #2953, a server-side graceful close (createMcpHandler/serveStdioclose()) now emits the emptysubscriptions/listenJSON-RPC result (the newSubscriptionsListenResult—_metacarries the subscriptionId) before closing the stream, replacing the previous server-originatednotifications/cancelled. On the client,McpSubscription.closednow resolves'graceful'for this signal (added alongside'local'and'remote'); a stream close without a result remains'remote'(unexpected disconnect). -
#2286
1823aaeThanks @felixweinberger! -subscriptions/listen(SEP-1865) is served by both serving entries on protocol revision 2026-07-28. The entry owns ack-first, per-stream filtering, subscription-id stamping, keepalive (HTTP), the pre-ack-32603capacity guard, and teardown (HTTP stream close; onenotifications/cancelledper subscription on stdio).server/discovernow advertiseslistChanged/subscribecapability bits — the rider that suppressed them until listen was served is discharged.Under
createMcpHandlerthe consumer's factory is constructed forsubscriptions/listen(a capabilities-only probe so the acknowledged filter reflects what the server advertises; the instance is never connected and is closed immediately). Per-request authorization performed inside the factory therefore sees listen requests; token verification still belongs at the middleware layer mounted in front of the entry.New public surface:
@modelcontextprotocol/server:ServerEventBus,ServerEvent,ServerNotifier(types);InMemoryServerEventBus(class).McpHttpHandlergains.notify(ServerNotifier:toolsChanged(),promptsChanged(),resourcesChanged(),resourceUpdated(uri)) and.bus(theServerEventBuslisten streams subscribe to).CreateMcpHandlerOptionsgainsbus?: ServerEventBus(an in-processInMemoryServerEventBusis created when omitted),maxSubscriptions?: number(default 1024), andkeepAliveMs?: number(default 15000).ServeStdioOptionsgainsmaxSubscriptions?: number(default 1024). On a modern-pinned connectionserveStdioroutes the pinned instance's existingsend*ListChanged()calls onto active subscriptions; legacy connections are unchanged.@modelcontextprotocol/server:SUBSCRIPTION_ID_META_KEY(const);SubscriptionFilter,SubscriptionsListenRequest,SubscriptionsListenRequestParams,SubscriptionsAcknowledgedNotification,SubscriptionsAcknowledgedNotificationParams(types).
-
#2286
1823aaeThanks @felixweinberger! - Wireserver/discover(protocol revision 2026-07-28) into the typed request funnel and serve it era-aware. The request joinsClientRequestSchema/ServerResultSchema/ResultTypeMap(per-era availability stays with the wire registries: only the 2026-era registry serves it), andClient.discover()issues it as a typed request on 2026-era connections. AServerwhosesupportedProtocolVersionslist carries a modern (2026-07-28+) revision installs theserver/discoverhandler, advertising ONLY its modern revisions and excluding the listChanged/subscribe-class capabilities until thesubscriptions/listenflow ships; servers with today's default list are unchanged and keep answering-32601. Theinitializehandshake is now era-aware in the other direction: its accept check and counter-offer consult only the legacy subset of the supported versions — a 2026-era revision is never negotiated viainitialize— so a 2025-era client can never be offered a 2026 version string; with the default list this is byte-identical to previous behavior. Serving the 2026 revision to ordinary HTTP/stdio traffic arrives with an upcoming server-side entry point: today the negotiation surface is client-side, andmode: 'auto'falls back cleanly against current SDK servers.
Patch Changes
-
#2286
1823aaeThanks @felixweinberger! - DeprecateServer.getClientCapabilities(),Server.getClientVersion()andServer.getNegotiatedProtocolVersion()in favor of the per-request handler context: on 2026-07-28 requests the validated_metaenvelope carries the client's identity (ctx.mcpReq.envelope), and instances serving that revision throughcreateMcpHandlerare backfilled per request so the accessors keep answering. Behavior on 2025-era connections is unchanged; the accessors remain functional. -
#2394
801111eThanks @felixweinberger! - Fix the published declaration files for consumers compiling withskipLibCheck: false: the bundled.d.mtsno longer leaves a danglingURIComponentreference (ajv's published types import it fromfast-uri, whose export-assigned namespace the dts bundler cannot link — the type is now inlined via a dts-only path mapping), and no longer importsjson-schema-typedfrom an undeclared dependency (it is inlined viadts.resolve).@modelcontextprotocol/nodeand@modelcontextprotocol/serverdrop staletypesVersionsentries pointing at subpaths that never shipped. Package READMEs note that TypeScript >=6.0 requires"types": ["node"]since the published declarations referenceBuffer. -
#2390
6cc7b1cThanks @felixweinberger! -isLegacyRequestdocs: lead with the single-argument form.isLegacyRequest(request)is the whole API — the body is read from an internal clone, so the request you pass stays readable for whichever handler you route it to.parsedBodyis an optional perf escape for a body you already hold parsed (and the way in for an already-consumed stream, e.g. behindexpress.json()), not a required companion. Documentation only; no behavior change. -
#2286
1823aaeThanks @felixweinberger! - Pin the modern (2026-07-28) HTTP serving path's rejection codes to the assignments the published conformance suite asserts: a header/body cross-check mismatch (MCP-Protocol-VersionorMcp-Methoddisagreeing with the request body) is now rejected with-32020(HeaderMismatch), and a request whose protocol-version header names a modern revision but whose body is missing the_metaenvelope (or its required protocol-version key) is rejected with-32602invalid params naming the missing key(s). Both keep HTTP 400. These cells previously emitted a provisional-32004while the upstream error-code discussion was open. The envelope-less rejection on a modern-only endpoint (-32022with the supported-versions list), the 2025-era serving paths, and the client-side probe handling are unchanged. -
#2286
1823aaeThanks @felixweinberger! -ctx.mcpReq.log()now emits itsnotifications/messagenotification request-related (like progress andctx.mcpReq.notify), so handler-emitted log messages are delivered when the server is hosted per request viacreateMcpHandlerinstead of being silently dropped. On a 2026-07-28 request the level filter consults the per-request_metaio.modelcontextprotocol/logLevelkey (the modern equivalent oflogging/setLevel); per the spec, an absent key means nonotifications/messageis sent for that request.2025-era delivery-channel change (spec-conformance correction). On a 2025-era sessionful Streamable HTTP transport, handler-emitted log messages now ride the per-request POST response stream instead of the standalone GET stream. This is a correction to the 2025-11-25 specification:
docs/specification/2025-11-25/basic/transports.mdx§"Sending Messages to the Server" item 6 says JSON-RPC messages on the POST response stream SHOULD relate to the originating client request, and §"Listening for Messages from the Server" item 4 says messages on the GET stream SHOULD be unrelated to any concurrently-running client request — so a log emitted from a handler context belongs on the POST stream. Clients reading handler logs off the standalone GET stream will now see them on the per-request POST stream instead. The eventStore-resumable case (a log emitted aftercloseSSE()while the client has not yet reconnected) is handled by the store-first persistence behavior inWebStandardStreamableHTTPServerTransport.send(). The session-scopedServer.sendLoggingMessage()API is unchanged. -
#2286
1823aaeThanks @felixweinberger! -WebStandardStreamableHTTPServerTransport: request-related events (progress,ctx.mcpReq.notify, handler-emitted log) and the final response are now persisted to the configuredeventStorewhenever the request is in flight, regardless of whether a live SSE writer currently exists — mirroring the standalone-SSE path's store-first semantics. This fixes thecloseSSE()poll-and-replay drop (events emitted aftercloseSSE()were previously silently lost) and aligns with the 2025-11-25 specification ("disconnection SHOULD NOT be interpreted as the client cancelling its request"). When aneventStoreis configured, a final response sent while no per-request stream is connected is stored for replay and returns cleanly instead of throwing "No connection established"; aLast-Event-IDreconnect after the request has been retired replays the stored response and then closes the resumed stream. When noeventStoreis configured, the same condition is surfaced viaonerror(the response is undeliverable) and the request id is retired. -
#2286
1823aaeThanks @felixweinberger! - Re-pin the 2026-07-28 draft references (spec reference types, vendored schema.json twins, example corpus) to the latest spec commit and align the 2026-era wire surface with it. Deliberate 2026-era wire behavior changes (the released 2025-11-25 surface is untouched):notifications/elicitation/completeis no longer part of the 2026-07-28 wire registry (the draft removed the notification together withelicitationIdon URL-mode elicitation). On connections negotiated at 2026-07-28, sending it — including viaServer.createElicitationCompletionNotifier()— now fails locally withSdkErrorCode.MethodNotSupportedByProtocolVersion, and inbound copies are dropped as unknown notifications. Both remain fully supported on 2025-11-25.notifications/cancelledon 2026-era connections now parses with a revision-exact schema that requiresrequestId(the draft made it required); the notification_metashape types theio.modelcontextprotocol/subscriptionIdkey. 2025-era parsing is unchanged.- The error code
-32001emitted for HTTP header/body mismatches is now defined by the draft schema (HEADER_MISMATCH); the emitted behavior is unchanged (documentation only).
No public API surface changes; the regenerated reference artifacts are internal/test-only.
-
#2286
1823aaeThanks @felixweinberger! - Internal: regenerate the 2026-07-28 spec reference types from the latest draft schema (DiscoverResultnow extendsCacheableResult;ElicitationCompleteNotificationParamsextracted as a named interface) and document the anchor lifecycle policy. Released-revision spec-type generation is now pinned to a fixed spec commit; draft anchors keep floating via the nightly refresh PRs. No public API or runtime behavior changes. -
#2286
1823aaeThanks @felixweinberger! - Freeze the per-era wire schemas as self-contained copies decoupled from the public types layer, and convertWireCodecto a function-only interface. Two small spec-conformance fixes ride along with the otherwise-pure refactor:- The 2026 wire-true
resultTypemember now defaults to'complete'when absent (the spec's receiver-side back-compat rule); the inbounddecodeResultstep continues to require it. Theserver/discoverresult accepts absent or malformedttlMs/cacheScope(falling back to0/'private'per the spec's receiver leniency in caching.mdx) so the version-negotiation probe classifier stays behavior-neutral. Other cacheable result schemas are unchanged here; general receiver leniency for those belongs to the response-cache surface. - The sampling
hasToolsdiscriminant now keys ontools || toolChoice(previouslytoolsonly), aligning the client and server selection of the with-tools result variant withclientCapabilityRequirements.
- The 2026 wire-true
2.0.0-alpha.3
Major Changes
- #2128
c8d7401Thanks @felixweinberger! - SEP-2663: remove 2025-11 experimental tasks (TaskManager, experimental.tasks.* accessors). Tasks are now Extensions Track.
Minor Changes
-
#1974
db83829Thanks @felixweinberger! - Add custom (non-spec) method support: a 3-argsetRequestHandler(method, schemas, handler)/setNotificationHandler(method, schemas, handler)form for vendor-prefixed methods, and arequest(req, resultSchema)overload (also onctx.mcpReq.send) for typed custom-method results. Spec-method calls are unchanged.Response result-schema validation failure now rejects with
SdkError(InvalidResult)instead of a rawZodError. AddsSdkErrorCode.InvalidResult. -
#2353
c59dc3aThanks @KKonstantinov! - Supporticonson the high-levelMcpServer.registerTool()andregisterPrompt()config objects (and on theirupdate()methods). Tool and prompt icons now surface intools/listandprompts/list. Resource, resource-template, and server-info (Implementation) icons already passed through. This closes the gap with the MCP spec, which allowsiconson tools, resources, resource templates, and prompts. -
#2354
0fb8406Thanks @KKonstantinov! - Add the v2IdJagTokenExchangeResponsetype to the public API and register its schema as an MCP spec type.IdJagTokenExchangeResponseis now exported from@modelcontextprotocol/clientand@modelcontextprotocol/server,'IdJagTokenExchangeResponse'joins theSpecTypeNameunion, andisSpecType.IdJagTokenExchangeResponse(value)/specTypeSchemas.IdJagTokenExchangeResponsevalidate it by name — matching how the OAuth/OpenID auth schemas are already exposed. The Zod schema itself,IdJagTokenExchangeResponseSchema, is available from@modelcontextprotocol/core. -
#2248
db28156Thanks @felixweinberger! - Restore the 2025-11-25 task wire types that were removed together with the task feature: the task schemas and inferred types, task members of the request/result/notification unions, thetaskrequest-params augmentation, thetaskscapability key, theisTaskAugmentedRequestParamsguard, andRELATED_TASK_META_KEY. The task feature itself remains removed — servers do not advertise thetaskscapability and inboundtasks/*requests receive-32601— but the wire surface stays so SDKs interoperate cleanly with peers on the 2025-11-25 revision. -
#1887
96db044Thanks @felixweinberger! - ExportisSpecTypeandspecTypeSchemasrecords for runtime validation of any MCP spec type by name.isSpecType.ContentBlock(value)is a type predicate;specTypeSchemas.ContentBlockis aStandardSchemaV1Sync<ContentBlock>validator —validate()returns the result synchronously. Guards are standalone functions, soarr.filter(isSpecType.ContentBlock)works. Also export theSpecTypeName,SpecTypes, andStandardSchemaV1Synctypes. -
#1871
9fc9070Thanks @felixweinberger! - Move stdio transports to a./stdiosubpath export. ImportStdioClientTransport,getDefaultEnvironment,DEFAULT_INHERITED_ENV_VARS, andStdioServerParametersfrom@modelcontextprotocol/client/stdio, andStdioServerTransportfrom@modelcontextprotocol/server/stdio. The@modelcontextprotocol/clientroot entry no longer pulls innode:child_process,node:stream, orcross-spawn, fixing bundling for browser and Cloudflare Workers targets; the@modelcontextprotocol/serverroot entry drops itsnode:streamreference. Node.js, Bun, and Deno consumers update the import path; runtime behavior is unchanged.
Patch Changes
-
#2280
e8c7180Thanks @felixweinberger! - Bound the protocol-version checks gating SSE resumability behavior (priming events andcloseSSEStreamcallbacks) in the Streamable HTTP server transport. Previously an open-ended>= '2025-11-25'comparison let unknown future protocol version strings from aninitializerequest body enable this behavior; the version must now also be one of the transport's supported protocol versions. Behavior for all currently supported protocol versions is unchanged. -
#1897
434b2f1Thanks @felixweinberger! - Stop bundling@cfworker/json-schemainto the main package barrel. PreviouslyCfWorkerJsonSchemaValidatorwas re-exported from the core internal barrel, so tsdown inlined the@cfworker/json-schemadependency into every consumer's bundle even when it was never used. The named validator classes are now reachable only via the explicit@modelcontextprotocol/{client,server}/validators/{ajv,cf-worker}subpaths and the runtime_shimsconditional, so consumers that import only from the root entry point no longer ship the validator dep. -
#2269
e84c3e9Thanks @mattzcarey! - Non-SEP draft spec conformance fixesMcpServernow eagerly installs list/read/call handlers for every primitive capability (tools,resources,prompts) declared inServerOptions.capabilities. Per the draft spec, a server that declares a capability MUST respond to its list method (potentially with an empty result) instead of returning "Method not found". Previously, handlers were only installed lazily on first registration, so a server constructed with e.g.capabilities: { tools: {} }and zero registered tools answeredtools/listwith-32601. Low-levelServerusers remain responsible for registering handlers for declared capabilities (documented onServerOptions.capabilities).- Fixed pagination doc examples on
Client.listTools/listPrompts/listResourcesto loopwhile (cursor !== undefined)instead ofwhile (cursor)— per the draft spec, clients MUST NOT treat an empty-string cursor as the end of results.
-
#1834
42cb6b2Thanks @felixweinberger! - ExportInMemoryTransportfor in-process testing. -
#1788
df4b6ccThanks @claygeo! - Prevent stack overflow in StreamableHTTPServerTransport.close() with re-entrant guard -
#1855
2c0c481Thanks @Christian-Sidak! - Add| undefinedto optional callback and function properties onWebStandardStreamableHTTPServerTransportOptions(sessionIdGenerator,onsessioninitialized,onsessionclosed) and corresponding private fields.This fixes TS2430 errors for consumers using
exactOptionalPropertyTypes: truewithoutskipLibCheck, where optional properties with function types need explicit| undefinedto match their emitted declarations. -
#1898
2a7611dThanks @felixweinberger! - Add top-leveltypesfield (andtypesVersionson client/server for their subpath exports) so consumers on legacymoduleResolution: "node"can resolve type declarations. Theexportsmap remains the source of truth fornodenext/bundlerresolution. ThetypesVersionsmap includes entries for subpaths added by sibling PRs in this series (zod-schemas,stdio); those entries are no-ops until the correspondingdist/*.d.mtsfiles exist. -
#1901
e15a8efThanks @felixweinberger! -registerTool/registerPromptaccept a raw Zod shape ({ field: z.string() }) forinputSchema/outputSchema/argsSchemain addition to a wrapped Standard Schema. Raw shapes are auto-wrapped withz.object(). The raw-shape overloads are@deprecated; prefer wrapping withz.object().Also widens the
completable()constraint fromStandardSchemaWithJSONtoStandardSchemaV1so v1'scompletable(z.string(), fn)continues to work. -
#2268
49c0a71Thanks @mattzcarey! - Mark the roots, sampling, and logging runtime APIs as@deprecatedper SEP-2577 (deprecated as of protocol version 2026-07-28; functional for at least twelve months). AnnotatesServer.createMessage/listRoots/sendLoggingMessage,McpServer.sendLoggingMessage,Client.setLoggingLevel/sendRootsListChanged, theServerContext.mcpReq.log/requestSamplinghelpers, and theroots/sampling/loggingcapability schema fields. JSDoc/docs only — no behavior change. -
#2275
1b53a41Thanks @KKonstantinov! - Add a configurablemaxBufferSize(default 10 MB) to the stdio transports. When a single message would push the read buffer past the limit, the transport now emits anonerrorand closes instead of growing the buffer unbounded. Configure vianew StdioClientTransport({ ..., maxBufferSize })ornew StdioServerTransport(stdin, stdout, { maxBufferSize }). The default is exported from@modelcontextprotocol/client/@modelcontextprotocol/serverasSTDIO_DEFAULT_MAX_BUFFER_SIZE. -
#2088
16d13abThanks @mattzcarey! - Bundle automatic JSON Schema validator defaults in@modelcontextprotocol/clientand@modelcontextprotocol/serverruntime shims.Client and server pick the right validator automatically based on the runtime: the Node shim uses AJV, the browser/workerd shim uses
@cfworker/json-schema. Both backends are bundled into the shim chunks that select them, so the default code path needs no extra installs —import { McpServer } from '@modelcontextprotocol/server'does not pullajvor@cfworker/json-schemainto the root entry chunk.The named validator classes remain part of the public surface for consumers who want to customize the built-in backend (pre-register schemas by
$id, register custom AJV formats, switch dialects, change@cfworker/json-schemadraft). They are exposed through explicit subpaths so they do not bloat the root index chunk:import { AjvJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/ajv'import { CfWorkerJsonSchemaValidator } from '@modelcontextprotocol/{client,server}/validators/cf-worker'
Importing from one of these subpaths means the corresponding peer dep (
ajv+ajv-formats, or@cfworker/json-schema) must be in yourpackage.json. The shim keeps its own vendored copy for the default path, so a project can use the subpath in some files and rely on the default in others.The
jsonSchemaValidatorinterface remains the public extension point for replacing validation entirely with a custom implementation. -
#1976
55b1f06Thanks @felixweinberger! - refactor: subclasses override_wrapHandlerhook instead of redeclaringsetRequestHandler. -
#1895
b256546Thanks @felixweinberger! - Fix runtime crash ontools/listwhen a tool'sinputSchemacomes from zod 4.0–4.1. The SDK requires~standard.jsonSchema(StandardJSONSchemaV1, added in zod 4.2.0); previously a missingjsonSchemacrashed atundefined[io].standardSchemaToJsonSchemanow detects zod 4 schemas lackingjsonSchemaand falls back to the SDK-bundledz.toJSONSchema(), emitting a one-time console warning. zod 3 schemas (which the bundled zod 4 converter cannot introspect) and non-zod schema libraries withoutjsonSchemaget a clear error pointing tofromJsonSchema(). The workspace zod catalog is also bumped to^4.2.0.
2.0.0-alpha.2
Patch Changes
- #1840
424cbaeThanks @KKonstantinov! - tsdown exports resolution fix
2.0.0-alpha.1
Major Changes
-
#1389
108f2f3Thanks @DePasqualeOrg! - Fix error handling for unknown tools and resources per MCP spec.Tools: Unknown or disabled tool calls now return JSON-RPC protocol errors with code
-32602(InvalidParams) instead ofCallToolResultwithisError: true. Callers who checkedresult.isErrorfor unknown tools should catch rejected promises instead.Resources: Unknown resource reads now return error code
-32002(ResourceNotFound) instead of-32602(InvalidParams).Added
ProtocolErrorCode.ResourceNotFound.
Minor Changes
-
#1673
462c3fcThanks @KKonstantinov! - refactor: extract task orchestration from Protocol into TaskManagerBreaking changes:
taskStore,taskMessageQueue,defaultTaskPollInterval, andmaxTaskQueueSizemoved fromProtocolOptionstocapabilities.tasksonClientOptions/ServerOptions
-
#1689
0784be1Thanks @felixweinberger! - Support Standard Schema for tool and prompt schemasTool and prompt registration now accepts any schema library that implements the Standard Schema spec: Zod v4, Valibot, ArkType, and others.
RegisteredTool.inputSchema,RegisteredTool.outputSchema, andRegisteredPrompt.argsSchemanow useStandardSchemaWithJSON(requires both~standard.validateand~standard.jsonSchema) instead of the Zod-specificAnySchematype.Zod v4 schemas continue to work unchanged — Zod v4 implements the required interfaces natively.
import { type } from 'arktype'; server.registerTool( 'greet', { inputSchema: type({ name: 'string' }) }, async ({ name }) => ({ content: [{ type: 'text', text: `Hello, ${name}!` }] }) );For raw JSON Schema (e.g. TypeBox output), use the new
fromJsonSchemaadapter:import { fromJsonSchema } from '@modelcontextprotocol/server'; import { AjvJsonSchemaValidator } from '@modelcontextprotocol/server/validators/ajv'; server.registerTool( 'greet', { inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator()) }, handler );Breaking changes:
experimental.tasks.getTaskResult()no longer accepts aresultSchemaparameter. ReturnsGetTaskPayloadResult(a looseResult); cast to the expected type at the call site.- Removed unused exports from
@modelcontextprotocol/core-internal:SchemaInput,schemaToJson,parseSchemaAsync,getSchemaShape,getSchemaDescription,isOptionalSchema,unwrapOptionalSchema. Use the newstandardSchemaToJsonSchemaandvalidateStandardSchemainstead. completable()remains Zod-specific (it relies on Zod's.shapeintrospection).
Patch Changes
-
#1758
e86b183Thanks @KKonstantinov! - tasks - disallow requesting a null TTL -
#1363
0a75810Thanks @DevJanderson! - Fix ReDoS vulnerability in UriTemplate regex patterns (CVE-2026-0621) -
#1372
3466a9eThanks @mattzcarey! - missing change for fix(client): replace body.cancel() with text() to prevent hanging -
#1824
fcde488Thanks @felixweinberger! - DropzodfrompeerDependencies(kept as direct dependency)Since Standard Schema support landed,
zodis purely an internal runtime dependency used for protocol message parsing. User-facing schemas (registerTool,registerPrompt) accept any Standard Schema library.zodremains independenciesand auto-installs; users no longer need to install it alongside the SDK. -
#1761
01954e6Thanks @felixweinberger! - Convert remaining capability-assertion throws toSdkError(SdkErrorCode.CapabilityNotSupported, ...). Follow-up to #1454 which missedClient.assertCapability(), the task capability helpers inexperimental/tasks/helpers.ts, and the sampling/elicitation capability checks inexperimental/tasks/server.ts. -
#1433
78bae74Thanks @codewithkenzo! - Fix transport errors being silently swallowed by adding missingonerrorcallback invocations before allcreateJsonErrorResponsecalls inWebStandardStreamableHTTPServerTransport. This ensures errors like parse failures, invalid headers, and session validation errors are properly reported via theonerrorcallback. -
#1660
689148dThanks @rechedev9! - fix(server): propagate negotiated protocol version to transport in _oninitialize -
#1568
f1ade75Thanks @stakeswky! - Handle stdout errors (e.g. EPIPE) inStdioServerTransportgracefully instead of crashing. When the client disconnects abruptly, the transport now catches the stdout error, surfaces it viaonerror, and closes. -
#1419
dcf708dThanks @KKonstantinov! - remove deprecated .tool, .prompt, .resource method signatures -
#1388
f66a55bThanks @mattzcarey! - reverting application/json in notifications -
#1534
69a0626Thanks @josefaidt! - remove npm references, use pnpm -
#1534
69a0626Thanks @josefaidt! - clean up package manager usage, all pnpm -
#1419
dcf708dThanks @KKonstantinov! - deprecated .tool, .prompt, .resource method removal -
#1279
71ae3acThanks @KKonstantinov! - Initial 2.0.0-alpha.0 client and server package