Claude Desktop: MCP OAuth dead-ends on evicted/expired DCR client_id instead of re-registering

Open 💬 2 comments Opened Jul 1, 2026 by itakserman-cloudinary

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

  1. Connect Claude Desktop to a remote MCP server that uses OAuth with DCR; authenticate successfully (Desktop caches the issued client_id).
  2. Cause the server-side registration to disappear (registration TTL expiry, store eviction, or backend reset) — i.e. the client_id is no longer known to the server.
  3. Start a new Desktop session and invoke the server.
  4. Desktop attempts /authorize with the cached client_id → server returns 400 invalid_client.
  5. Result: browser shows an error page; no in-app auth link; no automatic re-registration; user stuck.
  6. 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)

  1. Re-register on invalid_client: when /authorize (or the token exchange) reports the client_id is unknown/unregistered, discard the cached registration, POST /register for a fresh client_id, and retry — matching the CLI's behavior.
  2. Consume the recovery hints: honor the Link: rel="…/registration" header and/or the JSON registration_endpoint the server already returns, to trigger re-registration.
  3. Validate before browser hand-off: issue the /authorize preflight with Accept: application/json so Desktop receives the machine-readable invalid_client (rather than an HTML dead-end) and can recover before opening the browser.
  4. 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗