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)

オープン
#93,446 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る
area:auth area:mcp bug has repro platform:linux
主要言語
Python
スター
145k
フォーク
23.1k
PR マージ指標
PR 指標を取得中

説明

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

コントリビューションガイド

このリポジトリのコントリビューションガイドは索引されていません

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

評価

技術スタック
javascript, json, shell
領域
authentication, cli
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
明確に書かれている
初心者へのやさしさ
45/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。