Claude Desktop: MCP OAuth dead-ends on evicted/expired DCR client_id instead of re-registering
Claude Desktop: MCP OAuth dead-ends on evicted/expired DCR client_id instead of re-registering
Summary
When an MCP server's OAuth authorization endpoint rejects a cached Dynamic Client Registration (DCR) client_id with invalid_client (because the registration was evicted/expired server-side), Claude Desktop reuses the stale cached client_id, opens the authorize URL in the browser, lands on the server's terminal error page, and offers no recovery path. The user is stuck: the connector shows as configured, but no in-app authorization link appears and re-auth is impossible from the app.
The Claude Code CLI (claude mcp login <server>) recovers from the identical server state by performing a fresh DCR and retrying — so the same underlying agent-sdk clearly can re-register; Desktop just doesn't in this flow.
Environment
- Client:
claude-code/2.1.181 … 2.1.197 (claude-desktop, agent-sdk/0.3.181 … 0.3.197)— reproduced across multiple Desktop versions. - Server: FastMCP 2.14.5 OAuth Proxy fronting Okta, remote Streamable HTTP transport, DCR enabled.
- Multiple non-technical users affected; only workaround today is a terminal-based CLI login.
Expected behavior
When /authorize returns invalid_client / "client not registered" for a cached client_id, Claude Desktop should transparently re-run DCR (POST /register) to obtain a fresh client_id and retry authorization — and/or surface the in-app authorization link — without requiring the user to drop to a terminal. This is what the CLI already does.
Actual behavior
Desktop reuses its cached (now-invalid) client_id, opens /authorize?...client_id=<stale>... in the system browser, and the browser renders the server's HTTP 400 error page. No new authorization link is presented in-app; the connector appears configured but unusable. Users report: "it sees the config but says I need to reauthenticate, and I can't."
Why the server's recovery hints aren't used
The server (FastMCP) already returns spec-aligned recovery affordances on this error:
- A
Link: <https://…/register>; rel="…/registration"header, and - For
Accept: application/json, a body{ "error": "invalid_request", "error_description": "Client ID '…' is not registered … clients should automatically re-register by POSTing to the registration_endpoint and retry", "registration_endpoint": "…", "authorization_server_metadata": "…" }.
But because Desktop drives the flow by opening the authorize URL in a browser (Accept: text/html), the server returns the HTML variant of the error — a human-readable dead-end page — and the machine-readable hints (JSON body + Link header) are never seen or acted upon. So the re-registration signal exists; the Desktop flow just can't consume it.
Repro
- Connect Claude Desktop to a remote MCP server that uses OAuth with DCR; authenticate successfully (Desktop caches the issued
client_id). - Cause the server-side registration to disappear (registration TTL expiry, store eviction, or backend reset) — i.e. the
client_idis no longer known to the server. - Start a new Desktop session and invoke the server.
- Desktop attempts
/authorizewith the cachedclient_id→ server returns 400invalid_client. - Result: browser shows an error page; no in-app auth link; no automatic re-registration; user stuck.
- Contrast:
claude mcp login <server>from the CLI re-registers and completes auth successfully against the same server state.
Observed server logs (representative)
GET /authorize?response_type=code&client_id=893f9232-…&redirect_uri=http%3A%2F%2Flocalhost%3A11745%2Foauth%2Fcallback&… → 400
"Unregistered client_id=893f9232-…, returned HTML error response"
GET /authorize?response_type=code&client_id=a990fddd-…&redirect_uri=http%3A%2F%2Flocalhost%3A53786%2Fcallback&… → 400
"Unregistered client_id=a990fddd-…, returned HTML error response"
Suggested fix (any one of)
- Re-register on
invalid_client: when/authorize(or the token exchange) reports theclient_idis unknown/unregistered, discard the cached registration,POST /registerfor a freshclient_id, and retry — matching the CLI's behavior. - Consume the recovery hints: honor the
Link: rel="…/registration"header and/or the JSONregistration_endpointthe server already returns, to trigger re-registration. - Validate before browser hand-off: issue the
/authorizepreflight withAccept: application/jsonso Desktop receives the machine-readableinvalid_client(rather than an HTML dead-end) and can recover before opening the browser. - Fallback UX: if recovery isn't possible, surface an in-app "re-authenticate" affordance instead of leaving the connector in a configured-but-broken state.
Impact
Affects any Desktop user of an OAuth+DCR MCP server whose registration is not permanent server-side. Because Desktop caches the client_id indefinitely and never re-registers, a single server-side eviction permanently breaks the connector for that user with no in-app remedy — pushing non-technical users to a terminal/CLI workaround.
Related issues
- #42070 — "Clear Authentication / Reauthenticate button doesn't clear OAuth state for HTTP MCP servers" (related: even manual re-auth doesn't discard stale client state).
- #60572 — "OAuth MCP connectors: random mid-session disconnected requiring /mcp Reauthenticate" (related re-auth pain).
- #65469 — "[FEATURE] Batch MCP reauthentication" (adjacent).
This report is specifically about automatic recovery from an evicted/expired DCR client_id (invalid_client at /authorize) — which the CLI handles by re-registering but Desktop does not.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗