adaptOAuthProvider returns expired tokens without checking expiry, breaking long-running StreamableHTTP connections
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
- Domain
- authentication
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
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:
- Connect to an HTTP MCP server using StreamableHTTPClientTransport with OAuth
- Authenticate successfully — connection works
- Wait for the access token to expire
- Make a tool call through the MCP connection
- 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
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from modelcontextprotocol/typescript-sdk
-
Auth metadata discovery: fallback URL built on resource host instead of authorization-server host Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
modelcontextprotocol/typescript-sdk#2783 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
Difficulty 1/5 Under an hour Newbie friendliness 92/100
modelcontextprotocol/typescript-sdk#2766 · 1 comment ·
-
Difficulty 2/5 1-2 days Newbie friendliness 72/100
All issues in modelcontextprotocol/typescript-sdk
Similar issues
-
calcite-components needs triage refactor
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Esri/calcite-design-system#15203 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 91/100
-
community first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficulty 1/5 Under an hour Newbie friendliness 95/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Automattic/studio#4908 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 90/100