Claude Code에 나만의 도구를 연결하는 MCP 서버를 처음부터 끝까지 만들어봅니다.
MCP란 무엇인가?
MCP(Model Context Protocol)는 AI 모델이 외부 도구, 데이터베이스, API에 접근할 수 있게 해주는 오픈 소스 표준 프로토콜입니다.
쉽게 비유하면 이렇습니다:
스마트폰(Claude) ←→ USB 케이블(MCP) ←→ 외부 장치(도구/API/DB)
스마트폰이 USB 케이블 하나로 카메라, 키보드, 프린터 등 다양한 장치에 연결되듯이, Claude도 MCP 하나로 GitHub, 데이터베이스, 사내 API 등 다양한 도구에 연결됩니다.
MCP 아키텍처 한눈에 보기
┌─────────────────────┐
│ Claude Code │ ← MCP 클라이언트 (호스트)
│ (또는 Claude Desktop) │
└────────┬────────────┘
│ MCP 프로토콜 (JSON-RPC)
│
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ MCP │ │ MCP │ │ MCP │
│ 서버 A │ │ 서버 B │ │ 서버 C │
│ (GitHub)│ │ (DB) │ │ (나만의)│
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ GitHub │ │ Postgres│ │ 로컬 │
│ API │ │ │ │ 파일/API│
└─────────┘ └─────────┘ └─────────┘
MCP 서버가 제공하는 3가지 기능
기능 설명 예시
| Tools (도구) | LLM이 호출할 수 있는 함수 | 날씨 조회, DB 쿼리, 파일 생성 |
| Resources (리소스) | 읽을 수 있는 데이터 | API 응답, 파일 내용, 설정값 |
| Prompts (프롬프트) | 미리 작성된 템플릿 | 코드 리뷰 양식, 보고서 템플릿 |
이 가이드에서는 가장 많이 쓰이는 Tools에 집중합니다.
실전 예제: "할 일 관리" MCP 서버 만들기
날씨 API 예제는 이미 많으니, 더 실용적인 할 일(Todo) 관리 MCP 서버를 만들어보겠습니다. 이 서버를 연결하면 Claude Code에서 자연어로 할 일을 관리할 수 있게 됩니다.
"오늘 해야 할 일에 '블로그 글 작성'을 추가해줘"
→ Claude가 MCP 서버의 add_todo 도구를 호출
→ 할 일이 JSON 파일에 저장됨
방법 1: Python으로 MCP 서버 만들기
Step 1: 환경 준비
# uv 설치 (Python 패키지 매니저)
curl -LsSf https://astral.sh/uv/install.sh | sh
# 프로젝트 생성
uv init todo-mcp
cd todo-mcp
# 가상환경 생성 및 활성화
uv venv
source .venv/bin/activate # macOS/Linux
# .venv\Scripts\activate # Windows
# MCP SDK 설치
uv add "mcp[cli]"
Step 2: 서버 코드 작성
todo_server.py 파일을 만듭니다:
import json
import os
from datetime import datetime
from mcp.server.fastmcp import FastMCP
# ============================================
# 1. MCP 서버 인스턴스 생성
# ============================================
mcp = FastMCP("todo-manager")
# 할 일 데이터를 저장할 파일 경로
TODO_FILE = os.path.expanduser("~/todos.json")
# ============================================
# 2. 헬퍼 함수: 파일 읽기/쓰기
# ============================================
def load_todos() -> list[dict]:
"""JSON 파일에서 할 일 목록을 불러옵니다."""
if not os.path.exists(TODO_FILE):
return []
with open(TODO_FILE, "r", encoding="utf-8") as f:
return json.load(f)
def save_todos(todos: list[dict]) -> None:
"""할 일 목록을 JSON 파일에 저장합니다."""
with open(TODO_FILE, "w", encoding="utf-8") as f:
json.dump(todos, f, ensure_ascii=False, indent=2)
# ============================================
# 3. MCP 도구(Tool) 정의 — 핵심 부분!
# ============================================
@mcp.tool()
def add_todo(title: str, priority: str = "medium") -> str:
"""할 일을 추가합니다.
Args:
title: 할 일 제목 (예: "블로그 글 작성")
priority: 우선순위 — "high", "medium", "low" 중 하나
"""
todos = load_todos()
new_todo = {
"id": len(todos) + 1,
"title": title,
"priority": priority,
"done": False,
"created_at": datetime.now().isoformat()
}
todos.append(new_todo)
save_todos(todos)
return f"✅ 할 일 추가 완료: [{priority}] {title} (ID: {new_todo['id']})"
@mcp.tool()
def list_todos(show_done: bool = False) -> str:
"""현재 할 일 목록을 보여줍니다.
Args:
show_done: True이면 완료된 항목도 표시합니다
"""
todos = load_todos()
if not todos:
return "📋 할 일이 없습니다. 새로운 할 일을 추가해보세요!"
if not show_done:
todos = [t for t in todos if not t["done"]]
if not todos:
return "🎉 모든 할 일을 완료했습니다!"
lines = ["📋 할 일 목록:", ""]
for t in todos:
status = "✅" if t["done"] else "⬜"
priority_emoji = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
t["priority"], "⚪"
)
lines.append(
f" {status} [{t['id']}] {priority_emoji} {t['title']}"
)
return "\n".join(lines)
@mcp.tool()
def complete_todo(todo_id: int) -> str:
"""할 일을 완료 처리합니다.
Args:
todo_id: 완료할 할 일의 ID 번호
"""
todos = load_todos()
for t in todos:
if t["id"] == todo_id:
if t["done"]:
return f"이미 완료된 할 일입니다: {t['title']}"
t["done"] = True
save_todos(todos)
return f"✅ 완료: {t['title']}"
return f"❌ ID {todo_id}에 해당하는 할 일을 찾을 수 없습니다."
@mcp.tool()
def delete_todo(todo_id: int) -> str:
"""할 일을 삭제합니다.
Args:
todo_id: 삭제할 할 일의 ID 번호
"""
todos = load_todos()
original_count = len(todos)
todos = [t for t in todos if t["id"] != todo_id]
if len(todos) == original_count:
return f"❌ ID {todo_id}에 해당하는 할 일을 찾을 수 없습니다."
save_todos(todos)
return f"🗑️ 할 일이 삭제되었습니다. (ID: {todo_id})"
# ============================================
# 4. 서버 실행
# ============================================
def main():
mcp.run(transport="stdio")
if __name__ == "__main__":
main()
핵심 포인트 해설
코드에서 가장 중요한 부분은 @mcp.tool() 데코레이터입니다:
@mcp.tool()
def add_todo(title: str, priority: str = "medium") -> str:
"""할 일을 추가합니다. ← Claude가 읽는 도구 설명
Args:
title: 할 일 제목 ← 파라미터 설명 (자동으로 스키마 생성)
priority: 우선순위 — "high", "medium", "low"
"""
FastMCP는 함수의 타입 힌트와 독스트링을 읽어서 자동으로 도구 정의(JSON Schema)를 만들어줍니다. Claude는 이 정보를 보고 "아, 할 일을 추가하려면 add_todo 도구에 title을 넣어 호출하면 되겠구나"라고 판단합니다.
Step 3: Claude Code에 연결
# Claude Code에 MCP 서버 등록
claude mcp add todo-manager -- uv --directory /절대/경로/todo-mcp run todo_server.py
각 부분의 의미:
부분 의미
| claude mcp add | MCP 서버를 추가하는 명령 |
| todo-manager | 서버 이름 (원하는 이름) |
| -- | 이후는 실제 실행 명령어 |
| uv --directory ... run todo_server.py | 서버를 실행하는 명령 |
Step 4: 사용해보기
Claude Code를 실행하고 자연어로 요청합니다:
> 오늘 할 일에 "MCP 블로그 글 작성"을 높은 우선순위로 추가해줘
✅ 할 일 추가 완료: [high] MCP 블로그 글 작성 (ID: 1)
> "저녁 운동하기"도 추가해줘
✅ 할 일 추가 완료: [medium] 저녁 운동하기 (ID: 2)
> 현재 할 일 목록 보여줘
📋 할 일 목록:
⬜ [1] 🔴 MCP 블로그 글 작성
⬜ [2] 🟡 저녁 운동하기
> 1번 완료 처리해줘
✅ 완료: MCP 블로그 글 작성
방법 2: TypeScript로 MCP 서버 만들기
TypeScript를 선호한다면 이 방법을 사용하세요.
Step 1: 환경 준비
# 프로젝트 생성
mkdir todo-mcp-ts && cd todo-mcp-ts
npm init -y
npm install @modelcontextprotocol/sdk zod@3
npm install -D @types/node typescript
# 소스 디렉터리 생성
mkdir src
tsconfig.json 생성:
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"outDir": "./build",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
package.json에 추가:
{
"type": "module",
"scripts": {
"build": "tsc && chmod 755 build/index.js"
}
}
Step 2: 서버 코드 작성
src/index.ts:
#!/usr/bin/env node
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
// ============================================
// 1. 서버 인스턴스 생성
// ============================================
const server = new McpServer({
name: "todo-manager",
version: "1.0.0",
});
const TODO_FILE = path.join(os.homedir(), "todos.json");
// ============================================
// 2. 헬퍼 함수
// ============================================
interface Todo {
id: number;
title: string;
priority: string;
done: boolean;
created_at: string;
}
function loadTodos(): Todo[] {
if (!fs.existsSync(TODO_FILE)) return [];
return JSON.parse(fs.readFileSync(TODO_FILE, "utf-8"));
}
function saveTodos(todos: Todo[]): void {
fs.writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2), "utf-8");
}
// ============================================
// 3. 도구 등록 — 핵심 부분!
// ============================================
// 할 일 추가
server.registerTool(
"add_todo",
{
description: "할 일을 추가합니다",
inputSchema: {
title: z.string().describe("할 일 제목 (예: '블로그 글 작성')"),
priority: z
.enum(["high", "medium", "low"])
.default("medium")
.describe("우선순위"),
},
},
async ({ title, priority }) => {
const todos = loadTodos();
const newTodo: Todo = {
id: todos.length + 1,
title,
priority,
done: false,
created_at: new Date().toISOString(),
};
todos.push(newTodo);
saveTodos(todos);
return {
content: [
{
type: "text" as const,
text: `✅ 할 일 추가 완료: [${priority}] ${title} (ID: ${newTodo.id})`,
},
],
};
}
);
// 할 일 목록 조회
server.registerTool(
"list_todos",
{
description: "현재 할 일 목록을 보여줍니다",
inputSchema: {
show_done: z
.boolean()
.default(false)
.describe("완료된 항목도 표시할지 여부"),
},
},
async ({ show_done }) => {
let todos = loadTodos();
if (!todos.length) {
return {
content: [{ type: "text" as const, text: "📋 할 일이 없습니다." }],
};
}
if (!show_done) {
todos = todos.filter((t) => !t.done);
}
const priorityEmoji: Record<string, string> = {
high: "🔴",
medium: "🟡",
low: "🟢",
};
const lines = todos.map(
(t) =>
` ${t.done ? "✅" : "⬜"} [${t.id}] ${priorityEmoji[t.priority] || "⚪"} ${t.title}`
);
return {
content: [
{
type: "text" as const,
text: `📋 할 일 목록:\n\n${lines.join("\n")}`,
},
],
};
}
);
// 할 일 완료
server.registerTool(
"complete_todo",
{
description: "할 일을 완료 처리합니다",
inputSchema: {
todo_id: z.number().describe("완료할 할 일의 ID 번호"),
},
},
async ({ todo_id }) => {
const todos = loadTodos();
const todo = todos.find((t) => t.id === todo_id);
if (!todo) {
return {
content: [
{
type: "text" as const,
text: `❌ ID ${todo_id}에 해당하는 할 일을 찾을 수 없습니다.`,
},
],
};
}
todo.done = true;
saveTodos(todos);
return {
content: [
{ type: "text" as const, text: `✅ 완료: ${todo.title}` },
],
};
}
);
// ============================================
// 4. 서버 실행
// ============================================
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Todo MCP Server running on stdio");
}
main().catch(console.error);
Step 3: 빌드 및 Claude Code에 연결
# 빌드
npm run build
# Claude Code에 등록
claude mcp add todo-manager -- node /절대/경로/todo-mcp-ts/build/index.js
Claude Code에서 MCP 서버 관리하기
서버 등록 (3가지 범위)
# 현재 프로젝트에서만 나만 사용 (기본값)
claude mcp add my-server -- node /path/to/server.js
# 모든 프로젝트에서 나만 사용
claude mcp add my-server -s user -- node /path/to/server.js
# 프로젝트 팀 전체가 공유 (.mcp.json에 저장)
claude mcp add my-server -s project -- node /path/to/server.js
범위 저장 위치 공유 범위
| local (기본) | 프로젝트별 사용자 설정 | 나만 사용, 현재 프로젝트만 |
| user | 전역 사용자 설정 | 나만 사용, 모든 프로젝트 |
| project | .mcp.json (Git에 포함) | 팀 전체 공유 |
서버 상태 확인 및 관리
# 등록된 서버 목록 확인
claude mcp list
# 특정 서버 상세 정보
claude mcp get todo-manager
# 서버 제거
claude mcp remove todo-manager
# Claude Code 세션 안에서 상태 확인
/mcp
환경 변수가 필요한 경우
API 키 등 환경 변수를 전달하려면 --env 플래그를 사용합니다:
claude mcp add github-server -e GITHUB_TOKEN=ghp_xxxx -- node /path/to/github-mcp.js
JSON으로 직접 추가
이미 설정 JSON이 있다면 add-json 명령을 사용합니다:
claude mcp add-json my-server '{
"type": "stdio",
"command": "node",
"args": ["/path/to/server.js"],
"env": {"API_KEY": "abc123"}
}'
이미 만들어진 인기 MCP 서버 활용하기
직접 만들기 전에, 이미 만들어진 MCP 서버를 먼저 연결해 써보는 것도 좋습니다.
GitHub MCP 서버
claude mcp add -s user --transport http github \
https://api.githubcopilot.com/mcp \
-H "Authorization: Bearer YOUR_GITHUB_PAT"
Playwright (브라우저 자동화)
claude mcp add playwright -- npx @playwright/mcp@latest
Context7 (라이브러리 최신 문서 조회)
claude mcp add context7 -- npx -y @upstash/context7-mcp@latest
Sequential Thinking (체계적 사고)
claude mcp add sequential-thinking -- npx -y @modelcontextprotocol/server-sequential-thinking
자주 하는 실수와 해결법
1. STDIO 서버에서 stdout 사용
# ❌ 절대 하면 안 됨 — JSON-RPC 메시지가 깨집니다
print("서버 시작됨")
# ✅ stderr로 보내거나 logging 사용
import logging
logging.info("서버 시작됨")
// ❌ 절대 하면 안 됨
console.log("서버 시작됨");
// ✅ stderr는 안전합니다
console.error("서버 시작됨");
STDIO 기반 MCP 서버에서 print()나 console.log()를 사용하면 stdout으로 출력되어 JSON-RPC 메시지와 섞여 서버가 작동하지 않습니다.
2. 상대 경로 사용
# ❌ 상대 경로 → Claude Code가 실행 위치를 모름
claude mcp add my-server -- node ./server.js
# ✅ 절대 경로 사용
claude mcp add my-server -- node /Users/me/projects/my-mcp/build/index.js
3. Windows에서 npx 실행 오류
# ❌ Windows에서 직접 npx 실행 → "Connection closed" 에러
claude mcp add my-server -- npx -y @some/package
# ✅ cmd /c 래퍼 사용
claude mcp add my-server -- cmd /c npx -y @some/package
4. 도구 설명을 빈약하게 작성
# ❌ Claude가 언제 이 도구를 쓸지 판단하기 어려움
@mcp.tool()
def do_thing(x: str) -> str:
"""작업을 수행합니다."""
# ✅ 구체적이고 명확한 설명
@mcp.tool()
def add_todo(title: str, priority: str = "medium") -> str:
"""할 일 목록에 새 항목을 추가합니다.
Args:
title: 할 일 제목 (예: "주간 보고서 작성", "코드 리뷰")
priority: 우선순위 — "high"(긴급), "medium"(보통), "low"(여유)
"""
Claude는 도구의 설명과 파라미터 정보를 읽고 적절한 도구를 선택합니다. 설명이 명확할수록 Claude가 정확하게 도구를 활용합니다.
MCP 서버 개발 흐름 요약
1. 환경 준비
└─ Python: uv + mcp[cli]
└─ TypeScript: npm + @modelcontextprotocol/sdk + zod
2. 서버 코드 작성
└─ FastMCP 인스턴스 생성 (Python) 또는 McpServer (TS)
└─ @mcp.tool() 또는 server.registerTool()로 도구 정의
└─ 함수 구현 + 명확한 독스트링/설명 작성
└─ mcp.run(transport="stdio")로 실행
3. Claude Code에 연결
└─ claude mcp add <이름> -- <실행 명령>
4. 테스트
└─ Claude Code에서 자연어로 요청
└─ /mcp 명령으로 서버 상태 확인
5. 문제 해결
└─ claude doctor로 설치 진단
└─ stdout 출력 제거 확인
└─ 절대 경로 사용 확인
마무리
MCP 서버를 만드는 것은 생각보다 간단합니다. Python이라면 @mcp.tool() 데코레이터 하나로 함수를 도구로 노출할 수 있고, TypeScript라면 server.registerTool()로 같은 일을 합니다. 핵심은 함수의 설명을 명확하게 작성하여 Claude가 도구를 잘 이해하도록 하는 것입니다.
이 가이드의 Todo 예제를 기반으로, 자신만의 API 연동, 데이터베이스 조회, 파일 관리 등 다양한 MCP 서버를 만들어보세요.
참고 자료:
- 공식 MCP 문서: modelcontextprotocol.io
- Claude Code MCP 가이드: code.claude.com/docs/en/mcp
- MCP Python SDK: github.com/modelcontextprotocol/python-sdk
- MCP TypeScript SDK: github.com/modelcontextprotocol/typescript-sdk
'AI 배우기' 카테고리의 다른 글
| 🎬 바이브 코딩 실전 레퍼런스 & 핵심 파일 가이드 (0) | 2026.09.06 |
|---|---|
| Claude Code 완벽 입문 가이드: 설치부터 기초 명령어까지 (0) | 2026.02.09 |
| [Linux] 리눅스 마스터로 가는 길: 필수 Shell 명령어 완벽 정리 (0) | 2025.12.28 |
| 개발자의 필수 도구, Git 핵심 명령어 총정리 (1) | 2025.12.27 |
| open-webui를 사용할때 docker가 왜 필요한가? (0) | 2025.07.22 |