From 42ad7947fad100190cf54237186fb613dd61e939 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Tue, 18 Aug 2026 10:02:49 -0700 Subject: [PATCH] Cloudflare: MCP-only preset with codemode=false, warn on missing opt-out --- .changeset/cloudflare-mcp-codemode-opt-out.md | 8 ++++ .../mcp-cloudflare-codemode-warning.test.ts | 41 ++++++++++++++++++ .../mcp/src/react/AddMcpIntegration.tsx | 16 +++++++ .../mcp/src/sdk/cloudflare-codemode.test.ts | 42 +++++++++++++++++++ .../mcp/src/sdk/cloudflare-codemode.ts | 15 +++++++ packages/plugins/mcp/src/sdk/presets.ts | 7 +++- packages/plugins/openapi/src/sdk/presets.ts | 8 ---- 7 files changed, 127 insertions(+), 10 deletions(-) create mode 100644 .changeset/cloudflare-mcp-codemode-opt-out.md create mode 100644 e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts create mode 100644 packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts create mode 100644 packages/plugins/mcp/src/sdk/cloudflare-codemode.ts diff --git a/.changeset/cloudflare-mcp-codemode-opt-out.md b/.changeset/cloudflare-mcp-codemode-opt-out.md new file mode 100644 index 0000000000..780b98d435 --- /dev/null +++ b/.changeset/cloudflare-mcp-codemode-opt-out.md @@ -0,0 +1,8 @@ +--- +"@executor-js/plugin-mcp": patch +"@executor-js/plugin-openapi": patch +--- + +**Cloudflare ships as MCP-only, with code mode opted out** + +The Cloudflare OpenAPI preset is gone from the default catalog; the MCP preset is the one Cloudflare entry. Its endpoint now pins `?codemode=false` because Cloudflare's MCP server otherwise hides the tool catalog behind a single code-execution tool, and executor already provides the code-execution surface. Hand-entered `mcp.cloudflare.com` URLs missing the opt-out get an inline warning in the add flow telling the user to append `?codemode=false`. diff --git a/e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts b/e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts new file mode 100644 index 0000000000..f6bb3a068f --- /dev/null +++ b/e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts @@ -0,0 +1,41 @@ +// Cloudflare's MCP server defaults to code mode, which hides the tool catalog +// behind a single code-execution tool. Executor already provides the +// code-execution surface, so the add-MCP flow warns when a Cloudflare URL +// misses the `?codemode=false` opt-out. This guards the warning and that it +// clears once the opt-out is added; the probe's outcome is irrelevant to it. +import { Effect } from "effect"; + +import { scenario } from "../src/scenario"; +import { Browser, Target } from "../src/services"; + +const urlField = "https://mcp.example.com"; +const warning = "Cloudflare code mode is on"; + +scenario( + "MCP add flow · a Cloudflare URL without codemode=false gets the opt-out warning", + {}, + Effect.gen(function* () { + const target = yield* Target; + const browser = yield* Browser; + const identity = yield* target.newIdentity(); + + yield* browser.session(identity, async ({ page, step }) => { + await step("Open the add-MCP flow", async () => { + await page.goto("/integrations/add/mcp", { waitUntil: "networkidle" }); + await page.getByPlaceholder(urlField).waitFor(); + }); + + await step("Typing the Cloudflare URL without the opt-out shows the warning", async () => { + await page.getByPlaceholder(urlField).fill("https://mcp.cloudflare.com/mcp"); + await page.getByText(warning).waitFor(); + }); + + await step("Appending ?codemode=false clears the warning", async () => { + // The same placeholder survives the probe's re-render, so this resolves + // in both the bare-URL and probed layouts. + await page.getByPlaceholder(urlField).fill("https://mcp.cloudflare.com/mcp?codemode=false"); + await page.getByText(warning).waitFor({ state: "hidden" }); + }); + }); + }), +); diff --git a/packages/plugins/mcp/src/react/AddMcpIntegration.tsx b/packages/plugins/mcp/src/react/AddMcpIntegration.tsx index 6e1d0de26d..16aecb08b1 100644 --- a/packages/plugins/mcp/src/react/AddMcpIntegration.tsx +++ b/packages/plugins/mcp/src/react/AddMcpIntegration.tsx @@ -16,6 +16,7 @@ import { CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FloatActions } from "@executor-js/react/components/float-actions"; +import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info"; import { Input } from "@executor-js/react/components/input"; import { TagInput } from "@executor-js/react/components/tag-input"; import { @@ -38,6 +39,7 @@ import type { McpAuthMethodInput } from "../sdk/types"; import { probeMcpEndpoint, addMcpServer } from "./atoms"; import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; +import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode"; import { mcpPresets, type McpPreset } from "../sdk/presets"; // The remote add flow REGISTERS the server's declared auth methods through the @@ -430,6 +432,20 @@ export default function AddMcpIntegration(props: { onRetry={handleProbe} /> + {/* Cloudflare's MCP server defaults to code mode, which collapses the + catalog into one code-execution tool; executor already provides + code execution, so nudge the user toward the opt-out. */} + {cloudflareNeedsCodemodeOptOut(state.url) && ( + + Cloudflare code mode is on + + By default Cloudflare's MCP server hides its tools behind a single + code-execution tool. Add ?codemode=false to the + URL to get the full tool catalog. + + + )} + {/* Authentication — declares the auth methods to register through the shared list editor. The credentials themselves (API key value / OAuth sign-in) are added from the integration's detail hub after diff --git a/packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts b/packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts new file mode 100644 index 0000000000..e543fd5eb1 --- /dev/null +++ b/packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from "@effect/vitest"; + +import { cloudflareNeedsCodemodeOptOut } from "./cloudflare-codemode"; +import { mcpPresets } from "./presets"; + +describe("cloudflareNeedsCodemodeOptOut", () => { + it("flags the Cloudflare MCP endpoint without the opt-out", () => { + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp")).toBe(true); + }); + + it("flags codemode set to anything but false", () => { + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?codemode=true")).toBe( + true, + ); + }); + + it("accepts the opt-out regardless of other params or whitespace", () => { + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?codemode=false")).toBe( + false, + ); + expect( + cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?foo=bar&codemode=false"), + ).toBe(false); + expect(cloudflareNeedsCodemodeOptOut(" https://mcp.cloudflare.com/mcp?codemode=false ")).toBe( + false, + ); + }); + + it("ignores non-Cloudflare and unparseable endpoints", () => { + expect(cloudflareNeedsCodemodeOptOut("https://mcp.linear.app/mcp")).toBe(false); + expect(cloudflareNeedsCodemodeOptOut("https://bindings.mcp.cloudflare.com/sse")).toBe(false); + expect(cloudflareNeedsCodemodeOptOut("mcp.cloudflare.com/mcp")).toBe(false); + expect(cloudflareNeedsCodemodeOptOut("")).toBe(false); + }); + + it("the shipped Cloudflare preset carries the opt-out", () => { + const cloudflare = mcpPresets.find((preset) => preset.id === "cloudflare"); + expect(cloudflare?.transport).toBeUndefined(); + if (cloudflare === undefined || cloudflare.transport !== undefined) return; + expect(cloudflareNeedsCodemodeOptOut(cloudflare.endpoint)).toBe(false); + }); +}); diff --git a/packages/plugins/mcp/src/sdk/cloudflare-codemode.ts b/packages/plugins/mcp/src/sdk/cloudflare-codemode.ts new file mode 100644 index 0000000000..1d53c69a30 --- /dev/null +++ b/packages/plugins/mcp/src/sdk/cloudflare-codemode.ts @@ -0,0 +1,15 @@ +// Cloudflare's hosted MCP server (mcp.cloudflare.com) defaults to "code +// mode": it hides the tool catalog behind a single code-execution tool. +// Executor is itself a code-execution surface, so nesting code mode buries +// every real Cloudflare tool. The preset pins `?codemode=false`; hand-entered +// endpoints are flagged so the user adds the same opt-out. + +/** Whether `endpoint` targets Cloudflare's MCP server without the + * `codemode=false` opt-out. Unparseable and non-Cloudflare URLs are fine. */ +export const cloudflareNeedsCodemodeOptOut = (endpoint: string): boolean => { + const trimmed = endpoint.trim(); + if (!URL.canParse(trimmed)) return false; + const url = new URL(trimmed); + if (url.hostname !== "mcp.cloudflare.com") return false; + return url.searchParams.get("codemode") !== "false"; +}; diff --git a/packages/plugins/mcp/src/sdk/presets.ts b/packages/plugins/mcp/src/sdk/presets.ts index 0cced50e76..dd2cdc5cf7 100644 --- a/packages/plugins/mcp/src/sdk/presets.ts +++ b/packages/plugins/mcp/src/sdk/presets.ts @@ -134,8 +134,11 @@ export const mcpPresets: readonly McpPreset[] = [ id: "cloudflare", name: "Cloudflare", summary: "Workers, KV, D1, R2, and DNS management via MCP.", - url: "https://mcp.cloudflare.com/mcp", - endpoint: "https://mcp.cloudflare.com/mcp", + // `codemode=false` opts out of Cloudflare's code mode, which replaces the + // tool catalog with a single code-execution tool. Executor is already a + // code-execution surface, so nesting it would hide every real tool. + url: "https://mcp.cloudflare.com/mcp?codemode=false", + endpoint: "https://mcp.cloudflare.com/mcp?codemode=false", icon: "https://integrations.sh/logo/cloudflare.com", }, { diff --git a/packages/plugins/openapi/src/sdk/presets.ts b/packages/plugins/openapi/src/sdk/presets.ts index 6ecf2cd3dd..a469bde059 100644 --- a/packages/plugins/openapi/src/sdk/presets.ts +++ b/packages/plugins/openapi/src/sdk/presets.ts @@ -93,14 +93,6 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ icon: "https://integrations.sh/logo/vercel.com", featured: true, }, - { - id: "cloudflare", - name: "Cloudflare", - summary: "DNS, workers, pages, R2, and security rules.", - url: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", - icon: "https://integrations.sh/logo/cloudflare.com", - featured: true, - }, { id: "neon", name: "Neon",