History

Cloudflare: MCP-only preset with codemode=false, warn on missing opt-out (#1660)

Rhys Sullivan committed 2 months ago

Commit c11bef21 parent 73cbba97 files · +127 −10Browse files

Unified · Split

7 files
  1. .changeset/cloudflare-mcp-codemode-opt-out.md +8 −0
  2. e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts +41 −0
  3. packages/plugins/mcp/src/react/AddMcpIntegration.tsx +16 −0
  4. packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts +42 −0
  5. packages/plugins/mcp/src/sdk/cloudflare-codemode.ts +15 −0
  6. packages/plugins/mcp/src/sdk/presets.ts +5 −2
  7. packages/plugins/openapi/src/sdk/presets.ts +0 −8
.changeset/cloudflare-mcp-codemode-opt-out.md+8−0
@@ -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
8The 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`.
e2e/scenarios/mcp-cloudflare-codemode-warning.test.ts+41−0
@@ -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.
6import { Effect } from "effect";
7
8import { scenario } from "../src/scenario";
9import { Browser, Target } from "../src/services";
10
11const urlField = "https://mcp.example.com";
12const warning = "Cloudflare code mode is on";
13
14scenario(
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);
packages/plugins/mcp/src/react/AddMcpIntegration.tsx+16−0
@@ -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";
18import { FloatActions } from "@executor-js/react/components/float-actions";18import { FloatActions } from "@executor-js/react/components/float-actions";
19import { Info, InfoDescription, InfoTitle } from "@executor-js/react/components/info";
19import { Input } from "@executor-js/react/components/input";20import { Input } from "@executor-js/react/components/input";
20import { TagInput } from "@executor-js/react/components/tag-input";21import { TagInput } from "@executor-js/react/components/tag-input";
21import {22import {
@@ -38,6 +39,7 @@ import type { McpAuthMethodInput } from "../sdk/types"; · 16 unmodified lines
38import { probeMcpEndpoint, addMcpServer } from "./atoms";39import { probeMcpEndpoint, addMcpServer } from "./atoms";
39import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";40import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";
40import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";41import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
42import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";
41import { mcpPresets, type McpPreset } from "../sdk/presets";43import { mcpPresets, type McpPreset } from "../sdk/presets";
42 44
43// The remote add flow REGISTERS the server's declared auth methods through the45// 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&apos;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 the449 {/* 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 after451 OAuth sign-in) are added from the integration's detail hub after
packages/plugins/mcp/src/sdk/cloudflare-codemode.test.ts+42−0
@@ -0,0 +1,42 @@
1import { describe, expect, it } from "@effect/vitest";
2
3import { cloudflareNeedsCodemodeOptOut } from "./cloudflare-codemode";
4import { mcpPresets } from "./presets";
5
6describe("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});
packages/plugins/mcp/src/sdk/cloudflare-codemode.ts+15−0
@@ -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. */
9export 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};
packages/plugins/mcp/src/sdk/presets.ts+5−2
@@ -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 {
packages/plugins/openapi/src/sdk/presets.ts+0−8
@@ -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",

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t