MCP Cloudflare Builder
Scaffolds and deploys production-ready Model Context Protocol servers on Cloudflare Workers.
Philosophy
Most MCP servers start as stdio processes tied to a single machine. Remote MCP — hosted on Cloudflare Workers — changes the unit of deployment: a tool server becomes a durable, network-accessible endpoint that any authorized client can reach over HTTPS. This matters because it decouples tooling from the machines running agents, enables OAuth-protected sharing across teams, and makes the server's uptime and auth someone else's operational concern (Cloudflare's).
This skill exists because the gap between "I have a tool idea" and "the tool is deployed and connected" is all infrastructure. The scaffold here eliminates that gap.
Description
Creates remote MCP servers using the McpAgent Durable Object pattern on Cloudflare Workers. Covers tool definition with Zod validation, public vs. OAuth-protected modes, local testing with MCP Inspector, Wrangler deployment, and client connection. Distinct from mcp-builder (which covers general MCP server concepts) and mcp-server-builder (which scaffolds from OpenAPI specs) — this skill is Cloudflare-specific and deployment-complete.
When to Use
- User wants a remote (not stdio) MCP server
- User needs to expose tools to Claude Desktop or other MCP clients over HTTPS
- User wants MCP authentication via OAuth (GitHub, Google, Auth0, etc.)
- User is deploying on Cloudflare Workers specifically
Workflow
Step 1: Scaffold the Project
Public server (no auth):
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-authless
cd my-mcp-server && npm start
Server runs at http://localhost:8788/mcp.
OAuth-protected server:
npm create cloudflare@latest -- my-mcp-server \
--template=cloudflare/ai/demos/remote-mcp-github-oauth
cd my-mcp-server
Step 2: Define Tools
Tools are functions MCP clients can call. Define them in McpAgent.init():
import { McpAgent } from "agents/mcp";
import { z } from "zod";
export class MyMCP extends McpAgent {
server = new Server({ name: "my-mcp", version: "1.0.0" });
async init() {
this.server.tool(
"add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }],
})
);
this.server.tool(
"get_weather",
{ city: z.string() },
async ({ city }) => {
const response = await fetch(`https://api.weather.com/${city}`);
const data = await response.json();
return { content: [{ type: "text", text: JSON.stringify(data) }] };
}
);
}
}
Step 3: Configure Entry Point
// src/index.ts
import { MyMCP } from "./mcp";
export default {
fetch(request: Request, env: Env, ctx: ExecutionContext) {
const url = new URL(request.url);
if (url.pathname === "/mcp") {
return MyMCP.serveSSE("/mcp").fetch(request, env, ctx);
}
return new Response("MCP Server", { status: 200 });
},
};
export { MyMCP };
Step 4: Test Locally
npm start
# In another terminal:
npx @modelcontextprotocol/inspector@latest
# Open http://localhost:5173, connect to http://localhost:8788/mcp
Step 5: Deploy
npx wrangler deploy
# Server accessible at https://[worker-name].[account].workers.dev/mcp
Step 6: Connect Clients
Claude Desktop (claude_desktop_config.json):
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["mcp-remote", "https://my-mcp.workers.dev/mcp"]
}
}
}
Restart Claude Desktop after updating config.
Tool Patterns
Return Types
// Text response
return { content: [{ type: "text", text: "result" }] };
// Multiple content items
return {
content: [
{ type: "text", text: "Here's the data:" },
{ type: "text", text: JSON.stringify(data, null, 2) },
],
};
Input Validation with Zod
this.server.tool(
"create_user",
{
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(["admin", "user", "guest"]),
age: z.number().int().min(0).optional(),
},
async (params) => { /* params are fully typed and validated */ }
);
Accessing Cloudflare Bindings
export class MyMCP extends McpAgent<Env> {
async init() {
this.server.tool("query_db", { sql: z.string() }, async ({ sql }) => {
const result = await this.env.DB.prepare(sql).all();
return { content: [{ type: "text", text: JSON.stringify(result) }] };
});
}
}
Wrangler Configuration
name = "my-mcp-server"
main = "src/index.ts"
compatibility_date = "2024-12-01"
[durable_objects]
bindings = [{ name = "MCP", class_name = "MyMCP" }]
[[migrations]]
tag = "v1"
new_classes = ["MyMCP"]
# Optional: D1, KV bindings
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxx"
OAuth Setup
Supported providers: GitHub, Google, Auth0, Stytch, WorkOS, any OAuth 2.0 provider.
Set secrets via Wrangler:
wrangler secret put GITHUB_CLIENT_ID
wrangler secret put GITHUB_CLIENT_SECRET
Callback URL must match OAuth app config exactly. For local dev: http://localhost:8788/callback.
Common Issues
| Issue | Cause | Fix |
|---|---|---|
| "Tool not found" | Name mismatch | Verify exact case; ensure init() registers before connections |
| Connection fails | Wrong path | Confirm endpoint is /mcp; check CORS for browser clients |
| OAuth redirect error | Callback URL mismatch | Match OAuth app config; check secrets are set |
Best Practices
- Zod-validate every input. Never trust raw tool parameters — use
.email(),.min(),.enum()constraints to catch malformed input before it reaches your business logic or external APIs. - One tool, one intent. A tool that does two different things based on a flag is two tools. Split them — agents choose tools by reading descriptions, not by inspecting parameter combinations.
- Access Cloudflare bindings via
this.env, not global scope. D1, KV, and R2 are injected per-request; referencing them outsideMcpAgentloses the request context. - Set secrets with
wrangler secret put, never inwrangler.toml. Committed secrets in toml are a deployment risk even in private repos. - Match the OAuth callback URL exactly. A trailing slash difference between the OAuth app config and the Worker route causes silent redirect failures that are hard to diagnose.
- Test with MCP Inspector before deploying. Local iteration via
npm start+ Inspector catches tool-registration errors before they reach production clients.
Quality Checklist
Before calling the deployment complete:
- All tools are registered in
McpAgent.init()before any client connects - Every tool input uses Zod validation with explicit constraints
- Secrets are stored via
wrangler secret put, not hardcoded - The Worker entry point routes
/mcptoMcpAgent.serveSSE("/mcp") -
wrangler.tomlincludes the Durable Object binding and migration tag -
npm start+ MCP Inspector successfully lists and calls each tool locally - Claude Desktop (or target client) config points to the deployed
*.workers.dev/mcpURL - OAuth callback URL matches the OAuth app config exactly (if auth mode is
oauth)
Related Skills
mcp-server-builder— spec-driven MCP scaffold from an OpenAPI contract (language-agnostic, not Cloudflare-specific)skill-creation— for wrapping a built MCP server into a reusable agent skillprocess-extraction— for capturing the deployment workflow as a repeatable runbook
Output
A Cloudflare Workers project directory containing:
src/index.ts— entry point with SSE routingsrc/mcp.ts—McpAgentclass with tool definitionswrangler.toml— deployment configurationpackage.json— dependencies includingagents,zod,@modelcontextprotocol/sdk