Severity: All MCP OAuth providers advertising client_id_metadata_document_supported: true are broken in 2.1.80+
Problem
Starting in 2.1.80, Claude Code sends a portless redirect_uri via the published client metadata document, while the local OAuth callback server listens on a specific port (default 3118). This mismatch causes every provider that supports Client ID Metadata Documents (CIMD) to reject the authorization request with invalid_redirect_uri.
Root cause
In 2.1.80, the MCP OAuth provider class gained a clientMetadataUrl getter that always returns a value (defaults to https://claude.ai/oauth/claude-code-client-metadata). Previously this property was undefined on the class.
This changes the client registration path. The OAuth flow has this logic:
if (client_id_metadata_document_supported && clientMetadataUrl)
client_id = clientMetadataUrl // Use metadata document instead of dynamic registration
else
// Dynamic client registration (sends actual redirect_uris with port)
Before 2.1.80, clientMetadataUrl was undefined, so the flow always fell through to dynamic client registration, which sends the correct redirect_uris including the port (e.g. http://localhost:3118/callback).
Now, when a provider advertises client_id_metadata_document_supported: true, Claude Code uses the published metadata at https://claude.ai/oauth/claude-code-client-metadata. That document contains:
{
"redirect_uris": [
"http://localhost/callback",
"http://127.0.0.1/callback"
]
}
These URIs have no port. The actual redirect URI Claude Code constructs is http://localhost:3118/callback. The provider compares the two, they don't match, auth fails.
Reproduction
- Install Claude Code >= 2.1.80
- Configure any MCP server whose OAuth discovery metadata includes
"client_id_metadata_document_supported": true (e.g. Granola: https://mcp-auth.granola.ai/.well-known/oauth-authorization-server)
- Attempt to authenticate
- Browser opens, immediately shows
invalid_redirect_uri
Affected providers (confirmed)
- Granola (
mcp.granola.ai): advertises client_id_metadata_document_supported: true
- Slack (
mcp.slack.com): same error pattern, likely same root cause
Any provider supporting CIMD is affected.
Verification
Confirmed by diffing cli.js between 2.1.79 and 2.1.81. The only relevant change is the addition of get clientMetadataUrl() on the OAuth provider class, which flips the registration path for all CIMD-supporting providers.
Suggested fix
The published client metadata document needs to account for the dynamic port. Options in order of preference:
- Don't use CIMD when the redirect URI has a non-standard port. If the locally constructed
redirect_uri doesn't match any URI in the published metadata, fall back to dynamic client registration.
- Include the default port in the published metadata. Add
http://localhost:3118/callback and http://127.0.0.1:3118/callback to the redirect_uris array at https://claude.ai/oauth/claude-code-client-metadata.
- Use port 80 (implicit) for the local callback server when CIMD is active. This would match the portless URIs in the metadata but may conflict with other services.
Option 1 is the most robust since it handles custom ports via MCP_OAUTH_CALLBACK_PORT and random port fallback.
Secondary issue
When OAuth fails, Claude Code caches an empty accessToken with expiresAt: 0 in the macOS Keychain. On subsequent sessions it sees this entry and skips the OAuth flow entirely, so the browser never opens again. This was reported separately in #29934 but compounds the problem here: users can't even retry after the regression is fixed without manually clearing the Keychain.
Environment
- Claude Code 2.1.81 (regression introduced in 2.1.80)
- Last working version: 2.1.79
- macOS Darwin 25.3.0
Severity: All MCP OAuth providers advertising
client_id_metadata_document_supported: trueare broken in 2.1.80+Problem
Starting in 2.1.80, Claude Code sends a portless
redirect_urivia the published client metadata document, while the local OAuth callback server listens on a specific port (default 3118). This mismatch causes every provider that supports Client ID Metadata Documents (CIMD) to reject the authorization request withinvalid_redirect_uri.Root cause
In 2.1.80, the MCP OAuth provider class gained a
clientMetadataUrlgetter that always returns a value (defaults tohttps://claude.ai/oauth/claude-code-client-metadata). Previously this property was undefined on the class.This changes the client registration path. The OAuth flow has this logic:
Before 2.1.80,
clientMetadataUrlwas undefined, so the flow always fell through to dynamic client registration, which sends the correctredirect_urisincluding the port (e.g.http://localhost:3118/callback).Now, when a provider advertises
client_id_metadata_document_supported: true, Claude Code uses the published metadata athttps://claude.ai/oauth/claude-code-client-metadata. That document contains:{ "redirect_uris": [ "http://localhost/callback", "http://127.0.0.1/callback" ] }These URIs have no port. The actual redirect URI Claude Code constructs is
http://localhost:3118/callback. The provider compares the two, they don't match, auth fails.Reproduction
"client_id_metadata_document_supported": true(e.g. Granola:https://mcp-auth.granola.ai/.well-known/oauth-authorization-server)invalid_redirect_uriAffected providers (confirmed)
mcp.granola.ai): advertisesclient_id_metadata_document_supported: truemcp.slack.com): same error pattern, likely same root causeAny provider supporting CIMD is affected.
Verification
Confirmed by diffing
cli.jsbetween 2.1.79 and 2.1.81. The only relevant change is the addition ofget clientMetadataUrl()on the OAuth provider class, which flips the registration path for all CIMD-supporting providers.Suggested fix
The published client metadata document needs to account for the dynamic port. Options in order of preference:
redirect_uridoesn't match any URI in the published metadata, fall back to dynamic client registration.http://localhost:3118/callbackandhttp://127.0.0.1:3118/callbackto theredirect_urisarray athttps://claude.ai/oauth/claude-code-client-metadata.Option 1 is the most robust since it handles custom ports via
MCP_OAUTH_CALLBACK_PORTand random port fallback.Secondary issue
When OAuth fails, Claude Code caches an empty
accessTokenwithexpiresAt: 0in the macOS Keychain. On subsequent sessions it sees this entry and skips the OAuth flow entirely, so the browser never opens again. This was reported separately in #29934 but compounds the problem here: users can't even retry after the regression is fixed without manually clearing the Keychain.Environment