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
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:
claude mcp listdoes not accept--mcp-config(error: unknown option '--mcp-config'), so it can only health-check servers discovered from the project.mcp.json/~/.claude.json.- If the project
.mcp.jsondeclares a server (sayatlassian) without anAuthorizationheader (the interactive-OAuth variant), that health-check gets an OAuth challenge and writesatlassianintomcp-needs-auth-cache.json. - A subsequent
claude -p --mcp-config /tmp/mcp_with_auth_token.jsonwhere/tmp/mcp_with_auth_token.jsondeclares the same-namedatlassianwith a valid staticAuthorization: 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 on2.1.218) - Platform: macOS (shell-executor CI runner), also general
- Transport: HTTP (Streamable HTTP)
Repro
- Project
.mcp.json(discovered from cwd) — server nameddemo, no auth header:
``json``
{ "mcpServers": { "demo": { "type": "http", "url": "https://mcp.atlassian.com/v1/mcp" } } }
- Run the debug listing (as many CI setups do) — note it cannot be pointed at another config:
``demo
claude mcp list # health-checks the headerless , gets OAuth challenge``
# -> writes {"demo":{...}} into ~/.claude/mcp-needs-auth-cache.json
- 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
demo
**Expected:** connects using the static header from /tmp/mcp_with_auth_token.json.[DEBUG] MCP server "demo": Skipping connection (cached needs-auth)` — the header is never sent; the tool is unavailable.
**Actual:**
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)
- 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.)
- Let
--strict-mcp-configbypass the needs-auth cache for servers it defines (the user explicitly supplied a full config; a cache from another source shouldn't override it). - Allow
claude mcp listto accept--mcp-config/--strict-mcp-config, so health-checks and actual runs read the same config instead of silently diverging. - Scope the cache to process lifetime (in-memory) or add a documented flag/env to disable/clear it — important for SDK/CI where
/mcpre-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 -fworkaround. - #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.
3 Comments
Reproduced end-to-end on Claude Code 2.1.233 (Linux) with a fresh config dir and a local test MCP server:
claude mcp listdoes not accept--mcp-config(error: unknown option), confirmed — so it can only health-check servers from the project/user config.claude -p … --mcp-config <file> --strict-mcp-configrun, where the same-named server is declared with a valid staticAuthorizationheader, skips the connection because of that cache entry — verified from the server side: it receives zero HTTP requests, so the header is never sent.Authorizationheader 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-configdoesn't bypass it — exactly as described. Confirmed as a bug; thermworkaround you listed remains the practical mitigation for CI runners in the meantime.🤖 Generated with Claude Code
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
-psessionstarted 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.)
Another trigger of the same name-only key, without
claude mcp listor--mcp-config. On 2.1.295 (Linux): a git worktree checked out from an old branch had.mcp.jsonwithmy-serverpointing at a decommissioned URL with no token. The first session started there got "No token data found" and wrotemy-serverintomcp-needs-auth-cache.json. For the next minutes, every new interactive and-psession elsewhere, wheremy-serverpoints at the current URL with a valid token, loggedSkipping connection (cached needs-auth)and had no tools, whileclaude mcp listshowed✔ Connected. Keying the cache byname|hash(config)(likemcpOAuth) would avoid this.