1. 개요
- 소개: MCP 서버의 필요성
- 핵심 개념: MCP 프로토콜 및 도구 호출
- 시스템 아키텍처: 사용자에서 도구까지의 전체 경로
- 코드 구현: 덧셈 MCP 서버 만들기
- 완전한 호출 프로세스: LLM이 도구를 어떻게 사용하는지 분석
- 주요 통찰: LLM 추론과 도구 조정
- 최고의 실무 방법 및 자주 묻는 질문
2. 소개: MCP 서버의 필요성
2.1 대형 언어 모델(LLM)의 한계점
LLM은 강력하지만 다음과 같은 고유의 제약이 존재합니다:
| 한계 | 설명 | 예시 |
|---|---|---|
| 지식 한도 | 훈련 데이터에는 시간적 제한이 있음 | 실시간 주가 정보를 얻을 수 없음 |
| 계산 능력 | 추론 과정에서 정확한 계산을 수행하지 않음 | 9867 × 8734가 잘못 계산될 수 있음 |
| 외부 상호작용 | 외부 시스템에 직접 접근할 수 없음 | 파일 읽기, API 호출 불가능 |
| 도구 운영 | 시스템 명령을 실행할 수 없음 | Python 코드 실행, 데이터베이스 조작 불가 |
2.2 MCP: AI와 도구를 연결하는 다리
**MCP(Model Context Protocol)**는 Anthropic에서 개발한 오픈 프로토콜로, AI 에이전트가 안전하고 표준적으로 외부 도구를 호출할 수 있도록 합니다.
MCP 없을 때:
사용자 → LLM → 텍스트 응답 ❌ (작업 수행 불가)
MCP 있을 때:
사용자 → LLM → 도구 호출 → 실행 → 결과 → LLM → 응답 ✅
2.3 본문 목표
가장 간단하면서도 완전한 MCP 서버(덧셈 서비스)를 구축하여 다음 내용을 깊이 이해합니다:
- MCP 프로토콜 작동 원리
- HTTP 전송 및 JSON-RPC 2.0 규범
- Claude Code가 도구를 어떻게 발견하고 호출하는지
- LLM이 어떤 도구를 사용해야 하는지 "알아내는" 방식 ⭐ 핵심 문제
- 다중 단계 도구 조정 구현 방법
3. 핵심 개념: MCP 프로토콜 및 도구 호출
3.1 MCP 프로토콜 스택
┌──────────────────────────────────────┐
│ 애플리케이션 레이어 │
│ Claude Code / 기타 AI 에이전트 │
└─────────────┬────────────────────────┘
│
┌─────────────┴────────────────────────┐
│ 프로토콜 레이어 │
│ JSON-RPC 2.0 + MCP 확장 │
│ • initialize │
│ • tools/list │
│ • tools/call │
└─────────────┬────────────────────────┘
│
┌─────────────┴────────────────────────┐
│ 전송 레이어 │
│ HTTP (본문) / stdio / WebSocket │
└─────────────┬────────────────────────┘
│
┌─────────────┴────────────────────────┐
│ 도구 레이어 │
│ Python / JavaScript / Bash / ... │
└──────────────────────────────────────┘
3.2 핵심 메서드
| 메서드 | 역할 | 호출 시점 |
|---|---|---|
| initialize | 손잡이 협상 | Claude Code 시작 시 |
| tools/list | 모든 도구 나열 | 초기화 후 즉시 호출 ⭐ |
| tools/call | 특정 도구 실행 | LLM이 도구 사용을 결정할 때 |
3.3 도구 정의 규칙
{
"name": "add", # 도구 고유 식별자
"description": "두 수를 더함", # 기능 설명 ⭐ LLM이 이것을 보고 이해
"inputSchema": { # JSON Schema로 매개변수 정의
"type": "object",
"properties": {
"a": {"type": "number", "description": "첫 번째 수"},
"b": {"type": "number", "description": "두 번째 수"}
},
"required": ["a", "b"]
}
}
핵심 포인트:
description는 LLM이 도구 목적을 이해하는 유일한 정보입니다.inputSchema는 매개변수 구조를 정의하며, LLM은 이를 기반으로 매개변수를 생성합니다.- 설명이 더 명확할수록 LLM이 올바르게 사용할 가능성이 높습니다.
4. 시스템 아키텍처: 사용자에서 도구까지의 전체 경로
4.1 전체 아키텍처
┌─────────────────────────────────────────────────────────────────┐
│ 사용자 상호작용 레이어 │
│ 👤 터미널 / 쉘 │
│ $ claude "add 10+13+12" │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code CLI │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 세션 관리 │ │ 구성 로드 │ │ 권한 제어 │ │
│ │ Session │ │ .mcp.json │ │ Permission │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────┬────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 에이전트 루프 순환 │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ 요청 수신 → LLM 추론 → 도구 호출 → 결과 처리 → 응답 생성 │ │
│ └────────────────────────────────────────────────────────┘ │
└───────┬─────────────────────────────────┬───────────────────────┘
│ │
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────────────┐
│ LLM 추론 레이어 │ │ MCP 프로토콜 레이어 │
│ │ │ │
│ ┌────────────┐ │ │ ┌────────────────────────┐ │
│ │Claude API │ │ │ │ HTTP 전송 │ │
│ │🧠 Sonnet │ │◄───────────┤ │ JSON-RPC 2.0 │ │
│ │ 4.5 │ │ 도구 응답 │ │ │ │
│ └────────────┘ │ │ │ ┌──────────────────┐ │ │
│ │ │ │ │ SSE EventStream │ │ │
│ • 사용자 의도 분석 │ │ │ │ /sse 엔드포인트 │ │ │
│ • 도구 호출 결정 │ │ │ │ 손잡이 협상 │ │ │
│ • 반환된 결과 통합 │ │ │ └──────────────────┘ │ │
│ │ 도구 호출 │ │ │ │
│ ├───────────►│ │ POST / │ │
│ │ │ │ 도구 호출 │ │
└──────────────────┘ │ └────────────────────────┘ │
└────────┬─────────────────────┘
│
▼
┌───────────────────────┐
│ add-server MCP │
│ 🔧 HTTP 서버 │
├───────────────────────┤
│ │
│ Flask 웹 서버 │
│ • 호스트: 0.0.0.0 │
│ • 포트: 8080 │
│ • CORS: 활성화 │
│ │
│ MCP 엔드포인트: │
│ ┌─────────────────┐ │
│ │ GET /sse │ │
│ │ POST / │ │
│ │ GET /health │ │
│ └─────────────────┘ │
│ │
│ 제공 도구: │
│ ┌─────────────────┐ │
│ │ 📐 add(a, b) │ │
│ │ │ │
│ │ 입력: │ │
│ │ • a: 숫자 │ │
│ │ • b: 숫자 │ │
│ │ │ │
│ │ 출력: │ │
│ │ 결과 = a + b │ │
│ └─────────────────┘ │
│ │
│ 구현 파일: │
│ add_mcp_server.py │
└───────────────────────┘
│
▼
┌────────────────────────┐
│ Python 런타임 │
│ 덧셈 연산 실행 │
└────────────────────────┘
4.2 핵심 구성 요소 역할
| 구성 요소 | 역할 | 주요 포인트 |
|---|---|---|
| Claude Code | 세션 관리, 구성 로드 | .mcp.json 읽어서 서비스 발견 |
| LLM (Sonnet 4.5) | 의도 이해, 도구 호출 결정 | ⭐ 코드를 직접 보지 않고 도구 설명만 참조 |
| MCP 프로토콜 레이어 | 표준화된 통신 | JSON-RPC 2.0 + HTTP/SSE |
| add-server | 특정 도구 구현 | Flask 서버 + Python 논리 |
5. 코드 구현: 덧셈 MCP 서버 구축
5.1 프로젝트 구조
add-server/
├── add_mcp_server_http.py # 주 서버 코드
├── requirements.txt # 의존성: Flask, flask-cors
└── README.md # 설명 문서
5.2 핵심 코드 분석
5.2.1 서버 초기화
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # 다른 도메인에서 Claude Code가 접근할 수 있도록 허용
# 서버 메타정보
SERVER_INFO = {
"name": "add-server",
"version": "1.0.0"
}
# 도구 정의 ⭐ 이는 LLM이 보는 유일한 정보
TOOLS = [
{
"name": "add",
"description": "두 숫자를 더해서 결과를 반환합니다.",
"inputSchema": {
"type": "object",
"properties": {
"x": {"type": "number", "description": "첫 번째 숫자"},
"y": {"type": "number", "description": "두 번째 숫자"}
},
"required": ["x", "y"]
}
}
]
핵심 포인트:
TOOLS리스트는 모든 사용 가능한 도구를 정의합니다.description은 반드시 명확해야 하며, LLM이 도구 목적을 이해하는 데 사용됩니다.inputSchema는 JSON Schema 표준을 따릅니다.
5.2.2 서비스 발견 엔드포인트(SSE)
@app.route('/sse', methods=['GET'])
def sse_endpoint():
"""
SSE 엔드포인트 - 클라이언트에게 JSON-RPC 엔드포인트 URL 알림
동작 과정:
1. Claude Code 시작 시 /sse 방문
2. 서버는 JSON-RPC URL을 포함한 endpoint 이벤트 반환
3. 클라이언트는 이후 모든 요청을 해당 URL로 전송
"""
def generate():
endpoint_data = json.dumps({"url": "http://localhost:8080"})
yield f"event: endpoint\n"
yield f"data: {endpoint_data}\n\n"
return Response(
generate(),
mimetype='text/event-stream',
headers={
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
)
SSE가 필요한 이유:
- HTTP 전송의 MCP 서버는 서비스 발견 메커니즘이 필요합니다.
- SSE는 서버가 엔드포인트 정보를 클라이언트에게 적극적으로 전달하도록 허용합니다.
- 클라이언트는
/sse만 알면 JSON-RPC 엔드포인트를 동적으로 발견할 수 있습니다.
5.2.3 주요 요청 처리기
@app.route('/', methods=['POST'])
def handle_request():
message = request.get_json()
msg_id = message.get("id")
method = message.get("method")
params = message.get("params", {})
# ============================================================
# 메서드 1: initialize - 손잡이
# ============================================================
if method == "initialize":
return jsonify({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}}, # 도구 지원 선언
"serverInfo": SERVER_INFO
}
})
# ============================================================
# 메서드 2: tools/list - 도구 나열 ⭐ 주요 메서드
# ============================================================
elif method == "tools/list":
return jsonify({
"jsonrpc": "2.0",
"id": msg_id,
"result": {"tools": TOOLS} # 도구 정의 반환
})
# ============================================================
# 메서드 3: tools/call - 도구 실행
# ============================================================
elif method == "tools/call":
tool_name = params.get("name")
arguments = params.get("arguments", {})
if tool_name == "add":
x = arguments.get("x")
y = arguments.get("y")
result = float(x) + float(y)
return jsonify({
"jsonrpc": "2.0",
"id": msg_id,
"result": {
"content": [
{
"type": "text",
"text": f"{x}와 {y}의 합은 {result}입니다."
}
]
}
})
else:
# 찾을 수 없는 도구
return jsonify({
"jsonrpc": "2.0",
"id": msg_id,
"error": {
"code": -32601,
"message": f"찾을 수 없는 도구: {tool_name}"
}
}), 404
코드 하이라이트:
- 표준 JSON-RPC 2.0 형식
- 명확한 오류 처리
- 구조화된 응답 형식
5.2.4 건강 체크(선택 사항이지만 권장)
@app.route('/health', methods=['GET'])
def health_check():
"""간단한 건강 체크, 모니터링 및 디버깅 용이"""
return jsonify({
"status": "healthy",
"server": SERVER_INFO
})
5.3 서버 시작
if __name__ == '__main__':
print("🚀 Add MCP Server 시작")
print(f"📍 URL: http://localhost:8080")
print(f"🔧 사용 가능한 도구: add")
app.run(host='0.0.0.0', port=8080, debug=True)
실행 명령:
# 의존성 설치
pip install flask flask-cors
# 서버 시작
python3 add_mcp_server_http.py