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

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ツールの共有バックエンドを実現できます。