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 (the
inputRequired()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 the
withInputRequired()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-32002
to-32602on the wire; note that a-32002thrown withoutdata.uriis emitted as
a bare-32602and is no longer recognizable as resource-not-found — throw
ResourceNotFoundError(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 typedResourceNotFoundError
class carriesdata.uri, andProtocolError.fromErrorreconstructs it from a-32602
only whenerror.datais exactly{ uri: string }(and nothing else), and from a
legacy-32002wheneverdata.uriis a string; a bare-32002withoutdata.uri
stays 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