Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/cloudflare-mcp-codemode-opt-out.md
Original file line numberDiff line numberDiff line change
@@ -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`.
41 changes: 41 additions & 0 deletions e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts
Original file line numberDiff line numberDiff line change
@@ -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" });
});
});
}),
);
16 changes: 16 additions & 0 deletions packages/plugins/mcp/src/react/AddMcpIntegration.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand All@@ -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
Expand DownExpand Up@@ -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) && (
<Info variant="warning">
<InfoTitle>Cloudflare code mode is on</InfoTitle>
<InfoDescription>
By default Cloudflare&apos;s MCP server hides its tools behind a single
code-execution tool. Add <code className="font-mono">?codemode=false</code> to the
URL to get the full tool catalog.
</InfoDescription>
</Info>
)}

{/* 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
Expand Down
42 changes: 42 additions & 0 deletions packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts
Original file line numberDiff line numberDiff line change
@@ -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);
});
});
15 changes: 15 additions & 0 deletions packages/plugins/mcp/src/sdk/cloudflare-codemode.ts
Original file line numberDiff line numberDiff line change
@@ -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";
};
7 changes: 5 additions & 2 deletions packages/plugins/mcp/src/sdk/presets.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
},
{
Expand Down
8 changes: 0 additions & 8 deletions packages/plugins/openapi/src/sdk/presets.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -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",
Expand Down
Loading