[Bug] MCP OAuth refresh fails with static clientId—not persisted to credentials

Resolved 💬 3 comments Opened Apr 24, 2026 by EdwardSalter Closed Apr 28, 2026

Summary

When an MCP server is configured with a static pre-registered OAuth client via
mcpServers.<name>.oauth.clientId (instead of relying on dynamic client registration / DCR), the
initial authorization-code flow succeeds and tokens are stored, but clientId is never written onto
the mcpOAuth[<entry>] record in ~/.claude/.credentials.json. When the access token later expires,
refresh fails because the token request is formed without a client_id, which any spec-compliant
authorization server rejects with invalid_request. Claude Code surfaces this as "Failed to reconnect
to <server>".

### Environment

  • Claude Code 2.1.119
  • Windows 11 Pro 10.0.26200
  • Node v24.11.1
  • MCP server: remote HTTP + OAuth, authorization server is an IdentityServer-based OIDC provider,

static client pre-registered.

### Repro

  1. Configure an HTTP MCP server with a static OAuth client:

```json
"mcpServers": {
"example-server": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"oauth": { "clientId": "my-static-client" }
}
}

  2. Run Claude Code, connect, complete the browser auth flow. Tokens are stored successfully under    
  `mcpOAuth["example-server|<hash>"]`.
  3. Wait for the access token to expire (>1 h in my case).
  4. Restart Claude Code or run /mcp.

  Expected: Claude refreshes the access token using refresh_token + client_id=my-static-client + the   
  token endpoint discovered from .well-known/openid-configuration, and reconnects silently.

  Actual: /mcp reports "Failed to reconnect to example-server" with no further detail. No entry is     
  added to mcp-needs-auth-cache.json so the UI doesn't offer to re-auth.


  Manual verification against the actual authorization server:

  - Stored refresh_token + grant_type=refresh_token + client_id=my-static-client → 200 OK, returns a   
  fresh access token ✓
  - Same request with client_id omitted → {"error":"invalid_request"} ✗
  - Using the fresh access token to POST /mcp initialize → 200 OK, server responds normally ✓

  So the server, the refresh token, and the OIDC discovery chain all work; only the token request      
  Claude Code forms at refresh time is broken.

  Root cause (best guess)

  Only the DCR code path writes clientId onto the mcpOAuth entry. The static-clientId path reads       
  oauth.clientId from the MCP config at initial-auth time but doesn't copy it into the persisted       
  credential record. Refresh logic then reads clientId from the persisted record, finds nothing, and   
  issues a malformed token request.

  Suggested fix

  When an MCP server is configured with a static oauth.clientId, also write that value into
  mcpOAuth[<entry>].clientId at the end of the initial authorization flow (or, at refresh time, fall   
  back to reading oauth.clientId from the MCP config when the credential entry has none).

  Workaround

  1. Remove the affected entry from ~/.claude/.credentials.json → mcpOAuth.
  2. Restart Claude Code and complete the browser auth flow again.
  3. Manually add "clientId": "<your-static-id>" to the newly-created credential entry so subsequent   
  refreshes work.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗