[BUG] HTTP 403 (no WWW-Authenticate) is misclassified as "requires OAuth"; stale flag is keyed by server name and not cleared by mcp remove

Status Open
Reported on v2.1.211
Maintainer reply None cached
Activity 0 comments · opened Jul 16, 2026

[BUG] HTTP 403 (no WWW-Authenticate) is misclassified as "requires OAuth"; stale flag is keyed by server name and not cleared by mcp remove

Summary

When an HTTP MCP endpoint is fronted by a WAF/CDN (e.g. Cloudflare) that returns 403 Forbidden, Claude Code interprets it as "this server requires authentication," writes a persistent flag to ~/.claude/mcp-needs-auth-cache.json, and thereafter shows the server as ! Needs authentication.

Three compounding problems make this hard to diagnose and recover from:

  1. A 403 with no WWW-Authenticate header is treated as an OAuth challenge. Per the MCP authorization spec, the OAuth discovery flow should be triggered by 401 Unauthorized + WWW-Authenticate, not by a bare 403. A 403 is a WAF/geo/network block and should surface as a connection error, not an auth prompt.
  2. **The needs-auth flag is keyed by the free-text server name, not the canonical endpoint URL.** Changing a server's url (while keeping the same name) silently inherits the stale flag from the old URL.
  3. claude mcp remove (and re-add) does not purge the server's entry in mcp-needs-auth-cache.json. The only recovery is hand-editing the cache file.

Environment

  • Claude Code: 2.1.211
  • Platform: macOS 15.7.7 (24G720)
  • Transport: HTTP (Streamable HTTP)

Repro

  1. Register an HTTP MCP server named e.g. myserver pointing at a URL fronted by Cloudflare that returns 403 (WAF/bot-management block):
// ~/.claude.json
"mcpServers": {
  "myserver": { "type": "http", "url": "https://blocked.example.com/mcp" }
}

The blocked endpoint responds like this — note no WWW-Authenticate header, server: cloudflare, HTML body:

$ curl -sS -i -X POST https://blocked.example.com/mcp \
    -H 'Content-Type: application/json' \
    -H 'Accept: application/json, text/event-stream' \
    -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"diag","version":"0.0.1"}}}'

HTTP/2 403
content-type: text/html; charset=UTF-8
set-cookie: __cf_bm=...; HttpOnly; SameSite=None; Secure
server: cloudflare
cf-ray: ...

<!DOCTYPE html> ... CDN/WAF block page (HTML) ...
  1. Observe Claude Code records the server as needing auth:
// ~/.claude/mcp-needs-auth-cache.json
{ "myserver": { "timestamp": 1784225258749 } }   // note: keyed by name, no managed id
  1. Fix the URL to a working, public endpoint that returns a clean 200 (same server name):
"myserver": { "type": "http", "url": "https://working.example.com/mcp" }

The working endpoint responds correctly, no auth needed:

HTTP/1.1 200 OK
content-type: application/json

{"result":{"protocolVersion":"2025-06-18","capabilities":{...},
 "serverInfo":{"name":"...","version":"..."}},"jsonrpc":"2.0","id":1}
  1. claude mcp list still reports myserver ... ! Needs authentication, because the stale flag keyed by "myserver" survives the URL change. claude mcp remove myserver && claude mcp add ... does not clear it.

Expected

  • A 403 (or any non-401, or a 401 without a WWW-Authenticate header) should not trigger the OAuth flow or set the needs-auth flag; it should surface as a connection/HTTP error with the status code.
  • The needs-auth cache should key on the canonical endpoint URL so that changing a server's url does not inherit stale auth state.
  • claude mcp remove (and re-adding a server) should purge that server's mcp-needs-auth-cache.json entry.

Actual

  • 403 → server flagged ! Needs authentication.
  • Flag keyed by server name, so it persists across url changes.
  • Only recovery is manually deleting the entry from ~/.claude/mcp-needs-auth-cache.json and restarting.

Workaround

# Remove the poisoned entry, then restart Claude Code
python3 - <<'EOF'
import json, pathlib
p = pathlib.Path.home()/".claude/mcp-needs-auth-cache.json"
d = json.loads(p.read_text()); d.pop("myserver", None); p.write_text(json.dumps(d, indent=2))
EOF

Impact

Any MCP endpoint behind Cloudflare/Akamai/a corporate WAF that returns 403 to a subset of clients (bot heuristics, IP reputation, geo) gets permanently mislabeled as auth-gated. The failure is confusing because MCP Inspector connects fine (no per-name cache) and colleagues who added the server fresh are unaffected — so it presents as "works for everyone but me."

View original on GitHub ↗