MCP OAuth tokens are cached per server registration name, not per PRM `resource`
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
- Put two MCP servers behind one gateway that advertises the same PRM
resourcefor both (e.g. bare originhttps://gw.example.internal), on paths/a/mcpand/b/mcp. claude mcp add --transport http srv-a https://gw.example.internal/a/mcp -s user --client-id <id>and likewise forsrv-b.- Authenticate
srv-aonly. 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