effidevFlutter・Cloudflareエッジ・クラウドコスト最適化
日本語

Cloudflare WorkersでModel Context Protocol(MCP)サーバーを構築する: エッジAIエージェントツール開発実戦

Cloudflare WorkersでModel Context Protocol(MCP)サーバーを構築する

Anthropicが公開した Model Context Protocol (MCP) は、Claude DesktopやCursor、ZedなどのAIエージェント環境と外部ツール・データソースを接続する事実上の標準プロトコルとなりました。しかし、多くのガイドはローカル環境の stdio 通信に依存したNode.js/Python開発に焦点を当てており、チーム全体での共有ツール化や本番サービスへの展開時に課題が生じます。

本記事では、Cloudflare Workers上でServer-Sent Events (SSE) トランスポートベースのMCPサーバーをエッジにデプロイする実践パターン を解説します。

要約

  • stdio vs. SSE/HTTP: ローカルテストには stdio が便利ですが、本番運用およびマルチユーザー環境にはHTTPベースの SSE (Server-Sent Events) が不可欠です。
  • エッジインフラの優位性: V8 isolateによるコールドスタートのない設計により、世界300拠点以上のエッジで10〜30msの低遅延応答を実現します。
  • D1 & KV統合: Cloudflare D1 (SQLite) やKVをWorkers内で参照し、AIエージェントがエッジDBを直接検索できるMCP Toolsを作成可能です。

MCPトランスポートアーキテクチャ: stdioからSSEへ

項目 stdio Transport SSE / HTTP Transport
環境 ローカル開発(子プロセス起動) 本番 / エッジサーバーレス
通信方式 Standard Input / Output HTTP GET (SSE) + POST (JSON-RPC)
ネットワーク ローカルマシン限定 インターネット(HTTPS)経由でアクセス可能
バック엔드 Node.js, Python CLI Cloudflare Workers, AWS Lambda

Cloudflare Workersでの実装例

@modelcontextprotocol/sdk はWeb標準APIをサポートしているため、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: "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(`Unkown tool: ${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("Not found", 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;

まとめ

Model Context Protocol (MCP) をCloudflare Workersに構築することで、チーム間で安全かつ低遅延に利用できるAI Agentツールの共有バックエンドを実現できます。