What’s New

MCP TypeScript SDK 2.0.0-beta.3

2.0.0-beta.3Pre-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().

View original

Discussion