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)

Aperta
#93,446 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
area:auth area:mcp bug has repro platform:linux
Lingua principale
Python
Stelle
145k
Fork
23.1k
Metriche di merge delle PR
Metriche PR in attesa

Descrizione

### 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.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Direzione di ricerca

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.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
javascript, json, shell
Ambito
authentication, cli
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
45/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.