TypeScript로 첫 stdio MCP 서버 만들기

MCP·에이전트 만들기 2026-07-16 20:15:00 mcptypescriptstdio

글 목록 →

서버가 무엇을 노출하는지 개념으로 익혔다면, 이제 가장 작은 MCP 서버를 직접 만들 차례다. 목표는 소박하다. 도구(tool) 하나를 가진 stdio 서버를 TypeScript로 처음부터 끝까지 만든다. 이 글을 따라가면 Claude Code에 붙여 대화로 굴릴 수 있는 서버가 손에 남는다.

프로젝트 준비

먼저 패키지를 깐다. MCP 공식 SDK와, 도구 입력을 검증할 zod가 필요하다.

npm init -y
npm i @modelcontextprotocol/sdk zod
npm i -D typescript @types/node tsx

package.json"type": "module"을 넣는다. MCP SDK는 ES Module이라 이게 없으면 import부터 막힌다.

{
  "type": "module",
  "scripts": { "dev": "tsx watch src/index.ts" }
}

서버와 도구 정의

핵심은 McpServer 인스턴스를 만들고 거기에 도구를 등록하는 것이다. 도구는 이름, 설명, 입력 스키마, 그리고 실제로 동작하는 핸들러로 이뤄진다.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

export function createServer() {
  const server = new McpServer({ name: "hello-mcp", version: "0.0.0" });

  server.registerTool(
    "add",
    {
      title: "두 수 더하기",
      description: "정수 두 개를 받아 합을 돌려준다.",
      inputSchema: { a: z.number(), b: z.number() },
    },
    async ({ a, b }) => ({
      content: [{ type: "text", text: String(a + b) }],
    }),
  );

  return server;
}

description은 사람이 아니라 모델이 읽는다. 모델은 이 문장을 보고 도구를 부를지 판단하니, 무엇을 하는 도구인지 분명히 적어야 한다. inputSchema를 zod로 선언하면 런타임 검증과 타입이 한 번에 따라온다.

stdio로 연결하기

서버 객체만으로는 아무 일도 안 일어난다. transport에 연결해야 클라이언트와 대화를 시작한다. 로컬에서 쓸 도구라면 stdio가 가장 쉽다.

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

const server = createServer();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[hello-mcp] 서버 시작"); // 로그는 반드시 stderr로

여기서 규칙 하나만 지키면 된다. stdout은 JSON-RPC 통신 전용이므로, 사람이 볼 로그는 console.error로 stderr에 보낸다. console.log를 쓰면 프로토콜 스트림이 오염돼 연결이 깨진다.

빌드하고 Claude Code에 붙이기

TypeScript를 컴파일한 뒤, 빌드된 진입점을 Claude Code에 등록한다.

npx tsc
claude mcp add hello-mcp -- node dist/index.js

이제 대화창에서 “3하고 5 더해줘”라고 하면, 모델이 add 도구를 골라 호출하고 8을 돌려준다. 서버는 LLM을 부르지 않는다. 그냥 함수를 실행할 뿐이고, 언제 부를지는 모델이 정한다. 이 작은 서버 하나에 MCP의 핵심 흐름이 다 들어 있다. 도구를 더 얹고 싶으면 registerTool을 반복하면 되고, 읽기용 데이터가 필요하면 resource를, 반복되는 절차가 있으면 prompt를 같은 방식으로 등록하면 된다. 처음 한 개를 붙여본 순간부터, 나머지는 전부 이 패턴의 변주다.

댓글

이 글에 대한 의견은 아래 댓글로 남겨주세요 (GitHub 계정 필요). 로그인 없이 남기고 싶다면