needs-auth cache (keyed by server name) poisoned by `claude mcp list` blocks a later `--mcp-config` run of a same-named server with valid static auth

Status Open
Reported on v2.1.206
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Jul 23, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Summary

~/.claude/mcp-needs-auth-cache.json is keyed by the server name and shared across all Claude Code invocations on the machine. This makes a health-check of one config source for a server poison a completely different config source for the same name:

  1. claude mcp list does not accept --mcp-config (error: unknown option '--mcp-config'), so it can only health-check servers discovered from the project .mcp.json / ~/.claude.json.
  2. If the project .mcp.json declares a server (say atlassian) without an Authorization header (the interactive-OAuth variant), that health-check gets an OAuth challenge and writes atlassian into mcp-needs-auth-cache.json.
  3. A subsequent claude -p --mcp-config /tmp/mcp_with_auth_token.json where /tmp/mcp_with_auth_token.json declares the same-named atlassian with a valid static Authorization: Basic … header is then skipped:

``
[DEBUG] MCP server "atlassian": Skipping connection (cached needs-auth)
``
The valid header is never sent — the correct config is short-circuited by a cache entry created from a different config that was never used for this run.

--strict-mcp-config does not help: the cache is checked by server name before any connection is attempted (cf. the code cited in #48670: if (await isInNeedsAuthCache(serverName)) return {type:"needs-auth"}), independent of which config source defined the server.

This is especially painful on long-lived shell-executor CI runners: the cache survives across jobs, so one poisoning blocks every later job — including jobs whose --mcp-config is entirely correct.

Environment

  • Claude Code: 2.1.206 (also reproducible on 2.1.218)
  • Platform: macOS (shell-executor CI runner), also general
  • Transport: HTTP (Streamable HTTP)

Repro

  1. Project .mcp.json (discovered from cwd) — server named demo, no auth header:

``json
{ "mcpServers": { "demo": { "type": "http", "url": "https://mcp.atlassian.com/v1/mcp" } } }
``

  1. Run the debug listing (as many CI setups do) — note it cannot be pointed at another config:

``
claude mcp list # health-checks the headerless
demo, gets OAuth challenge
# -> writes {"demo":{...}} into ~/.claude/mcp-needs-auth-cache.json
``

  1. Run headless with a correct config for the same name, via a temp file:

``json
// /tmp/mcp_with_auth_token.json
{ "mcpServers": { "demo": { "type": "http", "url": "https://mcp.atlassian.com/v1/mcp",
"headers": { "Authorization": "Basic <base64(email:api_token)>" } } } }
`
`
claude -p "use demo mcp ..." --mcp-config /tmp/mcp_with_auth_token.json --strict-mcp-config
`
**Expected:**
demo connects using the static header from /tmp/mcp_with_auth_token.json.
**Actual:**
[DEBUG] MCP server "demo": Skipping connection (cached needs-auth)` — the header is never sent; the tool is unavailable.

Confirmed separately that the header/config in step 3 is valid: the same --mcp-config connects and works when the cache does not contain demo (fresh machine / after rm -f ~/.claude/mcp-needs-auth-cache.json).

Suggested fixes (any of)

  1. Key the cache by the resolved config identity (endpoint URL + auth scheme/source), not by free-text server name — so a headerless variant and a header-bearing variant of the same name don't share a cache entry. (Overlaps with #78220.)
  2. Let --strict-mcp-config bypass the needs-auth cache for servers it defines (the user explicitly supplied a full config; a cache from another source shouldn't override it).
  3. Allow claude mcp list to accept --mcp-config / --strict-mcp-config, so health-checks and actual runs read the same config instead of silently diverging.
  4. Scope the cache to process lifetime (in-memory) or add a documented flag/env to disable/clear it — important for SDK/CI where /mcp re-auth is impossible (cf. #48670).

Related

  • #78220 — needs-auth flag keyed by name (not URL), not cleared by mcp remove.
  • #48670 — needs-auth cache blocks reconnection; TTL 15 min; rm -f workaround.
  • #44830 — discovery poisoning persists; connection skipped before any HTTP request.

Workaround

rm -f ~/.claude/mcp-needs-auth-cache.json before the run, and avoid claude mcp list against a config whose servers can't authenticate non-interactively.

View original on GitHub ↗

3 Comments

bcherny collaborator · 1 month ago

Reproduced end-to-end on Claude Code 2.1.233 (Linux) with a fresh config dir and a local test MCP server:

  1. claude mcp list does not accept --mcp-config (error: unknown option), confirmed — so it can only health-check servers from the project/user config.
  2. Health-checking a server declared without an auth header (which gets an OAuth challenge) reports "Needs authentication" and writes an entry keyed by the bare server name into the needs-auth cache file.
  3. A subsequent claude -p … --mcp-config <file> --strict-mcp-config run, where the same-named server is declared with a valid static Authorization header, skips the connection because of that cache entry — verified from the server side: it receives zero HTTP requests, so the header is never sent.
  4. Control: deleting the cache file and re-running the identical command sends the Authorization header and attempts the connection normally.

So the cache entry created from one config source (headerless project config) short-circuits a different, fully-specified config source for the same server name, and --strict-mcp-config doesn't bypass it — exactly as described. Confirmed as a bug; the rm workaround you listed remains the practical mitigation for CI runners in the meantime.

🤖 Generated with Claude Code

HetCreep · 28 days ago

A third axis on the same missing-scope defect, alongside the config-source
case here and the account-profile case in #87702: on a machine running both
the Claude desktop app and the CLI, the needs-auth cache is shared across
environments too, with the same result — no scope key beyond the server
name.

Reproduced with a claude.ai connector this machine's cache already held a
needs-auth entry for (writer not independently confirmed, but the entry
predates any terminal activity that morning): a fresh terminal -p session
started within the cache's TTL made no connection attempt at all for that
server (zero debug lines naming it), while a sibling connector authenticated
in that same window connected in a few seconds. The TTL constants read directly from the CLI
2.1.273 binary (identical in the desktop's bundled 2.1.270) match what #87702 describes — a shorter default, and a longer one
specifically for claude.ai-proxy and plugin-provided HTTP/SSE servers.

Three independent reports now hit the same root cause from three different
scope axes (config source here, account profile in #87702, environment in
this comment). The fix both existing reports suggest — key the cache entry
by more than the bare server name, or cheaply re-check the current caller's
own credential before trusting a cached needs-auth entry — would close all
three at once.

(Measured 2026-09-16 on Claude Code CLI 2.1.273 and Claude desktop app
1.52386.6 with its bundled runtime 2.1.270, Windows 11 build 26200.)

antoni-libriseed · 5 days ago

Another trigger of the same name-only key, without claude mcp list or --mcp-config. On 2.1.295 (Linux): a git worktree checked out from an old branch had .mcp.json with my-server pointing at a decommissioned URL with no token. The first session started there got "No token data found" and wrote my-server into mcp-needs-auth-cache.json. For the next minutes, every new interactive and -p session elsewhere, where my-server points at the current URL with a valid token, logged Skipping connection (cached needs-auth) and had no tools, while claude mcp list showed ✔ Connected. Keying the cache by name|hash(config) (like mcpOAuth) would avoid this.