[BUG] v2.1.172 MCP OAuth regression: reused client registration causes registration↔authorization scope mismatch → invalid_scope ("not allowed to request scope 'openid'") on fresh server auth (Clerk)

Open 💬 0 comments Opened Jun 12, 2026 by ddehart

Bug Description

v2.1.172 introduced OAuth client-registration reuse in the MCP auth flow (debug-log strings: reusing registered port, Found client info, preserveClientRegistration). This creates a registration↔authorization scope mismatch that breaks every fresh claude mcp add + /mcp authenticate against scope-strict DCR providers (reproduced with Clerk):

OAuth error: invalid_scope - The requested scope is invalid, unknown, or malformed.
The OAuth 2.0 Client is not allowed to request scope 'openid'.

The same flow against the same unchanged server and same unchanged Clerk instance worked on v2.1.168/2.1.169.

Root cause (two-attempt interaction)

Attempt 1 — automatic connection at session startup (headless):
The transport-level auth path runs auth() before the wrapper has fetched the authorization-server metadata, so clientMetadata.scope is unset and DCR registers the client with no scope field. Clerk grants its default scope set for scope-less registrations: email offline_access profileno openid (verified directly via curl, below). The authorize URL built in this same pass is also scope-less (Scopes in URL: NOT FOUND) — internally consistent — but the attempt is headless, fails Unauthorized, and the client registration is persisted.

Attempt 2 — interactive /mcp → Authenticate:
The interactive flow fetches AS metadata and derives the request scope from scopes_supported (openid profile email public_metadata private_metadata offline_access user:org:read on Clerk). New in 2.1.172, it preserves the stored client registration from attempt 1 instead of clearing it (Using redirect port: 3118 (reusing registered port)Found client info → registration skipped). It then requests scope=openid+... at /oauth/authorize for a client that was never granted openid → the AS correctly rejects with invalid_scope.

Why ≤2.1.169 worked: the interactive flow unconditionally cleared stored credentials — including the dynamically-registered client — before each attempt, so /mcp always re-registered fresh with the metadata-derived scope. Registration and authorization scopes could never diverge across attempts.

Confirmed by diffing the shipped binaries: the preserveClientRegistration / reusing registered port code is present in 2.1.172 and 2.1.173, absent in 2.1.169 and 2.1.170 (2.1.171 was never published). The change is not mentioned in the 2.1.172 changelog.

Debug log of an actual failure (v2.1.173)

From ~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/…jsonl — note the same client_id in both attempts:

11:55:43  Initializing HTTP transport to https://<server>/mcp        [startup auto-connect]
11:55:44  Saving discovery state (authServer: https://<clerk-instance>)
11:55:44  No client info found                                       [→ DCR happens here, scope-less]
11:55:44  Authorization URL: …/oauth/authorize?…client_id=y805zkOEibMh0TlG…   [no scope param]
11:55:44  Scopes in URL: NOT FOUND
11:55:44  No scopes available from URL or metadata
11:55:44  HTTP Connection failed after 1081ms: Unauthorized
11:55:44  Authentication required for HTTP server

11:56:06  Using redirect port: 3118 (reusing registered port)        [interactive /mcp authenticate]
11:56:07  Fetched OAuth metadata with scope: openid profile email public_metadata private_metadata offline_access user:org:read
11:56:07  Using scope from metadata: openid profile email public_metadata private_metadata offline_access user:org:read
11:56:07  Found client info                                          [← attempt 1's scope-less client REUSED]
11:56:07  Authorization URL: …/oauth/authorize?…client_id=y805zkOEibMh0TlG…&scope=openid+profile+email+…
11:56:08  Error during auth completion: Error: OAuth error: invalid_scope - The requested scope is
          invalid, unknown, or malformed. The OAuth 2.0 Client is not allowed to request scope 'openid'.

Provider-side verification (Clerk grants what registration requests)

$ curl -X POST https://<clerk-instance>/oauth/register -H "Content-Type: application/json" \
    -d '{"client_name":"repro","redirect_uris":["http://localhost:3118/callback"],
         "grant_types":["authorization_code","refresh_token"],"response_types":["code"],
         "token_endpoint_auth_method":"none"}'
# → granted scope: "email offline_access profile"        (no openid)

Registering with "scope": "openid profile email offline_access" is granted openid and the subsequent authorize succeeds. So the AS behaves per RFC 7591/OAuth 2.1; the defect is the client requesting a scope at authorization that it never registered.

Steps to Reproduce

  1. Stand up any streamable-HTTP MCP server gated by Clerk OAuth (RFC 9728 PRM pointing at a Clerk accounts.dev instance; PRM without scopes_supported). Likely reproduces on any DCR provider that scopes clients to their registration request.
  2. claude mcp add --transport http test-server https://<server>/mcp (no stored credentials for this URL).
  3. Start claude in that project — the startup connection attempt registers the scope-less client and fails with "needs authentication" (expected).
  4. /mcp → select the server → Authenticate → browser opens → after login, invalid_scope … not allowed to request scope 'openid'.

On v2.1.169 the identical steps complete successfully (verified 2026-06-08 on the same server + same Clerk instance).

Expected Behavior

Either (a) don't reuse a stored client registration whose registered/granted scopes don't cover the scopes about to be requested — re-register instead (the ≤2.1.169 behavior), or (b) constrain the authorization request to the scopes the stored client was actually granted, or (c) register with the metadata-derived scope on the initial (startup) attempt so the persisted client is usable later.

Environment

  • Claude Code v2.1.173 (native install), also affected: v2.1.172; not affected: ≤ v2.1.170
  • macOS, darwin-arm64 (Darwin 25.5.0)
  • MCP server: custom streamable-HTTP server on Cloudflare Workers, OAuth-gated per MCP authorization spec (RFC 9728 PRM → Clerk authorization server)
  • OAuth provider: Clerk (*.clerk.accounts.dev), RFC 7591 DCR, scope-strict

Workaround

Pinning oauth.scopes on the server config makes registration and authorization use the same scope set on every attempt, avoiding the mismatch.

Related issues

  • #67258 — sibling v2.1.172 OAuth regression (pre-configured oauth.clientId still attempts DCR), suggesting the same release's OAuth refactor shipped multiple behavior changes
  • #55954 / #4540 / #7744 / #41610 — the pre-existing scope-selection design issues (greedy scopes_supported at authorize; scope-less DCR) that this new registration-reuse behavior turns into a hard failure
  • Same bug class in other MCP clients: openai/codex#23242, kirodotdev/Kiro#6045

View original on GitHub ↗