From bc5da1237290084c7f89b646e069d72b2f333dc2 Mon Sep 17 00:00:00 2001 From: Mattsson <111893710+mattssonn@users.noreply.github.com> Date: Thu, 3 Sep 2026 15:33:58 +0200 Subject: [PATCH] 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 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 Claude-Session: https://claude.ai/code/session_01DBFeTvQXgMCNXR6fG9drff --------- Co-authored-by: Claude Fable 5.1 --- .claude/rules/mcp-server.md | 2 +- DECISIONS.md | 2 + .../authorize/__tests__/route.test.ts | 61 +++++++++++++++++++ app/api/mcp-oauth/authorize/route.ts | 17 +++++- .../register/__tests__/route.test.ts | 29 +++++++++ docs/WHITELABEL.md | 2 +- lib/auth/__tests__/oauth-allowlist.test.ts | 15 +++++ lib/auth/__tests__/oauth-codes.test.ts | 10 +++ lib/auth/oauth-allowlist.ts | 25 +++++++- messages/en.json | 4 +- messages/sv.json | 4 +- 11 files changed, 160 insertions(+), 11 deletions(-) diff --git a/.claude/rules/mcp-server.md b/.claude/rules/mcp-server.md index f21caf40..e6ba775e 100644 --- a/.claude/rules/mcp-server.md +++ b/.claude/rules/mcp-server.md @@ -11,7 +11,7 @@ Accounted exposes its bookkeeping engine as an MCP server for Claude Desktop/Cod **MCP extension** (`extensions/general/mcp-server/`): 150+ tools (count `name: 'gnubok_` in `server.ts`; docs say "150+", never an exact number, because it drifts) covering transactions, categorization, customers/suppliers, invoices, accounts, fiscal periods, reports (trial balance, GL, BS, IS, AR/supplier ledger, VAT, KPI), reconciliation, salary runs, AGI, year-end, document upload, and loadable skills. JSON-RPC 2.0. Endpoint: `/api/extensions/ext/mcp-server/mcp`. -**OAuth 2.1** for Claude, ChatGPT and Grok connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `chatgpt.com/connector/oauth/*`, `chatgpt.com/connector_platform_oauth_redirect`, `grok.com/connectors-oauth-exchange-code/` (exact), `localhost`. +**OAuth 2.1** for Claude, ChatGPT, Grok and Cursor connectors: `.well-known/oauth-protected-resource` + `.well-known/oauth-authorization-server` discovery; `/api/mcp-oauth/authorize`, `/token` (PKCE), `/register`. Stateless AES-256-GCM auth codes (`lib/auth/oauth-codes.ts`). Single-use via `oauth_used_codes`. Allowlist: `claude.ai/api/*`, `claude.com/api/*`, `chatgpt.com/connector/oauth/*`, `chatgpt.com/connector_platform_oauth_redirect`, `grok.com/connectors-oauth-exchange-code/` (exact, trailing slash optional), `cursor://anysphere.cursor-mcp/oauth/callback` (exact; shown unverified on the consent page, loopback trust), `www.cursor.com/agents/mcp/oauth/callback` (exact), `localhost`. `/register` rejects the whole request if any one URI is unknown (Cursor sends three at once). **npm packages**: `packages/accounted-mcp` is the Accounted stdio-to-HTTP bridge for new installs. `packages/gnubok-mcp` is the permanent compatibility package for existing configurations. diff --git a/DECISIONS.md b/DECISIONS.md index 4e90996c..7e5a24c2 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1522,5 +1522,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-09-02] Nyckeltal "Resultat per månad" shows the exact per-month figures as an always-on list under the bars (#2198), not behind an "Anpassa" toggle: a preference would touch the type, the PUT schema, the strict preferences-body validator, the dialog and its tests for a switch nobody turns off. Per-bar compact labels are conditional on a glyph-width fit rule and fall back to the single latest label, so they never overlap. Left alone: the monthly path counts only posted entries while the year-total path also counts reversed originals (pinned as intended in tests/pg/kpi-report-aggregates-rpc.pg.test.ts), so a same-year storno makes the sum of months differ from Nettoresultat; visible as numbers now, founder call whether to align the two. [2026-09-03] Kundorder PGRST201 fixed by hinting the three sales_order_items embeds AND by teaching scripts/checks/ambiguous-embed.mjs to parse composite FOREIGN KEY (a, b) constraints, instead of a bespoke source-scan test: the guard is the sanctioned owner of this class (decision 2026-09-01) and it missed the pair only because its parser was single-column; with the fix it derives the same 17 pairs prod reports and flags all three shipped sites on main. [2026-09-03] Email-change double-submit gate is a dedicated per-user claim table + SECURITY DEFINER RPC (migration 20260903083000), not idempotency_keys and not an advisory lock: idempotency_keys requires a company_id the account-level route does not have, and a transaction-scoped advisory lock cannot cover the GoTrue call that happens outside the transaction. +[2026-09-03] Cursor MCP OAuth callbacks are built-in allowlist entries, exact-matched: cursor://anysphere.cursor-mcp/oauth/callback and https://www.cursor.com/agents/mcp/oauth/callback (the loopback http://localhost:8787/callback already passes the local rule). Cursor's DCR sends all three in one request and /register rejects the whole set on any unknown URI, so the Cursor path advertised in Settings never worked (reported by a byrå user 2026-09-03). Exact match, not a cursor.com prefix, so no other cursor.com path can receive codes; the custom scheme is accepted despite RFC 8252 section 8.4 because the code is PKCE-bound and the loopback form carries the same local-machine trust. Users cannot self-register the cursor:// form (the settings panel requires https), which is why it is built in. Grok Bot rides on Cursor's MCP stack and stays broken on Cursor's side (forum thread 168052, open as of 2026-09-02); nothing server-side fixes that. [2026-09-03] Skattekonto through Connect = the existing data proxy plus CONNECT_SKV_CANARY_COMPANIES, not a separate sync operation: the provider logic is two GETs and the dedup keys stay on the ledger; system (certificate) auth is still not brokered because hosted has no certificate configured, so every hosted skattekonto read is a user-token call the proxy already carries. [2026-09-03] Old-address social identities are unlinked by a BEFORE UPDATE trigger on auth.users (migration 20260903110000), not by the /auth/callback done path: the callback never runs for a completing click from a browser without a session, and admin-side changes bypass it entirely; the trigger covers every path and keeps the email identity, password and BankID intact. +[2026-09-03] The cursor:// deeplink is its own allowlist provider (cursor_deeplink) rendered "Din egen dator" and never "Verifierad", after the skeptic, CodeRabbit and Superagent all made the same point: a custom scheme can be claimed by any local app (RFC 8252 section 8.4), so it carries loopback trust, not vendor trust, and the consent page must not say otherwise; https://www.cursor.com/... keeps the verified label. Same pass fixed the consent-page CSP for custom schemes: new URL('cursor://...').origin is the string "null", so form-action became `'self' null` and Chromium would have blocked the post-consent 303 (correctness skeptic refutation); the header now uses the scheme-source (`cursor:`) when the origin is opaque. Not done: rejecting a missing code_challenge at /authorize. A code minted without one is unexchangeable (verifyPkce against an empty challenge is always false, now pinned by a test), so it is fail-closed; making it fail earlier is a separate change touching every client. diff --git a/app/api/mcp-oauth/authorize/__tests__/route.test.ts b/app/api/mcp-oauth/authorize/__tests__/route.test.ts index 115a0d2f..0610d2f8 100644 --- a/app/api/mcp-oauth/authorize/__tests__/route.test.ts +++ b/app/api/mcp-oauth/authorize/__tests__/route.test.ts @@ -41,6 +41,8 @@ import { GET, POST } from '../route' const CLAUDE: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'claude' } const CHATGPT: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'chatgpt' } const GROK: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'grok' } +const CURSOR: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'cursor' } +const CURSOR_DEEPLINK: RedirectUriResolution = { allowed: true, kind: 'built_in', provider: 'cursor_deeplink' } const REGISTERED: RedirectUriResolution = { allowed: true, kind: 'registered', @@ -384,6 +386,65 @@ describe('client identity on the consent page', () => { expect(html).not.toContain('En extern applikation') }) + it('names Cursor as a verified client for the cursor.com callback', async () => { + mocks.resolveRedirectUri.mockResolvedValue(CURSOR) + const html = await ( + await GET( + new Request( + buildAuthorizeUrl({ + ...params, + redirect_uri: 'https://www.cursor.com/agents/mcp/oauth/callback', + }), + ), + ) + ).text() + + expect(html).toContain('Cursor (Anysphere)') + expect(html).toContain('Verifierad') + expect(html).toContain('www.cursor.com') + expect(html).not.toContain('En extern applikation') + }) + + it('shows the cursor:// deeplink as Cursor but unverified, like localhost', async () => { + // Any local app can claim a custom scheme (RFC 8252 section 8.4), so the + // page must not present it as a vendor-verified callback. + mocks.resolveRedirectUri.mockResolvedValue(CURSOR_DEEPLINK) + const html = await ( + await GET( + new Request( + buildAuthorizeUrl({ + ...params, + redirect_uri: 'cursor://anysphere.cursor-mcp/oauth/callback', + }), + ), + ) + ).text() + + expect(html).toContain('Cursor (Anysphere)') + expect(html).toContain('Din egen dator') + expect(html).not.toContain('Verifierad') + expect(html).not.toContain('En extern applikation') + }) + + it('form-action uses a scheme-source for a custom-scheme redirect_uri', async () => { + // new URL('cursor://...').origin is the string "null", which CSP reads as + // a host named "null": with that the post-consent 303 to the deeplink is + // blocked in Chromium. The scheme-source form (cursor:) lets it through. + mocks.resolveRedirectUri.mockResolvedValue(CURSOR_DEEPLINK) + const response = await GET( + new Request( + buildAuthorizeUrl({ + ...params, + redirect_uri: 'cursor://anysphere.cursor-mcp/oauth/callback', + }), + ), + ) + expect(response.status).toBe(200) + const csp = response.headers.get('Content-Security-Policy') + expect(csp).toMatch(/form-action 'self' cursor:(;|$)/) + expect(csp).not.toContain('null') + }) + it('shows client_name and redirect host for a DB-registered client, never marked verified', async () => { mocks.resolveRedirectUri.mockResolvedValue(REGISTERED) const html = await ( diff --git a/app/api/mcp-oauth/authorize/route.ts b/app/api/mcp-oauth/authorize/route.ts index 068993ec..afefe8f8 100644 --- a/app/api/mcp-oauth/authorize/route.ts +++ b/app/api/mcp-oauth/authorize/route.ts @@ -828,12 +828,18 @@ export async function GET(request: Request) { // form-action re-checks every hop in the redirect chain. With only 'self' // the browser would block the post-consent redirect. The origin is safe // to whitelist here because resolveRedirectUri() already gated it above. - const redirectOrigin = new URL(redirectUri).origin + // + // A custom-scheme callback (cursor://...) has the opaque origin "null", + // which CSP would read as a host literally named "null" and match nothing, + // so the post-consent 303 would be blocked in Chromium. A scheme-source + // (cursor:) is the only form CSP offers for such a URI. + const redirectUrl = new URL(redirectUri) + const formActionSource = redirectUrl.origin === 'null' ? redirectUrl.protocol : redirectUrl.origin const csp = [ "default-src 'none'", `script-src 'nonce-${cspNonce}'`, "style-src 'unsafe-inline'", - `form-action 'self' ${redirectOrigin}`, + `form-action 'self' ${formActionSource}`, "base-uri 'none'", "frame-ancestors 'none'", ].join('; ') @@ -1094,6 +1100,13 @@ function describeClient( return { name: 'ChatGPT (OpenAI)', tag: 'Verifierad', verified: true } case 'grok': return { name: 'Grok (xAI)', tag: 'Verifierad', verified: true } + case 'cursor': + return { name: 'Cursor (Anysphere)', tag: 'Verifierad', verified: true } + case 'cursor_deeplink': + // A custom scheme can be claimed by any local app (RFC 8252 section + // 8.4), so it carries loopback trust, not vendor trust: same tag as + // localhost, never marked verified. + return { name: 'Cursor (Anysphere)', tag: 'Din egen dator', verified: false } case 'local': return { name: 'Lokal utveckling (localhost)', tag: 'Din egen dator', verified: false } } diff --git a/app/api/mcp-oauth/register/__tests__/route.test.ts b/app/api/mcp-oauth/register/__tests__/route.test.ts index df8cdd15..53ae4dfd 100644 --- a/app/api/mcp-oauth/register/__tests__/route.test.ts +++ b/app/api/mcp-oauth/register/__tests__/route.test.ts @@ -63,6 +63,35 @@ describe('POST /api/mcp-oauth/register', () => { expect(body.token_endpoint_auth_method).toBe('none') }) + it('accepts the three redirect_uris Cursor registers in one request', async () => { + // Cursor sends the legacy deeplink, the Cloud Agents web fallback and the + // RFC 8252 loopback together; one unknown URI used to fail the whole set. + const uris = [ + 'cursor://anysphere.cursor-mcp/oauth/callback', + 'https://www.cursor.com/agents/mcp/oauth/callback', + 'http://localhost:8787/callback', + ] + const response = await POST(createRequest({ + client_name: 'Cursor', + redirect_uris: uris, + token_endpoint_auth_method: 'none', + })) + expect(response.status).toBe(201) + const body = await response.json() + expect(body.redirect_uris).toEqual(uris) + }) + + it('rejects other cursor.com paths and other cursor:// authorities', async () => { + for (const uri of [ + 'https://www.cursor.com/agents/mcp/oauth/callback2', + 'https://cursor.com/agents/mcp/oauth/callback', + 'cursor://evil.extension/oauth/callback', + ]) { + const response = await POST(createRequest({ redirect_uris: [uri] })) + expect(response.status, uri).toBe(400) + } + }) + it('rejects other grok.com paths', async () => { const response = await POST(createRequest({ redirect_uris: ['https://grok.com/oauth/callback'], diff --git a/docs/WHITELABEL.md b/docs/WHITELABEL.md index e783299e..e0860cd1 100644 --- a/docs/WHITELABEL.md +++ b/docs/WHITELABEL.md @@ -100,7 +100,7 @@ A few things that look brand-related but are configured elsewhere: - **Supabase auth emails** (password reset, magic link): set in the Supabase dashboard for your project, not in code. - **Resend sending domain**: verify `noreply@your-brand.se` (or wherever) in Resend, set `RESEND_FROM_EMAIL`. - **DNS / domain**: point `app.your-brand.se` at your Vercel deployment. -- **OAuth redirect allowlist for MCP**: `app/api/mcp-oauth/authorize/route.ts` lists `claude.ai/api/*`, `claude.com/api/*`, and localhost. Your domain is the OAuth issuer, not a redirect target: no change needed unless you're integrating with new MCP clients. +- **OAuth redirect allowlist for MCP**: `lib/auth/oauth-allowlist.ts` has built-in entries for Claude (`claude.ai/api/*`, `claude.com/api/*`), ChatGPT, Grok, Cursor and localhost; anything else is registered per user under Settings > API & MCP > OAuth clients. Your domain is the OAuth issuer, not a redirect target: no change needed unless you're integrating with new MCP clients. - **iCal feed PRODID** (`lib/calendar/ics-generator.ts`): defaults to `erp-base.se`, callers may pass their domain. - **`NEXT_PUBLIC_APP_URL`**: used as the OAuth issuer and safe auth-link fallback. For a dedicated one-brand deployment, set this to your domain (e.g. `https://app.your-brand.se`). For a shared hosted deployment, keep the canonical main app URL here and register additional hosts through `NEXT_PUBLIC_WHITELABEL_DOMAINS`. - **Skatteverket submission identity**: `extensions/general/skatteverket/lib/api-client.ts` does not set a custom `User-Agent`; submissions go out with the Node/Vercel runtime default. If your deployment needs to identify itself to Skatteverket under a different brand, that's a future enhancement (env var + header), not something the current branding service covers. diff --git a/lib/auth/__tests__/oauth-allowlist.test.ts b/lib/auth/__tests__/oauth-allowlist.test.ts index bb03eecf..2fbd6fe5 100644 --- a/lib/auth/__tests__/oauth-allowlist.test.ts +++ b/lib/auth/__tests__/oauth-allowlist.test.ts @@ -26,6 +26,18 @@ describe('isBuiltInRedirectUri', () => { ['https://grok.com/other/path', false], ['https://grok.com.evil.com/connectors-oauth-exchange-code/', false], ['http://grok.com/connectors-oauth-exchange-code/', false], + ['cursor://anysphere.cursor-mcp/oauth/callback', true], + ['cursor://anysphere.cursor-mcp/oauth/callback/', false], + ['cursor://anysphere.cursor-mcp/oauth/callback?x=1', false], + ['cursor://anysphere.cursor-mcp/oauth/callback/extra', false], + ['cursor://evil.extension/oauth/callback', false], + ['https://www.cursor.com/agents/mcp/oauth/callback', true], + ['https://www.cursor.com/agents/mcp/oauth/callback/', false], + ['https://www.cursor.com/agents/mcp/oauth/callback2', false], + ['https://cursor.com/agents/mcp/oauth/callback', false], + ['https://www.cursor.com.evil.com/agents/mcp/oauth/callback', false], + ['http://www.cursor.com/agents/mcp/oauth/callback', false], + ['http://localhost:8787/callback', true], ['http://localhost:3000/cb', true], ['http://localhost/cb', true], ['http://127.0.0.1:8080/cb', true], @@ -46,6 +58,9 @@ describe('builtInRedirectProvider', () => { ['https://chatgpt.com/connector_platform_oauth_redirect', 'chatgpt'], ['https://grok.com/connectors-oauth-exchange-code/', 'grok'], ['https://grok.com/connectors-oauth-exchange-code/extra', null], + ['cursor://anysphere.cursor-mcp/oauth/callback', 'cursor_deeplink'], + ['https://www.cursor.com/agents/mcp/oauth/callback', 'cursor'], + ['http://localhost:8787/callback', 'local'], ['http://localhost:3000/cb', 'local'], ['http://127.0.0.1:8080/cb', 'local'], ['https://claude-login.example/cb', null], diff --git a/lib/auth/__tests__/oauth-codes.test.ts b/lib/auth/__tests__/oauth-codes.test.ts index 0dcd7b95..22452045 100644 --- a/lib/auth/__tests__/oauth-codes.test.ts +++ b/lib/auth/__tests__/oauth-codes.test.ts @@ -133,3 +133,13 @@ describe('hashAuthCode', () => { expect(hash1).toBe(hash2) }) }) + +describe('verifyPkce with a missing challenge', () => { + it('never verifies when the code was minted without a code_challenge', () => { + // /authorize does not reject a missing code_challenge; it mints the code + // with an empty one. That must stay unexchangeable, otherwise a + // custom-scheme (cursor://) hijacker could skip PKCE entirely. + expect(verifyPkce('any-verifier', '')).toBe(false) + expect(verifyPkce('', '')).toBe(false) + }) +}) diff --git a/lib/auth/oauth-allowlist.ts b/lib/auth/oauth-allowlist.ts index 1df66d1f..ea14d9c5 100644 --- a/lib/auth/oauth-allowlist.ts +++ b/lib/auth/oauth-allowlist.ts @@ -14,13 +14,13 @@ import { scopeKind, type ApiKeyScope } from './scope-catalog' /** * Identity of a built-in client, derived from the redirect URI pattern that * matched. Rendered on the consent page so the user can tell a real Claude / - * ChatGPT / Grok connector from a look-alike registration. + * ChatGPT / Grok / Cursor connector from a look-alike registration. */ -export type BuiltInProvider = 'claude' | 'chatgpt' | 'grok' | 'local' +export type BuiltInProvider = 'claude' | 'chatgpt' | 'grok' | 'cursor' | 'cursor_deeplink' | 'local' /** * Built-in redirect URI patterns. These bypass the DB lookup entirely so - * the Claude, ChatGPT and Grok connectors keep working without seeded rows, and so + * the Claude, ChatGPT, Grok and Cursor connectors keep working without seeded rows, and so * local development never depends on having a registration. * * ChatGPT uses a per-connector-instance callback path @@ -36,6 +36,23 @@ export type BuiltInProvider = 'claude' | 'chatgpt' | 'grok' | 'local' * itself: the slash form 308s to the no-slash form on the same origin, so * both are accepted. Matched as an exact path, never a prefix, so a future * grok.com path cannot ride on this entry. + * + * Cursor (IDE and CLI) registers through /register with three redirect URIs + * in one request: the legacy custom-scheme deeplink + * cursor://anysphere.cursor-mcp/oauth/callback, the web fallback used by + * Cloud Agents and Automations https://www.cursor.com/agents/mcp/oauth/callback, + * and the RFC 8252 loopback http://localhost:8787/callback that current + * builds actually redirect to (Cursor staff statement, forum.cursor.com + * thread 165019). /register rejects the whole set when any one URI is + * unknown, so the two non-loopback callbacks are allowlisted here as exact + * matches; the loopback already passes through the local rule. The custom + * scheme is accepted despite RFC 8252 section 8.4 (any local app can claim a + * scheme) because the code is PKCE-bound to the client that started the flow + * (S256 only, verifier required at /token, a code minted without a challenge + * can never be exchanged) and the loopback form carries the same + * local-machine trust. It is its own provider, `cursor_deeplink`, so the + * consent page can show it unverified like localhost: a scheme proves + * nothing about who receives the code, an https host does. */ const BUILT_IN_PATTERNS: readonly { pattern: RegExp; provider: BuiltInProvider }[] = [ { pattern: /^https:\/\/claude\.ai\/api\//, provider: 'claude' }, @@ -43,6 +60,8 @@ const BUILT_IN_PATTERNS: readonly { pattern: RegExp; provider: BuiltInProvider } { pattern: /^https:\/\/chatgpt\.com\/connector\/oauth\//, provider: 'chatgpt' }, { pattern: /^https:\/\/chatgpt\.com\/connector_platform_oauth_redirect$/, provider: 'chatgpt' }, { pattern: /^https:\/\/grok\.com\/connectors-oauth-exchange-code\/?$/, provider: 'grok' }, + { pattern: /^cursor:\/\/anysphere\.cursor-mcp\/oauth\/callback$/, provider: 'cursor_deeplink' }, + { pattern: /^https:\/\/www\.cursor\.com\/agents\/mcp\/oauth\/callback$/, provider: 'cursor' }, { pattern: /^http:\/\/localhost(:\d+)?(\/|$)/, provider: 'local' }, { pattern: /^http:\/\/127\.0\.0\.1(:\d+)?(\/|$)/, provider: 'local' }, ] diff --git a/messages/en.json b/messages/en.json index 29845edc..b9b65539 100644 --- a/messages/en.json +++ b/messages/en.json @@ -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 not add localhost or claude.ai: 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 not add localhost 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", diff --git a/messages/sv.json b/messages/sv.json index 6c076a43..797e903d 100644 --- a/messages/sv.json +++ b/messages/sv.json @@ -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 inte till localhost eller claude.ai: 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 inte till localhost 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",