Skip to content

Version Packages - #2808

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main
Open

github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@modelcontextprotocol/client@2.1.0

Minor Changes

  • #2629 dcc0102 Thanks @gbshankar! - Add DPoP (RFC 9449 / SEP-1932) sender-constrained access token support to the client.
    • Opt in by implementing OAuthClientProvider.dpop() returning a DpopSession (new, along with generateDpopKeyPair, accessTokenHash, isDpopNonceChallenge). auth() / exchangeAuthorization / refreshAuthorization / fetchToken then sign a DPoP proof into token requests (retrying once on an authorization-server use_dpop_nonce challenge, with client authentication re-applied per attempt), and StreamableHTTPClientTransport, SSEClientTransport and withOAuth present a token_type: "DPoP" access token as Authorization: DPoP <token> plus a fresh per-request proof, retry a resource-server use_dpop_nonce challenge once, and pick up a DPoP-Nonce delivered on any response. Tokens the AS issued as Bearer are still presented as Bearer.
    • DPoP is applied at the fetch layer: the transports wrap their resource-server fetch (including a caller-supplied fetch / eventSourceInit.fetch) with the new withDpopFromProvider(provider) middleware, so proofs are always bound to the request actually sent. withDpop(session, getToken) is exported for callers that manage tokens themselves (e.g. alongside a minimal AuthProvider); the AuthProvider interface itself is unchanged.
    • auth() now recovers from invalid_dpop_proof on refresh (e.g. a refresh token bound to a key that is no longer held) by discarding the tokens and re-authorizing, like invalid_grant. OAuthErrorCode gains InvalidDpopProof and UseDpopNonce; extractWWWAuthenticateParams recognizes the DPoP challenge scheme; OAuthMetadataSchema gains dpop_signing_alg_values_supported.

Patch Changes

  • #2726 6fa4227 Thanks @LuckTerence! - SdkError and SdkHttpError accept standard ErrorOptions as an optional fourth constructor argument and forward it to Error, so a wrapped error is reachable through the standard Error.cause chain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlying TypeError: fetch failed and the DNS or socket error beneath it surface via error.cause, so pino, Sentry, and util.inspect render ENOTFOUND / ECONNREFUSED / ETIMEDOUT instead of stopping at the SdkError ([v2] classifyNetworkError passes { cause } into SdkError's data slot, so the underlying network error never reaches Error.cause #2657). The previous error.data.cause slot is still populated for compatibility but is deprecated and slated for removal; read error.cause instead.

  • #2654 03842cd Thanks @pshah19! - Treat request id 0 as a real id. Two guards tested a RequestId for truthiness, so the legal JSON-RPC ids 0 and '' were read as absent. Id 0 is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the first sampling/createMessage, elicitation/create, or roots/list a server sends.

    • notifications/cancelled carrying id 0 was ignored, and the in-flight handler ran to completion with its AbortSignal never fired.
    • A notification sent with relatedRequestId: 0 wrongly passed the debounce gate (for methods opted into debouncedNotificationMethods). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.

    Absent is now the only value that means "no id".

  • #2668 3e90449 Thanks @KKonstantinov! - Stop sending notifications/cancelled for the initialize handshake. The spec is explicit that a client MUST NOT attempt to cancel its initialize request, but the outbound cancel path fired for any in-flight request: aborting the AbortSignal passed to connect(), or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.

    The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and connect() still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path.

  • #2475 b654261 Thanks @sanjibani! - StreamableHTTPClientTransport and SSEClientTransport now give their transport-managed headers precedence over same-named entries in requestInit.headers: Authorization when authProvider yields a token, mcp-protocol-version, and (Streamable HTTP) mcp-session-id. Header names compare case-insensitively and every HeadersInit form is covered (plain object, tuple array, Headers instance). Previously the caller-supplied value won, so a static Authorization placeholder (e.g. an env-var API key) kept overriding the OAuth token even after the provider obtained one and the fallback-to-OAuth flow never completed; a Headers instance or lowercase key produced a combined Bearer <fresh>, Bearer <stale> value instead. A configured Authorization is still sent while the provider has no token, and other configured headers pass through unchanged. Closes One line change to enable fallback authentication #2208.

  • #2581 5119ee7 Thanks @hugosmoreira! - Preserve the exact OAuth resource indicator from protected resource metadata when building authorization and token requests. Previously a pathless resource such as https://example.com was normalized to https://example.com/ via URL.href, which breaks authorization servers that require the resource parameter to match the published value exactly (Microsoft Entra ID rejects it with AADSTS9010010). The exported OAuth helpers (startAuthorization, exchangeAuthorization, refreshAuthorization, fetchToken, executeTokenRequest) now also accept a string for resource; selectResourceURL still returns a URL, and a provider's validateResourceURL result is used unchanged. Fixes OAuth resource indicator from protected resource metadata is normalized with a trailing slash #1968.

  • 3924de9 - Let saveTokens failures surface after a successful token refresh. In auth(), one try
    wrapped both refreshAuthorization() and the provider.saveTokens() that persists its
    result, and the catch deliberately swallows anything that is not an OAuthError — plus
    ServerError — so that a failed refresh falls through to a fresh authorization request.
    A persistence error thrown by the provider landed in that same branch: it was discarded
    with no log and no rethrow, and auth() continued to startAuthorization() and returned
    'REDIRECT'.

    Against an authorization server that rotates refresh tokens (the OAuth 2.1 default, and
    Keycloak's) this loses credentials rather than merely hiding an error. The exchange has
    already succeeded server-side, so the old refresh token is invalidated at the moment the
    new one is issued; dropping the new token set leaves nothing usable on either side. On a
    headless or CLI client, where redirectToAuthorization is typically a no-op, the fallthrough
    is silent and the client is left with stale tokens and no indication of why.

    The try/catch now covers only refreshAuthorization(). Persisting the result happens
    after it, on an unguarded path, so a provider's I/O error propagates to the caller.

    Refresh-request failures keep their existing control flow exactly: a ServerError or an
    unknown error still falls through to a new authorization flow, a non-ServerError
    OAuthError is still rethrown, and InsecureTokenEndpointError is still surfaced. The
    SEP-2352 issuer stamp written with the refreshed tokens is unchanged.

    Those fallbacks no longer happen in silence, though. Both routes to an unexplained
    re-authorization now emit a console.warn naming the cause: the in-place fallthrough in
    the refresh block, and auth()'s outer recovery for invalid_grant, invalid_client,
    and unauthorized_client, which discards stored credentials and retries. The second one
    matters most in practice — an expired, revoked, or rotation-reuse-detected refresh token
    is reported as invalid_grant, which is precisely the state a dropped token set leaves
    behind for the next call.

    Consumers whose OAuthClientProvider.saveTokens can reject should note that auth() may
    now reject where it previously returned 'REDIRECT' — that rejection is the failure that
    was being discarded.

  • #2613 70de0c8 Thanks @jwcarman! - Emit and validate the Mcp-Name header for tasks requests per SEP-2663's Streamable HTTP binding: the client transport now mirrors params.taskId into Mcp-Name on tasks/get / tasks/update / tasks/cancel (previously omitted, causing conforming servers to reject every task poll with -32020 HeaderMismatch), and the server-side standard-header validation cross-checks it via the same shared MCP_NAME_HEADER_SOURCE table.

    On the server, createMcpHandler now answers a modern (2026-07-28) tasks/get / tasks/update / tasks/cancel POST that omits Mcp-Name, or whose header disagrees with params.taskId, with 400 / -32020 (HeaderMismatch) at the standard-header-validation rung, the same treatment tools/call / prompts/get / resources/read already get. Legacy-era (2025-11-25) tasks traffic is unaffected. Clients built with this SDK release send the header; hand-rolled clients that omitted it must add it.

  • Updated dependencies [dcc0102]:

    • @modelcontextprotocol/core@2.1.0

@modelcontextprotocol/core@2.1.0

Minor Changes

  • #2629 dcc0102 Thanks @gbshankar! - Add DPoP (RFC 9449 / SEP-1932) sender-constrained access token support to the client.
    • Opt in by implementing OAuthClientProvider.dpop() returning a DpopSession (new, along with generateDpopKeyPair, accessTokenHash, isDpopNonceChallenge). auth() / exchangeAuthorization / refreshAuthorization / fetchToken then sign a DPoP proof into token requests (retrying once on an authorization-server use_dpop_nonce challenge, with client authentication re-applied per attempt), and StreamableHTTPClientTransport, SSEClientTransport and withOAuth present a token_type: "DPoP" access token as Authorization: DPoP <token> plus a fresh per-request proof, retry a resource-server use_dpop_nonce challenge once, and pick up a DPoP-Nonce delivered on any response. Tokens the AS issued as Bearer are still presented as Bearer.
    • DPoP is applied at the fetch layer: the transports wrap their resource-server fetch (including a caller-supplied fetch / eventSourceInit.fetch) with the new withDpopFromProvider(provider) middleware, so proofs are always bound to the request actually sent. withDpop(session, getToken) is exported for callers that manage tokens themselves (e.g. alongside a minimal AuthProvider); the AuthProvider interface itself is unchanged.
    • auth() now recovers from invalid_dpop_proof on refresh (e.g. a refresh token bound to a key that is no longer held) by discarding the tokens and re-authorizing, like invalid_grant. OAuthErrorCode gains InvalidDpopProof and UseDpopNonce; extractWWWAuthenticateParams recognizes the DPoP challenge scheme; OAuthMetadataSchema gains dpop_signing_alg_values_supported.

@modelcontextprotocol/codemod@2.1.0

Patch Changes

  • #2765 5ecc791 Thanks @claude! - Project-type inference no longer counts bare SDK paths that appear only in ordinary string data. The v1→v2 codemod's source scanner matched any quoted @modelcontextprotocol/sdk/client|server subpath anywhere in a file, so a server path stored as data (example text, a log message, a config value) misclassified a client-only project as both — rewriting shared type imports to @modelcontextprotocol/server and adding a server dependency the project never uses. The scanner now requires a module-specifier position: static imports and re-exports (from '...'), side-effect imports, dynamic import('...') (including webpack magic comments), require('...') / require.resolve('...'), and the vi./jest. mock-method calls the mock-paths transform rewrites. Known limitation: the scan is lexical, so a string whose text embeds a complete import statement still counts.

@modelcontextprotocol/express@3.0.0

Patch Changes

  • #2698 7b781ed Thanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
    WebStandardStreamableHTTPServerTransport (and the Node transport built on it),
    createMcpHandler, toNodeHandler, and createMcpHonoApp's JSON pre-parse — now stops at
    4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
    bound their reads too) and answers 413 Payload Too Large before anything is parsed.
    toWebRequest (when it reads the Node stream itself) now rejects once the body exceeds the
    limit with an error whose name is 'RequestBodyTooLargeError' and status is 413, and
    toNodeHandler answers that with 413; hand-wired callers of toWebRequest should handle the
    rejection or pass a pre-parsed body, and isLegacyRequest reports such a request as non-legacy
    so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
    batch is answered 400 / -32600 and none of it is dispatched.

    The limit is configurable with a new maxRequestBodySize option (bytes, default
    DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB, exported from @modelcontextprotocol/server) on
    WebStandardStreamableHTTPServerTransportOptions, CreateMcpHandlerOptions (forwarded to its
    stateless legacy leg; isLegacyRequest and legacyStatelessFallback take the same option),
    CreateMcpHonoAppOptions, and ToNodeHandlerOptions / ToWebRequestOptions (the adapter's
    bound applies before the handler's, so raise both). The bounded reader is exported as
    readRequestBody for adapter authors. Hosts that pre-parse the body and pass it as
    parsedBody skip the SDK's read and its size limit entirely; the batch bound applies either way.

    createMcpHonoApp and createMcpExpressApp now run their Host/Origin validation before the
    JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
    answered 403 rather than 400, and its body is not read.

  • Updated dependencies [6fa4227, 03842cd, 3e90449, 7b781ed, 75dc7ea, 70de0c8]:

    • @modelcontextprotocol/server@2.1.0

@modelcontextprotocol/fastify@3.0.0

Patch Changes

@modelcontextprotocol/hono@3.0.0

Patch Changes

  • #2698 7b781ed Thanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
    WebStandardStreamableHTTPServerTransport (and the Node transport built on it),
    createMcpHandler, toNodeHandler, and createMcpHonoApp's JSON pre-parse — now stops at
    4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
    bound their reads too) and answers 413 Payload Too Large before anything is parsed.
    toWebRequest (when it reads the Node stream itself) now rejects once the body exceeds the
    limit with an error whose name is 'RequestBodyTooLargeError' and status is 413, and
    toNodeHandler answers that with 413; hand-wired callers of toWebRequest should handle the
    rejection or pass a pre-parsed body, and isLegacyRequest reports such a request as non-legacy
    so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
    batch is answered 400 / -32600 and none of it is dispatched.

    The limit is configurable with a new maxRequestBodySize option (bytes, default
    DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB, exported from @modelcontextprotocol/server) on
    WebStandardStreamableHTTPServerTransportOptions, CreateMcpHandlerOptions (forwarded to its
    stateless legacy leg; isLegacyRequest and legacyStatelessFallback take the same option),
    CreateMcpHonoAppOptions, and ToNodeHandlerOptions / ToWebRequestOptions (the adapter's
    bound applies before the handler's, so raise both). The bounded reader is exported as
    readRequestBody for adapter authors. Hosts that pre-parse the body and pass it as
    parsedBody skip the SDK's read and its size limit entirely; the batch bound applies either way.

    createMcpHonoApp and createMcpExpressApp now run their Host/Origin validation before the
    JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
    answered 403 rather than 400, and its body is not read.

  • Updated dependencies [6fa4227, 03842cd, 3e90449, 7b781ed, 75dc7ea, 70de0c8]:

    • @modelcontextprotocol/server@2.1.0

@modelcontextprotocol/node@3.0.0

Patch Changes

  • #2698 7b781ed Thanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
    WebStandardStreamableHTTPServerTransport (and the Node transport built on it),
    createMcpHandler, toNodeHandler, and createMcpHonoApp's JSON pre-parse — now stops at
    4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
    bound their reads too) and answers 413 Payload Too Large before anything is parsed.
    toWebRequest (when it reads the Node stream itself) now rejects once the body exceeds the
    limit with an error whose name is 'RequestBodyTooLargeError' and status is 413, and
    toNodeHandler answers that with 413; hand-wired callers of toWebRequest should handle the
    rejection or pass a pre-parsed body, and isLegacyRequest reports such a request as non-legacy
    so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
    batch is answered 400 / -32600 and none of it is dispatched.

    The limit is configurable with a new maxRequestBodySize option (bytes, default
    DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB, exported from @modelcontextprotocol/server) on
    WebStandardStreamableHTTPServerTransportOptions, CreateMcpHandlerOptions (forwarded to its
    stateless legacy leg; isLegacyRequest and legacyStatelessFallback take the same option),
    CreateMcpHonoAppOptions, and ToNodeHandlerOptions / ToWebRequestOptions (the adapter's
    bound applies before the handler's, so raise both). The bounded reader is exported as
    readRequestBody for adapter authors. Hosts that pre-parse the body and pass it as
    parsedBody skip the SDK's read and its size limit entirely; the batch bound applies either way.

    createMcpHonoApp and createMcpExpressApp now run their Host/Origin validation before the
    JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
    answered 403 rather than 400, and its body is not read.

  • Updated dependencies [6fa4227, 03842cd, 3e90449, 7b781ed, 75dc7ea, 70de0c8]:

    • @modelcontextprotocol/server@2.1.0

@modelcontextprotocol/server@2.1.0

Patch Changes

  • #2726 6fa4227 Thanks @LuckTerence! - SdkError and SdkHttpError accept standard ErrorOptions as an optional fourth constructor argument and forward it to Error, so a wrapped error is reachable through the standard Error.cause chain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlying TypeError: fetch failed and the DNS or socket error beneath it surface via error.cause, so pino, Sentry, and util.inspect render ENOTFOUND / ECONNREFUSED / ETIMEDOUT instead of stopping at the SdkError ([v2] classifyNetworkError passes { cause } into SdkError's data slot, so the underlying network error never reaches Error.cause #2657). The previous error.data.cause slot is still populated for compatibility but is deprecated and slated for removal; read error.cause instead.

  • #2654 03842cd Thanks @pshah19! - Treat request id 0 as a real id. Two guards tested a RequestId for truthiness, so the legal JSON-RPC ids 0 and '' were read as absent. Id 0 is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the first sampling/createMessage, elicitation/create, or roots/list a server sends.

    • notifications/cancelled carrying id 0 was ignored, and the in-flight handler ran to completion with its AbortSignal never fired.
    • A notification sent with relatedRequestId: 0 wrongly passed the debounce gate (for methods opted into debouncedNotificationMethods). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.

    Absent is now the only value that means "no id".

  • #2668 3e90449 Thanks @KKonstantinov! - Stop sending notifications/cancelled for the initialize handshake. The spec is explicit that a client MUST NOT attempt to cancel its initialize request, but the outbound cancel path fired for any in-flight request: aborting the AbortSignal passed to connect(), or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.

    The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and connect() still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path.

  • #2698 7b781ed Thanks @maxisbey! - Read Streamable HTTP request bodies with a size limit. Every SDK-owned body read —
    WebStandardStreamableHTTPServerTransport (and the Node transport built on it),
    createMcpHandler, toNodeHandler, and createMcpHonoApp's JSON pre-parse — now stops at
    4 MiB by default (the limit the legacy SSE transport already uses; the Express adapter and stdio
    bound their reads too) and answers 413 Payload Too Large before anything is parsed.
    toWebRequest (when it reads the Node stream itself) now rejects once the body exceeds the
    limit with an error whose name is 'RequestBodyTooLargeError' and status is 413, and
    toNodeHandler answers that with 413; hand-wired callers of toWebRequest should handle the
    rejection or pass a pre-parsed body, and isLegacyRequest reports such a request as non-legacy
    so the modern handler answers it. JSON-RPC batch arrays are limited to 100 messages; a longer
    batch is answered 400 / -32600 and none of it is dispatched.

    The limit is configurable with a new maxRequestBodySize option (bytes, default
    DEFAULT_MAX_REQUEST_BODY_SIZE = 4 MiB, exported from @modelcontextprotocol/server) on
    WebStandardStreamableHTTPServerTransportOptions, CreateMcpHandlerOptions (forwarded to its
    stateless legacy leg; isLegacyRequest and legacyStatelessFallback take the same option),
    CreateMcpHonoAppOptions, and ToNodeHandlerOptions / ToWebRequestOptions (the adapter's
    bound applies before the handler's, so raise both). The bounded reader is exported as
    readRequestBody for adapter authors. Hosts that pre-parse the body and pass it as
    parsedBody skip the SDK's read and its size limit entirely; the batch bound applies either way.

    createMcpHonoApp and createMcpExpressApp now run their Host/Origin validation before the
    JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
    answered 403 rather than 400, and its body is not read.

  • #2590 75dc7ea Thanks @davidpavlovschi! - Reject a modern (2026-07-28) POST that omits the required MCP-Protocol-Version header.

    createMcpHandler accepted a request whose body carried a valid per-request _meta
    envelope but whose MCP-Protocol-Version header was absent: the request was classified
    modern, dispatched, and answered 200 — tool handlers ran. Only the mismatch case
    (header present, disagreeing with the body) was rejected, so of the standard headers
    SEP-2243 requires on a modern POST, presence was enforced for Mcp-Method (and for
    Mcp-Name on the methods that mirror params.name / params.uri) but not for
    MCP-Protocol-Version.

    Such a request is now refused with 400 Bad Request and JSON-RPC -32020
    (HeaderMismatch), matching the shape the sibling missing-header cells already emit and
    echoing the request id — per the Streamable HTTP spec, which requires the header on every
    POST and lists a missing required standard header as a HeaderMismatch failure. The
    spec's allowance to treat a header-less request as 2025-03-26 is available only to a
    server that also serves pre-2025-06-18 clients, and permits routing it to legacy
    handling — never serving it as 2026-07-28; under legacy: 'reject' the requirement is
    unconditional.

    Era classification is deliberately unchanged and stays body-primary: a proxy that strips
    the header still must not change the era, so such a request is still classified modern
    and is refused one rung later, at standard-header-validation — the same rung that
    already answers a missing Mcp-Method. Legacy-era traffic is untouched, notifications
    are unaffected, body-less GET / DELETE session operations are method-routed before
    any header validation, and stdio serving (which has no HTTP headers) is not involved.

    Clients built with this SDK always send the header, so no first-party client is affected;
    hand-rolled clients that omitted it must add it.

  • #2613 70de0c8 Thanks @jwcarman! - Emit and validate the Mcp-Name header for tasks requests per SEP-2663's Streamable HTTP binding: the client transport now mirrors params.taskId into Mcp-Name on tasks/get / tasks/update / tasks/cancel (previously omitted, causing conforming servers to reject every task poll with -32020 HeaderMismatch), and the server-side standard-header validation cross-checks it via the same shared MCP_NAME_HEADER_SOURCE table.

    On the server, createMcpHandler now answers a modern (2026-07-28) tasks/get / tasks/update / tasks/cancel POST that omits Mcp-Name, or whose header disagrees with params.taskId, with 400 / -32020 (HeaderMismatch) at the standard-header-validation rung, the same treatment tools/call / prompts/get / resources/read already get. Legacy-era (2025-11-25) tasks traffic is unaffected. Clients built with this SDK release send the header; hand-rolled clients that omitted it must add it.

  • Updated dependencies [dcc0102]:

    • @modelcontextprotocol/core@2.1.0

@modelcontextprotocol/server-legacy@2.1.0

Patch Changes

  • Updated dependencies [dcc0102]:
    • @modelcontextprotocol/core@2.1.0

@modelcontextprotocol/core-internal@2.0.1

Patch Changes

  • #2726 6fa4227 Thanks @LuckTerence! - SdkError and SdkHttpError accept standard ErrorOptions as an optional fourth constructor argument and forward it to Error, so a wrapped error is reachable through the standard Error.cause chain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlying TypeError: fetch failed and the DNS or socket error beneath it surface via error.cause, so pino, Sentry, and util.inspect render ENOTFOUND / ECONNREFUSED / ETIMEDOUT instead of stopping at the SdkError ([v2] classifyNetworkError passes { cause } into SdkError's data slot, so the underlying network error never reaches Error.cause #2657). The previous error.data.cause slot is still populated for compatibility but is deprecated and slated for removal; read error.cause instead.

  • #2654 03842cd Thanks @pshah19! - Treat request id 0 as a real id. Two guards tested a RequestId for truthiness, so the legal JSON-RPC ids 0 and '' were read as absent. Id 0 is not a corner case: the outbound request counter is zero-based, so it is the first id every peer assigns, which on the server→client leg is the first sampling/createMessage, elicitation/create, or roots/list a server sends.

    • notifications/cancelled carrying id 0 was ignored, and the in-flight handler ran to completion with its AbortSignal never fired.
    • A notification sent with relatedRequestId: 0 wrongly passed the debounce gate (for methods opted into debouncedNotificationMethods). Because the pending set is keyed by method alone, a second such notification in the same tick was silently dropped rather than sent.

    Absent is now the only value that means "no id".

  • #2668 3e90449 Thanks @KKonstantinov! - Stop sending notifications/cancelled for the initialize handshake. The spec is explicit that a client MUST NOT attempt to cancel its initialize request, but the outbound cancel path fired for any in-flight request: aborting the AbortSignal passed to connect(), or letting the handshake hit its timeout, put a forbidden cancellation on the wire naming the initialize request id.

    The local behaviour is unchanged — the caller's promise still rejects with the same abort/timeout error, and connect() still tears the connection down. Only the wire notification is suppressed. Every other method keeps the existing cancellation path.

  • #2590 75dc7ea Thanks @davidpavlovschi! - Reject a modern (2026-07-28) POST that omits the required MCP-Protocol-Version header.

    createMcpHandler accepted a request whose body carried a valid per-request _meta
    envelope but whose MCP-Protocol-Version header was absent: the request was classified
    modern, dispatched, and answered 200 — tool handlers ran. Only the mismatch case
    (header present, disagreeing with the body) was rejected, so of the standard headers
    SEP-2243 requires on a modern POST, presence was enforced for Mcp-Method (and for
    Mcp-Name on the methods that mirror params.name / params.uri) but not for
    MCP-Protocol-Version.

    Such a request is now refused with 400 Bad Request and JSON-RPC -32020
    (HeaderMismatch), matching the shape the sibling missing-header cells already emit and
    echoing the request id — per the Streamable HTTP spec, which requires the header on every
    POST and lists a missing required standard header as a HeaderMismatch failure. The
    spec's allowance to treat a header-less request as 2025-03-26 is available only to a
    server that also serves pre-2025-06-18 clients, and permits routing it to legacy
    handling — never serving it as 2026-07-28; under legacy: 'reject' the requirement is
    unconditional.

    Era classification is deliberately unchanged and stays body-primary: a proxy that strips
    the header still must not change the era, so such a request is still classified modern
    and is refused one rung later, at standard-header-validation — the same rung that
    already answers a missing Mcp-Method. Legacy-era traffic is untouched, notifications
    are unaffected, body-less GET / DELETE session operations are method-routed before
    any header validation, and stdio serving (which has no HTTP headers) is not involved.

    Clients built with this SDK always send the header, so no first-party client is affected;
    hand-rolled clients that omitted it must add it.

  • #2613 70de0c8 Thanks @jwcarman! - Emit and validate the Mcp-Name header for tasks requests per SEP-2663's Streamable HTTP binding: the client transport now mirrors params.taskId into Mcp-Name on tasks/get / tasks/update / tasks/cancel (previously omitted, causing conforming servers to reject every task poll with -32020 HeaderMismatch), and the server-side standard-header validation cross-checks it via the same shared MCP_NAME_HEADER_SOURCE table.

    On the server, createMcpHandler now answers a modern (2026-07-28) tasks/get / tasks/update / tasks/cancel POST that omits Mcp-Name, or whose header disagrees with params.taskId, with 400 / -32020 (HeaderMismatch) at the standard-header-validation rung, the same treatment tools/call / prompts/get / resources/read already get. Legacy-era (2025-11-25) tasks traffic is unaffected. Clients built with this SDK release send the header; hand-rolled clients that omitted it must add it.

  • Updated dependencies [dcc0102]:

    • @modelcontextprotocol/core@2.1.0

@github-actions
github-actions Bot requested a review from a team as a code owner September 13, 2026 14:05

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked two adjacent release-mechanics concerns and ruled them out: the codemod's V2_PACKAGE_VERSIONS map (packages/codemod/src/versions.ts) is inlined from the workspace manifests at build time, so it will emit ^3.0.0 for node/express consistently with the manifests in this commit rather than a stale range; and the fastify adapter's dependency-only changelog section is accurate, since packages/middleware/fastify/src has no body-parsing path that #2698 would have touched.

Extended reasoning...

This is a Changesets "Version Packages" commit touching only CHANGELOG.md, package.json version fields, and deleted .changeset files; no source changes. Three inline findings already cover the substantive questions (middleware major bump stranding the #2698 hardening, missing prose docs for the new body/batch caps, and the lost PR attribution on the saveTokens entry). The note above records two further candidates that were investigated and found consistent with the manifests, so a human confirming the 3.0.0 middleware bump does not need to re-check them.

2 verified lower-impact observations (convention, logging or cleanup points) were not posted.

Findings marked 🟡 are optional suggestions and need no follow-up push.

"name": "@modelcontextprotocol/hono",
"private": false,
"version": "2.0.0",
"version": "3.0.0",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Publishing @ modelcontextprotocol/hono, node and express as 3.0.0 strands the #2698 DoS hardening (4 MiB body cap, 413 before parse, Host/Origin validation moved ahead of the JSON parser) behind a major: every existing consumer on the documented ^2.0.0 range keeps 2.0.0 forever under npm update, dependabot minor/patch policies or lockfile refresh, because 2.0.0's peer ^2.0.0 still admits server 2.1.0 so nothing forces the upgrade. Fix: publish the middleware packages at 2.0.1 (set onlyUpdatePeerDependentsWhenOutOfRange: true or ___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH / hand-edit the version and CHANGELOG heading) so the patch reaches ^2.0.0 consumers automatically. [also at: packages/middleware/express/package.json:4 - The four middleware adapters (@ modelcontextprotocol/express, hono, node, fastify) are published as 3.0.0 although their changelogs list only patch changes: Changesets major-bumps every package with a workspace:^ peerDependency on @ modelcontextprotocol/server whenever server gets a minor, so…]

Extended reasoning...

Mechanism: .changeset/config.json has no onlyUpdatePeerDependentsWhenOutOfRange, so Changesets major-bumps every package whose peerDependency (@ modelcontextprotocol/server: workspace:^ at packages/middleware/hono/package.json:54, express:56, node:55, fastify:54) receives a minor. The consumed changeset request-body-size-limit.md marked node/hono/express as patch, and their CHANGELOG sections (packages/middleware/hono/CHANGELOG.md:3-32) are headed 'Patch Changes'. Consequence distinct from the already-posted 'spurious major' finding: the payload this patch carries is a denial-of-service hardening — createMcpHonoApp's parseJsonBody (packages/middleware/hono/src/hono.ts:80-137) now stops at 4 MiB and returns 413, and toNodeHandler (packages/middleware/node/src/toNodeHandler.ts:130-157, 294-295) rejects oversized bodies before allocation.…

Verification: normal — triggered for every existing consumer that installed the published @ modelcontextprotocol/{hono,node,express} 2.0.0 on a caret range and updates via npm update/dependabot minor-patch policy/lockfile refresh (the standard case), once this commit is merged and published. Mechanism verified from the diff itself. packages/middleware/hono/package.json:4, express:4, node:3, fastify:4 all…

JSON body parser, so a request from a disallowed Host or Origin with an invalid JSON body is
answered `403` rather than `400`, and its body is not read.

- [#2590](https://github.com/modelcontextprotocol/typescript-sdk/pull/2590) [`75dc7ea`](https://github.com/modelcontextprotocol/typescript-sdk/commit/75dc7ea6e2913e1ac37d4f06eec62cd5cfac9e7a) Thanks [@davidpavlovschi](https://github.com/davidpavlovschi)! - Reject a modern (2026-07-28) POST that omits the required `MCP-Protocol-Version` header.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) Publishing 2.1.0 ships a new default 4 MiB request-body cap (413) and a 100-message batch cap (400/-32600) to every server on ^2.0.0 via a routine minor upgrade, with no prose documentation anywhere: docs/ contains no mention of 413, maxRequestBodySize, or the batch bound, so an operator whose clients post large tool-call payloads (base64 images/files) gets 413s with no documented knob. Fix: before merging this release, add a migration/guide entry for the new limits and the maxRequestBodySize option (and the batch cap), covering WebStandardStreamableHTTPServerTransport, createMcpHandler, toNodeHandler and createMcpHonoApp.

Extended reasoning...
  1. This commit is the release that turns commit 7b781ed (fix(server): read HTTP request bodies with a size limit and bound JSON-RPC batch length #2698) into a published @ modelcontextprotocol/server@ 2.1.0; consumers declaring ^2.0.0 receive it on their next install. 2. git show --stat 7b781ed shows it touched only src/test/changeset files — no docs/**. grep -rln '413\|Payload Too Large' docs/ and grep -rn maxRequestBodySize docs/ both return nothing, so the only description of the new behaviour is the CHANGELOG text at packages/server/CHANGELOG.md:44 (and the middleware CHANGELOGs). 3. Population and rate: any server behind the SDK transports (WebStandardStreamableHTTPServerTransport, createMcpHandler, toNodeHandler, createMcpHonoApp) whose clients send bodies over 4 MiB or batches over 100 messages — e.g. tools/call carrying inline image/blob content. On the base (2.0.0) those requests succeeded; after this release they are answered 413 / 400 before dispatch. 4. CLAUDE.md and REVIEW.md require that behavior changes have prose docs and that docs/**/*.md not contradict the implementation; the dismissing finders argued this belongs to fix(server): read HTTP request bodies with a size limit and bound JSON-RPC batch length #2698, but…

Verification: nit — trigger: an operator on @ modelcontextprotocol/server@^2.0.0 (or the node/hono middleware) whose clients POST bodies over 4 MiB to a WebStandardStreamableHTTPServerTransport / createMcpHandler / toNodeHandler path, which previously read req.json() unbounded and after this release answers 413 (packages/server/src/server/streamableHttp.ts per git show 7b781ed: `const body = await…

result, and the `catch` deliberately swallows anything that is not an `OAuthError` — plus
`ServerError` — so that a failed refresh falls through to a fresh authorization request.
A persistence error thrown by the provider landed in that same branch: it was discarded
with no log and no rethrow, and `auth()` continued to `startAuthorization()` and returned

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 (optional) The 2.1.0 client changelog entry for the saveTokens fix is published with a bare commit link and no PR number or author, while its source commit 3924de9 has subject fix(client/auth): propagate saveTokens errors after refresh (#2053) — so the GitHub release notes and CHANGELOG lose the link to PR #2053 and the contributor credit that every sibling entry carries. Fix: the entry should read like its siblings, i.e. [#2053](.../pull/2053) [3924de9](...) Thanks [@ SAY-5]! - ...; regenerate the changelog (with a working GITHUB_TOKEN so changelog-github's commit->PR lookup succeeds) or hand-edit the entry before merge.

Extended reasoning...
  1. git show 3924de9 shows the commit subject ends in (#2053) and author Sai Asish Y, so the PR association exists on GitHub; @ changesets/changelog-github normally resolves it via the GitHub API and emits [#NNNN] [sha] Thanks [@ user]! (as it did for every other entry in this release). 2. packages/client/CHANGELOG.md:35 and core-internal's copy emit only [3924de9](commit) - Let saveTokens failures..., which means the API lookup silently failed (rate limit / token) for this one commit and the generator fell back to commit-only output. 3. Consequence after merge: the published CHANGELOG and the GitHub Release body (built from the same text) have no PR link and no contributor attribution for a credential-loss fix; the finder dismissed as cosmetic but it is a verifiably wrong release artifact and a silent generator failure the release process should not ship. Remedy: regenerate with a token that resolves the commit, or edit the entry to [#2053](https://github.com/modelcontextprotocol/typescript-sdk/pull/2053) [3924de9`](...) Thanks @ SAY-5! -…

Verification: nit. Triggering condition: whenever this release commit is merged and published as-is (the changelog text is the shipped artifact). Mechanism verified: /home/claude/typescript-sdk/packages/client/CHANGELOG.md:30 reads - [3924de9](https://github.com/modelcontextprotocol/typescript-sdk/commit/3924de99df834302d89f5997a1b64ca268282284) - Let saveTokens` failures surface after a successful token…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

One line change to enable fallback authentication OAuth resource indicator from protected resource metadata is normalized with a trailing slash

0 participants