effidevFlutter · Edge de Cloudflare · Optimización de costes en la nube
Español

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

Servidor MCP en Cloudflare Workers

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 stdio es 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.