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 에이전트 환경과 외부 데이터소스·도구를 연결하는 사실상의 오픈 표준으로 자리잡았습니다. 하지만 기존 MCP 서버 가이드 대부분은 stdio 통신을 기반으로 한 로컬 Node.js/Python 프로세스 실행에 집중되어 있어, 팀 단위의 원격 공유 도구나 프로덕션 에이전트 서비스로 확장할 때 한계에 부딪힙니다.

이 글에서는 Cloudflare Workers를 활용해 Server-Sent Events(SSE) 트랜스포트 기반의 MCP 서버를 에지(Edge)에 배포하는 실전 패턴을 다룹니다. Cloudflare의 300여 개 글로벌 데이터센터 인프라를 활용하여 Cold Start 없는 10ms 미만 지연시간의 에이전트 도구를 구축하는 방법부터, D1 데이터베이스 연동, OAuth/Bearer 토큰 보안 처리까지 코드로 살펴봅니다.

핵심 요약

  • stdio vs SSE/HTTP: 로컬 개발에서는 stdio 트랜스포트가 편리하지만, 멀티 사용자 지원 및 프로덕션 배포를 위해서는 HTTP 기반의 SSE(Server-Sent Events) 트랜스포트가 필수적입니다.
  • Cloudflare Workers의 이점: V8 isolate 기반 런타임으로 Cold Start가 없으며, 전 세계 300+ 에지 로케이션에서 10~30ms 내외의 응답 속도로 MCP 요청을 처리할 수 있습니다.
  • D1 & KV 연동: Worker 내부에서 Cloudflare D1(SQLite) 및 KV(Key-Value)를 연동하여 에이전트가 직접 에지 데이터베이스를 조회가 가능하도록 MCP Tools를 정의할 수 있습니다.
  • 보안 패턴: 인터넷에 노출되는 에지 MCP 서버는 Bearer Token 인증 미들웨어 및 Cloudflare Access를 결합하여 외부의 악의적 도구 호출을 차단해야 합니다.

MCP 트랜스포트 아키텍처: stdio에서 SSE로

MCP specification은 클라이언트(AI 에이전트 Host)와 서버간 메시지 교환을 위해 JSON-RPC 2.0 프로토콜을 사용합니다. 이때 물리적인 이동 통로는 크게 두 가지입니다.

항목 stdio Transport SSE / HTTP Transport
운영 환경 로컬 개발 환경 (Client가 자식 프로세스 생성) 프로덕션 / 에지 서버리스
연결 방식 Standard Input / Output 스트림 HTTP GET (SSE) + POST (JSON-RPC)
네트워크 접근성 로컬 머신 내부로 제한 인터넷(HTTPS) 어디서나 접근 가능
확장성 싱글 유저 (로컬 CLI) 멀티 유저 (인증/권한 부여 가능)
적합한 백엔드 Node.js, Python CLI Cloudflare Workers, AWS Lambda, Fastify

Cloudflare Workers 환경에서는 로컬 프로세스를 띄울 수 없으므로, HTTP 기반 SSE(Server-Sent Events) 패턴을 구현해야 합니다. 클라이언트는 /sse 엔드포인트로 GET 요청을 보내 실시간 이벤트 스트림을 유지하고, 전달받은 endpoint URL(예: /message?sessionId=...)로 POST 요청을 보내 툴 실행 및 리소스 조회를 요청합니다.


Cloudflare Workers용 MCP 서버 실전 구현

Official @modelcontextprotocol/sdk 패키지는 Web Standard API(Request/Response, ReadableStream)를 지원하므로 Cloudflare Workers 런타임에서 완벽히 동작합니다.

1. 프로젝트 설정 및 패키지 설치

npm init cloudflare@latest mcp-edge-worker -- --type=workers-types-typescript
cd mcp-edge-worker
npm install @modelcontextprotocol/sdk hono

wrangler.toml 파일에 D1 데이터베이스 바인딩을 추가합니다:

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. SSE 트랜스포트 및 MCP 핸들러 작성

아래는 Hono 프레임워크를 기반으로 MCP Server 및 SSEServerTransport를 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 }>();

// 활성화된 SSE 트랜스포트 세션 관리 (인메모리 Map)
const activeTransports = new Map<string, SSEServerTransport>();

// MCP Server 인스턴스 팩토리 함수
function createMcpServer(db: D1Database) {
  const server = new Server(
    { name: "effidev-edge-mcp", version: "1.0.0" },
    { capabilities: { tools: {} } }
  );

  // 1. 도구 목록(Tools List) 정의
  server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
      tools: [
        {
          name: "query_user_stats",
          description: "D1 에지 DB에서 특정 기간 사용자의 결제 및 활동 통계를 조회합니다.",
          inputSchema: {
            type: "object",
            properties: {
              startDate: { type: "string", description: "YYYY-MM-DD" },
              limit: { type: "number", description: "최대 조회 건수 (기본 10)" },
            },
            required: ["startDate"],
          },
        },
      ],
    };
  });

  // 2. 도구 실행(Call Tool) 핸들러
  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(`알 수 없는 도구: ${request.params.name}`);
  });

  return server;
}

// Bearer Token 인증 미들웨어
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 연결 엔드포인트 (GET /sse)
app.get("/sse", async (c) => {
  const sessionId = crypto.randomUUID();
  
  // TransformStream을 사용하여 Web ReadableStream 기반 SSE 생성
  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 메시지 수신 엔드포인트 (POST /message)
app.post("/message", async (c) => {
  const sessionId = c.req.query("sessionId");
  if (!sessionId || !activeTransports.has(sessionId)) {
    return c.text("Session not found or expired", 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;

Claude Desktop & Cursor 클라이언트 연결 설정

Cloudflare Workers에 배포된 MCP 서버 URL이 https://mcp.effidev.workers.dev 라면, 클라이언트에 HTTP/SSE 트랜스포트를 등록해야 합니다.

Claude Desktop (claude_desktop_config.json)

최신 Claude Desktop은 SSE 트랜스포트를 직접 지원하거나, mcp-remote 브릿지 유틸리티를 활용해 연결할 수 있습니다.

{
  "mcpServers": {
    "effidev-edge-tools": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/inspector",
        "--sse",
        "https://mcp.effidev.workers.dev/sse",
        "--header",
        "Authorization: Bearer YOUR_SECURE_TOKEN"
      ]
    }
  }
}

벤치마크 및 비용 비교

기존 AWS Fargate나 Self-hosted Node.js 서버에 MCP 서버를 띄우는 것 대비 Cloudflare Workers 인프라의 효율성을 수치로 비교했습니다.

성능/비용 지표 Node.js (AWS EC2 t4g.small) Cloudflare Workers Paid
월 고정 비용 $14.60 (24/7 인스턴스) $5.00 (월 1,000만 요청 포함)
TTFB (Time to First Byte) 120ms ~ 280ms (리전 이격 발생) 12ms ~ 35ms (글로벌 에지)
Cold Start 지연 없음 (항상 켜짐) 0ms (V8 Isolate)
최대 동시 SSE 커넥션 메모리 상한선 (약 2,000개) Workers 수평 확장 (무제한급)

결론: 에지 에이전트 인프라의 미래

Model Context Protocol(MCP)을 로컬 환경에 갇혀두지 않고 Cloudflare Workers 같은 글로벌 에지 인프라로 올리면, 팀원 누구나 공통의 데이터베이스와 내부 API 툴을 공유하는 ** enterprise-ready AI Agent 인프라**를 구축할 수 있습니다.

Cloudflare D1, KV, Vectorize와 같은 에지 렌더링 서버리스 스토리지와 MCP 표준을 조합하여 고성능 AI 에이전트 툴백엔드를 경험해 보세요.