What’s New

MCP TypeScript SDK

Frameworks & Libraries

The official TypeScript SDK for building Model Context Protocol servers and clients.

Latest 2.0.0 · by AnthropicWebsitemodelcontextprotocol/typescript-sdk

Changelog

2.0.0

Added 4
  • Export the Protocol base class and mergeCapabilities from the @modelcontextprotocol/client and @modelcontextprotocol/server package roots
  • Add configurable SSE keep-alive comment frames to Streamable HTTP transports and apply createMcpHandler's existing keepAliveMs option to every HTTP SSE stream it serves
  • Add runtime-neutral Bearer authentication to @modelcontextprotocol/server with requireBearerAuth for web-standard fetch request hosts
  • Add new public constant SERVER_INFO_META_KEY for 'io.modelcontextprotocol/serverInfo'
Changed 4
  • Move schema source modules into @modelcontextprotocol/core and resolve them as a regular runtime dependency instead of bundling a private copy into each package
  • Allow inputRequired.elicit() to accept a Standard Schema such as a Zod object for requestedSchema, with support for email, uri, date, and date-time formats
  • Align the 2026-07-28 wire with the final revision: serverInfo moves from the DiscoverResult body to the result _meta, and the per-request envelope's clientInfo demotes from required to SHOULD
  • Break DiscoverResult type to no longer declare serverInfo and make RequestMetaEnvelope's clientInfo optional
Minor Changes
  • #2501 1480241 Thanks @felixweinberger! - Export the Protocol base class and mergeCapabilities from the @modelcontextprotocol/client and @modelcontextprotocol/server package roots, restoring the v1 import for consumers that subclass Protocol (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 Protocol and mergeCapabilities imports from shared/protocol.js to the client or server package root, like the module's other symbols, instead of dropping them with an action-required marker.

  • #2477 8e1d2e9 Thanks @felixweinberger! - Move the schema source modules (spec schemas, OAuth schemas, protocol constants) into @modelcontextprotocol/core and 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/core gains a ./internal subpath (SDK-internal contract; may change in any release) and the four packages now version together.

  • #2513 f413763 Thanks @felixweinberger! - Align the 2026-07-28 wire with the final revision (spec PR #3002): serverInfo moves from the DiscoverResult body to the result _meta, and the per-request envelope's clientInfo demotes 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 body serverInfo failed parse, so the probe misclassified the server as legacy and attempted an initialize handshake 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 omit clientInfo.

    Now:

    • The 2026 wire schemas are the final revision exactly: no body serverInfo on DiscoverResult, envelope clientInfo optional (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-built subscriptions/listen graceful-close results — the spec's SubscriptionsListenResultMeta extends ResultMetaObject.
    • Clients keep sending clientInfo and read server identity from the discover result's _meta only. A server that stamps no identity is anonymous: getServerVersion() is undefined and the response cache partitions under a per-connection surrogate. A malformed _meta serverInfo value is treated as absent on receive (the spec marks the field self-reported, unverified, and display-only).
    • Breaking type changes: DiscoverResult no longer declares serverInfo; RequestMetaEnvelope's clientInfo is optional. New public constant SERVER_INFO_META_KEY ('io.modelcontextprotocol/serverInfo').
  • #2369 24be404 Thanks @mattzcarey! - Allow inputRequired.elicit() to accept a Standard Schema such as a Zod object for requestedSchema. The builder converts it to MCP's restricted form-elicitation JSON Schema, while the same schema can validate and type the response through acceptedContent() on handler re-entry. Zod formats mapping to email, uri, date, and date-time are 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 (use z.enum or z.literal(['a', 'b'])), and non-spec root keywords like z.strictObject()'s additionalProperties.

  • #2541 470678d Thanks @mattzcarey! - Add configurable SSE keep-alive comment frames to Streamable HTTP transports and apply createMcpHandler's existing keepAliveMs option to every HTTP SSE stream it serves.

  • #2420 7635115 Thanks @felixweinberger! - Add runtime-neutral Bearer authentication to @modelcontextprotocol/server: requireBearerAuth gates web-standard fetch(request) hosts (Cloudflare Workers, Deno, Bun, Hono), built on the exported verifyBearerToken and bearerAuthChallengeResponse pieces, with OAuthTokenVerifier now defined here. The Express middleware adapts the same core and is unchanged in behavior, except that WWW-Authenticate challenge values are now RFC 7235 quoted-string sanitized (quotes and backslashes escaped, control and non-ASCII characters replaced); @modelcontextprotocol/express re-exports OAuthTokenVerifier as before.

  • #2422 61866d7 Thanks @felixweinberger! - Add runtime-neutral OAuth discovery serving to @modelcontextprotocol/server: oauthMetadataResponse serves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents from web-standard fetch(request) hosts, built on the exported buildOAuthProtectedResourceMetadata, with getOAuthProtectedResourceMetadataUrl now defined here. The Express metadata router adapts the same core and is unchanged in behavior; the insecure-issuer escape hatch is an explicit dangerouslyAllowInsecureIssuerUrl option 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 with Vary.

  • #2483 3f07a32 Thanks @felixweinberger! - Add preloadSchemas(), 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. Calling preloadSchemas() 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 (its browser condition previously reused the workerd shim), so browser bundles keep lazy construction.

Patch Changes
  • #2402 a400259 Thanks @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 44797d7 Thanks @felixweinberger! - Restore the v1 parse tolerance for CallToolResult.content: an inbound legacy-era tools/call result without content defaults to [] instead of failing validation. Deployed servers — accepted by SDK v1 for years — return structuredContent-only (or otherwise content-less) results, and the strict parse turned every such call into an INVALID_RESULT error 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 content for 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 foreign resultType), 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 to content: [] before era validation on every leg, reaching the wire spec-valid.

    Conscious call: the nested sampling ToolResultContentSchema stays spec-strict — v1 had defaulted its content too, but it is params-side (tool results a caller authors into a sampling message), deliberately not restored.

  • #2431 1b90c96 Thanks @morluto! - Fix the CommonJS validators/ajv subpath so reading the exported Ajv class no longer throws ReferenceError: import_ajv is not defined. The subpath now re-exports the bundled provider's concrete Ajv value in CJS output, matching the existing ESM behavior.

  • #2405 f172626 Thanks @mattzcarey! - Ship CommonJS builds alongside ESM. Each package now emits both .mjs/.d.mts and .cjs/.d.cts (via tsdown format: ['esm', 'cjs']), and its exports map adds a require condition so require('@modelcontextprotocol/…') works from CommonJS consumers. Output extensions are normalized across all packages (@modelcontextprotocol/core moves from .js/.d.ts to .mjs/.d.mts); the public import paths are unchanged.

  • #2441 561c6d8 Thanks @felixweinberger! - POSTs whose Content-Type media type is not application/json are now rejected with 415 Unsupported Media Type; the header is parsed instead of substring-matched. Previously any value merely containing the substring passed the check (for example text/plain; a=application/json), case variants were wrongly rejected, and the 2026-07-28 entry did not inspect Content-Type at 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 like application/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 ce2f65d Thanks @felixweinberger! - instanceof on the SDK error classes (ProtocolError and its typed subclasses, SdkError/SdkHttpError, OAuthError, and the client's SseError, UnauthorizedError, and OAuth-client-flow error family — OAuthClientFlowError and its subclasses) now works across separately bundled copies of the SDK. The classes match by a stable brand (via Symbol.hasInstance and a registry symbol) instead of prototype identity, so a process that uses both @modelcontextprotocol/client and @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-based instanceof is 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-bundle SdkError used as an abort reason is now rethrown as-is instead of being wrapped as a RequestTimeout. 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 over instanceof. Also: UnauthorizedError now sets error.name to 'UnauthorizedError' (previously 'Error'), and per-package conformance tests enforce that every exported error class participates in branding. Version-negotiation probing now recognizes UnauthorizedError (previously a dead name-string check) and propagates it unchanged, so connect() on an auth-gated server rejects with the original UnauthorizedError (previously wrapped as the cause of an SdkError(EraNegotiationFailed)) — run finishAuth() and reconnect, and the retry probes with the token.

  • #2458 7c49b47 Thanks @felixweinberger! - Construct the default Ajv validation engine lazily on first validation. Creating a Client or Server no longer pays the ajv + ajv-formats instantiation cost at startup when no JSON Schema validation ever runs.

  • #2476 e0a0ab7 Thanks @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 7e69735 Thanks @mattzcarey! - Return JSON-RPC Invalid Params with the original URI and an invalid_uri reason when resources/read receives a syntactically malformed URI.

  • #2399 3c7ddaf Thanks @felixweinberger! - Return HTTP 400 for a MissingRequiredClientCapabilityError (-32021) produced after dispatch. The spec mandates 400 Bad Request for this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — the input_required gate 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 with responseMode: 'sse', which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream.

  • #2425 e8de519 Thanks @Sehlani042! - Stop advertising validator provider classes from the root client/server type declarations. The provider classes remain available from the explicit validator subpaths.

  • #2453 0ab5d14 Thanks @mattzcarey! - Strip RFC 9110 optional whitespace around inbound MCP-Protocol-Version, Mcp-Method, and Mcp-Name values before classifying and validating modern HTTP requests. This keeps valid requests portable across Fetch runtimes that expose raw leading or trailing SP/HTAB through Headers.get().

  • #2534 f130e1a Thanks @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's 2019-09/openAi targets) with 2019-09 semantics, on both the Ajv and Cloudflare Workers providers (with known engine differences documented in the migration guide). Schemas with no $schema still 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

Pre-release
Added 2
  • Export the Protocol base class and mergeCapabilities from the @modelcontextprotocol/client and @modelcontextprotocol/server package roots
  • Add public constant SERVER_INFO_META_KEY with value 'io.modelcontextprotocol/serverInfo'
Changed 5
  • The codemod now rewrites Protocol and mergeCapabilities imports from shared/protocol.js to the client or server package root instead of dropping them
  • Align the 2026-07-28 wire with the final revision: serverInfo moves from the DiscoverResult body to the result _meta, and the per-request envelope's clientInfo demotes from required to optional
  • Servers now stamp _meta['io.modelcontextprotocol/serverInfo'] on every 2026-era response including subscriptions/listen graceful-close results
  • Clients keep sending clientInfo and read server identity from the discover result's _meta only, with anonymous servers returning undefined from getServerVersion()
  • DiscoverResult no longer declares serverInfo and RequestMetaEnvelope's clientInfo is optional
Minor Changes
  • #2501 1480241 Thanks @felixweinberger! - Export the Protocol base class and mergeCapabilities from the @modelcontextprotocol/client and @modelcontextprotocol/server package roots, restoring the v1 import for consumers that subclass Protocol (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 Protocol and mergeCapabilities imports from shared/protocol.js to the client or server package root, like the module's other symbols, instead of dropping them with an action-required marker.

  • #2513 f413763 Thanks @felixweinberger! - Align the 2026-07-28 wire with the final revision (spec PR #3002): serverInfo moves from the DiscoverResult body to the result _meta, and the per-request envelope's clientInfo demotes 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 body serverInfo failed parse, so the probe misclassified the server as legacy and attempted an initialize handshake 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 omit clientInfo.

    Now:

    • The 2026 wire schemas are the final revision exactly: no body serverInfo on DiscoverResult, envelope clientInfo optional (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-built subscriptions/listen graceful-close results — the spec's SubscriptionsListenResultMeta extends ResultMetaObject.
    • Clients keep sending clientInfo and read server identity from the discover result's _meta only. A server that stamps no identity is anonymous: getServerVersion() is undefined and the response cache partitions under a per-connection surrogate. A malformed _meta serverInfo value is treated as absent on receive (the spec marks the field self-reported, unverified, and display-only).
    • Breaking type changes: DiscoverResult no longer declares serverInfo; RequestMetaEnvelope's clientInfo is optional. New public constant SERVER_INFO_META_KEY ('io.modelcontextprotocol/serverInfo').
Patch Changes
  • Updated dependencies [f413763]:
    • @modelcontextprotocol/core@2.0.0-beta.5

2.0.0-beta.4

Pre-release
Added 1
  • Add preloadSchemas(), an explicit opt-in to eager wire-schema construction, called automatically in Cloudflare Workers builds
Changed 5
  • Move schema source modules (spec schemas, OAuth schemas, protocol constants) into @modelcontextprotocol/core and resolve them from there as a regular runtime dependency instead of bundling a private copy into each package
  • Wire schemas are now built lazily by default instead of at import time
  • The server package gains a dedicated browser shim for lazy schema construction, separate from the workerd shim
  • Construct the default Ajv validation engine lazily on first validation instead of at startup
  • Build protocol-revision wire schemas lazily on first validation using a module-level memoized factory
Minor Changes
  • #2477 8e1d2e9 Thanks @felixweinberger! - Move the schema source modules (spec schemas, OAuth schemas, protocol constants) into @modelcontextprotocol/core and 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/core gains a ./internal subpath (SDK-internal contract; may change in any release) and the four packages now version together.

  • #2483 3f07a32 Thanks @felixweinberger! - Add preloadSchemas(), 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. Calling preloadSchemas() 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 (its browser condition previously reused the workerd shim), so browser bundles keep lazy construction.

Patch Changes
  • #2458 7c49b47 Thanks @felixweinberger! - Construct the default Ajv validation engine lazily on first validation. Creating a Client or Server no longer pays the ajv + ajv-formats instantiation cost at startup when no JSON Schema validation ever runs.

  • #2476 e0a0ab7 Thanks @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

Pre-release
Added 3
  • Allow `inputRequired.elicit()` to accept a Standard Schema such as a Zod object for `requestedSchema`, with support for Zod formats mapping to `email`, `uri`, `date`, and `date-time`
  • Add runtime-neutral Bearer authentication to `@modelcontextprotocol/server` with `requireBearerAuth`, `verifyBearerToken`, and `bearerAuthChallengeResponse` for web-standard `fetch(request)` hosts
  • Add runtime-neutral OAuth discovery serving to `@modelcontextprotocol/server` with `oauthMetadataResponse` and `buildOAuthProtectedResourceMetadata` for RFC 9728 and RFC 8414 metadata documents
Changed 3
  • Express middleware Bearer authentication now applies RFC 7235 quoted-string sanitization to `WWW-Authenticate` challenge values
  • OAuth metadata serving validates requests lazily, tolerates trailing slashes, supports HEAD requests, and marks reflected CORS preflights with `Vary`
  • Insecure issuer escape hatch changed from module-scope environment read to explicit `dangerouslyAllowInsecureIssuerUrl` option in OAuth metadata core
Fixed 3
  • Restore v1 parse tolerance for `CallToolResult.content` to default to `[]` when absent in inbound legacy-era `tools/call` results
  • Fix CommonJS `validators/ajv` subpath to properly export the bundled provider's `Ajv` class
  • Reject POSTs with non-`application/json` `Content-Type` media type with `415 Unsupported Media Type` and parse the header correctly instead of substring-matching
Minor Changes
  • #2369 24be404 Thanks @mattzcarey! - Allow inputRequired.elicit() to accept a Standard Schema such as a Zod object for requestedSchema. The builder converts it to MCP's restricted form-elicitation JSON Schema, while the same schema can validate and type the response through acceptedContent() on handler re-entry. Zod formats mapping to email, uri, date, and date-time are 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 (use z.enum or z.literal(['a', 'b'])), and non-spec root keywords like z.strictObject()'s additionalProperties.

  • #2420 7635115 Thanks @felixweinberger! - Add runtime-neutral Bearer authentication to @modelcontextprotocol/server: requireBearerAuth gates web-standard fetch(request) hosts (Cloudflare Workers, Deno, Bun, Hono), built on the exported verifyBearerToken and bearerAuthChallengeResponse pieces, with OAuthTokenVerifier now defined here. The Express middleware adapts the same core and is unchanged in behavior, except that WWW-Authenticate challenge values are now RFC 7235 quoted-string sanitized (quotes and backslashes escaped, control and non-ASCII characters replaced); @modelcontextprotocol/express re-exports OAuthTokenVerifier as before.

  • #2422 61866d7 Thanks @felixweinberger! - Add runtime-neutral OAuth discovery serving to @modelcontextprotocol/server: oauthMetadataResponse serves the RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server metadata documents from web-standard fetch(request) hosts, built on the exported buildOAuthProtectedResourceMetadata, with getOAuthProtectedResourceMetadataUrl now defined here. The Express metadata router adapts the same core and is unchanged in behavior; the insecure-issuer escape hatch is an explicit dangerouslyAllowInsecureIssuerUrl option 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 with Vary.

Patch Changes
  • #2456 44797d7 Thanks @felixweinberger! - Restore the v1 parse tolerance for CallToolResult.content: an inbound legacy-era tools/call result without content defaults to [] instead of failing validation. Deployed servers — accepted by SDK v1 for years — return structuredContent-only (or otherwise content-less) results, and the strict parse turned every such call into an INVALID_RESULT error 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 content for 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 foreign resultType), 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 to content: [] before era validation on every leg, reaching the wire spec-valid.

    Conscious call: the nested sampling ToolResultContentSchema stays spec-strict — v1 had defaulted its content too, but it is params-side (tool results a caller authors into a sampling message), deliberately not restored.

  • #2431 1b90c96 Thanks @morluto! - Fix the CommonJS validators/ajv subpath so reading the exported Ajv class no longer throws ReferenceError: import_ajv is not defined. The subpath now re-exports the bundled provider's concrete Ajv value in CJS output, matching the existing ESM behavior.

  • #2441 561c6d8 Thanks @felixweinberger! - POSTs whose Content-Type media type is not application/json are now rejected with 415 Unsupported Media Type; the header is parsed instead of substring-matched. Previously any value merely containing the substring passed the check (for example text/plain; a=application/json), case variants were wrongly rejected, and the 2026-07-28 entry did not inspect Content-Type at 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 like application/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 ce2f65d Thanks @felixweinberger! - instanceof on the SDK error classes (ProtocolError and its typed subclasses, SdkError/SdkHttpError, OAuthError, and the client's SseError, UnauthorizedError, and OAuth-client-flow error family — OAuthClientFlowError and its subclasses) now works across separately bundled copies of the SDK. The classes match by a stable brand (via Symbol.hasInstance and a registry symbol) instead of prototype identity, so a process that uses both @modelcontextprotocol/client and @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-based instanceof is 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-bundle SdkError used as an abort reason is now rethrown as-is instead of being wrapped as a RequestTimeout. 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 over instanceof. Also: UnauthorizedError now sets error.name to 'UnauthorizedError' (previously 'Error'), and per-package conformance tests enforce that every exported error class participates in branding. Version-negotiation probing now recognizes UnauthorizedError (previously a dead name-string check) and propagates it unchanged, so connect() on an auth-gated server rejects with the original UnauthorizedError (previously wrapped as the cause of an SdkError(EraNegotiationFailed)) — run finishAuth() and reconnect, and the retry probes with the token.

  • #2451 7e69735 Thanks @mattzcarey! - Return JSON-RPC Invalid Params with the original URI and an invalid_uri reason when resources/read receives a syntactically malformed URI.

  • #2425 e8de519 Thanks @Sehlani042! - Stop advertising validator provider classes from the root client/server type declarations. The provider classes remain available from the explicit validator subpaths.

  • #2453 0ab5d14 Thanks @mattzcarey! - Strip RFC 9110 optional whitespace around inbound MCP-Protocol-Version, Mcp-Method, and Mcp-Name values before classifying and validating modern HTTP requests. This keeps valid requests portable across Fetch runtimes that expose raw leading or trailing SP/HTAB through Headers.get().

2.0.0-beta.2

Pre-release
Changed 2
  • Ship CommonJS builds alongside ESM by emitting both .mjs/.d.mts and .cjs/.d.cts for each package and updating the exports map to add a require condition so require('@modelcontextprotocol/…') works from CommonJS consumers
  • Return HTTP 400 for a MissingRequiredClientCapabilityError (-32021) produced after dispatch instead of surfacing it in-band on HTTP 200
Patch Changes
  • #2405 f172626 Thanks @mattzcarey! - Ship CommonJS builds alongside ESM. Each package now emits both .mjs/.d.mts and .cjs/.d.cts (via tsdown format: ['esm', 'cjs']), and its exports map adds a require condition so require('@modelcontextprotocol/…') works from CommonJS consumers. Output extensions are normalized across all packages (@modelcontextprotocol/core moves from .js/.d.ts to .mjs/.d.mts); the public import paths are unchanged.

  • #2399 3c7ddaf Thanks @felixweinberger! - Return HTTP 400 for a MissingRequiredClientCapabilityError (-32021) produced after dispatch. The spec mandates 400 Bad Request for this error with no condition on where it arose, but only the pre-dispatch capability gate honored that; the post-handler emission — the input_required gate 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 with responseMode: 'sse', which opens its stream at dispatch end — keeps its committed 200 and carries the error in-stream.

2.0.0-beta.1

Pre-release
Added 1
  • Support for the MCP 2026-07-28 specification revision
Patch Changes
  • #2402 a400259 Thanks @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

Pre-release
Added 2
  • Export toNodeHandler from @modelcontextprotocol/node to adapt createMcpHandler for Node frameworks
  • Add optional onerror callback to toNodeHandler for adapter-level error fallback observability
Changed 14
  • Split the wire layer into per-era codecs and make protocol-revision deletions physical, with resultType no longer modeled by neutral wire schema and EmptyResultSchema now rejecting resultType bodies
  • Make CallToolResult.content and ToolResultContent.content required at the wire boundary, rejecting handler results without content with -32602 instead of silently defaulting
  • Custom handlers now receive _meta minus reserved envelope keys instead of having it deleted before params validation
  • Rescope specTypeSchemas to the neutral model with result validators no longer accepting resultType and task message-type validators removed from public set
  • Remove task vocabulary from role aggregate types and schemas while keeping deprecated Task* types importable
  • Make era-mismatched spec methods fail physically with -32601 for inbound era-deleted methods and SdkErrorCode.MethodNotSupportedByProtocolVersion for outbound sends
Fixed 1
  • Return JSON-RPC error code -32602 for resources/read with unknown URI on every protocol revision with error.data.uri echoing the requested URI
Removed 1
  • Remove duck-typed .node(req, res, parsedBody?) face from createMcpHandler
Deprecated 2
  • Mark task wire vocabulary as deprecated and exclude from typed method maps
  • Keep ProtocolErrorCode.ResourceNotFound (-32002) importable as receive-tolerated vocabulary
Major Changes
  • #2286 1823aae Thanks @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"):

    • resultType is no longer modeled by any neutral wire schema: EmptyResultSchema (strict) now rejects {resultType} bodies; on 2025-era connections a foreign resultType is stripped before validation instead of rejected; the member exists only inside the 2026-era codec, which requires it.
    • CallToolResult.content / ToolResultContent.content are required at the wire boundary (content.default([]) removed): handler results without content are rejected with -32602 instead of silently defaulted, and content-less wire results fail the client parse loudly.
    • Custom (3-arg) handlers now receive _meta minus the reserved envelope keys instead of having it deleted before params validation.
    • specTypeSchemas re-scoped to the neutral model: result validators no longer accept resultType; task message-type validators and RequestMetaEnvelope left the public set (SpecTypeName narrowed).
    • Role aggregate types/schemas (ClientRequest, ServerResult, …) no longer carry task vocabulary; the deprecated Task* types remain importable unchanged.
    • Era-mismatched spec methods fail physically: inbound era-deleted methods get -32601 even with a handler registered; outbound sends throw SdkErrorCode.MethodNotSupportedByProtocolVersion locally.
    • Value guards (isCallToolResult, …) are documented as neutral-shape consumer checks, not wire validators.
  • #2286 1823aae Thanks @felixweinberger! - createMcpHandler now returns a web-standards-only { fetch, close, notify, bus } handler — the shape Workers/Bun/Deno expect from export default. The duck-typed .node(req, res, parsedBody?) face is removed; Node frameworks (Express, Fastify, plain node:http) wrap the handler once with the new toNodeHandler(handler, { onerror? }) exported from @modelcontextprotocol/node, which converts the Node request to a web-standard Request, calls handler.fetch, and writes the Response back honoring write backpressure. The optional onerror receives the adapter-level error fallback (request conversion / handler.fetch throw) before the 500 response is written, restoring observability parity with the removed .node face. NodeIncomingMessageLike and NodeServerResponseLike move from @modelcontextprotocol/server to @modelcontextprotocol/node.

  • #2286 1823aae Thanks @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 new SdkErrorCode.UnsupportedResultType instead of masking into an empty success. The reserved _meta envelope 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 at ctx.mcpReq.envelope, ctx.mcpReq.inputResponses, and ctx.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 @deprecated and excluded from the typed method maps (RequestMethod/RequestTypeMap/ResultTypeMap/NotificationTypeMap), and callTool is typed as plain CallToolResult. See docs/migration/support-2026-07-28.md "Wire-only members hidden from public types".

  • #2286 1823aae Thanks @felixweinberger! - resources/read for an unknown URI now answers with JSON-RPC error code -32602 (Invalid Params) on every protocol revision, with error.data.uri echoing the requested URI. The 2026-07-28 specification requires -32602; the v1.x SDK already emitted -32602 on earlier revisions, so v1.x peers see no change.

    This supersedes an interim -32002 emission that shipped in earlier v2 alphas. The era-aware encode seam (WireCodec.encodeErrorCode) maps any handler-thrown -32002 to -32602 on the wire; note that a -32002 thrown without data.uri is emitted as a bare -32602 and is no longer recognizable as resource-not-found — throw ResourceNotFoundError (or include data: { uri }) to preserve the classification.

    ProtocolErrorCode.ResourceNotFound (-32002) remains importable as receive-tolerated vocabulary; clients should accept both -32602 and -32002 from peers (the specification's backwards-compatibility clause). The new typed ResourceNotFoundError class carries data.uri, and ProtocolError.fromError reconstructs it from a -32602 only when error.data is exactly { uri: string } (and nothing else), and from a legacy -32002 whenever data.uri is a string; a bare -32002 without data.uri stays a generic ProtocolError.

  • #2286 1823aae Thanks @felixweinberger! - SEP-1613 / SEP-2106 (JSON Schema 2020-12 posture): the Node default JSON Schema validator is now Ajv2020 (true draft 2020-12) instead of the draft-07 Ajv class — $defs/prefixItems/unevaluatedProperties/dependentRequired are 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 pass new AjvJsonSchemaValidator(ajv). Schemas declaring a $schema other than 2020-12 are rejected with a clear error rather than mis-validating. outputSchema may now have a non-object root and CallToolResult.structuredContent is widened to unknown (a deliberate source-level break for typed consumers — see the migration guide for the narrowing pattern). Toward 2025-era clients McpServer wraps a non-object outputSchema (and the matching structuredContent) in a {result: …} envelope so the tool stays callable, with same-document $ref/$dynamicRef pointers rewritten to keep resolving — low-level Server users (those bypassing McpServer and registering a tools/call handler directly) get the same wrap by routing the result through the new Server.projectCallToolResult(result, advertisedOutputSchema). Independently, on every era (the SEP's MUST applies regardless of client version), McpServer auto-appends a TextContent JSON serialisation when a handler returns non-object structuredContent without its own text block. The structuredContent presence check is !== undefined (not falsy) on both sides. Thanks @mattzcarey (#2249).

Minor Changes
  • #2286 1823aae Thanks @felixweinberger! - Add createRequestStateCodec({ key, ttlSeconds?, bind? }), an opt-in HMAC-SHA256 sealing helper for the multi-round-trip requestState: mint seals a JSON-serializable payload (with TTL and optional context binding) and verify drops directly into ServerOptions.requestState.verify. WebCrypto-based and runtime-neutral; verification is fail-closed and constant-time. The ServerOptions.requestState.verify hook's return type is widened to unknown | Promise<unknown> so the codec's verify is directly assignable; the seam captures the hook's resolved value (the decoded payload) and hands it to handlers via the typed ctx.mcpReq.requestState<T>() accessor.

  • #2286 1823aae Thanks @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 required ttlMs/cacheScope fields when served on that revision, defaulting to ttlMs: 0 / cacheScope: 'private'. Servers can configure the emitted values with the new ServerOptions.cacheHints option (per operation) and the new cacheHint member of the registerResource config (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 (RangeError on invalid values). Responses on 2025-era connections are unchanged and never carry these fields. Note for untyped callers: registerResource now interprets a cacheHint key 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 1823aae Thanks @felixweinberger! - Add SdkErrorCode.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 example tasks/get toward a 2026-07-28 peer). The protocol layer now resolves a per-era wire codec from the connection's negotiated protocol version (instance state on Client/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 1823aae Thanks @felixweinberger! - Revise createMcpHandler'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 new legacy: 'reject' value (the earlier alpha's default). The handler-valued legacy option (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 new isLegacyRequest(request, parsedBody?) export, which runs the entry's own classification step — it returns true only for requests with no per-request _meta envelope 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. legacyStatelessFallback remains exported as a standalone fetch-shaped handler with the same stateless serving as the default.

  • #2286 1823aae Thanks @felixweinberger! - Add createMcpHandler(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 the legacy option ('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 with toNodeHandler(handler) from @modelcontextprotocol/node), and close() tears down in-flight modern exchanges. Also exported: legacyStatelessFallback (the same stateless legacy serving as a standalone fetch-shaped handler), the PerRequestHTTPServerTransport single-exchange transport and the classifyInboundRequest classifier 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 — authInfo is pass-through and never derived from request headers.

  • #2381 f0bf785 Thanks @felixweinberger! - Serve input_required handlers on 2025-era connections: the legacy shim (on by default) converts each embedded request of an input_required return 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 collected inputResponses until a final result. Handlers are written once in the 2026 inputRequired(...) style and serve both eras; the previous loud -32603 failure remains available via ServerOptions.inputRequired.legacyShim: false. Knobs: inputRequired.maxRounds (default 8) and inputRequired.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 replaced inputResponses, byte-exact requestState echo with the verify hook running every round, paced requestState-only rounds, and elicitation accepted content passed through UNVALIDATED (the handler validates via the schema-aware acceptedContent overload, exactly as on the 2026 era). URL-mode legs synthesize the elicitationId the 2025-11-25 wire requires. Failures surface per family (tools/callisError tool 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.requestState is now a typed accessor: ctx.mcpReq.requestState<T>() returns the payload the configured requestState.verify hook resolved with (e.g. createRequestStateCodec.verify — the hook's return value is now load-bearing; verifiers that are not also decoders should resolve undefined), the raw wire string when no hook is configured, or undefined when the round carried no state. Code that read the property directly becomes a call: ctx.mcpReq.requestStatectx.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-aware acceptedContent(responses, key, schema) overload (validates untrusted accepted content against any synchronous Standard Schema) and inputResponse(responses, key) (discriminated missing | elicit | sampling | roots view, 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 1823aae Thanks @felixweinberger! - Add MissingRequiredClientCapabilityError, the typed error class for the 2026-07-28 -32021 protocol error (processing a request requires a capability the client did not declare). Its data.requiredCapabilities lists the missing capabilities and ProtocolError.fromError recognizes 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 status 400; 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 1823aae Thanks @felixweinberger! - Add the server side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). Handlers for tools/call, prompts/get, and resources/read can return the value built by inputRequired() (exported from the server package together with acceptedContent()) 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 as resultType: 'input_required', and the handler reads the responses on re-entry from ctx.mcpReq.inputResponses (with non-bare entries reported via ctx.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 -32021 error 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 via ServerOptions.inputRequired.legacyShim: false. A UrlElicitationRequiredError escaping a handler on a 2026-era request fails as an internal error with a clear steer to inputRequired.elicitUrl(...), so the -32042 error never reaches the 2026-07-28 wire; 2025-era serving keeps today's -32042 behavior exactly. The typed local error raised when push-style server-to-client request APIs are used while serving a 2026-era request now steers to inputRequired(...). Tool, prompt, and resource callback types accept the new return alongside their existing result types; 2025-era wire behavior is unchanged. An optional ServerOptions.requestState.verify hook lets a server integrity-check the echoed requestState before the handler runs — a throw answers the wire-level -32602 Invalid Params error with data.reason: 'invalid_request_state'; the SDK provides no default verification.

  • #2286 1823aae Thanks @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 gain originValidation / localhostOriginValidation middleware and a new allowedOrigins option 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 an Origin header pass — non-browser MCP clients are unaffected — while a present Origin that is not allowed or cannot be parsed (including the opaque null origin) is rejected with 403. The Node adapter ships hostHeaderValidation / originValidation request guards for plain node:http servers, which previously had no validation helpers.

  • #2286 1823aae Thanks @felixweinberger! - SEP-2243 Mcp-Param-* server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path, createMcpHandler now validates Mcp-Param-{Name} headers against the named tool's x-mcp-header declarations and the body arguments before 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 with 400 Bad Request and JSON-RPC -32020 (HeaderMismatch) — the same shape the existing standard-header cross-checks emit. A null/absent body value passes regardless of any header (the spec's "server MUST NOT expect the header" rows). McpServer.registerTool now warns at registration time when an x-mcp-header declaration violates the spec's constraints. The 2025-era serving paths and the low-level Server factory shape are unchanged.

  • #2286 1823aae Thanks @felixweinberger! - SEP-2243 standard-header server-side validation (protocol revision 2026-07-28). On the modern (2026-07-28) serving path, createMcpHandler now enforces the required Mcp-Method and Mcp-Name standard request headers in addition to the existing MCP-Protocol-Version and Mcp-Method cross-checks: a modern request without an Mcp-Method header, a tools/call / prompts/get / resources/read request without an Mcp-Name header, an Mcp-Name header carrying an invalid =?base64?…?= sentinel, and an Mcp-Name header whose (decoded) value disagrees with the body's params.name / params.uri are all rejected with 400 Bad Request and JSON-RPC -32020 (HeaderMismatch). The 2025-era serving paths are unchanged.

    New public surface:

    • @modelcontextprotocol/server: the mcpNameHeader field on InboundHttpRequest, and the 'standard-header-validation' member of InboundValidationRung (with client-capabilities / param-header-validation renumbered).
  • #2286 1823aae Thanks @felixweinberger! - Add serveStdio(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 2025 initialize handshake, 2026-07-28 per-request _meta envelope traffic, or a server/discover probe followed by either), and ONE instance from the factory is pinned to the connection and serves only that era — mirroring how createMcpHandler classifies 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. A transport option accepts a bring-your-own StdioServerTransport (for example over a Unix domain socket); onerror reports out-of-band errors; the returned handle's close() tears the connection down.

    Removed: ServerOptions.eraSupport (introduced in an earlier 2.0 alpha, never in a stable release). A hand-constructed Server/McpServer serves only the 2025-era protocol it was written for; serving the 2026-07-28 revision always goes through a serving entry. Migrate new McpServer(info, { eraSupport: 'dual-era' }) + connect(new StdioServerTransport()) to serveStdio(() => new McpServer(info)), and eraSupport: 'modern' to serveStdio(factory, { legacy: 'reject' }).

  • #2286 1823aae Thanks @felixweinberger! - Align the 2026-07-28 protocol error codes to the spec renumber: HeaderMismatch is now -32020 (was -32001), MissingRequiredClientCapability is now -32021 (was -32003), and UnsupportedProtocolVersion is 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, the HEADER_MISMATCH_ERROR_CODE constant, and the HEADER_MISMATCH / MISSING_REQUIRED_CLIENT_CAPABILITY / UNSUPPORTED_PROTOCOL_VERSION spec-type constants now carry the renumbered values; the UnsupportedProtocolVersionError and MissingRequiredClientCapabilityError classes (and ProtocolError.fromError recognition) follow. The client probe classifier recognizes -32022 for the corrective continuation and the SEP-2243 one-refresh-on-miss retry triggers on -32020.

  • #2286 1823aae Thanks @felixweinberger! - subscriptions/listen graceful close: per spec PR #2953, a server-side graceful close (createMcpHandler / serveStdio close()) now emits the empty subscriptions/listen JSON-RPC result (the new SubscriptionsListenResult_meta carries the subscriptionId) before closing the stream, replacing the previous server-originated notifications/cancelled. On the client, McpSubscription.closed now resolves 'graceful' for this signal (added alongside 'local' and 'remote'); a stream close without a result remains 'remote' (unexpected disconnect).

  • #2286 1823aae Thanks @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 -32603 capacity guard, and teardown (HTTP stream close; one notifications/cancelled per subscription on stdio). server/discover now advertises listChanged/subscribe capability bits — the rider that suppressed them until listen was served is discharged.

    Under createMcpHandler the consumer's factory is constructed for subscriptions/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).
    • McpHttpHandler gains .notify (ServerNotifier: toolsChanged(), promptsChanged(), resourcesChanged(), resourceUpdated(uri)) and .bus (the ServerEventBus listen streams subscribe to).
    • CreateMcpHandlerOptions gains bus?: ServerEventBus (an in-process InMemoryServerEventBus is created when omitted), maxSubscriptions?: number (default 1024), and keepAliveMs?: number (default 15000).
    • ServeStdioOptions gains maxSubscriptions?: number (default 1024). On a modern-pinned connection serveStdio routes the pinned instance's existing send*ListChanged() calls onto active subscriptions; legacy connections are unchanged.
    • @modelcontextprotocol/server: SUBSCRIPTION_ID_META_KEY (const); SubscriptionFilter, SubscriptionsListenRequest, SubscriptionsListenRequestParams, SubscriptionsAcknowledgedNotification, SubscriptionsAcknowledgedNotificationParams (types).
  • #2286 1823aae Thanks @felixweinberger! - Wire server/discover (protocol revision 2026-07-28) into the typed request funnel and serve it era-aware. The request joins ClientRequestSchema/ServerResultSchema/ResultTypeMap (per-era availability stays with the wire registries: only the 2026-era registry serves it), and Client.discover() issues it as a typed request on 2026-era connections. A Server whose supportedProtocolVersions list carries a modern (2026-07-28+) revision installs the server/discover handler, advertising ONLY its modern revisions and excluding the listChanged/subscribe-class capabilities until the subscriptions/listen flow ships; servers with today's default list are unchanged and keep answering -32601. The initialize handshake 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 via initialize — 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, and mode: 'auto' falls back cleanly against current SDK servers.

Patch Changes
  • #2286 1823aae Thanks @felixweinberger! - Deprecate Server.getClientCapabilities(), Server.getClientVersion() and Server.getNegotiatedProtocolVersion() in favor of the per-request handler context: on 2026-07-28 requests the validated _meta envelope carries the client's identity (ctx.mcpReq.envelope), and instances serving that revision through createMcpHandler are backfilled per request so the accessors keep answering. Behavior on 2025-era connections is unchanged; the accessors remain functional.

  • #2394 801111e Thanks @felixweinberger! - Fix the published declaration files for consumers compiling with skipLibCheck: false: the bundled .d.mts no longer leaves a dangling URIComponent reference (ajv's published types import it from fast-uri, whose export-assigned namespace the dts bundler cannot link — the type is now inlined via a dts-only path mapping), and no longer imports json-schema-typed from an undeclared dependency (it is inlined via dts.resolve). @modelcontextprotocol/node and @modelcontextprotocol/server drop stale typesVersions entries pointing at subpaths that never shipped. Package READMEs note that TypeScript >=6.0 requires "types": ["node"] since the published declarations reference Buffer.

  • #2390 6cc7b1c Thanks @felixweinberger! - isLegacyRequest docs: 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. parsedBody is an optional perf escape for a body you already hold parsed (and the way in for an already-consumed stream, e.g. behind express.json()), not a required companion. Documentation only; no behavior change.

  • #2286 1823aae Thanks @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-Version or Mcp-Method disagreeing 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 _meta envelope (or its required protocol-version key) is rejected with -32602 invalid params naming the missing key(s). Both keep HTTP 400. These cells previously emitted a provisional -32004 while the upstream error-code discussion was open. The envelope-less rejection on a modern-only endpoint (-32022 with the supported-versions list), the 2025-era serving paths, and the client-side probe handling are unchanged.

  • #2286 1823aae Thanks @felixweinberger! - ctx.mcpReq.log() now emits its notifications/message notification request-related (like progress and ctx.mcpReq.notify), so handler-emitted log messages are delivered when the server is hosted per request via createMcpHandler instead of being silently dropped. On a 2026-07-28 request the level filter consults the per-request _meta io.modelcontextprotocol/logLevel key (the modern equivalent of logging/setLevel); per the spec, an absent key means no notifications/message is 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 after closeSSE() while the client has not yet reconnected) is handled by the store-first persistence behavior in WebStandardStreamableHTTPServerTransport.send(). The session-scoped Server.sendLoggingMessage() API is unchanged.

  • #2286 1823aae Thanks @felixweinberger! - WebStandardStreamableHTTPServerTransport: request-related events (progress, ctx.mcpReq.notify, handler-emitted log) and the final response are now persisted to the configured eventStore whenever 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 the closeSSE() poll-and-replay drop (events emitted after closeSSE() were previously silently lost) and aligns with the 2025-11-25 specification ("disconnection SHOULD NOT be interpreted as the client cancelling its request"). When an eventStore is 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"; a Last-Event-ID reconnect after the request has been retired replays the stored response and then closes the resumed stream. When no eventStore is configured, the same condition is surfaced via onerror (the response is undeliverable) and the request id is retired.

  • #2286 1823aae Thanks @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/complete is no longer part of the 2026-07-28 wire registry (the draft removed the notification together with elicitationId on URL-mode elicitation). On connections negotiated at 2026-07-28, sending it — including via Server.createElicitationCompletionNotifier() — now fails locally with SdkErrorCode.MethodNotSupportedByProtocolVersion, and inbound copies are dropped as unknown notifications. Both remain fully supported on 2025-11-25.

    • notifications/cancelled on 2026-era connections now parses with a revision-exact schema that requires requestId (the draft made it required); the notification _meta shape types the io.modelcontextprotocol/subscriptionId key. 2025-era parsing is unchanged.

    • The error code -32001 emitted 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 1823aae Thanks @felixweinberger! - Internal: regenerate the 2026-07-28 spec reference types from the latest draft schema (DiscoverResult now extends CacheableResult; ElicitationCompleteNotificationParams extracted 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 1823aae Thanks @felixweinberger! - Freeze the per-era wire schemas as self-contained copies decoupled from the public types layer, and convert WireCodec to a function-only interface. Two small spec-conformance fixes ride along with the otherwise-pure refactor:

    • The 2026 wire-true resultType member now defaults to 'complete' when absent (the spec's receiver-side back-compat rule); the inbound decodeResult step continues to require it. The server/discover result accepts absent or malformed ttlMs/cacheScope (falling back to 0/'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 hasTools discriminant now keys on tools || toolChoice (previously tools only), aligning the client and server selection of the with-tools result variant with clientCapabilityRequirements.

2.0.0-alpha.1

Pre-release
Major Changes
  • #1389 108f2f3 Thanks @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 of CallToolResult with isError: true. Callers who checked result.isError for 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 462c3fc Thanks @KKonstantinov! - refactor: extract task orchestration from Protocol into TaskManager

    Breaking changes:

    • taskStore, taskMessageQueue, defaultTaskPollInterval, and maxTaskQueueSize moved from ProtocolOptions to capabilities.tasks on ClientOptions/ServerOptions
  • #1689 0784be1 Thanks @felixweinberger! - Support Standard Schema for tool and prompt schemas

    Tool and prompt registration now accepts any schema library that implements the Standard Schema spec: Zod v4, Valibot, ArkType, and others. RegisteredTool.inputSchema, RegisteredTool.outputSchema, and RegisteredPrompt.argsSchema now use StandardSchemaWithJSON (requires both ~standard.validate and ~standard.jsonSchema) instead of the Zod-specific AnySchema type.

    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 fromJsonSchema adapter:

    import { fromJsonSchema, AjvJsonSchemaValidator } from '@modelcontextprotocol/core';
    
    server.registerTool(
        'greet',
        {
            inputSchema: fromJsonSchema({ type: 'object', properties: { name: { type: 'string' } } }, new AjvJsonSchemaValidator())
        },
        handler
    );
    

    Breaking changes:

    • experimental.tasks.getTaskResult() no longer accepts a resultSchema parameter. Returns GetTaskPayloadResult (a loose Result); cast to the expected type at the call site.
    • Removed unused exports from @modelcontextprotocol/core: SchemaInput, schemaToJson, parseSchemaAsync, getSchemaShape, getSchemaDescription, isOptionalSchema, unwrapOptionalSchema. Use the new standardSchemaToJsonSchema and validateStandardSchema instead.
    • completable() remains Zod-specific (it relies on Zod's .shape introspection).
Patch Changes
  • #1758 e86b183 Thanks @KKonstantinov! - tasks - disallow requesting a null TTL

  • #1363 0a75810 Thanks @DevJanderson! - Fix ReDoS vulnerability in UriTemplate regex patterns (CVE-2026-0621)

  • #1372 3466a9e Thanks @mattzcarey! - missing change for fix(client): replace body.cancel() with text() to prevent hanging

  • #1824 fcde488 Thanks @felixweinberger! - Drop zod from peerDependencies (kept as direct dependency)

    Since Standard Schema support landed, zod is purely an internal runtime dependency used for protocol message parsing. User-facing schemas (registerTool, registerPrompt) accept any Standard Schema library. zod remains in dependencies and auto-installs; users no longer need to install it alongside the SDK.

  • #1761 01954e6 Thanks @felixweinberger! - Convert remaining capability-assertion throws to SdkError(SdkErrorCode.CapabilityNotSupported, ...). Follow-up to #1454 which missed Client.assertCapability(), the task capability helpers in experimental/tasks/helpers.ts, and the sampling/elicitation capability checks in experimental/tasks/server.ts.

  • #1433 78bae74 Thanks @codewithkenzo! - Fix transport errors being silently swallowed by adding missing onerror callback invocations before all createJsonErrorResponse calls in WebStandardStreamableHTTPServerTransport. This ensures errors like parse failures, invalid headers, and session validation errors are properly reported via the onerror callback.

  • #1660 689148d Thanks @rechedev9! - fix(server): propagate negotiated protocol version to transport in _oninitialize

  • #1568 f1ade75 Thanks @stakeswky! - Handle stdout errors (e.g. EPIPE) in StdioServerTransport gracefully instead of crashing. When the client disconnects abruptly, the transport now catches the stdout error, surfaces it via onerror, and closes.

  • #1419 dcf708d Thanks @KKonstantinov! - remove deprecated .tool, .prompt, .resource method signatures

  • #1388 f66a55b Thanks @mattzcarey! - reverting application/json in notifications

  • #1534 69a0626 Thanks @josefaidt! - remove npm references, use pnpm

  • #1534 69a0626 Thanks @josefaidt! - clean up package manager usage, all pnpm

  • #1419 dcf708d Thanks @KKonstantinov! - deprecated .tool, .prompt, .resource method removal

  • #1279 71ae3ac Thanks @KKonstantinov! - Initial 2.0.0-alpha.0 client and server package

Discussion