From 6fb259e33ad9353a3371e7fd9390da89de62f76d Mon Sep 17 00:00:00 2001 From: jonnyparris <6400000+jonnyparris@users.noreply.github.com> Date: Tue, 26 May 2026 13:40:55 +0100 Subject: [PATCH] fix(mcp): remove cf-portal catalog entry (loopback-only redirect URI policy) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After end-to-end debugging via chrome-devtools and worker tail, then cross-referencing OpenCode's source, the root cause is clear: cf-portal's OAuth authorize endpoint (cf-mcp.cloudflareaccess.com) only accepts redirect URIs that point at loopback (http://127.0.0.1:PORT/...) or pre-allowlisted Cloudflare-managed domains (e.g. seal-nightly.cloudflare.dev). DCR succeeds for any host, but the /authorize endpoint then rejects non-allowlisted redirect URIs with: error=invalid_request error_description=Redirect URI not allowed by application configuration This is by design — treating third-party hosted apps as untrusted clients is a sensible default. OpenCode/Cursor/Claude Desktop work because they run locally and spin up an http://127.0.0.1:19876 callback server. Dodo runs on a Worker; we can't bind 127.0.0.1 from the user's perspective. Remove the catalog entry to avoid the UX footgun of users clicking "Connect with OAuth" only to land on an unrecoverable error. The comment in mcp-catalog.ts documents the reasoning so the next person to come looking doesn't re-walk the same path. To use cf-portal tools in Dodo, the workaround is to run a local `mcp-remote` proxy and configure it as a static-headers MCP integration. (Not in scope here.) The supporting infrastructure (OAuth start/poll routes, popup UX, stale-registration cleanup, getAgentByName fix, /agents/oauth/callback path, integrations layout fix) remains in place and is still useful for any future OAuth-MCP entry that DOES allow remote redirect URIs. Tests: 817/817 pass. Typecheck clean. beep-boop-🤖 --- src/mcp-catalog.ts | 26 +++++++++++++------------- test/mcp-catalog-unit.test.ts | 14 +++++--------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/mcp-catalog.ts b/src/mcp-catalog.ts index 4333840..255d872 100644 --- a/src/mcp-catalog.ts +++ b/src/mcp-catalog.ts @@ -50,19 +50,19 @@ const DEFAULT_CLOUDFLARE_REMOTE_MCP_CATALOG: McpCatalogEntry[] = [ auth_type: "oauth", knownHosts: ["browser.mcp.cloudflare.com"], }, - { - id: "cf-portal", - name: "Cloudflare Portal", - description: - "Cloudflare internal MCP portal — Backstage catalog, Jira, GitLab, Sentry, Elasticsearch, Wiki, Prometheus, and more. Requires Cloudflare SSO.", - url: "https://portal.mcp.cfdata.org/mcp", - setupGuide: - "Connect with OAuth via Cloudflare Access SSO. Per-user Dynamic Client Registration; access token refreshes automatically.", - auth_type: "oauth", - // cf-mcp.cloudflareaccess.com is the OAuth dance host - // (authorization_endpoint / token_endpoint / registration_endpoint). - knownHosts: ["portal.mcp.cfdata.org", "cf-mcp.cloudflareaccess.com"], - }, + // NOTE: cf-portal (https://portal.mcp.cfdata.org/mcp) was tested and is + // intentionally NOT in this catalog. Its OAuth authorize endpoint + // (cf-mcp.cloudflareaccess.com) only accepts redirect URIs pointing at + // loopback (http://127.0.0.1:*) or Cloudflare-managed domains (e.g. + // seal-nightly.cloudflare.dev). DCR succeeds, but the authorize step + // returns "Redirect URI not allowed by application configuration" + // for any other host. This is by design — cf-portal treats third-party + // hosted apps as untrusted clients. OpenCode/Cursor/Claude Desktop work + // because they run locally on 127.0.0.1; Dodo runs on a Worker, which + // can't bind 127.0.0.1 from the user's POV. See OpenCode's + // `McpOAuthProvider.redirectUrl` for reference. To use cf-portal tools + // in Dodo, run a local mcp-remote proxy and add it as a static-headers + // integration. ]; export const DEPLOY_MCP_CATALOG_CONFIG: McpCatalogConfig = { diff --git a/test/mcp-catalog-unit.test.ts b/test/mcp-catalog-unit.test.ts index 71585f3..45768d0 100644 --- a/test/mcp-catalog-unit.test.ts +++ b/test/mcp-catalog-unit.test.ts @@ -8,16 +8,12 @@ import { describe, expect, it } from "vitest"; import { MCP_CATALOG } from "../src/mcp-catalog"; describe("MCP_CATALOG", () => { - it("includes cf-portal as an OAuth catalog entry", () => { + it("does NOT include cf-portal — its OAuth authorize endpoint rejects non-loopback redirect URIs", () => { + // Documented in mcp-catalog.ts. cf-portal works for OpenCode/Cursor/etc + // because those clients use http://127.0.0.1:PORT/... redirect URIs. + // A hosted Worker can't do that, and cf-portal rejects everything else. const entry = MCP_CATALOG.find((e) => e.id === "cf-portal"); - expect(entry).toBeDefined(); - expect(entry?.url).toBe("https://portal.mcp.cfdata.org/mcp"); - expect(entry?.auth_type).toBe("oauth"); - // Both the MCP host AND the Cloudflare Access OAuth dance host must be - // in knownHosts — otherwise `isHostAllowed()` rejects the start-auth - // call and the token endpoint round-trip during the dance. - expect(entry?.knownHosts).toContain("portal.mcp.cfdata.org"); - expect(entry?.knownHosts).toContain("cf-mcp.cloudflareaccess.com"); + expect(entry).toBeUndefined(); }); it("includes browser-rendering as an OAuth catalog entry", () => {