MCP 서버 직접 만드는 가이드. Model Context Protocol 개념·TypeScript/Python SDK 최소 서버·Tools Resources Prompts 차이·stdio Streamable HTTP 전송·Claude Desktop 등록·실전 가드레일까지.
지난 글에서 MCP(Model Context Protocol) 가 Claude를 Gmail·Notion·GitHub 같은 외부 도구에 연결하는 "AI용 USB-C" 라고 짧게 짚었습니다. 공식 마켓플레이스에 있는 MCP 서버만 써도 충분히 강력한데, 이걸 직접 만들면 회사 내부 DB·사내 API·로컬 파일까지 Claude가 자연어로 조작할 수 있게 됩니다.
오늘은 MCP 서버를 직접 만드는 법을 한 번에 정리합니다 — 개념·TypeScript/Python SDK·최소 동작 코드·Claude Desktop/Code 등록·실전 가드레일까지. 작성 시점(2026-05-15) 기준 공식 docs와 주요 튜토리얼 요약이에요.
1. MCP 한 줄 정리 — 왜 표준이 되었나
MCP — Anthropic이 2024년 11월에 공개한 JSON-RPC over stdio/HTTP 프로토콜. Claude(또는 다른 LLM)와 외부 도구 사이를 표준 인터페이스 로 연결합니다. 2025년 OpenAI·Google·Cursor·Cline까지 채택하면서 "AI 통합의 USB-C" 자리가 거의 확정됐어요.
핵심 아이디어 — 모델 쪽 코드와 도구 쪽 코드를 분리하는 것. 예전엔 Claude에 새 도구를 붙이려면 Anthropic SDK를 쓰는 모든 앱이 그 도구를 따로 구현해야 했어요. MCP가 등장한 뒤로는 MCP 서버 하나 만들어 두면 Claude Desktop·Claude Code·Cursor·Codex·Custom 앱이 모두 같은 서버를 공유합니다.
MCP의 세 컴포넌트
| 컴포넌트 | 역할 | 누가 정함 |
|---|---|---|
| Client | Claude Desktop·Code, Agent SDK, 사용자 앱 | 사용자가 어느 LLM 도구를 쓰느냐 |
| Transport | 통신 계층 (stdio 또는 Streamable HTTP) | 로컬이면 stdio, 원격이면 HTTP |
| Server | 도구·자원·프롬프트 노출 | 여기를 우리가 만든다 |
오늘 글의 주제는 세 번째 Server 입니다.
2. 서버가 노출하는 3가지 — Tools · Resources · Prompts
MCP 서버는 클라이언트에 세 종류의 능력(capability) 을 노출합니다. 이 셋의 차이를 처음 한 번 정확히 잡으면 설계 실수를 99% 막을 수 있어요.
| 항목 | 본질 | 누가 호출? | 예시 |
|---|---|---|---|
| Tools | 액션 — 실행되는 함수 | 모델이 결정 | send_email, run_migration, query_revenue |
| Resources | 읽기 전용 데이터 — URI로 참조 | 호스트(앱)가 결정 | postgres://customers/123, file:///docs/spec.md |
| Prompts | 재사용 가능 템플릿 | 사용자가 명시적으로 호출 | /review-code, /summarize-pr |
가장 흔한 실수 — 도구를 자원처럼 만들거나 자원을 도구처럼 만드는 것. "고객 정보 가져오기" 가 자원이냐 도구냐? 사용자가 "고객 정보 화면에 띄워줘" 라고 명시적으로 부르면 자원, 모델이 "이 분석을 위해 고객 정보가 필요하군" 하며 알아서 가져오면 도구예요. 같은 데이터라도 누가 부르느냐 로 갈립니다.
3. 두 가지 전송 방식 — stdio vs Streamable HTTP
서버를 Claude에 어떻게 연결 할지가 다음 결정. 두 가지 선택지가 있습니다.
3-1. stdio — 로컬 자식 프로세스
- Claude Desktop·Code가 서버를 자식 프로세스 로 띄움
- 표준 입출력(stdin·stdout)으로 JSON-RPC 메시지 주고받음
- 네트워크 X, 포트 X, 인증 X — 가장 단순
- 본인 노트북 안 자원에 접근할 때 99% 이쪽
3-2. Streamable HTTP — 원격 서버
- 별도 HTTP 서버를 띄워 클라이언트가 URL로 접속
- 인증(OAuth)·다중 사용자·클라우드 호스팅 가능
- 참고: 2025-03 SSE 폐기. 새 프로젝트는 Streamable HTTP 로 시작하세요
- 회사 동료가 같이 쓰는 사내 도구·SaaS 통합용
한 줄 결정 룰:
- 본인 노트북 안에서만 쓰는 도구 → stdio
- 팀·외부 사용자가 함께 쓰는 서비스 → Streamable HTTP
아래 코드 예시는 모두 stdio 기준입니다. HTTP는 같은 SDK에서 transport만 갈아끼면 됩니다.
4. TypeScript로 최소 서버 만들기
가장 인기 있는 SDK는 공식 TypeScript SDK (@modelcontextprotocol/sdk). npm 한 번 깔면 끝.
4-1. 프로젝트 초기화
mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
package.json 에 "type": "module" 추가해 ESM 사용.
4-2. 날씨 도구 하나짜리 서버
src/server.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
const server = new McpServer({
name: "weather-server",
version: "1.0.0",
});
server.tool(
"get_weather",
"Get current weather for a city",
{
city: z.string().describe("City name, e.g. Seoul"),
units: z.enum(["metric", "imperial"]).optional().default("metric"),
},
async ({ city, units }) => {
const apiKey = process.env.OPENWEATHER_API_KEY!;
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${apiKey}`;
const res = await fetch(url);
const data = await res.json();
return {
content: [
{
type: "text",
text: JSON.stringify({
city: data.name,
temp: data.main.temp,
condition: data.weather[0].description,
}),
},
],
};
},
);
// stdio 전송 시작
const transport = new StdioServerTransport();
await server.connect(transport);
핵심 패턴 4가지가 다 들어있어요:
new McpServer({...})— 서버 인스턴스 생성server.tool(name, desc, schema, handler)— 도구 등록- Zod 스키마로 입력 검증
StdioServerTransport로 연결
빌드·실행:
npx tsc
node dist/server.js
4-3. Claude Desktop에 등록
홈 디렉토리(macOS: ~/Library/Application Support/Claude/) 의 claude_desktop_config.json 파일을 열어 추가:
{
"mcpServers": {
"weather": {
"command": "node",
"args": ["/abs/path/to/dist/server.js"],
"env": {
"OPENWEATHER_API_KEY": "your_key_here"
}
}
}
}
Claude Desktop 재시작 → 도구 팔레트에 get_weather 가 뜨면 성공. "서울 날씨 어때?" 라고 물어보면 자동으로 도구 호출.
4-4. Claude Code에 등록
Claude Code는 .mcp.json 또는 명령 한 줄로 등록 가능:
claude mcp add weather node /abs/path/to/dist/server.js \
--env OPENWEATHER_API_KEY=your_key_here
또는 .claude/mcp_servers.json 에 같은 JSON을 박아두면 됩니다.
5. Python으로 최소 서버 만들기 — FastMCP
Python 진영은 FastMCP 가 사실상 표준. 데코레이터 기반이라 코드가 더 짧아져요.
5-1. 프로젝트 초기화
mkdir my-mcp-server && cd my-mcp-server
uv init # 또는 python -m venv .venv
uv add "mcp[cli]" # 또는 pip install mcp
5-2. 같은 날씨 도구
server.py:
import os
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-server")
@mcp.tool()
async def get_weather(city: str, units: str = "metric") -> dict:
"""Get current weather for a city."""
api_key = os.environ["OPENWEATHER_API_KEY"]
url = "https://api.openweathermap.org/data/2.5/weather"
params = {"q": city, "units": units, "appid": api_key}
async with httpx.AsyncClient() as client:
res = await client.get(url, params=params)
data = res.json()
return {
"city": data["name"],
"temp": data["main"]["temp"],
"condition": data["weather"][0]["description"],
}
if __name__ == "__main__":
mcp.run()
데코레이터 @mcp.tool() 한 줄로 도구가 등록됩니다. 함수 시그니처·타입힌트·docstring을 자동으로 스키마로 변환해 줘서 별도 schema 정의 코드가 거의 없어요.
실행 — python server.py 또는 uv run server.py.
Claude Desktop 등록 시 command 가 python 또는 uv 로 바뀌는 것 빼고 동일.
6. Resources와 Prompts도 추가하기
도구만 만들고 끝내는 게 보통이지만, 자원 과 프롬프트 까지 같이 노출하면 사용 경험이 훨씬 좋아집니다.
6-1. Resources — URI로 식별되는 읽기 데이터
TypeScript:
server.resource(
"user-profile",
"app://users/{userId}/profile",
async (uri, { userId }) => {
const profile = await db.users.findById(userId);
return {
contents: [
{
uri: uri.href,
mimeType: "application/json",
text: JSON.stringify(profile),
},
],
};
},
);
URI 템플릿({userId})으로 동적 자원 을 노출할 수 있어요. Claude Desktop의 Resources 메뉴에 자동으로 뜨고, 대화 중 "이 사용자 정보를 컨텍스트에 넣어줘" 같은 식으로 호출됩니다.
Python(FastMCP):
@mcp.resource("app://users/{user_id}/profile")
async def get_user_profile(user_id: str) -> dict:
"""User profile data."""
return await db.users.find_by_id(user_id)
6-2. Prompts — 사용자가 명시적으로 부르는 템플릿
server.prompt(
"review-code",
"Review code for best practices and bugs",
{ code: z.string(), language: z.string().optional() },
({ code, language }) => ({
messages: [{
role: "user",
content: {
type: "text",
text: `Please review the following ${language ?? ""} code.\n\n\`\`\`\n${code}\n\`\`\``,
},
}],
}),
);
사용자가 Claude Desktop에서 /review-code 슬래시 명령으로 호출하면 정해진 프롬프트가 그대로 들어가요. 팀 전체가 "코드 리뷰는 이 프롬프트로" 라는 표준을 공유할 수 있습니다.
7. 실전 패턴 — 가드레일·로깅·환경 변수
장난감 서버 → 프로덕션 서버로 넘어갈 때 박아야 할 5가지.
7-1. 환경 변수로 비밀 관리
API 키·DB 비밀번호는 절대 코드에 박지 마세요. claude_desktop_config.json 의 env 필드에 박는 게 표준이고, 더 진지한 시스템이라면 OS keychain·Vault를 쓰는 게 안전.
7-2. 입력 검증을 가장 먼저
const input = QuerySchema.parse(args); // 실패하면 즉시 throw
Zod(TS)·Pydantic(Python)으로 검증하면 "SQL 인젝션이 들어온다면?" 같은 우려가 한 줄로 막힙니다.
7-3. Pre-check / Post-check 가드레일
위험한 도구(파일 삭제, DB 마이그레이션)는 실행 전후로 검증을 박아야 해요:
server.tool("delete_file", "...", schema, async (args) => {
// Pre-check
const check = await canDelete(args.path);
if (!check.ok) {
return { content: [{ type: "text", text: `Denied: ${check.reason}` }] };
}
// 실행
const result = await fs.unlink(args.path);
// Post-check
logActivity({ tool: "delete_file", args, result });
return { content: [{ type: "text", text: "OK" }] };
});
7-4. 활동 로그
모델이 어떤 도구를 언제·왜 호출했는지 디스크에 남겨두면 디버깅이 비교가 안 되게 빨라집니다. 한 작업당 JSON 한 줄이면 충분.
7-5. 도구별 권한 분리
같은 서버 안 도구라도 읽기만 허용·쓰기는 차단 같은 권한 분리가 필요할 수 있어요. 환경 변수로 허용 도구 목록(allowlist) 을 받아 필터링하는 패턴이 흔합니다:
const allowedTools = (process.env.ALLOWED_TOOLS ?? "")
.split(",")
.map(s => s.trim());
const filteredTools = allTools.filter(t => allowedTools.includes(t.name));
8. 디버깅 — 안 될 때 보는 곳
처음 서버 만들 때 가장 자주 만나는 문제와 점검 순서.
| 증상 | 점검할 것 |
|---|---|
| Claude Desktop에 서버가 안 뜸 | claude_desktop_config.json 경로·JSON 오타 확인, Claude 재시작 |
| 서버는 떴는데 도구가 안 보임 | server.tool(...) 등록 코드 다시, 스키마 유효성 확인 |
| 도구 호출 시 에러 | Claude Desktop "Open Developer Tools" 로 stdout 로그 확인 |
| 변경한 코드가 반영 안 됨 | TS는 npx tsc 재빌드, Python은 캐시 확인. Claude 재시작 |
command not found | command 절대경로로 박기 (node 대신 /usr/local/bin/node) |
가장 유용한 도구 — Anthropic 공식 MCP Inspector. 서버를 Claude 거치지 않고 직접 띄워 도구·자원·프롬프트를 GUI로 호출·검증할 수 있어요:
npx @modelcontextprotocol/inspector node dist/server.js
브라우저가 자동으로 뜨면서 서버에 등록된 모든 능력을 클릭으로 테스트 가능. 본격 개발 들어가기 전에 한 번 깔아두면 디버깅 시간이 절반으로 줄어요.
9. 어디서 시작하는 게 좋나 — 첫 서버 추천
처음 만들 때 너무 야심차게 시작하면 중도 포기가 흔합니다. 다음 3개 중 하나로 시작하길 권합니다.
- Hello world 도구 (1시간) — 위 날씨 도구 코드 그대로. 등록·작동까지 한 번 끝내보기
- 로컬 메모 도구 (반나절) — 텍스트 파일에 메모 쓰기·읽기. 자원·도구 둘 다 노출하는 첫 연습
- 사내 DB 읽기 도구 (1~2일) — 회사 Postgres·MySQL에 read-only 권한으로 붙여 "고객 N명 매출 합산" 같은 도구. 가장 가치 큰 첫 서버
3번부터 시도해야 "왜 MCP 서버를 직접 만드나" 라는 질문에 대한 답이 손에 들어옵니다. "내 회사 데이터를 Claude가 자연어로 조작한다" — 그게 MCP의 진짜 매력이에요.
10. 마켓플레이스에 올려 공유하기 (옵션)
만든 서버를 팀·세계와 공유하고 싶으면 — 옵션 두 가지.
- npm 공개 —
npm publish한 줄로 누구나npx your-mcp-server로 띄울 수 있게 - GitHub MCP 마켓 —
modelcontextprotocol/servers리포에 PR로 등록, 또는 자체 마켓플레이스(anthropics/claude-plugins-official등)에 플러그인으로 패키징
플러그인으로 만들 거면 — 다음 글에서 다룰 Claude Code 커스텀 스킬 작성법 과 같이 묶어 .claude-plugin/marketplace.json 으로 배포하면 MCP 서버 + 스킬 + 슬래시 명령 한 묶음이 됩니다.
11. 한 줄 정리
MCP 서버 = Claude(또는 다른 LLM)에 자신의 도구·데이터를 노출하는 표준 인터페이스. TypeScript 또는 Python SDK로 30분이면 최소 서버, 반나절이면 사내 도구, 하루면 프로덕션급 가드레일까지 완성할 수 있어요.
핵심 결정 3가지:
- Tools/Resources/Prompts 중 어느 카테고리 — 모델이 부르나, 호스트가 부르나, 사용자가 부르나
- stdio 또는 Streamable HTTP — 로컬이면 stdio, 팀·원격이면 HTTP
- TypeScript 또는 Python — 본인이 빠르게 다루는 언어 (둘 다 1급 SDK)
Claude 사용법 → Superpowers 플러그인 → MCP 서버로 한 단계씩 깊어지면, "AI 잘 쓰는 사람" 에서 "AI에 회사 시스템을 붙이는 엔지니어" 로 자리가 바뀝니다. 그 자리가 2026년 가장 희소한 직무 중 하나예요.
참고 자료
- Model Context Protocol 공식 문서 — Build an MCP server
- 공식 TypeScript SDK — modelcontextprotocol/typescript-sdk
- 공식 Python SDK — modelcontextprotocol/python-sdk
- TypeScript SDK V2 Server Guide (공식)
- Anthropic Claude Code Docs — Custom Tools (MCP 연동)
- Developers Digest — MCP Production Guide (2026-04)
- Claude Lab — Build Your Own MCP Server (TS·Python)
- Channel — MCP Explained: First Server in TS·Python
- Lushbinary — MCP Developer Guide 2026
본 글은 일반 정보 정리용이며, MCP 사양·SDK는 빠르게 갱신됩니다. 최신 사양은 공식 사양 페이지와 각 SDK 리포의 RELEASE-NOTES에서 직접 확인하세요.