GitHub ↗
CHAPTER 08 OF 10
🔌

MCP Servers & Tool Design (ACI)

MCP 서버 & 도구 설계 — 에이전트 인터페이스 공학

MCP는 에이전트의 USB-C 포트다 — 도구를 추가하는 것보다 도구를 잘 설계하는 것이 훨씬 중요하고, Anthropic의 SWE-bench 팀은 프롬프트보다 도구 설계에 더 많은 시간을 썼다.

MCP Servers & Tool Design (ACI) cheatsheet
🍌 NANO BANANA CHEATSHEET · CH 08

Overview

개관

MCP(Model Context Protocol)는 Anthropic이 만든 개방형 표준으로, LLM이 외부 데이터 소스, API, 도구와 통신하는 방법을 표준화한다. 'AI의 USB-C 포트' — 어떤 MCP 호환 클라이언트도 어떤 MCP 호환 서버와 연결될 수 있다.

Anthropicr SWE-bench 팀의 중요한 발견: "우리는 전체 프롬프트 최적화보다 도구 설계에 더 많은 시간을 썼다." 절대 경로 vs. 상대 경로 하나만 바꿔도 오류의 전체 클래스가 사라졌다.

97%의 MCP 도구에 품질 문제가 있다는 연구 결과가 있다. 가장 흔한 실수는 REST API를 MCP로 래핑하는 것 — 이것은 API 설계가 아니라 에이전트 인터페이스 설계(ACI)가 필요하다. 완전히 다른 원칙이 적용된다.

🎯 Learning Goals
  • MCP의 3가지 핵심 프리미티브(Tools, Resources, Prompts)를 설명할 수 있다
  • 도구 설계 6원칙을 적용해서 에이전트 친화적 도구를 작성할 수 있다
  • 에러 메시지를 3-part 공식으로 자기수정 유도에 최적화할 수 있다
  • FastMCP로 커스텀 MCP 서버를 구현하고 Claude Code에 등록할 수 있다

Sections

본문

MCP 아키텍처 — 3가지 핵심 프리미티브

MCP는 JSON-RPC 2.0 기반 프로토콜로, 3가지 핵심 프리미티브를 제공한다:

프리미티브 역할 컨트롤 예시
Tools 에이전트가 호출하는 함수 Model-controlled (사용자 승인 필요) search_customer(email)
Resources 에이전트가 읽는 데이터 Application-controlled 파일 내용, API 응답
Prompts 재사용 가능한 프롬프트 템플릿 User-controlled 코드 리뷰 템플릿

Transport 방식:

  • stdio: 서버가 서브프로세스로 실행, stdin/stdout으로 통신. Claude Code와 Claude Desktop이 로컬 서버에 사용.
  • Streamable HTTP (2025년 11월 신규): 단일 HTTP 엔드포인트, POST로 JSON-RPC 전송, SSE로 스트리밍.

Tool Annotation Hints (4가지 핵심):

  • readOnlyHint: 승인 없이 호출 가능
  • destructiveHint: 사람 체크포인트 필요
  • idempotentHint: 재시도 안전
  • openWorldHint: 외부 시스템과 상호작용 (최고 위험)

도구 설계 6원칙 — 97%가 위반하는 것들

원칙 1: Operation이 아닌 Outcome으로 설계

  • 나쁜: get_customer_by_email() + list_orders() + get_status() = 3번 왕복
  • 좋은: track_latest_order(email) = 1번 왕복, 완전한 결과

원칙 2: 인수를 기본 타입으로 평탄화

  • 나쁜: filters: {status: 'pending', date_range: {...}}
  • 좋은: status: Literal['pending','shipped'], start_date: str

원칙 3: 풍부한 설명 (3-part 공식)

[무엇을 하는가] [무엇을 반환하는가] [언제 사용하는가]
예: "고객의 최신 주문 상태를 확인한다. 운송사, ETA, 추적 URL을 반환한다.
     배송 날짜 견적 전에 사용할 것."

원칙 4: 서버당 5-15개로 도구 수 제한

  • 소형 모델(8B): ~19개에서 최고 성능, 46개에서 실패
  • GitHub Copilot: 40개 → 13개로 줄인 후 벤치마크 향상
  • Block의 Linear MCP: 30개 → 2개

원칙 5: 접두사로 이름 짓기

  • 나쁜: get_data, send
  • 좋은: slack_send_message, linear_list_issues

원칙 6: 페이지네이션 기본 제공

  • limit=20 기본값, has_more, next_offset, total_count 반환

에러 메시지 설계 — 자기수정 프롬프트

에러 메시지는 Claude가 다음에 올바른 호출을 하도록 유도하는 프롬프트다. 3-part 공식:

[무엇이 잘못됐는가] + [기대값은 무엇인가] + [올바른 예시]

나쁜 예시:

Error: Invalid parameter

좋은 예시:

Error: 'departure_date' 파라미터 형식 오류: '15/04/2026'.
ISO 8601 형식 (YYYY-MM-DD)을 사용하세요.
예시: search_flights(departure_date='2026-04-15', ...)

커스텀 린터 에러 메시지 패턴 (Claude Code에서 자기수정 극대화):

ERROR: [무엇이 잘못됐는가]
  파일: [경로:줄번호]
  
  WHY: [왜 이 규칙이 있는가] → docs/adr/007 참조
  
  FIX: [구체적인 수정 방법]
  
  EXAMPLE:
    // 잘못됨:
    const result = await db.query('SELECT...')
    // 올바름:
    const result = await db.user.findMany({ where: { id } })

이런 에러 메시지는 Claude가 에러를 보자마자 정확한 수정을 한다. '더 많은 지시'가 아니라 '더 나은 에러'가 답이다.

커스텀 MCP 서버 구현

FastMCP를 사용한 Python MCP 서버 최소 구현:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP('my-project-tools')

@mcp.tool()
async def search_logs(
    query: str,
    severity: str = 'all',  # 'error' | 'warn' | 'info' | 'all'
    limit: int = 20
) -> dict:
    """애플리케이션 로그를 검색한다.
    
    query: 검색할 키워드 또는 패턴
    severity: 'error' | 'warn' | 'info' | 'all'
    
    오류 디버깅이나 특정 이벤트 추적 시 사용.
    """
    # ... 구현
    return {"logs": [], "total": 0, "has_more": False}

if __name__ == '__main__':
    mcp.run(transport='stdio')

settings.json에 등록:

{
  "mcpServers": {
    "my-project-tools": {
      "command": "python",
      "args": ["/path/to/server.py"]
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username"]
    },
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": {"GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}"}
    }
  }
}
💡 Analogy · 비유
레스토랑 메뉴 설계

MCP 도구 설계를 레스토랑 메뉴에 비유해보자.

나쁜 메뉴(API 래핑 방식): 재료 목록 + 조리 방법 + 플레이팅 지시서를 고객에게 준다. 고객은 파스타를 먹고 싶을 뿐인데 3번의 선택과 조합이 필요하다.

좋은 메뉴(Outcome 방식): '해산물 파스타 (30분, 어육 알레르기 표시)'. 고객이 원하는 결과를 한 번에 주문할 수 있다.

메뉴 수 제한(5-15개 원칙): 메뉴가 100개인 레스토랑에서 고객은 결정 장애에 빠진다. 가장 좋은 메뉴 15개만 남기면 주문이 빨라지고 실수가 줄어든다.

에러 메시지(자기수정): '주문 오류'보다 '해산물 파스타는 재료 소진되었습니다. 크림 파스타나 명란 파스타를 추천드립니다'가 다음 올바른 주문으로 이어진다.

프로젝트 특화 MCP 서버 템플릿. 데이터베이스 검색, 로그 조회, 환경 상태 확인 등 반복적인 개발 태스크를 도구화한다.

python
#!/usr/bin/env python3
"""project-mcp-server.py — 프로젝트 특화 MCP 서버 템플릿"""
from mcp.server.fastmcp import FastMCP
from typing import Literal
import subprocess
import json
import os

mcp = FastMCP('project-tools')

@mcp.tool()
async def run_tests(
    pattern: str = '',
    verbose: bool = False
) -> dict:
    """프로젝트 테스트를 실행한다.
    
    pattern: 실행할 테스트 파일/이름 패턴 (빈 문자열 = 전체)
    verbose: True면 전체 출력, False면 요약만
    
    코드 변경 후 검증 시 사용. PR 전 필수.
    결과: passed, failed, duration, output 반환
    """
    cmd = ['npm', 'test']
    if pattern:
        cmd.extend(['--', '--testPathPattern', pattern])
    if verbose:
        cmd.append('--verbose')
    
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    
    lines = result.stdout.split('\n')
    summary_lines = [l for l in lines if 'Tests:' in l or 'PASS' in l or 'FAIL' in l]
    
    return {
        'exit_code': result.returncode,
        'passed': result.returncode == 0,
        'summary': '\n'.join(summary_lines[-10:]),
        'output': result.stdout if verbose else result.stdout[-2000:],
        'error': result.stderr[-1000:] if result.returncode != 0 else None
    }

@mcp.tool()
async def search_codebase(
    query: str,
    file_pattern: str = '**/*.ts',
    context_lines: int = 3
) -> dict:
    """코드베이스에서 패턴을 검색한다.
    
    query: grep 패턴
    file_pattern: 검색할 파일 패턴 (기본: TypeScript 파일)
    context_lines: 매칭 전후 표시할 줄 수
    
    함수, 변수, import 위치 찾을 때 사용. Read 전에 먼저 이것을 써서 파일을 특정하라.
    결과: matches (파일경로, 줄번호, 내용), total_count 반환
    """
    try:
        result = subprocess.run(
            ['grep', '-rn', f'--include={file_pattern.replace("**","*")}',
             f'-A{context_lines}', f'-B{context_lines}', query, '.'],
            capture_output=True, text=True, timeout=30
        )
        matches = []
        for line in result.stdout.split('\n')[:100]:  # 최대 100줄
            if ':' in line and not line.startswith('--'):
                parts = line.split(':', 2)
                if len(parts) >= 3:
                    matches.append({
                        'file': parts[0],
                        'line': parts[1],
                        'content': parts[2].strip()
                    })
        return {'matches': matches, 'total_count': len(matches), 'truncated': len(matches) >= 100}
    except subprocess.TimeoutExpired:
        return {'error': '검색 시간 초과 — 더 구체적인 패턴을 사용하세요', 'matches': [], 'total_count': 0}

@mcp.tool()
async def get_env_status() -> dict:
    """현재 개발 환경 상태를 반환한다.
    
    Node.js 버전, 환경변수 설정 여부, 포트 사용 현황 확인.
    세션 시작 시 또는 '왜 안되지?' 디버깅 시 먼저 실행하라.
    """
    info = {}
    
    for cmd, key in [(['node', '--version'], 'node'), (['npm', '--version'], 'npm'), (['git', 'branch', '--show-current'], 'branch')]:
        try:
            r = subprocess.run(cmd, capture_output=True, text=True)
            info[key] = r.stdout.strip()
        except:
            info[key] = 'not found'
    
    required_env = ['DATABASE_URL', 'NEXTAUTH_SECRET', 'NEXTAUTH_URL']
    info['env_vars'] = {
        var: '✅ 설정됨' if os.environ.get(var) else '❌ 미설정'
        for var in required_env
    }
    
    return info

if __name__ == '__main__':
    mcp.run(transport='stdio')

3가지 도구를 구현했다: run_tests(테스트 실행), search_codebase(코드 검색), get_env_status(환경 상태). 각각 Outcome 중심으로 설계되어 한 번 호출로 원하는 결과를 얻을 수 있다. 설명에는 '무엇을', '무엇을 반환', '언제 사용' 3-part가 포함되어 있다.

🏭 현업에서의 평가
좋은 MCP 도구는 에이전트가 자연스럽게 '올바른 도구를 올바른 순서로' 사용하도록 유도한다. 나쁜 도구는 에이전트가 5번 호출할 것을 1번으로 줄인다.

✅ 시니어가 보는 것

  • 도구 설명에 무엇/반환/언제 3-part가 있는가
  • 도구 수가 15개 이하인가
  • 에러 메시지가 자기수정 프롬프트로 설계되어 있는가
  • readOnlyHint/destructiveHint annotation이 있는가

⚠️ 레드 플래그

  • REST API를 그대로 MCP로 래핑 (3번 왕복이 필요한 도구들)
  • 30개 이상의 도구 (모델이 선택 불가)
  • Generic 에러 메시지 ('Invalid parameter')
  • Hint annotation 없음 (승인 여부 결정 불가)

🎤 예상 인터뷰 질문

  1. MCP Tools, Resources, Prompts의 차이와 각각의 use case는?
  2. Anthropic SWE-bench 팀이 절대 경로 요구로 오류의 전체 클래스를 없앤 원리는?
  3. Block의 Linear MCP가 30개에서 2개로 줄인 후 성능이 향상된 이유는?
숙달 vs 익숙함: 단순히 아는 사람은 MCP 서버를 만든다. 마스터한 사람은 '우리 팀의 비즈니스 분석가'의 관점에서 도구를 설계한다 — 어떤 20%의 API 기능이 에이전트의 90% 사용 케이스를 커버하는지 파악해서 그것만 남긴다.

Key Takeaways

핵심 정리

MCP = AI의 USB-C

어떤 MCP 클라이언트도 어떤 MCP 서버와 연결 가능. 표준화된 도구 확장 인터페이스.

ACI ≠ API

에이전트 인터페이스(ACI)는 개발자 인터페이스(API)와 다른 설계 원칙이 필요하다.

Outcome > Operation

3번 왕복이 필요한 3개 도구보다 1번 호출로 완전한 결과를 주는 1개 도구가 낫다.

5-15개 원칙

소형 모델은 19개, 대형 모델도 100개 이상에서 급격히 실패한다. 15개 이하 유지.

3-part 설명

무엇을 하는가 + 무엇을 반환하는가 + 언제 사용하는가 — 이 공식이 올바른 도구 선택을 이끈다.

에러 = 자기수정 프롬프트

에러 메시지에 문제 + 기대값 + 올바른 예시를 넣으면 Claude가 즉시 수정한다.

readOnlyHint 필수

읽기 전용 도구에 annotation을 달면 불필요한 승인 요청이 사라진다.

프롬프트보다 도구

Anthropic SWE-bench 팀: 도구 설계에 프롬프트 최적화보다 더 많은 시간을 썼다.