anthropics / anthropics/claude-code
[BUG] MCP OAuth: --client-secret and oauth.clientSecret are silently discarded, making confidential-client MCP servers impossible to authenticate
- 主要语言
- Python
- 星标
- 145k
- 派生
- 23.1k
- PR 合并指标
- PR 指标待抓取
描述
## 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:
```bash
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:
```bash
# 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
```json
{"type": "http", "url": "https://example.invalid/mcp", "oauth": {"clientId": "probe-client-id"}}
```
- `~/.claude.json` → `oauth` contains **`clientId` only**. No `clientSecret`.
- 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:
```json
"google-chat": {
"type": "http",
"url": "https://chatmcp.googleapis.com/mcp/v1",
"oauth": {
"clientId": ".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`**:
```json
"google-chat|": {
"serverName": "google-chat",
"serverUrl": "https://chatmcp.googleapis.com/mcp/v1",
"accessToken": "",
"clientId": ".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
1. **Stale `resourceMetadataUrl`.** The cached `discoveryState` records `.../.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.
2. **`authorization_servers` trailing slash.** The protected-resource metadata advertises `["https://accounts.google.com/"]` with a trailing slash, while Google's own `issuer` is `https://accounts.google.com` without one. If the config's `authorizationServer` is matched by exact string anywhere, that mismatch is a trap; I supplied both forms to be safe.
3. **Long-running sessions overwrite the credential store.** With several `claude` processes running, edits to `~/.claude.json` or 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.
贡献指南
这个仓库没有索引到贡献指南
调研方向
Start by tracing `claude mcp add --client-secret` and `oauth.clientSecret` through configuration persistence, the `mcpOAuth` keychain entry, and the OAuth token exchange. Verify that the secret survives into stored credentials and is sent using an advertised client-authentication method, while preserving existing dynamic client registration behavior.
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- python
- 领域
- api, authentication, cli
- Issue 类型
- 缺陷
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 活跃度
- 活跃
- 描述清晰度
- 基本清楚
- 新手友好度
- 45/100