Bug report
Describe the bug
Once a dynamically-registered OAuth client for the hosted MCP server (https://mcp.supabase.com/mcp) stops being recognized by the authorization server, the client is permanently stuck and cannot recover on its own. Every re-auth attempt replays the dead client_id, the browser lands on a raw JSON blob, and the server is unusable indefinitely.
What the user sees in the browser after the client opens the auth URL:
{"message":"Unrecognized client_id"}
…and claude mcp list reports:
plugin:supabase:supabase: https://mcp.supabase.com/mcp (HTTP) - ! Needs authentication
Retrying authentication reproduces the identical page every time. There is no in-product path out of this state.
To Reproduce
The trigger is a stored DCR registration that the authorization server no longer recognizes. I confirmed all three cases below with curl against the live endpoints.
1. A client_id that was never registered → 422 with a non-OAuth error body
curl -s -i "https://api.supabase.com/v1/oauth/authorize?\
client_id=a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d&response_type=code&\
redirect_uri=http%3A%2F%2Flocalhost%3A3118%2Fcallback&scope=organizations%3Aread&\
state=x&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256"
HTTP/2 422
content-type: application/json; charset=utf-8
{"message":"Unrecognized client_id"}
2. A freshly registered client works immediately
curl -s -X POST "https://api.supabase.com/platform/oauth/apps/register" \
-H "Content-Type: application/json" \
-d '{"client_name":"probe","redirect_uris":["http://localhost:54545/callback"],
"grant_types":["authorization_code","refresh_token"],"response_types":["code"],
"token_endpoint_auth_method":"client_secret_post","scope":"organizations:read projects:read"}'
→ 201, and that client_id immediately returns 303 → https://supabase.com/dashboard/authorize?auth_id=… as expected. So DCR itself is healthy.
3. My actual stored registration is the one that died
My MCP client had two cached registrations for the same serverUrl (https://mcp.supabase.com/mcp). Replaying each against /v1/oauth/authorize:
stored client_id |
result |
6e1463bb-9600-4640-947d-c1eb4fa2c239 |
303 — still valid |
07a99c22-a701-4af5-9df7-3f59e4a3d2c8 |
422 {"message":"Unrecognized client_id"} |
(Client IDs are not secrets; the corresponding client_secrets are withheld. Feel free to look up 07a99c22-… on your side — that record should show whatever happened to it.)
So the failing state is not a malformed request from the client. It is a previously-valid, server-issued registration that the authorization server later stopped recognizing.
Expected behavior
Two separate things are broken here, and the second is what makes it unrecoverable.
1. Registrations should not silently stop being recognized
The DCR response advertises a non-expiring credential:
{
"id": "…",
"client_id": "…",
"client_secret": "…",
"client_secret_expires_at": 0,
"redirect_uris": ["http://localhost:54545/callback"]
}
Per RFC 7591 §3.2.1, client_secret_expires_at: 0 means "will not expire." Clients cache the registration on that promise. If these registrations are in fact pruned, GC'd, or invalidated after some period, that contract is being broken — and clients have no way to know it happened.
Either registrations should genuinely persist, or client_secret_expires_at should advertise the real lifetime so clients can re-register before it lapses.
2. The error must be machine-readable so clients can self-heal
This is the core issue. 422 {"message":"Unrecognized client_id"} is neither a human-facing page nor an OAuth-conformant error — so it helps nobody:
- For the user: they get a bare JSON blob on a white page. No explanation, no "reconnect" link, no indication that the fix is to re-authorize.
- For the client: there is no
error field. RFC 6749 §5.2 defines invalid_client for exactly this condition, and the MCP authorization spec builds on RFC 7591 DCR. A client that received error: "invalid_client" could discard the dead registration, re-run DCR, and recover transparently. Given a bespoke {"message": …} body, it cannot distinguish "your stored registration is gone, re-register" from any other failure — so it retries the same dead client_id forever.
I recognize that RFC 6749 §4.1.2.1 correctly says the AS MUST NOT redirect to an unvalidated redirect_uri and SHOULD instead inform the resource owner. Not redirecting is right. But the current response satisfies neither half of that: it doesn't inform the user in any usable way, and it doesn't give the client a standard error code to act on.
Suggested fix, roughly in priority order:
- Return a standard OAuth error code (
invalid_client) alongside the message, so MCP clients can detect a dead registration and re-register automatically. This alone turns a permanent dead end into a self-healing hiccup.
- Render an actual HTML error page for the
/authorize browser entry point explaining what happened and linking back to re-authorization, instead of serving raw JSON to a human.
- Make registration lifetime honest — either persist registrations as
client_secret_expires_at: 0 promises, or advertise the real expiry.
- Consider returning
registration_client_uri + registration_access_token (RFC 7592) so clients can verify a stored registration is still live before starting a user-facing browser flow.
Additional context
Environment
|
|
| MCP server |
https://mcp.supabase.com/mcp (hosted, "Supabase MCP (Beta)") |
| Authorization server |
https://api.supabase.com |
| Client |
Claude Code 2.1.223 (official supabase plugin 0.1.13), macOS 15 / arm64 |
| Redirect URI |
http://localhost:3118/callback |
Discovery itself is fine — /.well-known/oauth-protected-resource/mcp and /.well-known/oauth-authorization-server both resolve correctly, and the 401 challenge on /mcp carries a proper WWW-Authenticate header with resource_metadata. The failure is confined to replaying a stale client_id at /v1/oauth/authorize.
Workaround for anyone else hitting this: the stale registration is cached client-side, so you have to clear it. In Claude Code the cached entry lives in the OS keychain (Claude Code-credentials → mcpOAuth → plugin:supabase:supabase|…); removing that server's entry and re-authenticating forces a fresh DCR registration, which works. Not something a typical user will discover on their own — hence this report.
Possibly related to #177, which reports a different non-standard {"message": …} error shape from the same OAuth server.
Bug report
Describe the bug
Once a dynamically-registered OAuth client for the hosted MCP server (
https://mcp.supabase.com/mcp) stops being recognized by the authorization server, the client is permanently stuck and cannot recover on its own. Every re-auth attempt replays the deadclient_id, the browser lands on a raw JSON blob, and the server is unusable indefinitely.What the user sees in the browser after the client opens the auth URL:
{"message":"Unrecognized client_id"}…and
claude mcp listreports:Retrying authentication reproduces the identical page every time. There is no in-product path out of this state.
To Reproduce
The trigger is a stored DCR registration that the authorization server no longer recognizes. I confirmed all three cases below with
curlagainst the live endpoints.1. A
client_idthat was never registered →422with a non-OAuth error body2. A freshly registered client works immediately
→
201, and thatclient_idimmediately returns303 → https://supabase.com/dashboard/authorize?auth_id=…as expected. So DCR itself is healthy.3. My actual stored registration is the one that died
My MCP client had two cached registrations for the same
serverUrl(https://mcp.supabase.com/mcp). Replaying each against/v1/oauth/authorize:client_id6e1463bb-9600-4640-947d-c1eb4fa2c239303— still valid07a99c22-a701-4af5-9df7-3f59e4a3d2c8422 {"message":"Unrecognized client_id"}(Client IDs are not secrets; the corresponding
client_secrets are withheld. Feel free to look up07a99c22-…on your side — that record should show whatever happened to it.)So the failing state is not a malformed request from the client. It is a previously-valid, server-issued registration that the authorization server later stopped recognizing.
Expected behavior
Two separate things are broken here, and the second is what makes it unrecoverable.
1. Registrations should not silently stop being recognized
The DCR response advertises a non-expiring credential:
{ "id": "…", "client_id": "…", "client_secret": "…", "client_secret_expires_at": 0, "redirect_uris": ["http://localhost:54545/callback"] }Per RFC 7591 §3.2.1,
client_secret_expires_at: 0means "will not expire." Clients cache the registration on that promise. If these registrations are in fact pruned, GC'd, or invalidated after some period, that contract is being broken — and clients have no way to know it happened.Either registrations should genuinely persist, or
client_secret_expires_atshould advertise the real lifetime so clients can re-register before it lapses.2. The error must be machine-readable so clients can self-heal
This is the core issue.
422 {"message":"Unrecognized client_id"}is neither a human-facing page nor an OAuth-conformant error — so it helps nobody:errorfield. RFC 6749 §5.2 definesinvalid_clientfor exactly this condition, and the MCP authorization spec builds on RFC 7591 DCR. A client that receivederror: "invalid_client"could discard the dead registration, re-run DCR, and recover transparently. Given a bespoke{"message": …}body, it cannot distinguish "your stored registration is gone, re-register" from any other failure — so it retries the same deadclient_idforever.I recognize that RFC 6749 §4.1.2.1 correctly says the AS MUST NOT redirect to an unvalidated
redirect_uriand SHOULD instead inform the resource owner. Not redirecting is right. But the current response satisfies neither half of that: it doesn't inform the user in any usable way, and it doesn't give the client a standard error code to act on.Suggested fix, roughly in priority order:
invalid_client) alongside the message, so MCP clients can detect a dead registration and re-register automatically. This alone turns a permanent dead end into a self-healing hiccup./authorizebrowser entry point explaining what happened and linking back to re-authorization, instead of serving raw JSON to a human.client_secret_expires_at: 0promises, or advertise the real expiry.registration_client_uri+registration_access_token(RFC 7592) so clients can verify a stored registration is still live before starting a user-facing browser flow.Additional context
Environment
https://mcp.supabase.com/mcp(hosted, "Supabase MCP (Beta)")https://api.supabase.com2.1.223(officialsupabaseplugin0.1.13), macOS 15 / arm64http://localhost:3118/callbackDiscovery itself is fine —
/.well-known/oauth-protected-resource/mcpand/.well-known/oauth-authorization-serverboth resolve correctly, and the401challenge on/mcpcarries a properWWW-Authenticateheader withresource_metadata. The failure is confined to replaying a staleclient_idat/v1/oauth/authorize.Workaround for anyone else hitting this: the stale registration is cached client-side, so you have to clear it. In Claude Code the cached entry lives in the OS keychain (
Claude Code-credentials→mcpOAuth→plugin:supabase:supabase|…); removing that server's entry and re-authenticating forces a fresh DCR registration, which works. Not something a typical user will discover on their own — hence this report.Possibly related to #177, which reports a different non-standard
{"message": …}error shape from the same OAuth server.