MCP OAuth never completes: 11 cached registrations all have an empty accessToken, including clients where DCR succeeded
Summary
I went looking for why one MCP server wouldn't authenticate and found something a bit broader than I expected, so I wanted to bring the data over rather than guess at it. On this machine, every cached MCP OAuth registration has an empty accessToken — 11 of them, across servers that took different registration paths. Some of these are servers I'd assumed were working.
The part I think is genuinely new: this includes servers where Dynamic Client Registration succeeded and the provider issued a normal opaque client_id. Those didn't hit the CIMD redirect_uri problem at all, and they still ended up with no token. So the CIMD issue may be one cause among several rather than the whole story.
I'm not certain this is one bug rather than two or three sharing a symptom, and I may be misreading some of it — happy to be told it's expected, or to gather anything more useful.
Environment
- Claude Code 2.1.221
- macOS (Darwin 24.6.0), zsh
- Transport:
httpremote MCP servers, mix of directly-added and plugin-provided
Primary evidence
Structural projection of ~/.claude/.credentials.json under mcpOAuth. I've deliberately printed only field names, token lengths, and the client_id class — no identifiers or secret values:
| Server | accessToken length | client_id class | redirectUri | expiresAt |
|---|---|---|---|---|
| plugin:huggingface-skills:huggingface-skills | 0 | CIMD | http://localhost:3118/callback | absent |
| twoshot | 0 | CIMD | http://localhost:3118/callback | absent |
| ahrefs | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| cf-observability | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| plugin:cloudflare:cloudflare-api | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| plugin:cloudflare:cloudflare-bindings | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| plugin:cloudflare:cloudflare-builds | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| plugin:netlify-skills:netlify | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| plugin:wix:wix-mcp | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| se-ranking | 0 | opaque-DCR | http://localhost:3118/callback | absent |
| stripe | 0 | opaque-DCR | http://localhost:3118/callback | absent |
"CIMD" means client_id is literally https://claude.ai/oauth/claude-code-client-metadata. "opaque-DCR" means the provider issued its own client identifier, i.e. registration worked.
Two things stand out to me:
- 9 of 11 registered successfully via DCR and still have no token. Whatever fails, it seems to fail at or after the callback/token-exchange leg, not only at client registration.
- Every entry pins
redirectUrito port 3118, which matters for the CIMD subset below.
This file was last written Jul 28, so these are stale rather than a single bad afternoon — the flow doesn't appear to have completed at any point since.
The CIMD subset
Three of the servers configured here advertise client_id_metadata_document_supported: true, verified today:
https://mcp.higgsfield.ai cimd=true
https://mcp.sentry.dev cimd=true
https://mcp.canva.com cimd=true
And the client metadata document currently returns:
{
"client_id": "https://claude.ai/oauth/claude-code-client-metadata",
"client_name": "Claude Code",
"redirect_uris": ["http://localhost/callback", "http://127.0.0.1/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
The redirect_uris are portless, while the local callback runs on 3118, so a provider that exact-matches the allowlist rejects the authorize request. I know this ground was already covered in #37747, and I can see from that thread that application_type: "native" was identified as the real gap. From the outside the document doesn't appear to carry application_type yet — but I genuinely can't tell whether that's still queued, superseded by another approach, or something I'm looking at wrong. I mention it only because it's load-bearing for these three servers, not to relitigate a closed thread.
One thing that might be a useful lever: mcp.higgsfield.ai also exposes a working DCR endpoint —
registration_endpoint: https://mcp.higgsfield.ai/oauth2/register
so for that server, DCR was available and CIMD was preferred anyway. If the client fell back to DCR when the CIMD authorize leg is rejected, this particular server would have a path that works today without waiting on the metadata document. That may not generalize, and there may be a good reason to prefer CIMD that I'm not seeing.
For completeness on what isn't this: mcp.slack.com advertises neither CIMD nor a registration endpoint, so its auth failure here looks like a separate matter and I'm not folding it in.
Why pinning the callback port doesn't resolve the CIMD subset
#81643 reports the same shape against Sentry and notes it persists with --callback-port pinned, which led to that issue being titled "not port-related". I think both observations are consistent: any port fails against a portless exact-match allowlist, so pinning a different one doesn't change the outcome. That may also relate to #82096, which covers the localhost vs 127.0.0.1 hostname choice.
A second-order effect worth knowing about
The common workaround for all of this is injecting a static Authorization header via claude mcp add -H. When that's done, claude mcp list reports:
Server rejected the configured Authorization header (HTTP 401). Check that the token is valid for this MCP endpoint — OAuth fallback is disabled when headers.Authorization is set.
That message is clear and I appreciate that it says so explicitly. The practical trap is that the workaround and the bug produce overlapping symptoms, so a header-injected server can look "fixed" while actually being on a path that can't fall back. In my case a server reported ✔ Connected via claude mcp list while the running session had none of its tools registered — the CLI invocation reads cwd-scoped config in a fresh process, so it wasn't reflecting the session's actual state. That cost me a while to spot, and a short note in the docs might save someone else the same detour.
What I'd hope for
Roughly in order of what would help most here:
- Some clarity on whether the empty-token outcome for the DCR servers is a known/tracked issue — that's the piece I couldn't map to an existing report.
- Whether
application_type: "native"on the client metadata document is still planned. - Optionally, DCR fallback when a CIMD authorize leg is rejected.
Related
- #37747 — CIMD
redirect_urismissing port; closed, and the thread is whereapplication_type: "native"was identified - #81643 — same root cause against Sentry, currently open
- #82096 —
redirect_urihostname hardcoding - #82527 — browser reports success while the CLI still shows "Needs authentication"
- #78421 — stuck at unauthenticated, state mismatch
- #83681 — path-based well-known discovery
Thanks for reading a long one, and thanks for the engagement on #37747 — the application_type diagnosis in that thread is what let me narrow this down at all. Glad to run further diagnostics or test a patch if it's useful.