Minor Changes
- #2629
dcc0102Thanks @gbshankar! - Add DPoP (RFC 9449 / SEP-1932) sender-constrained access token support to the client.- Opt in by implementing
OAuthClientProvider.dpop()returning aDpopSession(new, along withgenerateDpopKeyPair,accessTokenHash,isDpopNonceChallenge).auth()/exchangeAuthorization/refreshAuthorization/fetchTokenthen sign a DPoP proof into token requests (retrying once on an authorization-serveruse_dpop_noncechallenge, with client authentication re-applied per attempt), andStreamableHTTPClientTransport,SSEClientTransportandwithOAuthpresent atoken_type: "DPoP"access token asAuthorization: DPoP <token>plus a fresh per-request proof, retry a resource-serveruse_dpop_noncechallenge once, and pick up aDPoP-Noncedelivered on any response. Tokens the AS issued asBearerare still presented as Bearer. - DPoP is applied at the fetch layer: the transports wrap their resource-server
fetch(including a caller-suppliedfetch/eventSourceInit.fetch) with the newwithDpopFromProvider(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 minimalAuthProvider); theAuthProviderinterface itself is unchanged. auth()now recovers frominvalid_dpop_proofon refresh (e.g. a refresh token bound to a key that is no longer held) by discarding the tokens and re-authorizing, likeinvalid_grant.OAuthErrorCodegainsInvalidDpopProofandUseDpopNonce;extractWWWAuthenticateParamsrecognizes theDPoPchallenge scheme;OAuthMetadataSchemagainsdpop_signing_alg_values_supported.
- Opt in by implementing
Patch Changes
-
#2726
6fa4227Thanks @LuckTerence! -SdkErrorandSdkHttpErroraccept standardErrorOptionsas an optional fourth constructor argument and forward it toError, so a wrapped error is reachable through the standardError.causechain. Version-negotiation probe failures (SdkErrorCode.EraNegotiationFailed) now use it: the underlyingTypeError: fetch failedand the DNS or socket error beneath it surface viaerror.cause, so pino, Sentry, andutil.inspectrenderENOTFOUND/ECONNREFUSED/ETIMEDOUTinstead of stopping at theSdkError(#2657). The previouserror.data.causeslot is still populated for compatibility but is deprecated and slated for removal; readerror.causeinstead. -
#2654
03842cdThanks @pshah19! - Treat request id0as a real id. Two guards tested aRequestIdfor truthiness, so the legal JSON-RPC ids0and''were read as absent. Id0is 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 firstsampling/createMessage,elicitation/create, orroots/lista server sends.notifications/cancelledcarrying id0was ignored, and the in-flight handler ran to completion with itsAbortSignalnever fired.- A notification sent with
relatedRequestId: 0wrongly passed the debounce gate (for methods opted intodebouncedNotificationMethods). 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".
-
#2043
c4248a9Thanks @ChrisJr404! - On Windows, stdio servers spawned byStdioClientTransportnow also inheritCOMSPEC,PATHEXT,PROGRAMDATA,PROGRAMFILES(X86),PROGRAMW6432, andWINDIR(added toDEFAULT_INHERITED_ENV_VARS). Programs a server launches can depend on them: PowerShell finds no native executables withoutPATHEXT, and Windows OpenSSH exits 255 withoutProgramData. -
#2668
3e90449Thanks @KKonstantinov! - Stop sendingnotifications/cancelledfor theinitializehandshake. The spec is explicit that a client MUST NOT attempt to cancel itsinitializerequest, but the outbound cancel path fired for any in-flight request: aborting theAbortSignalpassed toconnect(), 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
b654261Thanks @sanjibani! -StreamableHTTPClientTransportandSSEClientTransportnow give their transport-managed headers precedence over same-named entries inrequestInit.headers:AuthorizationwhenauthProvideryields a token,mcp-protocol-version, and (Streamable HTTP)mcp-session-id. Header names compare case-insensitively and everyHeadersInitform is covered (plain object, tuple array,Headersinstance). Previously the caller-supplied value won, so a staticAuthorizationplaceholder (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; aHeadersinstance or lowercase key produced a combinedBearer <fresh>, Bearer <stale>value instead. A configuredAuthorizationis still sent while the provider has no token, and other configured headers pass through unchanged. Closes #2208. -
#2581
5119ee7Thanks @hugosmoreira! - Preserve the exact OAuth resource indicator from protected resource metadata when building authorization and token requests. Previously a pathlessresourcesuch ashttps://example.comwas normalized tohttps://example.com/viaURL.href, which breaks authorization servers that require theresourceparameter to match the published value exactly (Microsoft Entra ID rejects it withAADSTS9010010). The exported OAuth helpers (startAuthorization,exchangeAuthorization,refreshAuthorization,fetchToken,executeTokenRequest) now also accept astringforresource;selectResourceURLstill returns aURL, and a provider'svalidateResourceURLresult is used unchanged. Fixes #1968. -
3924de9- LetsaveTokensfailures surface after a successful token refresh. Inauth(), onetry
wrapped bothrefreshAuthorization()and theprovider.saveTokens()that persists its
result, and thecatchdeliberately swallows anything that is not anOAuthError— 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, andauth()continued tostartAuthorization()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, whereredirectToAuthorizationis typically a no-op, the fallthrough
is silent and the client is left with stale tokens and no indication of why.The
try/catchnow covers onlyrefreshAuthorization(). 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
ServerErroror an
unknown error still falls through to a new authorization flow, a non-ServerError
OAuthErroris still rethrown, andInsecureTokenEndpointErroris still surfaced. The
SEP-2352issuerstamp written with the refreshed tokens is unchanged.Those fallbacks no longer happen in silence, though. Both routes to an unexplained
re-authorization now emit aconsole.warnnaming the cause: the in-place fallthrough in
the refresh block, andauth()'s outer recovery forinvalid_grant,invalid_client,
andunauthorized_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 asinvalid_grant, which is precisely the state a dropped token set leaves
behind for the next call.Consumers whose
OAuthClientProvider.saveTokenscan reject should note thatauth()may
now reject where it previously returned'REDIRECT'— that rejection is the failure that
was being discarded. -
#2613
70de0c8Thanks @jwcarman! - Emit and validate theMcp-Nameheader for tasks requests per SEP-2663's Streamable HTTP binding: the client transport now mirrorsparams.taskIdintoMcp-Nameontasks/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 sharedMCP_NAME_HEADER_SOURCEtable.On the server,
createMcpHandlernow answers a modern (2026-07-28)tasks/get/tasks/update/tasks/cancelPOST that omitsMcp-Name, or whose header disagrees withparams.taskId, with400/-32020(HeaderMismatch) at thestandard-header-validationrung, the same treatmenttools/call/prompts/get/resources/readalready 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