Building Model Context Protocol (MCP) Servers on Cloudflare Workers: Hands-on Edge AI Agent Tools

Anthropic’s Model Context Protocol (MCP) has rapidly emerged as the open standard for connecting AI agent host environments (such as Claude Desktop, Cursor, and Zed) with external tools and data sources. However, most MCP tutorials focus on local Node.js/Python processes using stdio transport, which hits scalability barriers when attempting to share agent tools across a engineering team or deploy production agent services.
This article details how to deploy Server-Sent Events (SSE) transport-based MCP servers to Cloudflare Workers edge infrastructure. We cover everything from leveraging Cloudflare’s 300+ global datacenters for sub-10ms latency tools with zero cold starts to connecting D1 databases and securing endpoints with OAuth/Bearer tokens.
Key Takeaways
- stdio vs. SSE/HTTP: While
stdiois convenient for local CLI testing, HTTP-based Server-Sent Events (SSE) transport is mandatory for multi-user collaboration and production deployments.- Cloudflare Workers Edge Advantage: V8 isolate architecture eliminates cold starts and delivers MCP tool execution in 10-30ms globally.
- D1 & KV Integration: Access Cloudflare D1 (SQLite) and KV key-value stores directly inside Worker handlers so AI agents can query edge databases on demand.
- Edge Security Patterns: Internet-accessible MCP servers must use Bearer token middleware and Cloudflare Access to prevent unauthorized tool invocation.
MCP Transport Architecture: From stdio to SSE
The MCP specification utilizes JSON-RPC 2.0 for message exchanges between client hosts and tool servers.
| Feature | stdio Transport | SSE / HTTP Transport |
|---|---|---|
| Target Environment | Local Development (Client spawns child process) | Production / Edge Serverless |
| Communication | Standard Input / Output streams | HTTP GET (SSE) + POST (JSON-RPC) |
| Network Scope | Restricted to local machine | Accessible globally over HTTPS |
| Scalability | Single user | Multi-user with auth controls |
| Ideal Backend | Node.js, Python CLI | Cloudflare Workers, AWS Lambda |
On Cloudflare Workers, running persistent child processes is impossible, making HTTP SSE (Server-Sent Events) the ideal transport. Clients issue a GET request to /sse to maintain a real-time event stream and receive a session endpoint URL (e.g. /message?sessionId=...) for submitting JSON-RPC requests via POST.
Implementing an Edge MCP Server on Cloudflare Workers
The official @modelcontextprotocol/sdk fully supports Web Standard APIs (Request, Response, ReadableStream), allowing it to run seamlessly on Cloudflare Workers.
1. Project Setup
npm init cloudflare@latest mcp-edge-worker -- --type=workers-types-typescript
cd mcp-edge-worker
npm install @modelcontextprotocol/sdk hono
Add the D1 database binding in wrangler.toml:
name = "mcp-edge-worker"
main = "src/index.ts"
compatibility_date = "2026-07-01"
[[d1_databases]]
binding = "DB"
database_name = "production-db"
database_id = "your-d1-database-id"
2. Writing SSE Transport and MCP Handlers
Here is a full TypeScript implementation using Hono and @modelcontextprotocol/sdk:
import { Hono } from "hono";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
type Bindings = {
DB: D1Database;
AUTH_TOKEN: string;
};
const app = new Hono<{ Bindings: Bindings }>();
const activeTransports = new Map<string, SSEServerTransport>();
function createMcpServer(db: D1Database) {
const server = new Server(
{ name: "effidev-edge-mcp", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "query_user_stats",
description: "Query user payment and activity statistics from Cloudflare D1 edge DB.",
inputSchema: {
type: "object",
properties: {
startDate: { type: "string", description: "YYYY-MM-DD" },
limit: { type: "number", description: "Max rows to return (default 10)" },
},
required: ["startDate"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "query_user_stats") {
const startDate = String(request.params.arguments?.startDate);
const limit = Number(request.params.arguments?.limit ?? 10);
const { results } = await db
.prepare("SELECT user_id, status, amount FROM transactions WHERE created_at >= ? LIMIT ?")
.bind(startDate, limit)
.all();
return {
content: [
{
type: "text",
text: JSON.stringify({ success: true, count: results.length, data: results }),
},
],
};
}
throw new Error(`Unknown tool: ${request.params.name}`);
});
return server;
}
// Bearer token authentication middleware
app.use("*", async (c, next) => {
const authHeader = c.req.header("Authorization");
if (!authHeader || authHeader !== `Bearer ${c.env.AUTH_TOKEN}`) {
return c.text("Unauthorized MCP Request", 401);
}
await next();
});
// SSE connection endpoint (GET /sse)
app.get("/sse", async (c) => {
const sessionId = crypto.randomUUID();
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
const transport = new SSEServerTransport(`/message?sessionId=${sessionId}`, {
write: async (message) => {
const data = `data: ${JSON.stringify(message)}\n\n`;
await writer.write(new TextEncoder().encode(data));
},
close: async () => {
await writer.close();
activeTransports.delete(sessionId);
},
});
activeTransports.set(sessionId, transport);
const server = createMcpServer(c.env.DB);
server.connect(transport);
return new Response(readable, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
});
// JSON-RPC message endpoint (POST /message)
app.post("/message", async (c) => {
const sessionId = c.req.query("sessionId");
if (!sessionId || !activeTransports.has(sessionId)) {
return c.text("Session not found", 404);
}
const transport = activeTransports.get(sessionId)!;
const body = await c.req.json();
await transport.handlePostMessage(c.req.raw, c.res.raw, body);
return c.text("Accepted", 202);
});
export default app;
Client Integration: Claude Desktop & Cursor
Assuming your Worker URL is https://mcp.effidev.workers.dev:
{
"mcpServers": {
"effidev-edge-tools": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/inspector",
"--sse",
"https://mcp.effidev.workers.dev/sse",
"--header",
"Authorization: Bearer YOUR_SECURE_TOKEN"
]
}
}
}
Performance & Cost Metrics
| Benchmark | Node.js (AWS EC2 t4g.small) | Cloudflare Workers Paid |
|---|---|---|
| Monthly Fixed Cost | $14.60 | $5.00 (Includes 10M requests) |
| TTFB (Latency) | 120ms - 280ms | 12ms - 35ms (Global Edge) |
| Cold Start | None | 0ms (V8 Isolate) |
| Max Concurrent SSE | Memory bound (~2,000) | Uncapped Edge Scaling |
Conclusion
Deploying MCP servers to Cloudflare Workers elevates AI agent tooling from isolated local scripts into production-ready, globally distributed enterprise infrastructure. Combine Cloudflare D1, KV, and Vectorize with the MCP standard for ultra-fast, serverless agent tool execution.