anthropics / anthropics/claude-code
[BUG] `mcp add-json --client-secret` stores secret under headers-stripped key, login looks up headers-included key (#67528 closed as stale, still present in 2.1.267)
- Langage dominant
- Python
- Étoiles
- 145k
- Forks
- 23.1k
- Métriques de merge des PR
- Métriques de PR en attente
Description
### Preflight Checklist
- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
### What's Wrong?
`claude mcp add-json '' --client-secret` for an HTTP MCP server whose JSON has a non-empty `headers` field stores the OAuth client secret under a credential-store key computed **without** the headers, while `claude mcp login ` looks the secret up under a key computed **with** the headers. The lookup misses, the token exchange goes out with no `client_secret`, and the authorization server rejects it.
With GitHub's remote MCP server (`https://api.githubcopilot.com/mcp/`, custom GitHub App as the OAuth client, `X-MCP-Toolsets` header) this surfaces as:
```
Couldn't complete authentication for "github": The client_id and/or client_secret passed are incorrect.
```
This is the same defect reported in #67528 (v2.1.173), which was closed by the stale bot on 2026-07-22 with "open a new issue if this is still relevant". It is still relevant: I hit it on 2.1.197 and confirmed the defective code path is unchanged in the 2.1.267 linux-x64 binary (latest on npm at time of filing).
**Root cause (from the bundled binary):**
Store and lookup share one key function:
```js
// 2.1.197 name: OC / 2.1.267 name: ha
function keyFor(serverName, cfg) {
const s = JSON.stringify({ type: cfg.type, url: cfg.url, headers: cfg.headers || {} });
return `${serverName}|${sha256(s).hex.slice(0, 16)}`;
}
```
The `add-json` handler persists the **full** config, then stores the secret against a **rebuilt, minimal** config that drops `headers`:
```js
// add-json handler, 2.1.267
await fP(s, i, f, m); // full config (incl. headers) -> .claude.json
...
await Z().saveMcpClientSecret(s, { type: i.type === "sse" ? "sse" : "http", url: i.url }, P); // headers dropped -> key hashed with headers:{}
```
At login, `clientInformation()` calls `keyFor(serverName, serverConfig)` with the full stored config, so it hashes the real headers and computes a different key. I reproduced both keys from my credential store exactly:
| Where | Key | Hashed input |
|---|---|---|
| `mcpOAuthClientConfig` (secret stored by `add-json`) | `github\|1eea5f274543f247` | `{"type":"http","url":"https://api.githubcopilot.com/mcp/","headers":{}}` |
| `mcpOAuth` (session created by `mcp login`) | `github\|01759ec9120e7ef8` | `{"type":"http","url":"https://api.githubcopilot.com/mcp/","headers":{"X-MCP-Toolsets":"default,actions"}}` |
The plain `claude mcp add --transport http --header ... --client-id ... --client-secret` path is **not** affected: it passes the same full config object it persists (`h`, including `headers`) to the store function. Only `add-json` rebuilds a stripped `{type, url}` object.
### What Should Happen?
`add-json` should pass the same config object it persists to the secret-store function (as `claude mcp add` already does), so the store key and the login-time lookup key match and the secret is included in the token exchange.
### Error Messages/Logs
```shell
$ claude mcp login github
Starting authentication for "github"…
If the browser didn't open, visit:
https://github.com/login/oauth/authorize?response_type=code&client_id=&code_challenge=...&code_challenge_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A7878%2Fcallback&state=...&scope=repo+read%3Aorg+...+offline_access&resource=https%3A%2F%2Fapi.githubcopilot.com%2Fmcp
Waiting for authorization… (^C to cancel)
Or paste the redirect URL here:
Couldn't complete authentication for "github": The client_id and/or client_secret passed are incorrect.
```
### Steps to Reproduce
No OAuth login is needed to see the mismatch; it's visible in the credential store immediately after step 1. Steps 3–4 show the end-to-end failure.
1. Add an HTTP server with BOTH a `headers` field AND a pre-configured client secret:
```bash
claude mcp add-json repro '{"type":"http","url":"https://example.com/mcp","headers":{"x-custom":"value"},"oauth":{"clientId":"","callbackPort":12345}}' --client-secret -s local
```
Enter any secret at the prompt (or set `MCP_CLIENT_SECRET`).
2. Inspect the credential store (`$CLAUDE_CONFIG_DIR/.credentials.json` on Linux; the `Claude Code-credentials` keychain item on macOS). `mcpOAuthClientConfig` has the secret under `repro|
` where
`H1 = sha256('{"type":"http","url":"https://example.com/mcp","headers":{}}').slice(0,16)` — computed with **empty** headers.
3. `claude mcp login repro` and complete the browser flow.
4. The `mcpOAuth` session entry is created under `repro|
` where
`H2 = sha256('{"type":"http","url":"https://example.com/mcp","headers":{"x-custom":"value"}}').slice(0,16)`. `H1 != H2`, no secret is found, and the token exchange fails with the provider's "invalid client secret" error.
Real-world case that led me here: GitHub's remote MCP server with a custom GitHub App as the OAuth client:
```bash
claude mcp add-json github '{"type":"http","url":"https://api.githubcopilot.com/mcp/","oauth":{"clientId":"","callbackPort":7878},"headers":{"X-MCP-Toolsets":"default,actions"}}' --client-secret -s local
claude mcp login github
```
**Workaround (verified):** copy the `clientSecret` entry in `mcpOAuthClientConfig` from the headers-stripped key to the headers-included key, then re-run `claude mcp login ` **without** re-adding the server. `claude mcp get ` then reports `client_secret configured` and `✔ Connected`. Alternatively, use `claude mcp add ... --header ... --client-id ... --client-secret` instead of `add-json`.
### Claude Model
Not sure / Multiple models
### Is this a regression?
I don't know
### Last Working Version
_No response_
### Claude Code Version
2.1.197 (Claude Code) — reproduced here. Defective `add-json` code path confirmed unchanged in the 2.1.267 linux-x64 binary (latest on npm at time of filing).
### Platform
Anthropic API
### Operating System
Ubuntu/Debian Linux
### Terminal/Shell
VS Code integrated terminal
### Additional Information
- Environment: VS Code devcontainer (Debian-based `python:3.13-slim` image), zsh, Claude Team subscription (claude.ai login). Credential store is `$CLAUDE_CONFIG_DIR/.credentials.json` (no OS keychain), which made the two divergent keys easy to see side by side.
- The add-json handler also normalizes `streamable-http` → `http` in the object it hashes for the secret key. If the persisted config retains `type: "streamable-http"`, that would be a second source of key divergence on the same line; I have not tested that variant.
- Independently reproduced the key derivation with Node's `crypto` (`sha256(JSON.stringify({type,url,headers})).slice(0,16)`) — both hashes in the table above match the entries in my store byte-for-byte.
- Suggested fix: in the `add-json` handler, call `saveMcpClientSecret(name, i, secret)` with the same `i` that was just persisted, rather than a rebuilt `{type, url}` literal. That mirrors what `claude mcp add` does.
Guide de contribution
Aucun guide de contribution indexé pour ce dépôt
Piste de recherche
Start at the `mcp add-json` and `mcp login` entry points in the bundled Claude Code binary, then inspect the credential-store key derivation and the `.credentials.json` entries described in the report. Compare the config passed when storing the secret with the config used during lookup; done means both paths use the same key and the reproduced login flow includes the client secret.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Évaluation
- Stack technique
- javascript, json, shell
- Domaine
- authentication, cli
- Type d'issue
- Bug
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Activité
- Active
- Clarté
- Clairement spécifiée
- Accessibilité débutants
- 45/100