Unified · Split
| @@ -0,0 +1,8 @@ | ||
| 1 | +--- | |
| 2 | +"@executor-js/plugin-mcp": patch | |
| 3 | +"@executor-js/plugin-openapi": patch | |
| 4 | +--- | |
| 5 | + | |
| 6 | +**Cloudflare ships as MCP-only, with code mode opted out** | |
| 7 | + | |
| 8 | +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`. | |
| @@ -0,0 +1,41 @@ | ||
| 1 | +// Cloudflare's MCP server defaults to code mode, which hides the tool catalog | |
| 2 | +// behind a single code-execution tool. Executor already provides the | |
| 3 | +// code-execution surface, so the add-MCP flow warns when a Cloudflare URL | |
| 4 | +// misses the `?codemode=false` opt-out. This guards the warning and that it | |
| 5 | +// clears once the opt-out is added; the probe's outcome is irrelevant to it. | |
| 6 | +import { Effect } from "effect"; | |
| 7 | + | |
| 8 | +import { scenario } from "../src/scenario"; | |
| 9 | +import { Browser, Target } from "../src/services"; | |
| 10 | + | |
| 11 | +const urlField = "https://mcp.example.com"; | |
| 12 | +const warning = "Cloudflare code mode is on"; | |
| 13 | + | |
| 14 | +scenario( | |
| 15 | + "MCP add flow · a Cloudflare URL without codemode=false gets the opt-out warning", | |
| 16 | + {}, | |
| 17 | + Effect.gen(function* () { | |
| 18 | + const target = yield* Target; | |
| 19 | + const browser = yield* Browser; | |
| 20 | + const identity = yield* target.newIdentity(); | |
| 21 | + | |
| 22 | + yield* browser.session(identity, async ({ page, step }) => { | |
| 23 | + await step("Open the add-MCP flow", async () => { | |
| 24 | + await page.goto("/integrations/add/mcp", { waitUntil: "networkidle" }); | |
| 25 | + await page.getByPlaceholder(urlField).waitFor(); | |
| 26 | + }); | |
| 27 | + | |
| 28 | + await step("Typing the Cloudflare URL without the opt-out shows the warning", async () => { | |
| 29 | + await page.getByPlaceholder(urlField).fill("https://mcp.cloudflare.com/mcp"); | |
| 30 | + await page.getByText(warning).waitFor(); | |
| 31 | + }); | |
| 32 | + | |
| 33 | + await step("Appending ?codemode=false clears the warning", async () => { | |
| 34 | + // The same placeholder survives the probe's re-render, so this resolves | |
| 35 | + // in both the bare-URL and probed layouts. | |
| 36 | + await page.getByPlaceholder(urlField).fill("https://mcp.cloudflare.com/mcp?codemode=false"); | |
| 37 | + await page.getByText(warning).waitFor({ state: "hidden" }); | |
| 38 | + }); | |
| 39 | + }); | |
| 40 | + }), | |
| 41 | +); | |
| @@ -16,6 +16,7 @@ import { · 15 unmodified lines | ||
| 16 | 16 | CardStackEntryField, |
| 17 | 17 | } from "@executor-js/react/components/card-stack"; |
| 18 | 18 | import { FloatActions } from "@executor-js/react/components/float-actions"; |
| 19 | +import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info"; | |
| 19 | 20 | import { Input } from "@executor-js/react/components/input"; |
| 20 | 21 | import { TagInput } from "@executor-js/react/components/tag-input"; |
| 21 | 22 | import { |
| @@ -38,6 +39,7 @@ import type { McpAuthMethodInput } from "../sdk/types"; · 16 unmodified lines | ||
| 38 | 39 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; |
| 39 | 40 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; |
| 40 | 41 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; |
| 42 | +import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode"; | |
| 41 | 43 | import { mcpPresets, type McpPreset } from "../sdk/presets"; |
| 42 | 44 | |
| 43 | 45 | // The remote add flow REGISTERS the server's declared auth methods through the |
| @@ -430,6 +432,20 @@ export default function AddMcpIntegration(props: { · 386 unmodified lines | ||
| 430 | 432 | onRetry={handleProbe} |
| 431 | 433 | /> |
| 432 | 434 | |
| 435 | + {/* Cloudflare's MCP server defaults to code mode, which collapses the | |
| 436 | + catalog into one code-execution tool; executor already provides | |
| 437 | + code execution, so nudge the user toward the opt-out. */} | |
| 438 | + {cloudflareNeedsCodemodeOptOut(state.url) && ( | |
| 439 | + <Info variant="warning"> | |
| 440 | + <InfoTitle>Cloudflare code mode is on</InfoTitle> | |
| 441 | + <InfoDescription> | |
| 442 | + By default Cloudflare's MCP server hides its tools behind a single | |
| 443 | + code-execution tool. Add <code className="font-mono">?codemode=false</code> to the | |
| 444 | + URL to get the full tool catalog. | |
| 445 | + </InfoDescription> | |
| 446 | + </Info> | |
| 447 | + )} | |
| 448 | + | |
| 433 | 449 | {/* Authentication — declares the auth methods to register through the |
| 434 | 450 | shared list editor. The credentials themselves (API key value / |
| 435 | 451 | OAuth sign-in) are added from the integration's detail hub after |
| @@ -0,0 +1,42 @@ | ||
| 1 | +import { describe, expect, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { cloudflareNeedsCodemodeOptOut } from "./cloudflare-codemode"; | |
| 4 | +import { mcpPresets } from "./presets"; | |
| 5 | + | |
| 6 | +describe("cloudflareNeedsCodemodeOptOut", () => { | |
| 7 | + it("flags the Cloudflare MCP endpoint without the opt-out", () => { | |
| 8 | + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp")).toBe(true); | |
| 9 | + }); | |
| 10 | + | |
| 11 | + it("flags codemode set to anything but false", () => { | |
| 12 | + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?codemode=true")).toBe( | |
| 13 | + true, | |
| 14 | + ); | |
| 15 | + }); | |
| 16 | + | |
| 17 | + it("accepts the opt-out regardless of other params or whitespace", () => { | |
| 18 | + expect(cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?codemode=false")).toBe( | |
| 19 | + false, | |
| 20 | + ); | |
| 21 | + expect( | |
| 22 | + cloudflareNeedsCodemodeOptOut("https://mcp.cloudflare.com/mcp?foo=bar&codemode=false"), | |
| 23 | + ).toBe(false); | |
| 24 | + expect(cloudflareNeedsCodemodeOptOut(" https://mcp.cloudflare.com/mcp?codemode=false ")).toBe( | |
| 25 | + false, | |
| 26 | + ); | |
| 27 | + }); | |
| 28 | + | |
| 29 | + it("ignores non-Cloudflare and unparseable endpoints", () => { | |
| 30 | + expect(cloudflareNeedsCodemodeOptOut("https://mcp.linear.app/mcp")).toBe(false); | |
| 31 | + expect(cloudflareNeedsCodemodeOptOut("https://bindings.mcp.cloudflare.com/sse")).toBe(false); | |
| 32 | + expect(cloudflareNeedsCodemodeOptOut("mcp.cloudflare.com/mcp")).toBe(false); | |
| 33 | + expect(cloudflareNeedsCodemodeOptOut("")).toBe(false); | |
| 34 | + }); | |
| 35 | + | |
| 36 | + it("the shipped Cloudflare preset carries the opt-out", () => { | |
| 37 | + const cloudflare = mcpPresets.find((preset) => preset.id === "cloudflare"); | |
| 38 | + expect(cloudflare?.transport).toBeUndefined(); | |
| 39 | + if (cloudflare === undefined || cloudflare.transport !== undefined) return; | |
| 40 | + expect(cloudflareNeedsCodemodeOptOut(cloudflare.endpoint)).toBe(false); | |
| 41 | + }); | |
| 42 | +}); | |
| @@ -0,0 +1,15 @@ | ||
| 1 | +// Cloudflare's hosted MCP server (mcp.cloudflare.com) defaults to "code | |
| 2 | +// mode": it hides the tool catalog behind a single code-execution tool. | |
| 3 | +// Executor is itself a code-execution surface, so nesting code mode buries | |
| 4 | +// every real Cloudflare tool. The preset pins `?codemode=false`; hand-entered | |
| 5 | +// endpoints are flagged so the user adds the same opt-out. | |
| 6 | + | |
| 7 | +/** Whether `endpoint` targets Cloudflare's MCP server without the | |
| 8 | + * `codemode=false` opt-out. Unparseable and non-Cloudflare URLs are fine. */ | |
| 9 | +export const cloudflareNeedsCodemodeOptOut = (endpoint: string): boolean => { | |
| 10 | + const trimmed = endpoint.trim(); | |
| 11 | + if (!URL.canParse(trimmed)) return false; | |
| 12 | + const url = new URL(trimmed); | |
| 13 | + if (url.hostname !== "mcp.cloudflare.com") return false; | |
| 14 | + return url.searchParams.get("codemode") !== "false"; | |
| 15 | +}; | |
| @@ -134,8 +134,11 @@ export const mcpPresets: readonly McpPreset[] = [ · 133 unmodified lines | ||
| 134 | 134 | id: "cloudflare", |
| 135 | 135 | name: "Cloudflare", |
| 136 | 136 | summary: "Workers, KV, D1, R2, and DNS management via MCP.", |
| 137 | − url: "https://mcp.cloudflare.com/mcp", | |
| 138 | − endpoint: "https://mcp.cloudflare.com/mcp", | |
| 137 | + // `codemode=false` opts out of Cloudflare's code mode, which replaces the | |
| 138 | + // tool catalog with a single code-execution tool. Executor is already a | |
| 139 | + // code-execution surface, so nesting it would hide every real tool. | |
| 140 | + url: "https://mcp.cloudflare.com/mcp?codemode=false", | |
| 141 | + endpoint: "https://mcp.cloudflare.com/mcp?codemode=false", | |
| 139 | 142 | icon: "https://integrations.sh/logo/cloudflare.com", |
| 140 | 143 | }, |
| 141 | 144 | { |
| @@ -93,14 +93,6 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ · 92 unmodified lines | ||
| 93 | 93 | icon: "https://integrations.sh/logo/vercel.com", |
| 94 | 94 | featured: true, |
| 95 | 95 | }, |
| 96 | − { | |
| 97 | − id: "cloudflare", | |
| 98 | − name: "Cloudflare", | |
| 99 | − summary: "DNS, workers, pages, R2, and security rules.", | |
| 100 | − url: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", | |
| 101 | − icon: "https://integrations.sh/logo/cloudflare.com", | |
| 102 | − featured: true, | |
| 103 | − }, | |
| 104 | 96 | { |
| 105 | 97 | id: "neon", |
| 106 | 98 | name: "Neon", |