[BUG] OAuth access token refresh not triggered on 401 — expired tokens cause permanent tool failure
- Dominant language
- TypeScript
- Stars
- 2.2k
- Forks
- 283
- Avg merge
- 11h 39m
- Merged PRs (30d)
- 740
Description
# Issue
When an MCP server's OAuth access token expires during an active session...
* tool calls from the `Tools` tab fail permanently with `Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token"}`.
* tool calls from the `App Builder` tab fail permanently _silently_; no response is shown anywhere.
The Inspector does not use the refresh token to obtain a new access token, even though the SDK (`MCPClientManager`) seems to supports automatic 401 → refresh → retry.
I've asked our good friend Claude why, it says:
> The root cause is in `createServerConfig()` in `mcpjam-inspector/client/src/lib/oauth/mcp-oauth.ts`. It bakes the access token into a static `Authorization` header and never passes `refreshToken` or `clientId` to the backend:
> ```typescript
> function createServerConfig(serverUrl: string, tokens: any): HttpServerConfig {
> // Note: We don't include authProvider in the config because it can't be serialized
> // when sent to the backend via JSON. The backend will use the Authorization header instead.
> // Token refresh should be handled separately if the token expires.
> return {
> url: serverUrl,
> requestInit: {
> headers: tokens.access_token
> ? { Authorization: `Bearer ${tokens.access_token}` }
> : {},
> },
> };
> }
> ```
> Because `refreshToken` and `clientId` are absent from the config, `MCPClientManager.connectViaHttp()` never instantiates a `RefreshTokenOAuthProvider` and the `StreamableHTTPClientTransport` has no `authProvider` — so when it receives a 401 there is no refresh path.
# How to reproduce
1. Configure an MCP server with OAuth 2.0 that issues short-lived access tokens (e.g. `expires_in: 10`).
2. Connect to the server via the Inspector — OAuth flow completes successfully, tools are listed.
3. Wait for the access token to expire (10+ seconds in this example).
4. Click **Run** on any tool.
5. The Response panel shows: `Streamable HTTP error: Error POSTing to endpoint: {"error":"invalid_token"}`.
6. No refresh token exchange is attempted. All subsequent tool calls also fail.
# Expected Behavior
When a tool call receives HTTP 401 with `WWW-Authenticate: Bearer error="invalid_token"`, the Inspector should use the refresh token to obtain a new access token from the server's token endpoint, then retry the request.
# Screenshots
# Platform
Linux, MCPJam Inspector v2.0.5 (latest)
# Additional Context
Our good friend Claude had the following suggestion:
> The fix should be straightforward: `createServerConfig()` should pass `refreshToken` and `clientId` (both available in `localStorage`) as top-level fields in the `HttpServerConfig` instead of a static `Authorization` header:
> ```typescript
> function createServerConfig(
> serverUrl: string,
> tokens: any,
> clientId?: string,
> ): HttpServerConfig {
> return {
> url: serverUrl,
> refreshToken: tokens.refresh_token,
> clientId: clientId,
> };
> }
> ```
>
> This would cause `MCPClientManager.connectViaHttp()` (line ~1105) to create a `RefreshTokenOAuthProvider` and pass it as `authProvider` to the transport, enabling the automatic 401 → refresh → retry flow that the SDK already supports and tests (`sdk/tests/refresh-token-auth.test.ts`).
Contributor guide
Research direction
Start in mcpjam-inspector/client/src/lib/oauth/mcp-oauth.ts and trace createServerConfig() into MCPClientManager.connectViaHttp(). Compare the configuration with sdk/tests/refresh-token-auth.test.ts, then reproduce an expired-token tool call and verify that both Inspector tabs recover through refresh and retry rather than failing permanently.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- api, authentication
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100