본문 바로가기

AI관련

TypeScript MCP 서버를 npm에 배포하기

반응형

MCP 서버를 직접 만들면 Claude에게 나만의 도구를 줄 수 있다. 이 글에서는 Python이 아닌 TypeScript로 프로덕션급 MCP 서버를 만들고, npm에 배포하는 전체 과정을 다루다.

사전 지식: MCP 개념은 이전 글을, FastMCP 기초는 MCP 서버 직접 만들기를 참고한다.

 

1. 왜 TypeScript인가

이전 글에서 Python FastMCP로 간단한 TODO 서버를 만들었다. 빠른 프로토타이핑에는 Python이 좋지만, 프로덕션 배포에는 TypeScript가 유리하다.

기준 Python (FastMCP) TypeScript (SDK)
프로토타이핑 빠름 보통
타입 안전성 런타임 검증 컴파일 타임 검증
npm 배포 불가 (PyPI) npx로 즉시 실행
Claude Code 호환 좋음 최적 (공식 SDK)

핵심 차이는 배포다. TypeScript로 만들면 사용자가 이렇게 쓸 수 있다:

# 설치 없이 즉시 실행
npx my-mcp-server

Python은 uv나 pip install이 필요하지만, npm은 npx 한 줄이면 끝난다.

 

2. 프로젝트 구조

TypeScript MCP 서버의 기본 구조는 다음과 같다.

my-mcp-server/
├── src/
│   ├── index.ts          # 진입점
│   ├── tools/            # Tool 구현
│   ├── db/               # 데이터베이스
│   └── utils/
├── tests/
├── package.json
├── tsconfig.json
└── README.md

 

3. 핵심 의존성

{
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.0.0",
    "better-sqlite3": "^12.6.2",
    "zod": "^3.23.0"
  },
  "devDependencies": {
    "typescript": "^5.7.0",
    "vitest": "^1.6.0"
  }
}
패키지 역할
@modelcontextprotocol/sdk MCP 프로토콜 공식 SDK
better-sqlite3 로컬 데이터 영속화 (WAL 모드)
zod 입력 스키마 검증
vitest 테스트 프레임워크

 

4. MCP 서버 기본 구조

서버 초기화

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

const server = new McpServer({
  name: "my-mcp-server",
  version: "1.0.0",
});

const transport = new StdioServerTransport();
await server.connect(transport);

StdioServerTransport는 stdin/stdout으로 통신한다. Claude Code가 MCP 서버를 실행하면 이 채널로 JSON-RPC 메시지를 주고받는다.

Tool 등록

import { z } from "zod";

server.tool(
  "search_items",
  "키워드로 항목을 검색한다",
  {
    query: z.string().describe("검색 키워드"),
    limit: z.number().optional().default(10).describe("최대 결과 수"),
  },
  async ({ query, limit }) => {
    const results = db.search(query, limit);
    return {
      content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
    };
  }
);

핵심: z.string().describe()의 describe가 매우 중요하다. Claude는 이 설명을 읽고 언제 이 Tool을 호출할지 판단한다.

 

5. SQLite로 데이터 영속화

MCP 서버가 재시작해도 데이터를 유지하려면 SQLite를 쓴다.

import Database from "better-sqlite3";
import { join } from "path";
import { homedir } from "os";

const DB_PATH = join(homedir(), ".my-mcp", "data.db");
const db = new Database(DB_PATH);

db.pragma("journal_mode = WAL");
db.pragma("foreign_keys = ON");

위반 사례

// [Bad] 매번 쿼리마다 DB 연결을 새로 만든다
async function search(query: string) {
  const db = new Database(DB_PATH);
  const results = db.prepare("SELECT * FROM items").all();
  db.close();
  return results;
}

개선안

// [Good] 싱글턴으로 DB 연결을 재사용한다
class DatabaseManager {
  private static instance: Database.Database | null = null;

  static get(): Database.Database {
    if (!this.instance) {
      this.instance = new Database(DB_PATH);
      this.instance.pragma("journal_mode = WAL");
    }
    return this.instance;
  }
}

 

6. Zod로 입력 검증

Zod는 런타임 타입 검증과 MCP 스키마 생성을 동시에 처리한다.

const TaskSchema = z.object({
  title: z.string().min(1).describe("할 일 제목"),
  priority: z.enum(["high", "medium", "low"]).default("medium").describe("우선순위"),
  tags: z.array(z.string()).optional().describe("태그 목록"),
});

Zod의 .describe()가 자동으로 MCP 스키마의 설명 필드가 된다.

 

7. 테스트 작성

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import Database from "better-sqlite3";

describe("DatabaseManager", () => {
  let db: Database.Database;

  beforeEach(() => {
    db = new Database(":memory:");
  });

  afterEach(() => {
    db.close();
  });

  it("항목을 추가하고 검색할 수 있다", () => {
    const stmt = db.prepare("INSERT INTO items (title) VALUES (?)");
    stmt.run("테스트 항목");
    const results = db.prepare("SELECT * FROM items").all();
    expect(results).toHaveLength(1);
  });
});

핵심: :memory:로 테스트용 임시 DB를 만들면 파일 정리가 필요 없다.

 

8. npm 배포 설정

package.json 핵심 필드

{
  "name": "my-mcp-server",
  "version": "1.0.0",
  "main": "dist/index.js",
  "bin": { "my-mcp-server": "dist/index.js" },
  "type": "module",
  "scripts": {
    "build": "tsc",
    "test": "vitest run",
    "prepublishOnly": "npm run build && npm test"
  },
  "files": ["dist"]
}
필드 역할
bin npx my-mcp-server로 실행 가능하게 만든다
files npm에 dist/ 폴더만 업로드한다
prepublishOnly 배포 전 빌드 + 테스트를 자동 실행한다

배포 명령

npm login
npm publish
npm version patch
npm publish

 

9. Claude Code에 등록

# npx로 실행 (설치 불필요)
claude mcp add my-mcp-server -- npx my-mcp-server

# 프로젝트 범위로 등록 (팀 공유)
claude mcp add my-mcp-server --scope project -- npx my-mcp-server

 

10. 실전 팁

Tool 설명은 최대한 구체적으로

// [Bad] 설명이 모호하다
server.tool("search", "검색한다", ...);

// [Good] Claude가 정확히 언제 써야 하는지 알 수 있다
server.tool(
  "search_solutions",
  "과거에 해결한 에러 솔루션을 키워드로 검색한다.",
  ...
);

토큰 효율을 고려하라

// [Bad] 전체 데이터를 한 번에 반환
server.tool("list_all", "전체 목록", {}, async () => {
  const all = db.getAll();
  return { content: [{ type: "text", text: JSON.stringify(all) }] };
});

// [Good] 요약 목록 먼저, 상세는 별도 Tool로
server.tool("list_items", "항목 목록 (제목만)", {
  limit: z.number().default(20),
}, async ({ limit }) => {
  const items = db.listTitles(limit);
  return { content: [{ type: "text", text: JSON.stringify(items) }] };
});

목록 조회와 상세 조회를 분리하면 토큰을 절약할 수 있다.

 

정리

  • TypeScript MCP 서버는 @modelcontextprotocol/sdk로 만들고 npx로 배포할 수 있다
  • better-sqlite3로 데이터를 영속화하고, FTS5로 전문 검색을 지원한다
  • zod.describe()가 MCP 스키마 설명이 되므로 구체적으로 작성해야 한다
  • Tool 설명이 구체적일수록 Claude가 정확한 타이밍에 호출한다
  • 목록/상세를 분리하면 토큰 효율이 올라간다
  • prepublishOnly로 배포 전 빌드+테스트를 자동화한다

 

관련 링크

참고 자료

반응형