[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)

Status Open
Reported on v2.1.197
Maintainer reply None cached
Activity 1 comment · opened Sep 10, 2026

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.

  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":"<CLIENT_ID>","callbackPort":12345}}' --client-secret -s local
`
Enter any secret at the prompt (or set
MCP_CLIENT_SECRET`).

  1. 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|<H1> where

H1 = sha256('{"type":"http","url":"https://example.com/mcp","headers":{}}').slice(0,16) — computed with empty headers.

  1. claude mcp login repro and complete the browser flow.
  2. The mcpOAuth session entry is created under repro|<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-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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗