TypeScript로 견고한 MCP 서버 구축

발행: (2026년 5월 24일 PM 08:18 GMT+9)
5 분 소요
원문: Dev.to

TypeScript로 구축하는 MCP 서버, 무너지지 않게

당신의 MCP 서버는 도구 3개 정도에서는 잘 동작합니다. 하지만 도구 12개가 되면 손대기 두려운 스위치 케이스들의 더미가 됩니다. 여기서는 도메인‑주도 설계(DDD)에서 직접 차용한 TypeScript 아키텍처를 소개합니다. 이 아키텍처는 서버가 성장해도 코드를 깔끔하게 유지해 줍니다.

대부분의 MCP 서버 튜토리얼은 다음과 같은 코드로 시작합니다:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server(
  { name: "my-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    { name: "get_user", description: "\"Get a user by ID\", inputSchema: { ... } },"
    { name: "create_order", description: "\"Create an order\", inputSchema: { ... } },"
    { name: "send_email", description: "\"Send an email\", inputSchema: { ... } },"
    { name: "get_product", description: "\"Get product details\", inputSchema: { ... } },"
    // ...8 more tools
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  switch (request.params.name) {
    case "get_user": return handleGetUser(request.params.arguments);
    case "create_order": return handleCreateOrder(request.params.arguments);
    case "send_email": return handleSendEmail(request.params.arguments);
    // ...
  }
});

익숙하시나요? 문제는 코드가 잘못됐다는 것이 아니라, 정확히 한 사람이 3주 동안만 유지보수할 수 있을 정도로만 확장된다는 점입니다.

실제 운영 환경에 들어서면—예를 들어 사용자 로직과 주문 로직을 담당하는 주체가 다르고, 인증 컨텍스트가 서로 다르며, 상태를 갖는 리소스가 필요해지면—구조가 필요합니다. 바로 여기서 도메인‑주도 설계(DDD)의 가치가 발휘됩니다.

600페이지짜리 책을 읽어야 하는 것은 아닙니다. 여기서는 90% 이상의 상황을 커버하는 세 가지 개념만 알면 충분합니다.

DDD 개념의미MCP에서의 대응
경계된 컨텍스트용어가 일관된 의미를 갖는 네임스페이스도구 이름 접두사 (users__get, orders__create)
애그리게이트 루트관련된 상태들의 클러스터에 대한 단일 진입점각 핸들러에 전달되는 타입이 지정된 서버 컨텍스트 객체
도메인 이벤트시스템에서 발생한 사실변경 후 발행되는 MCP 알림

이 분야에 대한 번역 규칙: “아키텍처” 글을 쓰지 말고, 코드를 통해 아키텍처를 가르치는 TypeScript 튜토리얼을 작성하라. 이제 실제로 만들어 보겠습니다.

DDD에서 경계된 컨텍스트는 특정 도메인 모델이 적용되는 경계를 의미합니다. MCP 서버에서는 이것을 도구 명명 규칙관련 도구들을 묶는 팩터리 함수로 깔끔하게 구현할 수 있습니다.

다음은 createDomainTools 팩터리 예시입니다:

// src/domains/types.ts

import { Tool, CallToolResult } from "@modelcontextprotocol/sdk/types.js";

export interface DomainTool {
  definition: Tool;
  handler: (input: TInput) => Promise;
}

export interface DomainModule {
  namespace: string;
  tools: DomainTool[];
}

export function createDomainModule(
  namespace: string,
  tools: DomainTool[]
): DomainModule {
  return {
    namespace,
    tools: tools.map((tool) => ({
      ...tool,
      definition: {
        ...tool.definition,
        name: `${namespace}__${tool.definition.name}`,
      },
    })),
  };
}

이제 users 도메인을 정의해 보겠습니다:

// src/domains/users.ts

import { createDomainModule } from "./types.js";

export const usersDomain = createDomainModule("users", [
  {
    definition: {
      name: "get",
      description: "Get a user by ID",
      inputSchema: {
        type: "object",
        properties: {
          id: { type: "string", description: "User UUID" },
        },
        required: ["id"],
      },
    },
    handler: async ({ id }: { id: string }) => {
      const user = await db.users.findById(id);
      return {
        content: [{ type: "text", text: JSON.stringify(user) }],
      };
    },
  },
  {
    definition: {
      name: "list",
      description: "List users with optional filters",
      inputSchema: {
        type: "object",
        properties: {
          limit: { type: "number" },
          role: { type: "string" },
        },
      },
    },
    handler: async ({ limit = 20, role }: { limit?: number; role?: string }) => {
      const users = await db.users.findMany({ limit, role });
      return {
        content: [{ type: "text", text: JSON.stringify(users) }],
      };
    },
  },
]);

생성된 도구 이름은 users__getusers__list

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.