HTTP MCP: valid token treated as expired — "OAuth session expired and could not be refreshed" when provider issues no refresh token (mcp.facebook.com/ads)
Summary
A remote HTTP MCP server (Meta's official ads server at https://mcp.facebook.com/ads, registered with --client-id) stopped connecting. The surface error is a 30s timeout, but --debug reveals the real one:
Failed to authenticate: OAuth session expired and could not be refreshed
The stored access token is valid and not expired (issued the same day, 60-day lifetime). The client appears to discard it and attempt a refresh; this provider issues no refresh token, so the refresh fails and the connection is dropped.
This worked from 2026-07-27 through 2026-08-12 and broke on 2026-08-13 with no config change on our side.
Environment
- Claude Code 2.1.231 (also reproduced on 2.1.228)
- macOS 15, Apple Silicon
- Server registered as:
claude mcp add --transport http --client-id <META_APP_ID> meta-ads https://mcp.facebook.com/ads
Steps to reproduce
- Register the Meta ads MCP server as above.
- Authenticate through
/mcp— OAuth completes, "Got new credentials". claude mcp get meta-ads→✘ Failed to connect ... timed out after 30000ms.claude --debug mcp get meta-ads→Failed to authenticate: OAuth session expired and could not be refreshed.
The protocol itself works — it is not the server or the token
Using the exact token the client stored, a plain HTTPS client completes the full handshake in under a second:
initialize 0.5s 200
notifications/initialized 0.3s 200
tools/list 0.3s 200, 66 tools
GET /me/permissions on the same token returns all required scopes as granted, including the provider-specific one. Unauthenticated requests to the endpoint return 401 instantly and a bogus bearer returns 403 instantly, so the endpoint is healthy and fast on every path. Only the Claude Code client hangs.
Likely cause: issuer mismatch + no refresh token
The stored credential has:
issuer: https://www.facebook.com
discoveryState.authorizationServerUrl: https://mcp.facebook.com/ads
refreshToken: (absent)
The protected-resource metadata advertises authorization_servers: ["https://mcp.facebook.com/ads"], while the authorization-server metadata served at that path declares issuer: "https://www.facebook.com" — the two do not match, which is exactly what issuer-isolation logic validates. A related warning is printed on every read:
[mcp-sdk] SEP-2352: stored OAuth credential has no 'issuer' stamp
(pre-upgrade storage or provider not round-tripping the value).
SEP-2352 isolation is inactive for this read; ensure your provider round-trips the issuer field.
Whatever the trigger, the failure mode looks wrong in two ways:
- A valid, unexpired access token is treated as expired instead of being used.
- When a provider issues no refresh token, a failed refresh should not be fatal — falling back to the stored access token (or prompting re-auth) would be better than dropping the connection.
Also a UX bug: the reported error is not the real one
claude mcp get reports a 30-second connection timeout. The actual failure is an immediate authentication decision. The timeout message sends users chasing network and server problems; the real cause is only visible under --debug. Surfacing the auth error directly would save a lot of time.
Additional data point: removing a server deletes its credential
claude mcp remove <name> also deletes the stored OAuth credential for that server. Re-registering the same server therefore requires a full re-authentication. This makes any "remove and re-add" troubleshooting step (which the provider's own docs recommend) cost an interactive browser login each time, and it silently breaks anything else relying on that credential. A prompt, or preserving the credential for a short grace period, would be friendlier.
What we ruled out
- Re-authenticating (fresh credential, same result)
remove+addwith--client-id, per the provider's documented fix- Both client versions (2.1.231 and 2.1.228)
- Network and endpoint health (verified independently, sub-second responses)
- Token validity and scopes (verified against the provider's API)
Workaround in use
A local stdio MCP server that forwards JSON-RPC to the same HTTPS endpoint with the stored token. Same credential, same permissions, all 66 tools load normally. This confirms the break is confined to the client's OAuth handling of this HTTP transport.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗