fix(mcp-oauth): allowlist Cursor's OAuth callbacks so its dynamic registration succeeds (#2225)

* fix(mcp-oauth): allowlist Cursor's OAuth callbacks so its dynamic registration succeeds

Cursor (IDE, CLI, and Grok Bot on top of it) registers three redirect URIs
in one /register request: cursor://anysphere.cursor-mcp/oauth/callback,
https://www.cursor.com/agents/mcp/oauth/callback and
http://localhost:8787/callback. Only the loopback matched a built-in
pattern and /register fails the whole set on any unknown URI, so every
Cursor connection to the URL we hand out in Settings died with
"Redirect URI not allowed". Users cannot self-register the cursor://
form either (the settings panel requires https).

Add a built-in `cursor` provider with the two non-loopback callbacks as
exact matches (no cursor.com prefix), name it "Cursor (Anysphere)" on
the consent page, list the pre-approved clients in the OAuth clients
settings text (sv + en) and the mcp-server rules, and cover the
register, allowlist and consent paths with tests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBFeTvQXgMCNXR6fG9drff

* fix(mcp-oauth): show the cursor:// deeplink unverified and let CSP pass its post-consent redirect

Review findings on #2225, one pass:

- Skeptic (correctness), REFUTED: new URL('cursor://...').origin is the
  string "null", so the consent page emitted form-action 'self' null and
  Chromium would block the 303 to the deeplink after Allow. The header
  now uses the scheme-source (cursor:) when the origin is opaque; a test
  pins the header on the cursor:// URI.
- Skeptic (security), CodeRabbit (Major) and Superagent (P2): a custom
  scheme can be claimed by any local app (RFC 8252 section 8.4), so it
  must not be presented as a vendor-verified callback. The deeplink is
  its own provider, cursor_deeplink, rendered "Cursor (Anysphere)" with
  the localhost tag "Din egen dator" and verified: false. The https
  cursor.com callback keeps the verified label. A test pins that a code
  minted without a code_challenge can never be exchanged, which is what
  keeps a scheme hijack from turning into a token.
- CodeRabbit (Minor): the rules doc now says the Grok callback matches
  with or without the trailing slash.
- Regression skeptic: docs/WHITELABEL.md listed only Claude and
  localhost and pointed at the wrong file; now lists the built-ins and
  points at lib/auth/oauth-allowlist.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DBFeTvQXgMCNXR6fG9drff

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-09-03 15:33:58 +02:00
committed by GitHub
co-authored by Claude Fable 5.1
parent d900fea1a8
commit bc5da12372
11 changed files with 160 additions and 11 deletions
+2 -2
View File
@@ -2695,7 +2695,7 @@
},
"settings_oauth_clients": {
"title": "OAuth clients",
"description": "Register redirect URIs for self-built MCP clients. Claude.ai and localhost are already allowed by default: only register here if you're building your own app.",
"description": "Register redirect URIs for self-built MCP clients. Claude, ChatGPT, Grok, Cursor and localhost are already allowed by default: only register here if you're building your own app.",
"register_uri": "Register URI",
"default_client_name": "OAuth client",
"toast_fetch_failed": "Could not load OAuth clients",
@@ -2710,7 +2710,7 @@
"registered_on": "Registered",
"revoke_aria": "Revoke {name}",
"register_dialog_title": "Register redirect URI",
"register_dialog_description": "Only for self-built MCP clients with a public HTTPS callback. Do <bold>not</bold> add <code>localhost</code> or <code>claude.ai</code>: they already work without registration. The URI is compared verbatim against the redirect_uri parameter in the OAuth flow.",
"register_dialog_description": "Only for self-built MCP clients with a public HTTPS callback. Do <bold>not</bold> add <code>localhost</code> or the Claude, ChatGPT, Grok or Cursor callbacks: they already work without registration. The URI is compared verbatim against the redirect_uri parameter in the OAuth flow.",
"client_name_label": "Client name",
"client_name_placeholder": "e.g. My bookkeeping agent",
"redirect_uri_label": "Redirect URI",
+2 -2
View File
@@ -2695,7 +2695,7 @@
},
"settings_oauth_clients": {
"title": "OAuth-klienter",
"description": "Registrera redirect-URI:er för egenutvecklade MCP-klienter. Claude.ai och localhost är redan godkända som standard: registrera bara här om du bygger en egen app.",
"description": "Registrera redirect-URI:er för egenutvecklade MCP-klienter. Claude, ChatGPT, Grok, Cursor och localhost är redan godkända som standard: registrera bara här om du bygger en egen app.",
"register_uri": "Registrera URI",
"default_client_name": "OAuth-klient",
"toast_fetch_failed": "Kunde inte hämta OAuth-klienter",
@@ -2710,7 +2710,7 @@
"registered_on": "Registrerad",
"revoke_aria": "Återkalla {name}",
"register_dialog_title": "Registrera redirect URI",
"register_dialog_description": "Bara för egenbyggda MCP-klienter med en publik HTTPS-callback. Lägg <bold>inte</bold> till <code>localhost</code> eller <code>claude.ai</code>: de fungerar redan utan registrering. URI:n jämförs ord-för-ord mot redirect_uri-parametern i OAuth-flödet.",
"register_dialog_description": "Bara för egenbyggda MCP-klienter med en publik HTTPS-callback. Lägg <bold>inte</bold> till <code>localhost</code> eller callbacks för Claude, ChatGPT, Grok eller Cursor: de fungerar redan utan registrering. URI:n jämförs ord-för-ord mot redirect_uri-parametern i OAuth-flödet.",
"client_name_label": "Klientnamn",
"client_name_placeholder": "t.ex. Min bokföringsagent",
"redirect_uri_label": "Redirect URI",