HTTP MCP OAuth: valid token silently corrupted by status check (wrong redirectUri, empty accessToken)

Status Open
Reported on v2.1.246
Maintainer reply None cached
Activity 0 comments · opened Aug 26, 2026

Summary

For a remote HTTP MCP server added with claude mcp add --transport http <name> <url> --callback-port <port> --client-id <id>, the OAuth token record in ~/.claude/.credentials.json gets silently corrupted (empty accessToken, expiresAt: 0, and redirectUri reverted to a stale/wrong port) every time the server's connection status is checked (claude mcp list, or any tool-discovery pass that re-checks MCP health) — even immediately after a fully successful browser OAuth flow.

Environment

  • Claude Code version: 2.1.246
  • OS: Linux (WSL2), 6.6.87.1-microsoft-standard-WSL2
  • Transport: HTTP MCP server behind a Keycloak (OIDC) authorization server, PKCE + dynamic-ish local callback

Steps to reproduce

  1. claude mcp add --transport http wsi-ces-governance https://mcp.timebank.tw/mcp --callback-port 8766 --client-id claude-code-local-mcp-client
  2. Confirm at the identity provider (Keycloak) that this client is registered with exactly one redirect URI: http://localhost:8766/callback.
  3. In a session, call the server's authenticate tool. It returns an authorization URL with redirect_uri=http%3A%2F%2Flocalhost%3A8766%2Fcallback.
  4. Open that URL, complete login/consent. Browser redirects to http://localhost:8766/callback?code=...&state=... and (when a local listener catches it) shows "Authentication successful / Connected."
  5. Immediately inspect ~/.claude/.credentials.jsonmcpOAuth["<name>|<hash>"]: at this point redirectUri correctly reads http://localhost:8766/callback, but accessToken is already an empty string and expiresAt is 0.
  6. Run claude mcp list. Output:

``
[mcp-sdk] SEP-2352: stored OAuth credential has no 'issuer' stamp (pre-upgrade storage or provider not round-tripping the value). SEP-2352 isolation is inactive for this read; ensure your provider round-trips the issuer field.
<name>: <url> (HTTP) - ! Needs authentication
``

  1. Re-inspect ~/.claude/.credentials.json: the record has now been rewritten with redirectUri: http://localhost:3118/callback (a port never used anywhere in this setup — not passed to mcp add, not present in any config, not registered at the identity provider) and accessToken still empty.
  2. Manually deleting this mcpOAuth entry and repeating steps 3–7 reproduces the exact same corrupted record every time, including the same non-existent port 3118.

Expected behavior

  • After a successful OAuth authorization + token exchange, claude mcp list / tool discovery should read the real access token from disk without mutating it, and the server's real tools should become available.
  • If a status/health check needs to re-verify or refresh a token, it should not silently overwrite a valid (or freshly-written) token record with an empty one.

Actual behavior

  • The stored token is corrupted (wiped) by what appears to be the status-check/health-check code path itself, every single time that path runs — independent of whether the underlying OAuth flow actually succeeded.
  • The redirectUri written back is http://localhost:3118/callback, which does not correspond to anything in the local config (--callback-port 8766 was passed explicitly at mcp add time) or the identity provider's registered redirect URIs. This suggests a hardcoded/leftover default value is being used somewhere in the health-check or token-refresh code path, unconditionally overwriting the good record.

Ruled out

  • Identity-provider misconfiguration: confirmed (via the IdP admin API) that the OAuth client for this server is registered with exactly one redirect URI, http://localhost:8766/callback, matching what's passed via --callback-port.
  • Stale local cache: deleting the corrupted mcpOAuth entry and ~/.claude/mcp-needs-auth-cache.json before retrying does not prevent the same corruption from reappearing.

Impact

Any HTTP MCP server requiring a fixed/pre-registered OAuth redirect URI (a common requirement for OIDC providers like Keycloak, which reject dynamic/arbitrary ports) can never complete authentication in Claude Code — the token is destroyed by the CLI's own status-check logic before it can be used, even though the browser-side OAuth flow itself completes successfully.

View original on GitHub ↗