[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)
Preflight Checklist
- [x] I have searched existing issues 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 <name> '<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 <name> 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:
// 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:
// 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 <name> <url> --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
$ 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=<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.
- Add an HTTP server with BOTH a
headersfield 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":"<CLIENT_ID>","callbackPort":12345}}' --client-secret -s local
MCP_CLIENT_SECRET`).
Enter any secret at the prompt (or set
- Inspect the credential store (
$CLAUDE_CONFIG_DIR/.credentials.jsonon Linux; theClaude Code-credentialskeychain item on macOS).mcpOAuthClientConfighas the secret underrepro|<H1>where
H1 = sha256('{"type":"http","url":"https://example.com/mcp","headers":{}}').slice(0,16) — computed with empty headers.
claude mcp login reproand complete the browser flow.- The
mcpOAuthsession entry is created underrepro|<H2>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:
claude mcp add-json github '{"type":"http","url":"https://api.githubcopilot.com/mcp/","oauth":{"clientId":"<GITHUB_APP_CLIENT_ID>","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 <name> without re-adding the server. claude mcp get <name> 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-slimimage), 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→httpin the object it hashes for the secret key. If the persisted config retainstype: "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-jsonhandler, callsaveMcpClientSecret(name, i, secret)with the sameithat was just persisted, rather than a rebuilt{type, url}literal. That mirrors whatclaude mcp adddoes.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗