Construcción de Servidores Model Context Protocol (MCP) en Cloudflare Workers: Guía Práctica

El Model Context Protocol (MCP) de Anthropic se ha convertido en el estándar abierto para conectar entornos de agentes IA (como Claude Desktop, Cursor o Zed) con fuentes de datos y herramientas externas.
En este artículo, exploramos cómo desplegar servidores MCP basados en Server-Sent Events (SSE) en la red edge de Cloudflare Workers, logrando latencias inferiores a 10ms y eliminando arranques en frío (cold starts).
Resumen Clave
- stdio vs. SSE/HTTP: Mientras que
stdioes ideal para pruebas locales, SSE (Server-Sent Events) es imprescindible para entornos multiusuario y producción.- Ventajas de Cloudflare Workers: V8 isolates eliminan los cold starts y procesan invocaciones en 10-30ms globalmente.
- Integración con D1 & KV: Permite a los agentes de IA consultar bases de datos SQLite (D1) directamente en el borde.
Implementación de un Servidor MCP en Cloudflare Workers
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 () => ({
tools: [
{
name: "query_user_stats",
description: "Consulta estadísticas de uso de la base de datos D1.",
inputSchema: {
type: "object",
properties: { startDate: { type: "string" }, limit: { type: "number" } },
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, data: results }) }]
};
}
throw new Error(`Herramienta desconocida: ${request.params.name}`);
});
return server;
}
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) => {
await writer.write(new TextEncoder().encode(`data: ${JSON.stringify(message)}\n\n`));
},
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" }
});
});
app.post("/message", async (c) => {
const sessionId = c.req.query("sessionId");
if (!sessionId || !activeTransports.has(sessionId)) return c.text("Sesión no encontrada", 404);
const transport = activeTransports.get(sessionId)!;
await transport.handlePostMessage(c.req.raw, c.res.raw, await c.req.json());
return c.text("Accepted", 202);
});
export default app;
Conclusión
Desplegar MCP en Cloudflare Workers transforma scripts de prueba locales en infraestructura empresarial escalable y segura en el borde.