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 | CardStackEntryField, | 16 | CardStackEntryField, |
| 17 | } from "@executor-js/react/components/card-stack"; | 17 | } from "@executor-js/react/components/card-stack"; |
| 18 | import { FloatActions } from "@executor-js/react/components/float-actions"; | 18 | import { FloatActions } from "@executor-js/react/components/float-actions"; |
| 19 | import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info"; | ||
| 19 | import { Input } from "@executor-js/react/components/input"; | 20 | import { Input } from "@executor-js/react/components/input"; |
| 20 | import { TagInput } from "@executor-js/react/components/tag-input"; | 21 | import { TagInput } from "@executor-js/react/components/tag-input"; |
| 21 | import { | 22 | import { |
| @@ -38,6 +39,7 @@ import type { McpAuthMethodInput } from "../sdk/types"; · 16 unmodified lines | |||
| 38 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; | 39 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; |
| 39 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; | 40 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; |
| 40 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; | 41 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; |
| 42 | import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode"; | ||
| 41 | import { mcpPresets, type McpPreset } from "../sdk/presets"; | 43 | import { mcpPresets, type McpPreset } from "../sdk/presets"; |
| 42 | 44 | ||
| 43 | // The remote add flow REGISTERS the server's declared auth methods through the | 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 | onRetry={handleProbe} | 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 | {/* Authentication — declares the auth methods to register through the | 449 | {/* Authentication — declares the auth methods to register through the |
| 434 | shared list editor. The credentials themselves (API key value / | 450 | shared list editor. The credentials themselves (API key value / |
| 435 | OAuth sign-in) are added from the integration's detail hub after | 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 | id: "cloudflare", | 134 | id: "cloudflare", |
| 135 | name: "Cloudflare", | 135 | name: "Cloudflare", |
| 136 | summary: "Workers, KV, D1, R2, and DNS management via MCP.", | 136 | summary: "Workers, KV, D1, R2, and DNS management via MCP.", |
| 137 | url: "https://mcp.cloudflare.com/mcp", | 137 | // `codemode=false` opts out of Cloudflare's code mode, which replaces the |
| 138 | endpoint: "https://mcp.cloudflare.com/mcp", | 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 | icon: "https://integrations.sh/logo/cloudflare.com", | 142 | icon: "https://integrations.sh/logo/cloudflare.com", |
| 140 | }, | 143 | }, |
| 141 | { | 144 | { |
| @@ -93,14 +93,6 @@ const openApiOnlyPresets: readonly OpenApiPreset[] = [ · 92 unmodified lines | |||
| 93 | icon: "https://integrations.sh/logo/vercel.com", | 93 | icon: "https://integrations.sh/logo/vercel.com", |
| 94 | featured: true, | 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 | id: "neon", | 97 | id: "neon", |
| 106 | name: "Neon", | 98 | name: "Neon", |