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", () => {