github modelcontextprotocol/typescript-sdk @modelcontextprotocol/client@2.0.0-alpha.4

Major Changes

  • #2286 1823aae Thanks @felixweinberger! - Client.listTools() / listPrompts() / listResources() / listResourceTemplates() now auto-aggregate every page when called without a cursor and return the complete result with nextCursor: 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 pluggable ResponseCacheStore (default: a fresh per-instance InMemoryResponseCacheStore); a ClientResponseCache collaborator owns the eviction-generation guard and the derived tools/list index that callTool's output validation and SEP-2243 Mcp-Param-* mirroring read. New exports: ResponseCacheStore, CacheKey, CacheEntry, CacheScope, MaybePromise, InMemoryResponseCacheStore; new ClientOptions.responseCacheStore / ClientOptions.listMaxPages (caps the auto-aggregate walk at 64 pages by default; throws SdkError with SdkErrorCode.ListPaginationExceeded on 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-supplied cachePartition, 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 cached tools/list entry, not eagerly inside listTools(). listTools() no longer throws on an uncompilable outputSchema (every tool stays listed; the compile failure is captured per-tool); calling callTool() on the affected tool throws ProtocolError(InvalidParams, "Tool 'X' has an invalid outputSchema: …") before the request is sent — output-schema validation is never silently skipped. A pluggable jsonSchemaValidator provider therefore observes compilation at callTool time, not listTools time. The legacy-era listTools() path is unchanged at the wire level but is observably different at the validator-lifecycle level.

  • #2286 1823aae Thanks @felixweinberger! - Split the wire layer into per-era codecs and make protocol-revision deletions physical. Deliberate wire/schema behavior changes (see docs/migration/support-2026-07-28.md "Per-era wire codecs"):

    • resultType is no longer modeled by any neutral wire schema: EmptyResultSchema (strict) now rejects {resultType} bodies; on 2025-era connections a foreign resultType is stripped before validation instead of rejected; the member exists only inside the 2026-era codec, which requires it.
    • CallToolResult.content / ToolResultContent.content are required at the wire boundary (content.default([]) removed): handler results without content are rejected with -32602 instead of silently defaulted, and content-less wire results fail the client parse loudly.
    • Custom (3-arg) handlers now receive _meta minus the reserved envelope keys instead of having it deleted before params validation.
    • specTypeSchemas re-scoped to the neutral model: result validators no longer accept resultType; task message-type validators and RequestMetaEnvelope left the public set (SpecTypeName narrowed).
    • Role aggregate types/schemas (ClientRequest, ServerResult, …) no longer carry task vocabulary; the deprecated Task* types remain importable unchanged.
    • Era-mismatched spec methods fail physically: inbound era-deleted methods get -32601 even with a handler registered; outbound sends throw SdkErrorCode.MethodNotSupportedByProtocolVersion locally.
    • Value guards (isCallToolResult, …) are documented as neutral-shape consumer checks, not wire validators.
  • #2286 1823aae Thanks @felixweinberger! - Hide wire-only protocol members from the public surface, at the type level and at runtime. resultType (the 2026-07-28 result discrimination field) is no longer declared on any public result type — the wire schemas keep parsing it, and the client funnel now consumes it raw-first: 'complete' results are stripped to the public shape and any other kind (e.g. input_required) rejects with the new SdkErrorCode.UnsupportedResultType instead of masking into an empty success. The reserved _meta envelope keys are lifted out of inbound requests and notifications before handlers run, and the multi-round-trip retry fields (inputResponses, requestState) out of inbound requests only (the spec reserves those names on client-initiated requests; notification params keep them), so handler params keep the 2025-era shape; for requests the lifted material surfaces at ctx.mcpReq.envelope, ctx.mcpReq.inputResponses, and ctx.mcpReq.requestState (notifications have no ctx — their lifted envelope keys are not surfaced). High-level client/server methods now return the named public result types (Promise<CallToolResult> etc.). Task wire vocabulary stays importable but is @deprecated and excluded from the typed method maps (RequestMethod/RequestTypeMap/ResultTypeMap/NotificationTypeMap), and callTool is typed as plain CallToolResult. See docs/migration/support-2026-07-28.md "Wire-only members hidden from public types".

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

Minor Changes

  • #2286 1823aae Thanks @felixweinberger! - Add connect(transport, { prior: DiscoverResult }) for zero-round-trip reconnect (the gateway / distributed-client pattern). Supplying a previously-obtained DiscoverResult skips the server/discover probe: on a 2026-era server connect() sends nothing on the wire and callTool() etc. work immediately. Pair with the new client.getDiscoverResult() (populated by the 'auto'-mode probe, by client.discover(), and by connect({ prior }) itself) — the value round-trips through JSON.stringify, so a gateway can probe once, persist the blob, and feed it to every worker. Only reuse a persisted DiscoverResult across clients that present the same authorization context as the client that obtained it.

  • #2286 1823aae Thanks @felixweinberger! - Add opt-in protocol version negotiation on ClientOptions.versionNegotiation. The default is unchanged: without the option (or with mode: 'legacy') the client performs today's 2025 connect sequence byte-identically. mode: 'auto' probes the server with server/discover at
    connect time and conservatively falls back to the plain legacy initialize handshake 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 to initialize on 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 under probe: { timeoutMs? } — the probe inherits the standard request timeout. The probe's MCP-Protocol-Version/Mcp-Method headers 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 the SdkErrorCode.EraNegotiationFailed code for negotiation-phase connect failures.

  • #2286 1823aae Thanks @felixweinberger! - Dynamic Client Registration hygiene for the 2026-07-28 authorization requirements (SEP-837, SEP-2207). New resolveClientMetadata(provider) reads provider.clientMetadata and applies the spec defaults — application_type derived from the redirect URIs (loopback or custom scheme → 'native', otherwise 'web'), grant_types: ['authorization_code', 'refresh_token'] when omitted — and auth() feeds the resolved document to DCR only (scope selection still reads the raw consumer-supplied clientMetadata so statically-registered/CIMD clients are not pushed into offline_access + prompt=consent); consumer-set values are never overwritten. DCR rejection now throws the new RegistrationRejectedError carrying the HTTP status, raw body, and submitted metadata — breaking for direct registerClient() callers: rejection no longer throws OAuthError, so update instanceof checks. OAuthClientMetadata gains a typed application_type?: string field (expected 'native' / 'web'; tolerant on parse). OAuthErrorCode adds InvalidRedirectUri. The token-exchange, refresh, and Cross-App Access (requestJwtAuthorizationGrant / exchangeJwtAuthGrant) paths now throw the new InsecureTokenEndpointError for a non-https: token endpoint (localhost / 127.0.0.1 / ::1 exempt), and auth() surfaces it on the refresh branch instead of silently re-authorizing.

  • #2286 1823aae Thanks @felixweinberger! - SEP-2468 follow-up: transport.finishAuth() gains a URLSearchParams overload (preferred) that extracts code/iss, validates iss first, and on mismatch throws a sanitized IssuerMismatchError (no callback error_description text); callers remain responsible for state. Behavior change for @modelcontextprotocol/server-legacy: mcpAuthRouter now advertises authorization_response_iss_parameter_supported (default true; ProxyOAuthServerProvider reports false) and the bundled authorize handler appends iss (RFC 9207) to every res.redirect(...) your OAuthServerProvider.authorize() issues to the client's redirect_uri. If your provider redirects another way (res.writeHead, a separate consent-page response, or a standalone authorizationHandler({provider}) without issuerUrl), append params.issuer as iss yourself or set authorizationResponseIssParameterSupported: false — otherwise RFC 9207-compliant clients (including this SDK) will reject the callback.

  • #2286 1823aae Thanks @felixweinberger! - Implement RFC 9207 / RFC 8414 §3.3 OAuth issuer validation (SEP-2468). discoverAuthorizationServerMetadata() now rejects metadata whose issuer does not match the discovery URL (opt out via skipIssuerValidation / AuthOptions.skipIssuerMetadataValidation — security-weakening). auth(), exchangeAuthorization(), fetchToken(), and transport.finishAuth(code, iss?) now validate the authorization-callback iss against the recorded issuer before redeeming the code; new IssuerMismatchError and validateAuthorizationResponseIssuer() are exported.

  • #2286 1823aae Thanks @felixweinberger! - Per-authorization-server credential isolation (SEP-2352). auth() now stamps an issuer field onto every value it passes to saveTokens() / saveClientInformation() and threads { issuer } to tokens() / clientInformation(); on read, a stored credential whose stamp names a different authorization server is treated as undefined, so a client_id / refresh_token issued 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 on ctx.issuer. New AuthorizationServerMismatchError (callback-leg gate). OAuthClientProvider.saveAuthorizationServerUrl() / authorizationServerUrl() are deprecated (still written, never read). ClientCredentialsProvider, PrivateKeyJwtProvider, StaticPrivateKeyJwtProvider, and CrossAppAccessProvider gain expectedIssuer and no longer define saveClientInformation().

  • #2286 1823aae Thanks @felixweinberger! - Add the public surface for the 2026-07-28 authorization requirements. New AuthOptions type names the auth() options object and adds iss and skipIssuerMetadataValidation fields. OAuthClientProvider.clientInformation() / .saveClientInformation() / .tokens() / .saveTokens() accept an optional OAuthClientInformationContext carrying the authorization server's issuer so providers can key persisted credentials per authorization server. New StoredOAuthTokens / StoredOAuthClientInformation aliases add an issuer stamp 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. New OAuthClientFlowError base class in authErrors.ts for the flow-specific error classes that follow. All changes are additive — existing OAuthClientProvider implementations compile unchanged; the new fields are inert until the behavior changes that follow wire them up.

  • #2286 1823aae Thanks @felixweinberger! - Client now honours the server-stamped SEP-2549 ttlMs/cacheScope cache hints on the cacheable verbs (listTools(), listPrompts(), listResources(), listResourceTemplates(), readResource()): a still-fresh held entry is served without a round trip. New CacheableRequestOptions.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 sends ttlMs: 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 serverInfo after connect, encoded collision-free via JSON.stringify); ClientOptions.cachePartition is the opaque per-principal slot for 'private'-scoped entries — set it to your principal identifier (e.g. the auth subject) when one responseCacheStore backs several principals. With the default '' every entry lives at the connected server's shared partition (the safe single-tenant posture). ClientOptions.defaultCacheTtlMs (default 0) supplies the TTL when a result lacks one (e.g. a legacy-era response); the server-supplied ttlMs is clamped at 24 h (MAX_CACHE_TTL_MS). The list verbs always store the aggregate (so callTool's mirroring/output-validation index keeps working at any TTL); readResource stores only when the resolved TTL is positive. notifications/resources/updated evicts the cached resources/read body for that URI. ResponseCacheStore gained delete(key); InMemoryResponseCacheStore is now bounded ({ maxEntries }, default 512, oldest-first eviction). New exports: CacheMode, CacheableRequestOptions, InMemoryResponseCacheStoreOptions, MAX_CACHE_TTL_MS.

  • #2286 1823aae Thanks @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 POSTing notifications/cancelled. Cancellation on a 2025-era connection, and on stdio at any era, still sends notifications/cancelled as before. Adds the optional Transport.hasPerRequestStream capability flag (set on StreamableHTTPClientTransport) for the protocol layer to route the per-transport cancel path.

  • #2286 1823aae Thanks @felixweinberger! - Add SdkErrorCode.MethodNotSupportedByProtocolVersion: a typed local error raised before anything reaches the transport when a spec method is sent toward a peer whose negotiated protocol version's wire era does not define it (for example tasks/get toward a 2026-07-28 peer). The protocol layer now resolves a per-era wire codec from the connection's negotiated protocol version (instance state on Client/Server, with the legacy era as the pre-negotiation default) and resolves per-method schemas at dispatch time instead of registration time; an edge classification on an inbound message is validated against that instance era, and a mismatch is rejected as an entry/routing error. Behavior on existing (2025-era) connections is unchanged.

  • #2286 1823aae Thanks @felixweinberger! - Per-request _meta envelope auto-emission on modern-era connections: once a client negotiates a 2026-07-28+ protocol revision (via versionNegotiation: { mode: 'auto' } or { pin }), it automatically attaches the reserved protocol-version / client-info / client-capabilities
    _meta keys to every outgoing request and notification — you no longer set the envelope by hand. User-supplied _meta keys 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), the ProtocolEra type, Client.setVersionNegotiation() for configuring negotiation pre-connect on an already-constructed instance, and the probe.maxRetries knob (default 0) which governs probe-timeout
    re-sends only — the spec-mandated -32022 corrective continuation is never counted against it. The versionNegotiation default remains 'legacy': absent (or mode: 'legacy'), connect() runs the plain 2025 sequence, byte-identical to a v1.x client.

  • #2286 1823aae Thanks @felixweinberger! - Add MissingRequiredClientCapabilityError, the typed error class for the 2026-07-28 -32021 protocol error (processing a request requires a capability the client did not declare). Its data.requiredCapabilities lists the missing capabilities and ProtocolError.fromError recognizes the code/data shape. The 2026-07-28 HTTP entry gains a pre-dispatch gate that refuses a request requiring an undeclared client capability with this error and HTTP status 400; no method served on the 2026-07-28 registry currently carries such a requirement, so observable behavior is unchanged until methods with capability requirements exist.

  • #2286 1823aae Thanks @felixweinberger! - Add the client side of multi round-trip requests (protocol revision 2026-07-28, SEP-2322). The neutral InputRequest/InputResponse/InputRequests/InputResponses/InputRequiredResult types and the isInputRequiredResult() guard ship as the neutral surface (the
    inputRequired() builder family and the acceptedContent() 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, an input_required answer to tools/call, prompts/get, or resources/read on 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 collected inputResponses, a byte-exact echo of the opaque requestState, and a fresh request id, up to inputRequired.maxRounds rounds (default 10; exhaustion raises a typed InputRequiredRoundsExceeded error carrying the last result).
    client.callTool() and its siblings keep returning their plain result types. ClientOptions.inputRequired (autoFulfill, maxRounds) configures the driver; manual mode is autoFulfill: false plus the per-call allowInputRequired: true request option and the
    withInputRequired() schema wrapper. Retried requests surface their inputResponses to server handlers as bare response objects — entries in a wrapped {method, result} shape are dropped and reported via ctx.mcpReq.droppedInputResponseKeys. 2025-era behavior is unchanged:
    the legacy wire has no input_required vocabulary and the legacy server-to-client request flow is untouched.

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

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

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

  • #2286 1823aae Thanks @felixweinberger! - SEP-2243 Mcp-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 with x-mcp-header in the tool's inputSchema into Mcp-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 connection Client.listTools() (and the client's internal tools/list cache) exclude tool definitions whose x-mcp-header declarations violate the spec's constraints, logging a warning naming the tool and the reason. The legacy-era callTool and listTools paths 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 a tools/call whose body carries a non-null value for an x-mcp-header parameter when the matching header is absent, so calling such a tool with that argument from a browser is a known limitation. New CallToolRequestOptions.toolDefinition lets callers supply the tool definition directly so mirroring and output-schema validation can run without a prior tools/list. TransportSendOptions.headers is 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-Name standard header on every modern-enveloped request (params.name for tools/call/prompts/get, params.uri for resources/read), sentinel-encoded.

    Behavior change (modern era only): on a modern-enveloped request the Streamable HTTP transport now surfaces an HTTP 400 whose body is a well-formed JSON-RPC error response addressed to the pending request id in-band as a ProtocolError (instead of SdkHttpError), so the HEADER_MISMATCH recovery retry can fire. Legacy-era exchanges are unchanged.

  • #2286 1823aae Thanks @felixweinberger! - SEP-2350 scope step-up: on 403 insufficient_scope, StreamableHTTPClientTransport now 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). New onInsufficientScope: 'reauthorize' | 'throw' (default 'reauthorize') and maxStepUpRetries (default 1) on StreamableHTTPClientTransportOptions; 'throw' raises the new InsufficientScopeError. 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 1823aae Thanks @felixweinberger! - Align the 2026-07-28 protocol error codes to the spec renumber: HeaderMismatch is now -32020 (was -32001), MissingRequiredClientCapability is now -32021 (was -32003), and UnsupportedProtocolVersion is now -32022 (was -32004). These codes are part of the draft 2026-07-28 protocol revision only and have never appeared on a 2025-era wire — the 2025 serving paths and the SDK-conventional -32001 (Session not found) on the stateful Streamable HTTP transport are unchanged. ProtocolErrorCode.MissingRequiredClientCapability, ProtocolErrorCode.UnsupportedProtocolVersion, the HEADER_MISMATCH_ERROR_CODE constant, and the HEADER_MISMATCH / MISSING_REQUIRED_CLIENT_CAPABILITY / UNSUPPORTED_PROTOCOL_VERSION spec-type constants now carry the renumbered values; the UnsupportedProtocolVersionError and MissingRequiredClientCapabilityError classes (and ProtocolError.fromError recognition) follow. The client probe classifier recognizes -32022 for the corrective continuation and the SEP-2243 one-refresh-on-miss retry triggers on -32020.

  • #2286 1823aae Thanks @felixweinberger! - Client.listen(filter) opens a subscriptions/listen stream on a 2026-07-28-era connection, resolving once the server's acknowledged notification arrives with an McpSubscription { honoredFilter, close(), closed }. closed is a Promise<'local' | 'remote'> that resolves exactly once when the subscription terminates ('local' = you called close(); '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 existing setNotificationHandler registrations — the same handlers the 2025-era unsolicited notifications fire on a legacy connection — so listen() is era-transparent for consumers that already register those. close() aborts the listen request's stream (where the transport supports it) and sends notifications/cancelled referencing the listen id — both, on every transport; no automatic re-listen. On a 2025-era connection listen() throws a typed MethodNotSupportedByProtocolVersion steering to resources/subscribe and ClientOptions.listChanged. ClientOptions.listChanged now auto-opens a listen stream on a modern connection — the filter is the intersection of the configured sub-options and the server-advertised listChanged capabilities; auto-open is skipped (client.autoOpenedSubscription stays undefined) when that intersection is empty; otherwise the auto-opened subscription is exposed at client.autoOpenedSubscription. TransportSendOptions gains requestSignal (per-request abort) and onRequestStreamEnd (fires when a per-request response stream ends or errors for any non-deliberate reason) on the Streamable HTTP transport.

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

  • #2286 1823aae Thanks @felixweinberger! - Wire server/discover (protocol revision 2026-07-28) into the typed request funnel and serve it era-aware. The request joins ClientRequestSchema/ServerResultSchema/ResultTypeMap (per-era availability stays with the wire registries: only the 2026-era registry serves
    it), and Client.discover() issues it as a typed request on 2026-era connections. A Server whose supportedProtocolVersions list carries a modern (2026-07-28+) revision installs the server/discover handler, advertising ONLY its modern revisions and excluding the
    listChanged/subscribe-class capabilities until the subscriptions/listen flow ships; servers with today's default list are unchanged and keep answering -32601. The initialize handshake is now era-aware in the other direction: its accept check and counter-offer consult
    only the legacy subset of the supported versions — a 2026-era revision is never negotiated via initialize — so a 2025-era client can never be offered a 2026 version string; with the default list this is byte-identical to previous behavior. Serving the 2026 revision to
    ordinary HTTP/stdio traffic arrives with an upcoming server-side entry point: today the negotiation surface is client-side, and mode: 'auto' falls back cleanly against current SDK servers.

Patch Changes

  • #2286 1823aae Thanks @felixweinberger! - 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 in input_required results), and the stdio transport forbids the
    client from writing JSON-RPC responses. Dropped requests are surfaced via onerror. Legacy-era connections, responses, and notifications are unchanged.

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

  • #2286 1823aae Thanks @felixweinberger! - Complete the SEP-2577 @deprecated sweep on the public type surface (SEP-2596 Tier-1 obligation). Marks the full Logging type stack (LoggingLevel, SetLevelRequest, LoggingMessageNotification and params), the full Sampling type stack (CreateMessageRequest/Result, SamplingMessage, ModelPreferences/ModelHint, ToolChoice, ToolUseContent/ToolResultContent, and the includeContext enum values), the full Roots type stack (Root, ListRootsRequest/Result, RootsListChangedNotification), and registerClient (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 1823aae Thanks @felixweinberger! - Re-pin the 2026-07-28 draft references (spec reference types, vendored schema.json twins, example corpus) to the latest spec commit and align the 2026-era wire surface with it. Deliberate 2026-era wire behavior changes (the released 2025-11-25 surface is untouched):

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

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

    • The error code -32001 emitted for HTTP header/body mismatches is now defined by the draft schema (HEADER_MISMATCH); the emitted behavior is unchanged (documentation only).

      No public API surface changes; the regenerated reference artifacts are internal/test-only.

  • #2286 1823aae Thanks @felixweinberger! - Internal: regenerate the 2026-07-28 spec reference types from the latest draft schema (DiscoverResult now extends CacheableResult; ElicitationCompleteNotificationParams extracted as a named interface) and document the anchor lifecycle policy. Released-revision spec-type generation is now pinned to a fixed spec commit; draft anchors keep floating via the nightly refresh PRs. No public API or runtime behavior changes.

  • #2286 1823aae Thanks @felixweinberger! - Freeze the per-era wire schemas as self-contained copies decoupled from the public types layer, and convert WireCodec to a function-only interface. Two small spec-conformance fixes ride along with the otherwise-pure refactor:

    • The 2026 wire-true resultType member now defaults to 'complete' when absent (the spec's receiver-side back-compat rule); the inbound decodeResult step continues to require it. The server/discover result accepts absent or malformed ttlMs/cacheScope (falling back to 0/'private' per the spec's receiver leniency in caching.mdx) so the version-negotiation probe classifier stays behavior-neutral. Other cacheable result schemas are unchanged here; general receiver leniency for those belongs to the response-cache surface.
    • The sampling hasTools discriminant now keys on tools || toolChoice (previously tools only), aligning the client and server selection of the with-tools result variant with clientCapabilityRequirements.

Don't miss a new typescript-sdk release

NewReleases is sending notifications on new releases.