부록 A. MCP 서버 직접 개발 실습
Model Context Protocol(MCP) 서버를 TypeScript와 Python으로 직접 만들어 Claude Code/Claude Desktop에 연결하는 전 과정을 다룹니다. 공용 MCP 서버가 없는 사내 시스템(JIRA, 사내 ERP, 자체 DB)을 AI에 연결해야 할 때 필요한 핵심 기술입니다.
Claude Code v2.1.89(2026-04 첫째 주 릴리스)부터 MCP 도구의 응답 한도가 500,000 문자로 대폭 상향됐습니다. 큰 파일·DB 쿼리 결과·로그를 잘림 없이 그대로 반환할 수 있어, 페이지네이션 래퍼나 서머라이저 미들웨어가 더 이상 필수가 아닙니다. 또한 PreToolUse 훅에 새 옵션
defer가 추가되어 외부 시그널(인간 승인·CI 게이트 등) 대기 패턴을 자연스럽게 구현할 수 있습니다.
A.1 언제 MCP 서버를 직접 만들까?
| 상황 | 해결책 | 개발 비용 |
|---|---|---|
| 공개 서비스(GitHub, Slack, Gmail, Notion 등) | 공식/커뮤니티 MCP 서버 설치 | 낮음(설정만) |
| 사내 REST API(주문 조회, 사용자 검색 등) | 커스텀 MCP 서버 작성 | 중간(1~3일) |
| 레거시 DB 직접 쿼리 | 커스텀 MCP 서버 + SQL 래퍼 | 중간(2~5일) |
| 멀티 시스템 오케스트레이션 | 에이전트 레벨 하네스 + MCP | 높음(1~4주) |
핵심 판단 기준: AI가 같은 내부 데이터/작업을 반복적으로 요구받고 있고, 프롬프트나 RAG로 처리하기에는 "실행(write)"이 필요할 때 MCP 서버가 정답입니다.
A.2 MCP 서버 아키텍처 요약
MCP 서버는 3가지 기본 기능을 JSON-RPC 2.0 위에서 노출합니다:
- Tools — AI가 호출할 수 있는 함수(예:
search_orders(customer_id)). 가장 자주 씁니다. - Resources — AI가 읽을 수 있는 문서/데이터(예:
file://runbook.md). RAG 대체로 활용. - Prompts — 재사용 가능한 프롬프트 템플릿. 팀 표준 프롬프트를 배포할 때 유용.
전송 계층은 stdio(로컬 프로세스) 또는 HTTP/SSE(원격). 사내 배포는 stdio로 시작해 팀 공유 필요 시 HTTP로 승격하는 패턴이 안전합니다.
A.3 실습 1 — Hello MCP 서버 (TypeScript, 5분)
가장 작은 MCP 서버를 만들어 Claude Desktop에 연결해 봅니다.
Step 1: 프로젝트 초기화
mkdir hello-mcp && cd hello-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
npx tsc --init
Step 2: 서버 코드 작성 (src/index.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: "hello-mcp", version: "0.1.0" });
// 가장 단순한 Tool: 현재 시간 반환
server.tool(
"get_current_time",
"현재 서버 시간을 ISO 8601로 반환합니다.",
{},
async () => ({
content: [{ type: "text", text: new Date().toISOString() }]
})
);
// 인자 받는 Tool: 두 수 덧셈
server.tool(
"add",
"두 숫자를 더합니다.",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
Step 3: Claude Desktop에 등록
~/.config/claude/claude_desktop_config.json(Windows: %APPDATA%\Claude\claude_desktop_config.json)에 추가:
{
"mcpServers": {
"hello": {
"command": "npx",
"args": ["tsx", "C:/path/to/hello-mcp/src/index.ts"]
}
}
}
Claude Desktop을 재시작하면 연결된 MCP가 🔌 아이콘으로 표시됩니다. "지금 몇 시야?" 또는 "3 더하기 5"라고 물어보면 Tool이 호출됩니다.
A.4 실습 2 — 사내 REST API 연동 MCP (Python, 30분)
사내 주문 관리 API를 MCP로 감싸 AI가 주문을 조회/취소할 수 있게 합니다.
Step 1: 의존성
pip install mcp httpx python-dotenv
Step 2: 서버 코드 (order_mcp.py)
import os
import httpx
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
load_dotenv()
BASE = os.environ["ORDER_API_BASE"]
TOKEN = os.environ["ORDER_API_TOKEN"]
mcp = FastMCP("order-mcp")
def _client():
return httpx.Client(
base_url=BASE,
headers={"Authorization": f"Bearer {TOKEN}"},
timeout=10.0,
)
@mcp.tool()
def search_orders(customer_id: str, status: str | None = None) -> list[dict]:
"""고객 ID로 주문 목록을 조회합니다. status로 필터링 가능(pending/paid/shipped/cancelled)."""
params = {"customer_id": customer_id}
if status: params["status"] = status
with _client() as c:
r = c.get("/orders", params=params)
r.raise_for_status()
return r.json()["items"]
@mcp.tool()
def cancel_order(order_id: str, reason: str) -> dict:
"""주문을 취소합니다. reason은 취소 사유(감사 로그용, 필수)."""
with _client() as c:
r = c.post(f"/orders/{order_id}/cancel", json={"reason": reason})
r.raise_for_status()
return r.json()
if __name__ == "__main__":
mcp.run()
Step 3: 안전 장치 3가지 (필수)
- 쓰기 작업(cancel_order)은 화이트리스트
Claude Code
.claude/settings.json의permissions.allow에 특정 MCP Tool만 허용하고, 나머지는 프롬프트로 확인. - 토큰은 최소 권한
ORDER_API_TOKEN은 주문 조회/취소 스코프만. 계정 삭제 스코프는 절대 포함하지 말 것.
- 감사 로그
Tool 내부에서
audit.log()호출. 누가/언제/왜 실행했는지 남겨야 사고 시 추적 가능.
A.5 디버깅과 테스트
MCP Inspector — 공식 디버거
npx @modelcontextprotocol/inspector python order_mcp.py
브라우저 UI에서 Tool 목록, 입력/출력 스키마, 실제 호출 결과를 확인할 수 있습니다. Claude에 연결하기 전 반드시 Inspector로 먼저 검증하세요.
단위 테스트 패턴
import pytest
from order_mcp import search_orders
def test_search_orders_empty(httpx_mock):
httpx_mock.add_response(
url="http://api/orders?customer_id=c1",
json={"items": []}
)
assert search_orders("c1") == []
A.6 배포 체크리스트
| 항목 | 질문 | 통과 기준 |
|---|---|---|
| 인증 | 토큰이 코드/레포에 하드코딩되어 있지 않은가? | .env / Secret Manager |
| 스코프 | Tool 권한이 실제 업무 범위를 넘지 않는가? | 읽기/쓰기 분리, 최소 권한 |
| 감사 | 쓰기 작업이 모두 로그되는가? | 구조화 로그 + 보존 90일↑ |
| 에러 | API 장애 시 AI에게 명확한 에러 메시지를 주는가? | HTTPError → 한국어 설명으로 변환 |
| 성능 | Tool 응답이 5초 이내인가? | 타임아웃 + 페이지네이션 |
| 테스트 | Inspector로 정상 동작 확인 + 단위 테스트 | 전체 Tool 커버리지 |
A.7 더 읽을거리
- MCP 공식 사양 — modelcontextprotocol.io
- Anthropic MCP 가이드 — docs.claude.com/mcp
- 공식 SDK: TypeScript(
@modelcontextprotocol/sdk) · Python(mcp) · Go 커뮤니티 포트 - 사내 확장: 10.6 하네스 엔지니어링에서 MCP + 에이전트 조합 패턴 참고