fix: use auth provider's current token instead of a connect-time snapshot#1695
Open
justinedelson wants to merge 2 commits into
Open
fix: use auth provider's current token instead of a connect-time snapshot#1695justinedelson wants to merge 2 commits into
justinedelson wants to merge 2 commits into
Conversation
…shot After a successful OAuth token refresh, direct (non-proxy) connections kept sending the old access token, so the session entered an unrecoverable 401 loop once the initial access token expired. useConnection read the access token from the provider once at connection setup and baked "Authorization: Bearer <token>" into requestInit.headers (and the custom fetch's header override). The transports already receive that same provider as authProvider and add a fresh Authorization from it on every request (refreshing on 401), but the SDK's _commonHeaders() spreads requestInit.headers after the provider token, so the connect-time snapshot always shadowed the refreshed token. Stop snapshotting the OAuth token for direct connections and rely on the authProvider as the single, always-current source. A user-supplied static Authorization header still flows through and intentionally overrides the provider. The SSE custom fetch is also reordered so the SDK-provided init (carrying the fresh token) wins over the connect-time header snapshot. Fixes modelcontextprotocol#1434
justinedelson
marked this pull request as ready for review
July 16, 2026 21:01
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
After a successful OAuth token refresh, direct (non-proxy) connections keep sending the old access token, so the session enters a
401loop it can't recover from. Once the initial access token expires:POST /mcpreturns401, the SDK refreshes,POST …/token(grant_type=refresh_token) returns200, and the provider stores the new token.POST /mcpstill carries the oldAuthorization: Bearer …, gets another401.This is rarely hit with a long token TTL, but a short TTL breaks the session at the first expiry. Confirmed against a real OAuth server by hashing (SHA-256, never raw tokens) the token issued by a successful refresh vs. the bearer token sent on the very next request — the retry token was byte-identical to the pre-refresh token and different from the one just issued.
Type of Change
Changes Made
Root cause
When relying on OAuth,
useConnectionread the access token from the provider once at connection setup and bakedAuthorization: Bearer <token>intorequestInit.headers(and the customfetchwrapper's header override).The transports already receive that same provider as their
authProvider, so the SDK adds a freshAuthorizationfrom it on every request (and refreshes it on401). But the SDK's_commonHeaders()spreadsrequestInit.headersafter the provider token:So the connect-time snapshot always shadowed the provider's current token. The provider does store the refreshed token — it just never got used.
Fix
For direct connections, stop snapshotting the OAuth-provider token into
requestInit. The provider is the single, always-current source of the token, and the SDK injects and refreshes it per request. Specifically:Authorizationheader was injected from the OAuth provider (vs. supplied by the user) and, for direct connections, strip that provider-injected header fromrequestHeadersso it isn't baked intorequestInit.Authorizationcustom header still flows through and intentionally overrides the provider.fetchwrapper so the SDK-providedinit(which already carries the fresh, provider-injected token) wins over the connect-time header snapshot, matching the streamable-http wrapper.The proxy-connection path is intentionally left unchanged in this PR.
This affects connections that use the SDK's OAuth
authProviderand is independent of the malformed-Content-Typefix (#1160).Related Issues
Fixes #1434
Testing
Test Results and/or Instructions
Added
useConnection.test.tsxcases asserting that, for direct connections:requestInit.headers(andauthProvideris wired up so the SDK injects/refreshes it), andAuthorizationheader still flows through unchanged.npm run prettier-fix, clientnpm run lint,tsc --noEmit, and the fulluseConnection.test.tsxsuite (48 tests) pass.Checklist
npm run prettier-fix)AI Generated, Human reviewed