adaptOAuthProvider returns expired tokens without checking expiry, breaking long-running StreamableHTTP connections

Open Beginner friendly
#1,954 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
74/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Quiet
Tech stack
typescript

Research direction

Start in packages/client/src/client/auth.ts at adaptOAuthProvider(), then read hasValidTokens() and the existing onUnauthorized flow to confirm the expiry buffer. Reproduce the expired-token case with a StreamableHTTPClientTransport connection and verify that near-expiry tokens no longer produce an Authorization header, allowing the server's 401 refresh path to run.

Written by the indexing model from the issue text.

Description

auth bug P2 ready for work

Describe the bug
adaptOAuthProvider() in auth.ts does not check token expiry before returning the access token. The token: adapter calls provider.tokens() and returns access_token regardless of whether it has expired. OAuthClientProvider.tokens() calculates expires_in: Math.max(0, expiresAt - now), so expired tokens come back with expires_in: 0 and are sent to the server as-is.

The refresh logic in auth() (which checks token validity and attempts refresh) only runs via onUnauthorized when the server returns HTTP 401. However, many MCP servers — particularly those acting as proxies to upstream APIs (see #1294) — wrap upstream auth errors in HTTP 200 JSON-RPC responses rather than returning 401. In these cases, the expired token causes a server-side failure that is never surfaced as a 401, so the client's retry/refresh path never fires.

To Reproduce
Steps to reproduce the behavior:

  1. Connect to an HTTP MCP server using StreamableHTTPClientTransport with OAuth
  2. Authenticate successfully — connection works
  3. Wait for the access token to expire
  4. Make a tool call through the MCP connection
  5. Request fails — expired token is sent, server uses it against an upstream API, upstream rejects it, error is wrapped in a 200 JSON-RPC response
  6. Only recovery is manual re-authentication or process restart

Expected behavior
adaptOAuthProvider().token() should check expires_in before returning the access token. If the token is expired or near-expiry (≤60 seconds remaining, matching the buffer already used in hasValidTokens()), it should return undefined so the transport sends the request without an Authorization header, triggering a 401 from the server, which invokes onUnauthorized → auth() → refresh flow.

Suggested Fix

packages/client/src/client/auth.ts:

 // Current:
 export function adaptOAuthProvider(provider: OAuthClientProvider): AuthProvider {
     return {
         token: async () => {
             const tokens = await provider.tokens();
             return tokens?.access_token;
         },
         onUnauthorized: async ctx => handleOAuthUnauthorized(provider, ctx)
     };
 }
 
 // Fixed — check expiry before returning:
 export function adaptOAuthProvider(provider: OAuthClientProvider): AuthProvider {
     return {
         token: async () => {
             const tokens = await provider.tokens();
             if (!tokens?.access_token) return undefined;
             if (tokens.expires_in !== undefined && tokens.expires_in <= 60) {
                 return undefined;
             }
             return tokens.access_token;
         },
         onUnauthorized: async ctx => handleOAuthUnauthorized(provider, ctx)
     };
 }

Note: the 60-second buffer matches the existing hasValidTokens() logic which uses the same hardcoded value.

Logs

 # Successful OAuth connection:
 21:02:21.240Z [ERROR] MCP client for [Redacted] connected, took 97ms
 21:02:21.241Z [ERROR] Started MCP client for remote server [Redacted] with OAuth
 
 # 1h42m later, expired token sent:
 22:44:53.036Z [ERROR] MCP client for [Redacted] errored [Redacted]: The resource parameter provided in the request doesn't match with the requested scopes.
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 12h
Merged PRs (30d)
3

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from modelcontextprotocol/typescript-sdk

All issues in modelcontextprotocol/typescript-sdk

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.