Building MCP Servers on Cloudflare
Creates production-ready Model Context Protocol servers on Cloudflare Workers with tools, authentication, and deployment.
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
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