첫 번째 MCP 서버 구축하기: AI 에이전트 도구 호출 메커니즘 이해

1. 개요

  1. 소개: MCP 서버의 필요성
  2. 핵심 개념: MCP 프로토콜 및 도구 호출
  3. 시스템 아키텍처: 사용자에서 도구까지의 전체 경로
  4. 코드 구현: 덧셈 MCP 서버 만들기
  5. 완전한 호출 프로세스: LLM이 도구를 어떻게 사용하는지 분석
  6. 주요 통찰: LLM 추론과 도구 조정
  7. 최고의 실무 방법 및 자주 묻는 질문

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

태그: MCP json-rpc flask

5월 22일 15:15에 게시됨