[Bug] MCP OAuth refresh fails with static clientId—not persisted to credentials
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
- 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗