[BUG] MCP OAuth: --client-secret and oauth.clientSecret are silently discarded, making confidential-client MCP servers impossible to authenticate
Summary
claude mcp add --client-secret is accepted by the CLI but the secret is persisted nowhere, and a hand-written oauth.clientSecret in ~/.claude.json is ignored. As a result an MCP server whose authorization server requires a confidential client can never complete the OAuth token exchange — /mcp shows the authorization server's error (client_secret is missing.) indefinitely.
Reproduced against https://chatmcp.googleapis.com/mcp/v1 (Google Chat MCP), whose authorization server is accounts.google.com. Google advertises token_endpoint_auth_methods_supported: ["client_secret_post", "client_secret_basic"] and not none, so a PKCE-only public-client exchange is rejected — a client secret is mandatory for Desktop and Web OAuth client types.
Environment
- Claude Code 2.1.252
- macOS (Darwin 25.6.0), Apple Silicon
- Transport:
http - Credential store: macOS Keychain, generic password, service
Claude Code-credentials
Steps to reproduce
The bug reproduces with a throwaway server name and fake credentials — no real secret needed:
export MCP_CLIENT_SECRET='probe-secret-value-12345'
claude mcp add --transport http \
--client-id 'probe-client-id' \
--client-secret \
zz-probe https://example.invalid/mcp -s user
Then inspect both persistence locations:
# 1. the config
python3 -c "import json;print(json.load(open('$HOME/.claude.json'))['mcpServers']['zz-probe'])"
# 2. the keychain
security find-generic-password -s "Claude Code-credentials" -w \
| python3 -c "import sys,json;print(json.load(sys.stdin)['mcpOAuth'])"
Expected
The client secret is stored somewhere retrievable, and included in the token request as client_secret_post / client_secret_basic.
Actual
{"type": "http", "url": "https://example.invalid/mcp", "oauth": {"clientId": "probe-client-id"}}
~/.claude.json→oauthcontainsclientIdonly. NoclientSecret.- Keychain → no entry at all for the server.
The secret is silently dropped. --client-secret produces no warning and no error.
Tested via the MCP_CLIENT_SECRET environment variable. I did not test the interactive prompt path, which may behave differently.
The config workaround is also ignored
Writing the keys directly into ~/.claude.json does not work either:
"google-chat": {
"type": "http",
"url": "https://chatmcp.googleapis.com/mcp/v1",
"oauth": {
"clientId": "<REDACTED>.apps.googleusercontent.com",
"clientSecret": "<35-char secret>",
"authorizationServer": ["https://accounts.google.com/", "https://accounts.google.com"]
}
}
The effective state lives in the keychain under mcpOAuth, and it is re-derived from the config with the secret dropped. Observed directly: I removed the cached entry, and a running session recreated it within seconds carrying clientId (picked up from the config) and still no clientSecret:
"google-chat|<hash>": {
"serverName": "google-chat",
"serverUrl": "https://chatmcp.googleapis.com/mcp/v1",
"accessToken": "",
"clientId": "<REDACTED>.apps.googleusercontent.com",
"discoveryState": {
"authorizationServerUrl": "https://accounts.google.com/",
"resourceMetadataUrl": "https://chatmcp.googleapis.com/.well-known/oauth-protected-resource/mcp",
"oauthMetadataFound": true
}
}
The keychain schema clearly supports the field — an entry for another server created via dynamic client registration does contain clientSecret:
keys present: ['accessToken', 'clientId', 'clientSecret', 'discoveryState', 'redirectUri', 'serverName', 'serverUrl']
So the storage exists; only the path from user-supplied configuration into it is missing. Servers that support DCR work; servers that require a pre-registered confidential client cannot be configured.
Proof that the credential itself is valid
To rule out a bad secret, I probed Google's token endpoint directly with a deliberately invalid authorization code. Only the client-authentication outcome matters:
| client auth sent | Google's response |
|---|---|
| client_id only | 400 invalid_request — client_secret is missing. ← identical to the /mcp error |
| client_id + the configured secret | 400 invalid_grant — Malformed auth code. (client auth accepted) |
| client_id + a wrong secret | 401 invalid_client — The provided client secret is invalid. |
Row 2 confirms the secret is correct and would be accepted. Row 1 is what Claude Code actually sends.
Secondary observations
- Stale
resourceMetadataUrl. The cacheddiscoveryStaterecords.../.well-known/oauth-protected-resource/mcp, while the server's live metadata is served at.../.well-known/oauth-protected-resource/mcp/v1(note the/v1). The cached value does not appear to be refreshed. authorization_serverstrailing slash. The protected-resource metadata advertises["https://accounts.google.com/"]with a trailing slash, while Google's ownissuerishttps://accounts.google.comwithout one. If the config'sauthorizationServeris matched by exact string anywhere, that mismatch is a trap; I supplied both forms to be safe.- Long-running sessions overwrite the credential store. With several
claudeprocesses running, edits to~/.claude.jsonor to the keychain are reverted from a live session's in-memory state within seconds. This makes any manual repair unreliable unless every session is closed first, and it may explain reports of MCP auth settings "resetting on their own".
Impact
Any MCP server whose authorization server mandates client authentication — which includes anything fronted by Google OAuth using a Desktop or Web client type — cannot be authenticated at all. There is no supported configuration and no working workaround on this version. The CLI advertising --client-secret while discarding the value makes this hard to diagnose: the user sees only the authorization server's client_secret is missing. and reasonably concludes their own credential is wrong.
Suggested fix
Persist --client-secret / oauth.clientSecret into the same keychain mcpOAuth entry that DCR already populates, and include it in the token request using the method the authorization server advertises in token_endpoint_auth_methods_supported. Failing that, at minimum warn when --client-secret is supplied but not retained, rather than dropping it silently.