TypeScript SDK로 최소 MCP 서버 만들기: 도구 하나부터 연결 확인까지

SDK v2 패키지 설치, registerTool, stdio 실행, 클라이언트 등록과 점검

핵심 요약

  1. MCP TypeScript SDK v2는 @modelcontextprotocol/server 패키지로 서버를 만들고 2026-07-28 명세를 구현한다.
  2. 도구 설명에는 입력 형식과 실패 조건을 적어 모델의 호출 판단을 돕는다.
  3. 출력 스키마가 있으면 structuredContent와 같은 내용의 텍스트를 함께 반환한다.
  4. stdio 서버는 stdout에 로그를 쓰지 말고 stderr를 사용한다.

사내 API 하나를 AI 에이전트에서 쓰게 하려면 가장 먼저 도구 하나짜리 MCP 서버를 만들어 보는 것이 좋다. MCP TypeScript SDK는 v2가 안정 릴리스 라인이며 2026-07-28 명세를 구현한다. v2부터 서버와 클라이언트 패키지가 @modelcontextprotocol/server, @modelcontextprotocol/client로 나뉘었고, Node.js·Bun·Deno에서 실행된다. 이 글은 사내 재고 조회 도구 하나를 가진 서버를 만들고 클라이언트에 연결해 확인하는 과정까지 다룬다. 명세 변경 배경은 2026-07-28 스펙 마이그레이션 글을 참고한다.

1단계: 프로젝트와 패키지 준비

mkdir inventory-mcp && cd inventory-mcp
npm init -y
npm install @modelcontextprotocol/server zod
npm install -D typescript @types/node tsx
npx tsc --init

SDK는 Standard Schema를 지원해 도구 입력 스키마에 Zod v4, Valibot, ArkType 같은 라이브러리를 쓸 수 있다. 이 글에서는 SDK 예제와 같은 zod/v4를 쓴다. package.json에는 ESM을 쓰도록 "type": "module"을 추가한다.

2단계: 도구 하나를 가진 서버 작성

// src/server.ts
import { McpServer } from '@modelcontextprotocol/server';
import { serveStdio } from '@modelcontextprotocol/server/stdio';
import * as z from 'zod/v4';

const STOCK: Record<string, number> = { 'SKU-1001': 42, 'SKU-1002': 0 };

serveStdio(() => {
  const server = new McpServer({ name: 'inventory', version: '0.1.0' });

  server.registerTool(
    'get_stock',
    {
      title: '재고 조회',
      description: '상품 SKU로 현재 재고 수량을 조회한다. 존재하지 않는 SKU는 오류를 반환한다.',
      inputSchema: z.object({ sku: z.string().regex(/^SKU-\d{4}$/) }),
      outputSchema: z.object({ sku: z.string(), quantity: z.number().int() }),
      annotations: { readOnlyHint: true, openWorldHint: false },
    },
    async ({ sku }) => {
      if (!(sku in STOCK)) {
        return {
          isError: true,
          content: [{ type: 'text', text: '알 수 없는 SKU: ' + sku + '. SKU-0000 형식인지 확인하세요.' }],
        };
      }
      const result = { sku, quantity: STOCK[sku] };
      return {
        content: [{ type: 'text', text: JSON.stringify(result) }],
        structuredContent: result,
      };
    }
  );

  return server;
});

코드의 핵심은 세 가지다. 첫째, 도구 설명은 모델이 언제 이 도구를 쓸지 판단하는 근거이므로 입력 형식과 실패 조건까지 적는다. 둘째, 명세는 출력 스키마가 있으면 서버가 스키마에 맞는 structuredContent를 반환해야 하고, 하위 호환을 위해 같은 JSON을 텍스트 콘텐츠로도 넣는 것을 권한다. 셋째, 입력 오류나 업무 로직 오류는 프로토콜 오류가 아니라 isError: true인 도구 실행 결과로 돌려준다. 그래야 모델이 메시지를 읽고 인자를 고쳐 다시 시도할 수 있다.

3단계: stdio 서버에서 로그를 남기는 방법

stdio 전송은 표준 입출력으로 JSON-RPC 메시지를 주고받는다. SDK 문서는 stdio를 쓸 때 stdout에 직접 쓰지 말고 로그는 stderr로 보내라고 경고한다. console.log를 디버깅에 쓰면 프로토콜 메시지 사이에 일반 텍스트가 섞여 클라이언트가 응답을 해석하지 못한다.

// 잘못된 예: stdout을 오염시킨다
console.log('get_stock 호출', sku);

// 올바른 예: stderr로 보낸다
console.error('[inventory] get_stock 호출', sku);

4단계: 실행과 클라이언트 등록

# 개발 중 실행 확인(입력을 기다리며 멈춰 있으면 정상)
npx tsx src/server.ts

# 빌드 후 실행
npx tsc && node dist/server.js

클라이언트 설정 파일 형식은 제품마다 조금씩 다르지만, 대부분 실행 명령과 인자를 등록하는 구조다. 다음은 프로젝트 단위 .mcp.json 형식의 예다.

{
  "mcpServers": {
    "inventory": {
      "command": "node",
      "args": ["/절대경로/inventory-mcp/dist/server.js"]
    }
  }
}

상대 경로는 클라이언트가 서버를 실행하는 작업 디렉터리에 따라 달라지므로 절대 경로를 쓰는 편이 안전하다. VS Code 확장에 연결하는 예는 VS Code Continue MCP 연결 글에 정리했다.

5단계: 배포 전에 확인할 항목

항목확인 방법기대 결과
도구 목록MCP Inspector로 tools/list 호출get_stock과 입력·출력 스키마 표시
정상 호출sku=SKU-1001로 호출structuredContent에 수량 반환
입력 오류sku=1001로 호출스키마 검증 실패 처리
업무 오류없는 SKU로 호출isError: true와 수정 방법이 담긴 메시지
stdout 오염로그를 남긴 상태로 Inspector 연결연결 오류 없이 동작
# Inspector 웹 UI로 서버 실행(브라우저에서 출력된 URL 열기)
npx @modelcontextprotocol/inspector node dist/server.js

# CLI 모드로 도구 목록만 확인
npx @modelcontextprotocol/inspector --cli node dist/server.js --method tools/list

사내 API로 확장할 때의 작업 순서

  1. 목업 데이터(STOCK)를 실제 API 호출 함수로 바꾸되, 도구 핸들러에는 입력 변환과 결과 정리만 남기고 호출 로직은 별도 모듈로 분리한다.
  2. API 키는 코드에 쓰지 않고 클라이언트 설정의 env로 주입한다.
  3. 외부 API 실패(타임아웃, 5xx)는 isError: true 결과로 바꿔 모델이 재시도 여부를 판단하게 한다.
  4. 도구를 하나 추가할 때마다 Inspector로 목록·정상 호출·오류 호출을 다시 확인한다.
  5. 도구가 3~4개를 넘으면 이름에 inventory. 같은 접두사를 붙여 다른 서버 도구와 구분한다.

사내 API를 실제로 붙일 때는 입력 검증, 접근 제어, 호출 빈도 제한, 출력 정제가 명세상 서버의 의무라는 점을 기억한다. 원격 HTTP 서버로 확장하면 인가가 필요하므로 MCP 서버 OAuth 글의 필수 구현 항목을 함께 점검한다. SDK는 Express, Hono, Fastify, 웹 표준 런타임용 HTTP 서빙 가이드를 별도로 제공한다.

초보자가 자주 실수하는 포인트

  • console.log로 디버깅해 stdio 프로토콜 메시지를 깨뜨리는 경우
  • 업무 오류를 예외로 던져 모델이 원인을 읽지 못하게 하는 경우
  • 클라이언트 설정에 상대 경로를 써서 서버 실행이 실패하는 경우
  • 도구 설명을 한 단어로 써서 모델이 언제 호출할지 판단하지 못하는 경우

체크리스트

  • package.json에 "type": "module"을 설정했는가
  • 도구 설명에 입력 형식과 실패 조건이 있는가
  • 업무 오류를 isError: true 결과로 반환하는가
  • 로그가 모두 stderr로 나가는가
  • Inspector에서 정상·오류 호출을 모두 확인했는가

자주 묻는 질문

v1 SDK 예제의 StdioServerTransport는 이제 못 쓰나요?

SDK 저장소 예제는 StdioServerTransport와 connect를 쓰는 방식도 보여 주고, v2 문서는 serveStdio 헬퍼를 소개한다. 설치한 버전의 문서 예제를 기준으로 고른다.

Zod 대신 다른 검증 라이브러리를 써도 되나요?

SDK는 Standard Schema를 지원하므로 Zod v4, Valibot, ArkType 등 호환 라이브러리를 쓸 수 있다.

도구 주석의 readOnlyHint를 넣으면 클라이언트가 승인 없이 실행하나요?

주석은 힌트일 뿐이며 명세는 신뢰하지 않는 서버의 주석을 믿지 말라고 한다. 승인 방식은 클라이언트 정책에 따른다.

참고 자료 · 검증 기준

위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.

이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.