Repository navigation
Client should silently refresh access tokens on 401 when a refresh token is available #2031
Description
The MCP HTTP client should automatically perform an OAuth 2.1 refresh_token grant when a remote MCP server returns HTTP 401 with WWW-Authenticate: Bearer error="invalid_token", and retry the original request — without surfacing the failure to the model or requiring user reauthentication.
This complements SEP-2207 (OIDC-flavored refresh token guidance), which standardized how clients request refresh tokens via offline_access. SEP-2207 stops at issuance; it does not specify how the client should use the refresh token on 401. That's the gap this issue addresses.
Context: how this surfaces today
I maintain a remote MCP connector for Power BI / Azure Data Factory analytics. The connector:
- Requests
offline_accessat sign-in (per SEP-2207) — refresh token IS issued by the Entra Authorization Server. - Returns
HTTP 401withWWW-Authenticate: Bearer resource_metadata="<RFC 9728 URL>", error="invalid_token", error_description="The access token expired"when the bearer is pastexp. (Per RFC 6750 §3.) - Publishes Protected Resource Metadata at
/.well-known/oauth-protected-resource/mcp.
So all four spec-compliant prerequisites are met:
- Server returns 401 (not 403, not 200-with-error)
- Server includes
WWW-Authenticate: Bearer error="invalid_token" - Server publishes RFC 9728 Protected Resource Metadata
- Client has a refresh token from
offline_accessat sign-in
Despite this, in some Claude client sessions, the 401 propagates to the model as a tool error and the user has to manually reauth. In other sessions (Claude Desktop with a fresh access token in cache), tool calls go through cleanly — but I have no way to confirm whether the silent-refresh path is exercised, since I haven't been able to force expiry deterministically.
Reproduction
- Configure a remote MCP server that meets the four prerequisites above.
- Sign into the server through the MCP client.
- Wait for the access token to age past
exp(typically 60 minutes for Entra). - Issue a tool call.
Observed: the 401 is sometimes surfaced to the model as a tool error.
Expected: the MCP HTTP client framework intercepts the 401, performs grant_type=refresh_token against the AS token endpoint, retries the original request with the new access token, and returns the success result to the model. The user sees nothing.
Proposed behavior in the SDK
When the MCP HTTP transport receives a 401 from a remote MCP server:
- Parse the
WWW-Authenticateheader. - If
error="invalid_token"AND a stored refresh token exists for that connector:- POST to the AS token endpoint with
grant_type=refresh_token,refresh_token=<stored>,client_id=<from registration>. Use the AS'stoken_endpointfrom its OAuth metadata (already discoverable per SEP-2207 path). - On 200: replace stored access_token + refresh_token (Entra rotates refresh tokens), and re-issue the original tool call with the new access token. Preserve the original
tool_use_id. - On 4xx (refresh token expired/revoked): surface a structured
needs_reautherror to the model so the user is prompted via the Connectors UI.
- POST to the AS token endpoint with
- Else (no
error="invalid_token", or no refresh token available): surface the original 401 to the model.
The retry must be capped at 1 attempt per request to prevent infinite loops if the AS keeps rejecting.
Why this matters
- User experience. Mid-session reauth interrupts a working tool flow for a token-lifecycle reason that OAuth 2.1 + RFC 6750 were specifically designed to hide.
- B2B deployments. Enterprise users running multi-hour sessions hit access-token expiry routinely. Each one becoming a manual reauth is operational noise, not a security event.
- Spec alignment. SEP-2207 already mandates that clients request refresh tokens; if the SDK doesn't use them on 401, the SEP-2207 work is half-finished from a UX perspective.
- Precedent in the local-MCP path. stdio MCP servers using MSAL / Azure CLI credentials already refresh transparently. Remote MCP behavior should match.
Out of scope for this issue
- The MCP spec itself. SEP-2207 already covers issuance; a new SEP could be written for refresh-on-401 behavior. This SDK issue takes the simpler path: implement the documented-but-not-spec-mandated client convention in the reference SDK.
- Server-side caching of user refresh tokens. Servers should not store user refresh tokens; this proposal is purely about client-side token use.
Server-side checklist (for SDK maintainers triaging)
For silent refresh to work end-to-end, the server must:
- Return
401withWWW-Authenticate: Bearer error="invalid_token"per RFC 6750 §3. - Publish RFC 9728 Protected Resource Metadata.
- Have requested
offline_accessat sign-in (per SEP-2207) so a refresh token was issued.
If those are in place, the SDK's behavior should be deterministic.
Happy to help
I have a spec-compliant repro server and packet captures. Happy to share if useful for SDK-side testing.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
waddah12alhajar commentedon May 8, 2026
Cross-linking #1954 — related but not a duplicate.
#1954 reports that
adaptOAuthProvider().token()returns the access_token even when expired (no expiry check before sending). It tackles the proactive layer: stop sending tokens you already know are expired locally.This issue (#2031) tackles the reactive layer: when the server returns
401 invalid_token(for any reason — clock skew, server-side revocation, conditional-access policy change, etc.), the client should silently refresh and retry.An important observation buried in #1954 from the reporter:
So the TS SDK already has refresh-on-401 plumbing via
onUnauthorized → auth(). The remaining questions for this issue:StreamableHTTPClientTransportusers, or are there configurations where it's not?onUnauthorized?needs_reauthfailure mode (when the refresh token itself is invalid) to a structured error code, so client UIs can branch on it cleanly?#1954 + #2031 together cover the full lifecycle: don't send known-expired tokens (#1954), and on any server-driven 401 with a refresh token available, refresh + retry transparently (#2031).
On the spec side: drafting a SEP that builds on SEP-2207 to specify silent-refresh-on-401 client behavior normatively. Will link the PR here when filed. The SDK fix can land independently — they're complementary tracks, not blockers on each other.
waddah12alhajar commentedon May 8, 2026
Update: Filed the corresponding SEP PR upstream — modelcontextprotocol/modelcontextprotocol#2702 — proposing silent-refresh-on-401 as normative MCP client behavior building on SEP-2207. Crosslinked here for the SDK-side reference implementation track.
Ram9199 commentedon Jun 13, 2026