Building MCP Servers on Cloudflare
Your knowledge of the MCP SDK and Cloudflare Workers integration may be outdated. Prefer retrieval over pre-training for any MCP server task.
Retrieval Sources
Source How to retrieve Use for
MCP docs https://developers.cloudflare.com/agents/mcp/
Server setup, auth, deployment
MCP spec https://modelcontextprotocol.io/
Protocol spec, tool/resource definitions
Workers docs Search tool or https://developers.cloudflare.com/workers/
Runtime APIs, bindings, config
When to Use
-
User wants to build a remote MCP server
-
User needs to expose tools via MCP
-
User asks about MCP authentication or OAuth
-
User wants to deploy MCP to Cloudflare Workers
Prerequisites
-
Cloudflare account with Workers enabled
-
Node.js 18+ and npm/pnpm/yarn
-
Wrangler CLI (npm install -g wrangler )
Quick Start
Option 1: 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
Option 2: Authenticated Server (OAuth)
npm create cloudflare@latest -- my-mcp-server
--template=cloudflare/ai/demos/remote-mcp-github-oauth
cd my-mcp-server
Requires OAuth app setup. See references/oauth-setup.md.
Core Workflow
Step 1: Define Tools
Tools are functions MCP clients can call. Define them using server.tool() :
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() { // Simple tool with parameters this.server.tool( "add", { a: z.number(), b: z.number() }, async ({ a, b }) => ({ content: [{ type: "text", text: String(a + b) }], }) );
// Tool that calls external API
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 2: Configure Entry Point
Public server (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 };
Authenticated server — See references/oauth-setup.md.
Step 3: Test Locally
Start server
npm start
In another terminal, test with MCP Inspector
npx @modelcontextprotocol/inspector@latest
Open http://localhost:5173, enter http://localhost:8788/mcp
Step 4: Deploy
npx wrangler deploy
Server accessible at https://[worker-name].[account].workers.dev/mcp
Step 5: 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 Environment/Bindings
export class MyMCP extends McpAgent<Env> { async init() { this.server.tool("query_db", { sql: z.string() }, async ({ sql }) => { // Access D1 binding const result = await this.env.DB.prepare(sql).all(); return { content: [{ type: "text", text: JSON.stringify(result) }] }; }); } }
Authentication
For OAuth-protected servers, see references/oauth-setup.md.
Supported providers:
-
GitHub
-
Google
-
Auth0
-
Stytch
-
WorkOS
-
Any OAuth 2.0 compliant provider
Wrangler Configuration
Minimal wrangler.toml :
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"]
With bindings (D1, KV, etc.):
[[d1_databases]] binding = "DB" database_name = "my-db" database_id = "xxx"
[[kv_namespaces]] binding = "KV" id = "xxx"
Common Issues
"Tool not found" in Client
-
Verify tool name matches exactly (case-sensitive)
-
Ensure init() registers tools before connections
-
Check server logs: wrangler tail
Connection Fails
-
Confirm endpoint path is /mcp
-
Check CORS if browser-based client
-
Verify Worker is deployed: wrangler deployments list
OAuth Redirect Errors
-
Callback URL must match OAuth app config exactly
-
Check GITHUB_CLIENT_ID and GITHUB_CLIENT_SECRET are set
-
For local dev, use http://localhost:8788/callback
References
-
references/examples.md — Official templates and production examples
-
references/oauth-setup.md — OAuth provider configuration
-
references/tool-patterns.md — Advanced tool examples
-
references/troubleshooting.md — Error codes and fixes