MCP OAuth tokens are cached per server registration name, not per PRM `resource`

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

Summary

Claude Code stores MCP OAuth credentials keyed by the server registration name. When several registrations sit behind one authorization server and advertise an identical resource in their Protected Resource Metadata (RFC 9728), each registration still requires its own full authorization. Sharing the resource value has no effect.

Reproduce

  1. Put two MCP servers behind one gateway that advertises the same PRM resource for both (e.g. bare origin https://gw.example.internal), on paths /a/mcp and /b/mcp.
  2. claude mcp add --transport http srv-a https://gw.example.internal/a/mcp -s user --client-id <id> and likewise for srv-b.
  3. Authenticate srv-a only.
  4. claude mcp list

Observed

srv-a is ✔ Connected; srv-b stays ! Needs authentication. Authenticating srv-b requires a full browser round trip including credential entry.

~/.claude.json shows why: OAuth state is stored under mcpServers.<registration-name>.oauth.*, split per registration. In our case all registrations share the same static clientId and callback port, so the split is purely in the credential store, not in client identity.

Expected

Tokens obtained for a given PRM resource are reusable across registrations that advertise that same resource, since that value is precisely what the token is audienced for.

Why it matters

With an N-service gateway this turns "one authorization per day" into N, each on its own expiry clock, so the re-auth prompts scatter through the working day. We reached this after closing three separate gateway-side workarounds; the remaining constraint is entirely on the client side.

Environment

  • Claude Code 2.1.220, Linux
  • Authorization server: Keycloak, static public client + PKCE S256, offline_access

View original on GitHub ↗