Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
83 KiB
@modelcontextprotocol/client
2.0.0-beta.4
Minor Changes
-
#2468
5db6e38Thanks @felixweinberger! - The response cache now stores results as JSON-serialized documents (serialize on write, parse on read) instead of live object graphs isolated withstructuredClone. Same mutation isolation, but no dependency on thestructuredCloneglobal — whose absence (jest+jsdom, Node < 17) previously made every cache write throw into the store-error swallow, silently disabling caching and output-schema lookups for the session. A value without a JSON representation now fails the write loudly to the error sink, and an undecodable document in an external store is reported, dropped, and read as a miss.Migration for custom
ResponseCacheStoreimplementations:CacheEntry.value(and theset()entry value) is nowstring— persist and return it verbatim,JSON.parseto inspect. Entries persisted by a previous SDK version fail decode once (reported, dropped) and are rewritten on the next fetch. -
#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
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. -
#2469
9b41b56Thanks @felixweinberger! - The Streamable HTTP client transport no longer attaches a session ID to a POST containing aninitializerequest — a new session starts "without a session ID attached" (2025-11-25 transports §Session Management) — and it only captures themcp-session-idresponse header from a successful initialize response, since the spec assigns the session ID "at initialization time … on the HTTP response containing the InitializeResult". Previously the transport stored the header from any response, so a legacy server answering a protocol-version probe with an error that happened to carry a session ID would poison the fallback initialize, which then went out with a session ID it should not have had. A stale session ID from a previous connection is likewise no longer leaked onto the initialize handshake, and a successful initialize response that carries no session ID now clears any stale ID the transport was holding — clients include only an ID "returned by the server during initialization", so an ID the server never returned this session is outside the session model. Ignoringmcp-session-idheaders mid-session is the complement of the spec's one actual rotation mechanism: a server that wants a new session terminates the old one (it "MAY terminate the session at any time") and answers 404, after which the client "MUST start a new session by sending a new InitializeRequest without a session ID attached". Rotation exists as session replacement via 404 + re-initialize, never as a header swap on a live session, so a server that rotates per the spec's own flow is handled correctly by this transport. -
#2455
cc70c5eThanks @felixweinberger! - Version negotiation no longer discards transport handlers the caller set beforeconnect(). The probe window now saves any pre-setonmessage/onerror/onclose, forwards error and close events to them while the probe is in flight, and restores them when the window closes — soProtocol.connect()chains them exactly as it does on a plain connect. Previously, connecting withversionNegotiationsilently cleared pre-set handlers (e.g. anonerrorused to detect session-expiry auth failures), leaving them permanently detached for the life of the connection. -
#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.
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.
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! -Client.listTools()/listPrompts()/listResources()/listResourceTemplates()now auto-aggregate every page when called without acursorand return the complete result withnextCursor: undefined(matching the C#, Java, and mcp.d SDKs). Pass an explicit{ cursor }string to fetch a single page; the per-page path is unchanged. Existing manual pagination loops keep working — the first iteration returns everything and the loop exits — but can be deleted. The aggregated result is written to the new pluggableResponseCacheStore(default: a fresh per-instanceInMemoryResponseCacheStore); aClientResponseCachecollaborator owns the eviction-generation guard and the derivedtools/listindex thatcallTool's output validation and SEP-2243Mcp-Param-*mirroring read. New exports:ResponseCacheStore,CacheKey,CacheEntry,CacheScope,MaybePromise,InMemoryResponseCacheStore; newClientOptions.responseCacheStore/ClientOptions.listMaxPages(caps the auto-aggregate walk at 64 pages by default; throwsSdkErrorwithSdkErrorCode.ListPaginationExceededon overrun so a partial aggregate is never cached). The store interface is async-ready (MaybePromise<…>); the in-memory default stays synchronous. Entries are automatically scoped by the connected server's identity and (when set) the consumer-suppliedcachePartition, so a shared store does not collide across servers or principals; evictions are likewise scoped to the connected server's partitions.Behavior change (every era): output-schema validator compilation is now lazy — validators are compiled on the first
callTool()against the cachedtools/listentry, not eagerly insidelistTools().listTools()no longer throws on an uncompilableoutputSchema(every tool stays listed; the compile failure is captured per-tool); callingcallTool()on the affected tool throwsProtocolError(InvalidParams, "Tool 'X' has an invalid outputSchema: …")before the request is sent — output-schema validation is never silently skipped. A pluggablejsonSchemaValidatorprovider therefore observes compilation atcallTooltime, notlistToolstime. The legacy-eralistTools()path is unchanged at the wire level but is observably different at the validator-lifecycle level. -
#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! - 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! - 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! - Addconnect(transport, { prior: DiscoverResult })for zero-round-trip reconnect (the gateway / distributed-client pattern). Supplying a previously-obtainedDiscoverResultskips theserver/discoverprobe: on a 2026-era serverconnect()sends nothing on the wire andcallTool()etc. work immediately. Pair with the newclient.getDiscoverResult()(populated by the'auto'-mode probe, byclient.discover(), and byconnect({ prior })itself) — the value round-trips throughJSON.stringify, so a gateway can probe once, persist the blob, and feed it to every worker. Only reuse a persistedDiscoverResultacross clients that present the same authorization context as the client that obtained it. -
#2286
1823aaeThanks @felixweinberger! - Add opt-in protocol version negotiation onClientOptions.versionNegotiation. The default is unchanged: without the option (or withmode: 'legacy') the client performs today's 2025 connect sequence byte-identically.mode: 'auto'probes the server withserver/discoverat connect time and conservatively falls back to the plain legacyinitializehandshake on the same connection unless the outcome is definitive modern evidence (with a supported-versions list that has no 2025-era entry there is nothing to fall back to, and connect rejects with a typed error instead); a network outage rejects with a typed connect error, and a probe timeout is transport-aware — on stdio it indicates a legacy server and falls back toinitializeon the same stream, on HTTP it rejects with a typed timeout error.mode: { pin: '<version>' }negotiates exactly the pinned modern revision with no fallback. Probe policy lives underprobe: { timeoutMs? }— the probe inherits the standard request timeout. The probe'sMCP-Protocol-Version/Mcp-Methodheaders derive from the probe message body; the transport version slot is never touched during negotiation, so legacy-era traffic carries zero 2026 headers by construction. Adds theSdkErrorCode.EraNegotiationFailedcode for negotiation-phase connect failures. -
#2286
1823aaeThanks @felixweinberger! - Dynamic Client Registration hygiene for the 2026-07-28 authorization requirements (SEP-837, SEP-2207). NewresolveClientMetadata(provider)readsprovider.clientMetadataand applies the spec defaults —application_typederived from the redirect URIs (loopback or custom scheme →'native', otherwise'web'),grant_types: ['authorization_code', 'refresh_token']when omitted — andauth()feeds the resolved document to DCR only (scope selection still reads the raw consumer-suppliedclientMetadataso statically-registered/CIMD clients are not pushed intooffline_access+prompt=consent); consumer-set values are never overwritten. DCR rejection now throws the newRegistrationRejectedErrorcarrying the HTTP status, raw body, and submitted metadata — breaking for directregisterClient()callers: rejection no longer throwsOAuthError, so updateinstanceofchecks.OAuthClientMetadatagains a typedapplication_type?: stringfield (expected'native'/'web'; tolerant on parse).OAuthErrorCodeaddsInvalidRedirectUri. The token-exchange, refresh, and Cross-App Access (requestJwtAuthorizationGrant/exchangeJwtAuthGrant) paths now throw the newInsecureTokenEndpointErrorfor a non-https:token endpoint (localhost/127.0.0.1/::1exempt), andauth()surfaces it on the refresh branch instead of silently re-authorizing. -
#2286
1823aaeThanks @felixweinberger! - SEP-2468 follow-up:transport.finishAuth()gains aURLSearchParamsoverload (preferred) that extractscode/iss, validatesissfirst, and on mismatch throws a sanitizedIssuerMismatchError(no callbackerror_descriptiontext); callers remain responsible forstate. Behavior change for@modelcontextprotocol/server-legacy:mcpAuthRouternow advertisesauthorization_response_iss_parameter_supported(defaulttrue;ProxyOAuthServerProviderreportsfalse) and the bundled authorize handler appendsiss(RFC 9207) to everyres.redirect(...)yourOAuthServerProvider.authorize()issues to the client'sredirect_uri. If your provider redirects another way (res.writeHead, a separate consent-page response, or a standaloneauthorizationHandler({provider})withoutissuerUrl), appendparams.issuerasissyourself or setauthorizationResponseIssParameterSupported: false— otherwise RFC 9207-compliant clients (including this SDK) will reject the callback. -
#2286
1823aaeThanks @felixweinberger! - Implement RFC 9207 / RFC 8414 §3.3 OAuth issuer validation (SEP-2468).discoverAuthorizationServerMetadata()now rejects metadata whoseissuerdoes not match the discovery URL (opt out viaskipIssuerValidation/AuthOptions.skipIssuerMetadataValidation— security-weakening).auth(),exchangeAuthorization(),fetchToken(), andtransport.finishAuth(code, iss?)now validate the authorization-callbackissagainst the recorded issuer before redeeming the code; newIssuerMismatchErrorandvalidateAuthorizationResponseIssuer()are exported. -
#2286
1823aaeThanks @felixweinberger! - Per-authorization-server credential isolation (SEP-2352).auth()now stamps anissuerfield onto every value it passes tosaveTokens()/saveClientInformation()and threads{ issuer }totokens()/clientInformation(); on read, a stored credential whose stamp names a different authorization server is treated asundefined, so aclient_id/refresh_tokenissued by one AS is never sent to another. Providers that round-trip stored values verbatim are protected with no code change; multi-AS providers may key storage onctx.issuer. NewAuthorizationServerMismatchError(callback-leg gate).OAuthClientProvider.saveAuthorizationServerUrl()/authorizationServerUrl()are deprecated (still written, never read).ClientCredentialsProvider,PrivateKeyJwtProvider,StaticPrivateKeyJwtProvider, andCrossAppAccessProvidergainexpectedIssuerand no longer definesaveClientInformation(). -
#2286
1823aaeThanks @felixweinberger! - Add the public surface for the 2026-07-28 authorization requirements. NewAuthOptionstype names theauth()options object and addsissandskipIssuerMetadataValidationfields.OAuthClientProvider.clientInformation()/.saveClientInformation()/.tokens()/.saveTokens()accept an optionalOAuthClientInformationContextcarrying the authorization server'sissuerso providers can key persisted credentials per authorization server. NewStoredOAuthTokens/StoredOAuthClientInformationaliases add anissuerstamp field on top of the wire types (kept off the wire schemas so an authorization server cannot populate it) and become the parameter/return types of the credential methods. NewOAuthClientFlowErrorbase class inauthErrors.tsfor the flow-specific error classes that follow. All changes are additive — existingOAuthClientProviderimplementations compile unchanged; the new fields are inert until the behavior changes that follow wire them up. -
#2286
1823aaeThanks @felixweinberger! -Clientnow honours the server-stamped SEP-2549ttlMs/cacheScopecache hints on the cacheable verbs (listTools(),listPrompts(),listResources(),listResourceTemplates(),readResource()): a still-fresh held entry is served without a round trip. NewCacheableRequestOptions.cacheMode('use'— the default;'refresh'— always fetch and re-store;'bypass'— fetch without consulting or writing the cache) gives per-call control. The behaviour is opt-in by hint: a server that sendsttlMs: 0(the conservative default this SDK's server stamps) sees byte-identical behaviour — every call fetches.Entries are automatically scoped by connected-server identity (derived from
serverInfoafter connect, encoded collision-free viaJSON.stringify);ClientOptions.cachePartitionis the opaque per-principal slot for'private'-scoped entries — set it to your principal identifier (e.g. the auth subject) when oneresponseCacheStorebacks several principals. With the default''every entry lives at the connected server's shared partition (the safe single-tenant posture).ClientOptions.defaultCacheTtlMs(default0) supplies the TTL when a result lacks one (e.g. a legacy-era response); the server-suppliedttlMsis clamped at 24 h (MAX_CACHE_TTL_MS). The list verbs always store the aggregate (socallTool's mirroring/output-validation index keeps working at any TTL);readResourcestores only when the resolved TTL is positive.notifications/resources/updatedevicts the cachedresources/readbody for that URI.ResponseCacheStoregaineddelete(key);InMemoryResponseCacheStoreis now bounded ({ maxEntries }, default 512, oldest-first eviction). New exports:CacheMode,CacheableRequestOptions,InMemoryResponseCacheStoreOptions,MAX_CACHE_TTL_MS. -
#2286
1823aaeThanks @felixweinberger! - Client request cancellation on a 2026-07-28 Streamable HTTP connection now closes that request's SSE response stream — the spec cancellation signal — instead of POSTingnotifications/cancelled. Cancellation on a 2025-era connection, and on stdio at any era, still sendsnotifications/cancelledas before. Adds the optionalTransport.hasPerRequestStreamcapability flag (set onStreamableHTTPClientTransport) for the protocol layer to route the per-transport cancel path. -
#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! - Per-request_metaenvelope auto-emission on modern-era connections: once a client negotiates a 2026-07-28+ protocol revision (viaversionNegotiation: { mode: 'auto' }or{ pin }), it automatically attaches the reserved protocol-version / client-info / client-capabilities_metakeys to every outgoing request and notification — you no longer set the envelope by hand. User-supplied_metakeys take precedence over the auto-attached ones; the auto-attached client-capabilities reflect what the client actually registered. Legacy-era connections (the default, and the'auto'-mode fallback) never gain these keys, so 2025-era outbound traffic is byte-identical to before.Adds
Client.getProtocolEra()('legacy' | 'modern' | undefined), theProtocolEratype,Client.setVersionNegotiation()for configuring negotiation pre-connect on an already-constructed instance, and theprobe.maxRetriesknob (default0) which governs probe-timeout re-sends only — the spec-mandated-32022corrective continuation is never counted against it. TheversionNegotiationdefault remains'legacy': absent (ormode: 'legacy'),connect()runs the plain 2025 sequence, byte-identical to a v1.x client. -
#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 client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutralInputRequest/InputResponse/InputRequests/InputResponses/InputRequiredResulttypes and theisInputRequiredResult()guard ship as the neutral surface (theinputRequired()builder family and theacceptedContent()reader are exported by the server package as part of the server-side change); the 2026-07-28 wire codec models the in-band vocabulary (embedded requests and bare responses) and the retry-channel request fields. On the client, aninput_requiredanswer totools/call,prompts/get, orresources/readon a 2026-07-28 connection is now fulfilled automatically by default: the embedded requests are dispatched to the client's already-registered elicitation/sampling/roots handlers, and the original call is retried with the collectedinputResponses, a byte-exact echo of the opaquerequestState, and a fresh request id, up toinputRequired.maxRoundsrounds (default 10; exhaustion raises a typedInputRequiredRoundsExceedederror carrying the last result).client.callTool()and its siblings keep returning their plain result types.ClientOptions.inputRequired(autoFulfill,maxRounds) configures the driver; manual mode isautoFulfill: falseplus the per-callallowInputRequired: truerequest option and thewithInputRequired()schema wrapper. Retried requests surface theirinputResponsesto server handlers as bare response objects — entries in a wrapped{method, result}shape are dropped and reported viactx.mcpReq.droppedInputResponseKeys. 2025-era behavior is unchanged: the legacy wire has noinput_requiredvocabulary and the legacy server-to-client request flow is untouched. -
#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-2243Mcp-Param-*client-side mirroring (protocol revision 2026-07-28). On a 2026-07-28 connection over Streamable HTTP,Client.callTool()now mirrors tool arguments designated withx-mcp-headerin the tool'sinputSchemaintoMcp-Param-{Name}HTTP headers (with the spec's=?base64?…?=sentinel encoding for values that are not safe plain-ASCII field values), and on a non-stdio modern connectionClient.listTools()(and the client's internaltools/listcache) exclude tool definitions whosex-mcp-headerdeclarations violate the spec's constraints, logging a warning naming the tool and the reason. The legacy-eracallToolandlistToolspaths are unchanged at the wire level. Browser environments skip mirroring (dynamically named headers cannot be statically allow-listed for credentialed CORS); a conforming SEP-2243 server will reject atools/callwhose body carries a non-null value for anx-mcp-headerparameter when the matching header is absent, so calling such a tool with that argument from a browser is a known limitation. NewCallToolRequestOptions.toolDefinitionlets callers supply the tool definition directly so mirroring and output-schema validation can run without a priortools/list.TransportSendOptions.headersis added (additive, optional) for per-request HTTP headers; the Streamable HTTP transport skips reserved standard/auth header names (authorization,mcp-protocol-version,mcp-method,mcp-name,mcp-session-id,content-type); transports that share a single channel (stdio, in-memory) ignore it.The Streamable HTTP transport now emits the
Mcp-Namestandard header on every modern-enveloped request (params.namefortools/call/prompts/get,params.uriforresources/read), sentinel-encoded.Behavior change (modern era only): on a modern-enveloped request the Streamable HTTP transport now surfaces an HTTP
400whose body is a well-formed JSON-RPC error response addressed to the pending request id in-band as aProtocolError(instead ofSdkHttpError), so theHEADER_MISMATCHrecovery retry can fire. Legacy-era exchanges are unchanged. -
#2286
1823aaeThanks @felixweinberger! - SEP-2350 scope step-up: on403 insufficient_scope,StreamableHTTPClientTransportnow re-authorizes with the union of the previously-requested and challenged scopes (computeScopeUnion), bypassing the refresh-token branch when the union is a strict superset of the current token's granted scope (isStrictScopeSuperset,AuthOptions.forceReauthorization). NewonInsufficientScope: 'reauthorize' | 'throw'(default'reauthorize') andmaxStepUpRetries(default 1) onStreamableHTTPClientTransportOptions;'throw'raises the newInsufficientScopeError. The GET listen-stream open path now applies the same step-up handling. The previous verbatim-header retry guard is replaced by the bounded per-send counter. -
#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! -Client.listen(filter)opens asubscriptions/listenstream on a 2026-07-28-era connection, resolving once the server's acknowledged notification arrives with anMcpSubscription { honoredFilter, close(), closed }.closedis aPromise<'local' | 'remote'>that resolves exactly once when the subscription terminates ('local'= you calledclose();'remote'= the server cancelled, the stream ended, or the transport dropped — re-listen if you still want events) and never rejects. Change notifications delivered on the stream dispatch to the existingsetNotificationHandlerregistrations — the same handlers the 2025-era unsolicited notifications fire on a legacy connection — solisten()is era-transparent for consumers that already register those.close()aborts the listen request's stream (where the transport supports it) and sendsnotifications/cancelledreferencing the listen id — both, on every transport; no automatic re-listen. On a 2025-era connectionlisten()throws a typedMethodNotSupportedByProtocolVersionsteering toresources/subscribeandClientOptions.listChanged.ClientOptions.listChangednow auto-opens a listen stream on a modern connection — the filter is the intersection of the configured sub-options and the server-advertisedlistChangedcapabilities; auto-open is skipped (client.autoOpenedSubscriptionstaysundefined) when that intersection is empty; otherwise the auto-opened subscription is exposed atclient.autoOpenedSubscription.TransportSendOptionsgainsrequestSignal(per-request abort) andonRequestStreamEnd(fires when a per-request response stream ends or errors for any non-deliberate reason) on the Streamable HTTP transport. -
#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! - 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! - Drop inbound JSON-RPC requests on connections that negotiated the 2026-07-28 draft revision instead of answering them: the modern era has no server→client request channel (server-initiated interactions are carried ininput_requiredresults), and the stdio transport forbids the client from writing JSON-RPC responses. Dropped requests are surfaced viaonerror. Legacy-era connections, responses, and notifications are unchanged. -
#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. -
#2286
1823aaeThanks @felixweinberger! - Complete the SEP-2577@deprecatedsweep on the public type surface (SEP-2596 Tier-1 obligation). Marks the full Logging type stack (LoggingLevel,SetLevelRequest,LoggingMessageNotificationand params), the full Sampling type stack (CreateMessageRequest/Result,SamplingMessage,ModelPreferences/ModelHint,ToolChoice,ToolUseContent/ToolResultContent, and theincludeContextenum values), the full Roots type stack (Root,ListRootsRequest/Result,RootsListChangedNotification), andregisterClient(Dynamic Client Registration; prefer Client ID Metadata Documents per SEP-991). Mirrors the markers already present on the per-revision reference types. JSDoc only — wire behavior is unchanged; everything remains fully functional during the deprecation window (at least twelve months). -
#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
-
#2049
4f226c1Thanks @KKonstantinov! - AddSdkHttpErrorsubclass with typed.status/.statusTextaccessors for HTTP transport failures.StreamableHTTPClientTransportnow throwsSdkHttpError(which extendsSdkError) for non-OK HTTP responses;SSEClientTransportthrowsSdkHttpErrorfor 401-after-reauth (circuit breaker). -
#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. -
#1653
6bec24aThanks @rechedev9! - AddvalidateClientMetadataUrl()utility for early validation ofclientMetadataUrlExports a
validateClientMetadataUrl()function thatOAuthClientProviderimplementations can call in their constructors to fail fast on invalid URL-based client IDs, instead of discovering the error deep in the auth flow. -
#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
-
#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. -
#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. -
#1655
1eb3123Thanks @nielskaspers! - fix(client): append custom Accept headers to spec-required defaults in StreamableHTTPClientTransportCustom Accept headers provided via
requestInit.headersare now appended to the spec-mandated Accept types instead of being overwritten. This ensures the required media types (application/json, text/event-streamfor POST;text/event-streamfor GET SSE) are always present while allowing users to include additional types for proxy/gateway routing. -
#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
- #1783
045c62aThanks @felixweinberger! - RemoveWebSocketClientTransport. WebSocket is not a spec-defined transport; use stdio or Streamable HTTP. TheTransportinterface remains exported for custom implementations. See #142.
Minor Changes
-
#1527
dc896e1Thanks @felixweinberger! - AdddiscoverOAuthServerInfo()function and unified discovery state caching for OAuth- New
discoverOAuthServerInfo(serverUrl)export that performs RFC 9728 protected resource metadata discovery followed by authorization server metadata discovery in a single call. Use this for operations like token refresh and revocation that need the authorization server URL outside ofauth(). - New
OAuthDiscoveryStatetype and optionalOAuthClientProvidermethodssaveDiscoveryState()/discoveryState()allow providers to persist all discovery results (auth server URL, resource metadata URL, resource metadata, auth server metadata) across sessions. This avoids redundant discovery requests and handles browser redirect scenarios where discovery state would otherwise be lost. - New
'discovery'scope forinvalidateCredentials()to clear cached discovery state. - New
OAuthServerInfotype exported for the return value ofdiscoverOAuthServerInfo().
- New
-
#1673
462c3fcThanks @KKonstantinov! - refactor: extract task orchestration from Protocol into TaskManagerBreaking changes:
taskStore,taskMessageQueue,defaultTaskPollInterval, andmaxTaskQueueSizemoved fromProtocolOptionstocapabilities.tasksonClientOptions/ServerOptions
-
#1763
6711ed9Thanks @felixweinberger! - AddreconnectionScheduleroption toStreamableHTTPClientTransport. Lets non-persistent environments (serverless, mobile, desktop sleep/wake) override the defaultsetTimeout-based SSE reconnection scheduling. The scheduler may return a cancel function that is invoked ontransport.close(). -
#1443
4aec5f7Thanks @NSeydoux! - The client credentials providers now support scopes being added to the token request. -
#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).
-
#1710
e563e63Thanks @felixweinberger! - AddAuthProviderfor composable bearer-token auth; transports adaptOAuthClientProviderautomatically- New
AuthProviderinterface:{ token(): Promise<string | undefined>; onUnauthorized?(ctx): Promise<void> }. Transports calltoken()before every request andonUnauthorized()on 401 (then retry once). - Transport
authProvideroption now acceptsAuthProvider | OAuthClientProvider. OAuth providers are adapted internally viaadaptOAuthProvider()— no changes needed to existingOAuthClientProviderimplementations. - For simple bearer tokens (API keys, gateway-managed tokens, service accounts):
{ authProvider: { token: async () => myKey } }— one-line object literal, no class. - New
adaptOAuthProvider(provider)export for explicit adaptation. - New
handleOAuthUnauthorized(provider, ctx)helper — the standard OAuthonUnauthorizedbehavior. - New
isOAuthClientProvider()type guard. - New
UnauthorizedContexttype. - Exported previously-internal auth helpers for building custom flows:
applyBasicAuth,applyPostAuth,applyPublicAuth,executeTokenRequest.
Transports are simplified internally — ~50 lines of inline OAuth orchestration (auth() calls, WWW-Authenticate parsing, circuit-breaker state) moved into the adapter's
onUnauthorized()implementation.OAuthClientProvideritself is unchanged. - New
-
#1614
1a78b01Thanks @pcarleton! - Apply resolved scope consistently to both DCR and the authorization URL (SEP-835)When
scopes_supportedis present in the protected resource metadata (/.well-known/oauth-protected-resource), the SDK already uses it as the default scope for the authorization URL. This change applies the same resolved scope to the dynamic client registration request body, ensuring both use a consistent value.registerClient()now accepts an optionalscopeparameter that overridesclientMetadata.scopein the registration body.auth()now computes the resolved scope once (WWW-Authenticate → PRMscopes_supported→clientMetadata.scope) and passes it to both DCR and the authorization request.
Patch Changes
-
#1758
e86b183Thanks @KKonstantinov! - tasks - disallow requesting a null TTL -
#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. -
#1632
d99f3eeThanks @matantsach! - Continue OAuth metadata discovery on 502 (Bad Gateway) responses, matching the existing behavior for 4xx. This fixes MCP servers behind reverse proxies that return 502 for path-aware metadata URLs. Other 5xx errors still throw to avoid retrying against overloaded servers. -
#1772
5276439Thanks @felixweinberger! - Always setwindowsHidewhen spawning stdio server processes on Windows, not just in Electron environments. Prevents unwanted console windows in non-Electron Windows applications. -
#1390
9bc9abcThanks @DePasqualeOrg! - Fix StreamableHTTPClientTransport to handle error responses in SSE streams -
#1343
4b5fdcbThanks @christso! - Fix OAuth error handling for servers returning errors with HTTP 200 statusSome OAuth servers (e.g., GitHub) return error responses with HTTP 200 status instead of 4xx. The SDK now checks for an
errorfield in the JSON response before attempting to parse it as tokens, providing users with meaningful error messages. -
#1534
69a0626Thanks @josefaidt! - remove npm references, use pnpm -
#1386
00249ceThanks @PederHP! - Respect capability negotiation in list methods by returning empty lists when server lacks capabilityThe Client now returns empty lists instead of sending requests to servers that don't advertise the corresponding capability:
listPrompts()returns{ prompts: [] }if server lacks prompts capabilitylistResources()returns{ resources: [] }if server lacks resources capabilitylistResourceTemplates()returns{ resourceTemplates: [] }if server lacks resources capabilitylistTools()returns{ tools: [] }if server lacks tools capability
This respects the MCP spec requirement that "Both parties SHOULD respect capability negotiation" and avoids unnecessary server warnings and traffic. The existing
enforceStrictCapabilitiesoption continues to throw errors when set totrue. -
#1534
69a0626Thanks @josefaidt! - clean up package manager usage, all pnpm -
#1595
13a0d34Thanks @bhosmer-ant! - Don't swallow fetchTypeErroras CORS in non-browser environments. Network errors (DNS resolution failure, connection refused, invalid URL) in Node.js and Cloudflare Workers now propagate from OAuth discovery instead of being silently misattributed to CORS and returningundefined. This surfaces the real error to callers rather than masking it as "metadata not found." -
#1279
71ae3acThanks @KKonstantinov! - Initial 2.0.0-alpha.0 client and server package