[Bug] MCP servers silently unavailable when tokenless credential stub blocks Keychain refresh

Status Open
Reported on v2.1.224
Maintainer reply None cached
Activity 2 comments · opened Aug 17, 2026

Bug Description
An mcpOAuth entry in ~/.claude/.credentials.json that has the key structure but empty accessToken/refreshToken and no expiresAt (the code calls this a "tokenless stub") is treated as "this server needs authentication," even when the macOS Keychain holds a perfectly valid, unexpired token for that server. Startup then writes a needs-auth marker to ~/.claude/mcp-needs-auth-cache.json and skips the connection. Skipping the connection is what prevents reaching the code that would refresh the token or clear the stub. The state is therefore self-sealing and never expires on its own. Net effect: MCP servers are silently unavailable in every new session, indefinitely, while every ordinary diagnostic reports healthy. Re-authenticating does not survive a restart. ## Environment - Claude Code 2.1.224 (also reproduced on 2.1.223), Homebrew cask, /opt/homebrew/bin/claude - macOS 26.6 (25G72), arm64 - Affected servers: two remote HTTP MCP servers (https://mcp.notion.com/mcp, and a plugin-sourced https://mcp.slack.com/mcp) - Persisted from ~Aug 1 to Aug 17 2026 across multiple version upgrades and several repair attempts ## Symptoms, and why they mislead Every routine check reports green: - claude mcp list✔ Connected for both servers. It bypasses the needs-auth cache and reads the Keychain, so it reports on credentials, not on what a session will actually load. - The Keychain entries are valid and unexpired. - /mcp re-auth works — but only for the live process; the state is back on next launch. The only places the failure surfaces: - the startup banner ("N MCP servers need authentication") - headless runs logging Skipping connection (cached needs-auth) - the session's tool list containing only mcp__<server>__authenticate / mcp__<server>__complete_authentication with the real tools absent Because a session that connected before the stub was written keeps working from its in-memory tokens, a post-repair verification performed from that session looks fully healthy. The only valid test is a fresh launch. ## Root cause ### The stub is a recognized state with dedicated repair code From the 2.1.224 bundle (minified identifiers; Aa() is the credentials store, Am() builds the <serverName>|<sha256-16> key): ``js async function k1o(e, t) { let r = Am(e, t), n = (await Aa().readAsync())?.mcpOAuth?.[r]; if (!n || n.accessToken || n.refreshToken) return; // has tokens → nothing to do try { await Aa().mutate((o) => { let i = o.mcpOAuth?.[r]; if (!i || i.accessToken || i.refreshToken) return o; let s = { ...o.mcpOAuth }; return delete s[r], { ...o, mcpOAuth: s }; // delete the stub }); } catch (o) { bt(e, clear tokenless stub failed: ${ue(o)}); } } ` So a tokenless stub is understood to be a transient artifact that should be deleted on sight. The defect is that when it is *not* cleared, it is indistinguishable from a genuine needs-auth state. ### The needs-auth cache suppresses the only path that repairs it `js function v_t() { return path.join(Tn(), "mcp-needs-auth-cache.json") } function E_t() { if (!mun) mun = read(v_t()).then(Gt).catch(() => ({})); return mun } function Tyr() { mun = null } function vyr() { mun = null; delete(v_t()).catch(() => {}) } var mun = null; ` mun is a module-level memo: within a process the cache is read once and never re-read. Combined with the connection-skip path: `js if (S.type !== "claudeai-proxy" && S.pluginSource === void 0) bt(b, "Skipping connection (cached needs-auth)"); n({ client: { name: b, type: "needs-auth", config: S }, tools: R9e(b, S), commands: [] }); return; ` ...the loop closes: stub → needs-auth verdict → cache stamped → connection skipped → token never refreshed and k1o never reached → stub persists → repeat on every launch. Clearing the cache alone does not help, because the stub regenerates the marker on the next start. ### Observed timeline (single morning, mtime + ps elimination) | time | event | |---|---| | 09:24:11 | manual repair: .credentials.json{}, cache deleted, claude mcp login notion | | 09:24:27 | session A starts, reads {}, falls through to Keychain → **MCP works** (verified with live tool calls) | | 09:29:05 | .credentials.json rewritten with **blanked** entries for both servers. Session A was the only claude` process alive | | 09:37 | cache still absent, Keychain healthy, live calls still succeed — appears fixed | | 09:51:55 | next session starts, reads the blanked file, marks both servers needs-auth, stamps the…
Note: Content was truncated.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗