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로 배포 전 빌드+테스트를 자동화한다
관련 링크
- MCP 개념과 설정 가이드: https://leesmemo.tistory.com/69
- Python FastMCP 서버 만들기: https://leesmemo.tistory.com/70
참고 자료
- MCP TypeScript SDK: https://github.com/modelcontextprotocol/typescript-sdk
- MCP 공식 문서: https://modelcontextprotocol.io/
- Claude Code MCP 설정: https://code.claude.com/docs/ko/mcp
- Zod 공식 문서: https://zod.dev/
'AI관련' 카테고리의 다른 글
| 자기학습하는 macOS 자동화 MCP 배포 (0) | 2026.03.25 |
|---|---|
| Claude Code Hooks로 AI 개발 자동화하기 (0) | 2026.03.22 |
| MCP 서버 직접 만들기: Python FastMCP (0) | 2026.01.29 |
| Claude MCP(Model Context Protocol) (0) | 2026.01.27 |
| 효율적인 Claude 프롬프트 엔지니어링 (1) | 2026.01.18 |