본문으로 건너뛰기
Previous
Next
MCP (Model Context Protocol) 완전 가이드 | AI 에이전트 표준 프로토콜

MCP (Model Context Protocol) 완전 가이드 | AI 에이전트 표준 프로토콜

MCP (Model Context Protocol) 완전 가이드 | AI 에이전트 표준 프로토콜

이 글의 핵심

MCP(Model Context Protocol)는 AI 모델이 파일 시스템, 데이터베이스, 웹 API 등 외부 리소스와 표준화된 방식으로 통신하는 오픈 프로토콜입니다. Claude Desktop, Cursor, VS Code 등에서 지원합니다.

MCP란?

MCP(Model Context Protocol)는 Anthropic이 2024년 말 공개한 오픈소스 프로토콜입니다. AI 모델이 파일 시스템, 데이터베이스, 웹 API, 개발 도구 등 외부 리소스와 표준화된 방식으로 통신할 수 있게 합니다.

클라이언트 (Claude Desktop, Cursor, VS Code)
    ↕ MCP 프로토콜
서버 (파일 시스템, DB, 웹 API, Git, Slack...)

왜 MCP인가?

이전에는 각 AI 앱마다 외부 도구 연동 코드를 따로 작성해야 했습니다. MCP는 USB처럼 표준 인터페이스를 제공해서 한 번 만든 MCP 서버를 모든 호환 클라이언트에서 재사용할 수 있습니다.

이전 방식MCP 방식
앱마다 API 연동 코드 별도 작성MCP 서버 한 번 작성 → 모든 클라이언트 사용
모델 변경 시 코드 수정 필요모델/클라이언트 무관하게 동작
보안/인증 중복 구현서버 측에서 일관되게 처리

MCP 아키텍처

┌─────────────────────────────────────────┐
│           MCP 클라이언트                 │
│  (Claude Desktop / Cursor / VS Code)    │
└────────────────┬────────────────────────┘
                 │ JSON-RPC over stdio / SSE
┌────────────────▼────────────────────────┐
│              MCP 서버                    │
│  ┌──────────┐  ┌──────────┐  ┌───────┐ │
│  │  Tools   │  │Resources │  │Prompts│ │
│  └──────────┘  └──────────┘  └───────┘ │
└────────────────┬────────────────────────┘

     ┌───────────┼───────────┐
     ▼           ▼           ▼
  파일시스템    데이터베이스   웹 API

3가지 핵심 개념

  • Tools: AI가 호출할 수 있는 함수 (Function Calling과 유사)
  • Resources: AI가 읽을 수 있는 데이터 (파일, DB 레코드 등)
  • Prompts: 재사용 가능한 프롬프트 템플릿

빠른 시작: Python MCP 서버 만들기

설치

pip install mcp
# 또는 uv 사용 (권장)
pip install uv
uv add mcp

첫 번째 MCP 서버

아래는 list_tools 함수 구현 예제입니다. 위에서 설명한 핵심 로직을 담고 있습니다.

# server.py
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import mcp.types as types

# 서버 인스턴스 생성
server = Server("my-first-server")

@server.list_tools()
async def list_tools() -> list[Tool]:
    """사용 가능한 도구 목록 반환"""
    return [
        Tool(
            name="calculate",
            description="수식을 계산합니다",
            inputSchema={
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "계산할 수식 (예: 2 + 3 * 4)"
                    }
                },
                "required": ["expression"]
            }
        ),
        Tool(
            name="get_current_time",
            description="현재 시간을 반환합니다",
            inputSchema={
                "type": "object",
                "properties": {}
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    """도구 호출 처리"""
    if name == "calculate":
        expression = arguments.get("expression", "")
        try:
            # 안전한 수식 평가 (실제 프로덕션에서는 더 안전한 방법 사용)
            result = eval(expression, {"__builtins__": {}}, {})
            return [TextContent(type="text", text=f"{expression} = {result}")]
        except Exception as e:
            return [TextContent(type="text", text=f"오류: {str(e)}")]
    
    elif name == "get_current_time":
        from datetime import datetime
        now = datetime.now().strftime("%Y년 %m월 %d일 %H:%M:%S")
        return [TextContent(type="text", text=f"현재 시간: {now}")]
    
    raise ValueError(f"알 수 없는 도구: {name}")

async def main():
    # stdio 모드로 서버 실행 (로컬 Claude Desktop 연결용)
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationOptions(
                server_name="my-first-server",
                server_version="1.0.0",
                capabilities=server.get_capabilities(
                    notification_options=None,
                    experimental_capabilities={}
                )
            )
        )

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

실전 예제: 파일 시스템 MCP 서버

아래는 list_tools 함수 구현 예제입니다. 위에서 설명한 핵심 로직을 담고 있습니다.

# file_server.py
from mcp.server import Server
from mcp.server.models import InitializationOptions
from mcp.server.stdio import stdio_server
from mcp.types import Tool, Resource, TextContent, ResourceTemplate
import mcp.types as types
from pathlib import Path
import os

server = Server("file-system-server")
ALLOWED_DIR = Path(os.environ.get("ALLOWED_DIR", "/tmp/mcp-files"))
ALLOWED_DIR.mkdir(exist_ok=True)

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="read_file",
            description="파일 내용을 읽습니다",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "파일 경로"}
                },
                "required": ["path"]
            }
        ),
        Tool(
            name="write_file",
            description="파일에 내용을 씁니다",
            inputSchema={
                "type": "object",
                "properties": {
                    "path": {"type": "string", "description": "파일 경로"},
                    "content": {"type": "string", "description": "저장할 내용"}
                },
                "required": ["path", "content"]
            }
        ),
        Tool(
            name="list_files",
            description="디렉토리의 파일 목록을 반환합니다",
            inputSchema={
                "type": "object",
                "properties": {
                    "directory": {"type": "string", "description": "디렉토리 경로 (생략 시 기본 디렉토리)"}
                }
            }
        )
    ]

def validate_path(path_str: str) -> Path:
    """보안: 허용된 디렉토리 내 경로만 허용"""
    path = (ALLOWED_DIR / path_str).resolve()
    if not str(path).startswith(str(ALLOWED_DIR)):
        raise ValueError("허용된 디렉토리 밖의 경로는 접근할 수 없습니다")
    return path

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "read_file":
        path = validate_path(arguments["path"])
        if not path.exists():
            return [TextContent(type="text", text=f"오류: 파일을 찾을 수 없습니다: {path}")]
        content = path.read_text(encoding="utf-8")
        return [TextContent(type="text", text=content)]
    
    elif name == "write_file":
        path = validate_path(arguments["path"])
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(arguments["content"], encoding="utf-8")
        return [TextContent(type="text", text=f"파일 저장 완료: {path.name}")]
    
    elif name == "list_files":
        dir_path = validate_path(arguments.get("directory", "."))
        if not dir_path.is_dir():
            return [TextContent(type="text", text="디렉토리가 아닙니다")]
        
        files = []
        for item in sorted(dir_path.iterdir()):
            item_type = "📁" if item.is_dir() else "📄"
            size = item.stat().st_size if item.is_file() else 0
            files.append(f"{item_type} {item.name} ({size:,} bytes)" if item.is_file() else f"{item_type} {item.name}/")
        
        result = "\n".join(files) if files else "파일 없음"
        return [TextContent(type="text", text=result)]
    
    raise ValueError(f"알 수 없는 도구: {name}")

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(read_stream, write_stream,
            InitializationOptions(server_name="file-system-server", server_version="1.0.0",
                capabilities=server.get_capabilities(notification_options=None, experimental_capabilities={})))

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

TypeScript로 MCP 서버 만들기

npm install @modelcontextprotocol/sdk

TypeScript/JavaScript 예제 코드입니다.

// server.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import fetch from "node-fetch";

const server = new Server(
  { name: "weather-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// 도구 목록 정의
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "get_weather",
      description: '도시의 현재 날씨를 가져옵니다. MCP (Model Context Protocol) 완전 가이드에 대한 완전한 가이드입니다. 실전 예제와 함께 핵심 개념부터 고급 활용까지 다룹니다.',
      inputSchema: {
        type: "object",
        properties: {
          city: {
            type: "string",
            description: "도시 이름 (영문)",
          },
        },
        required: ["city"],
      },
    },
  ],
}));

// 도구 호출 처리
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "get_weather") {
    const city = args?.city as string;
    const apiKey = process.env.OPENWEATHER_API_KEY;

    const response = await fetch(
      `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${apiKey}&units=metric&lang=kr`
    );
    const data = (await response.json()) as any;

    if (data.cod !== 200) {
      return {
        content: [{ type: "text", text: `오류: ${data.message}` }],
      };
    }

    return {
      content: [
        {
          type: "text",
          text: `${city} 날씨: ${data.weather[0].description}, 기온 ${data.main.temp}°C, 체감 ${data.main.feels_like}°C`,
        },
      ],
    };
  }

  throw new Error(`알 수 없는 도구: ${name}`);
});

// 서버 시작
const transport = new StdioServerTransport();
await server.connect(transport);

Claude Desktop에 연결하기

설정 파일 예시입니다.

// ~/Library/Application Support/Claude/claude_desktop_config.json (macOS)
// %APPDATA%\Claude\claude_desktop_config.json (Windows)
{
  "mcpServers": {
    "my-calculator": {
      "command": "python",
      "args": ["/path/to/server.py"],
      "env": {}
    },
    "file-system": {
      "command": "python",
      "args": ["/path/to/file_server.py"],
      "env": {
        "ALLOWED_DIR": "/Users/username/Documents"
      }
    },
    "weather": {
      "command": "node",
      "args": ["/path/to/weather-server/build/index.js"],
      "env": {
        "OPENWEATHER_API_KEY": "your-api-key-here"
      }
    }
  }
}

Claude Desktop을 재시작하면 채팅 창 하단에 🔧 도구 아이콘이 표시됩니다.


Resources와 Prompts 사용

Resources (데이터 읽기)

아래는 list_resources 함수 구현 예제입니다. 위에서 설명한 핵심 로직을 담고 있습니다.

from mcp.types import Resource, BlobResourceContents, TextResourceContents

@server.list_resources()
async def list_resources() -> list[Resource]:
    return [
        Resource(
            uri="file:///config.json",
            name="설정 파일",
            description="애플리케이션 설정",
            mimeType="application/json"
        )
    ]

@server.read_resource()
async def read_resource(uri: str) -> str | bytes:
    if uri == "file:///config.json":
        return '{"version": "1.0", "debug": false}'
    raise ValueError(f"알 수 없는 리소스: {uri}")

Prompts (재사용 가능한 프롬프트)

아래는 list_prompts 함수 구현 예제입니다. 위에서 설명한 핵심 로직을 담고 있습니다.

from mcp.types import Prompt, PromptArgument, GetPromptResult, PromptMessage

@server.list_prompts()
async def list_prompts() -> list[Prompt]:
    return [
        Prompt(
            name="code_review",
            description="코드 리뷰 요청 프롬프트",
            arguments=[
                PromptArgument(name="language", description="프로그래밍 언어", required=True),
                PromptArgument(name="focus", description="리뷰 중점 사항", required=False)
            ]
        )
    ]

@server.get_prompt()
async def get_prompt(name: str, arguments: dict | None) -> GetPromptResult:
    if name == "code_review":
        language = arguments.get("language", "코드") if arguments else "코드"
        focus = arguments.get("focus", "전반적인 품질") if arguments else "전반적인 품질"
        
        return GetPromptResult(
            description=f"{language} 코드 리뷰",
            messages=[
                PromptMessage(
                    role="user",
                    content=TextContent(
                        type="text",
                        text=f"""다음 {language} 코드를 리뷰해주세요. 특히 {focus}에 집중해주세요:
                        
1. 코드 품질 및 가독성
2. 잠재적 버그 및 오류
3. 성능 최적화 기회
4. 보안 취약점
5. 개선 제안 (코드 예제 포함)"""
                    )
                )
            ]
        )
    raise ValueError(f"알 수 없는 프롬프트: {name}")

인기 MCP 서버 모음

Anthropic과 커뮤니티가 제공하는 공식 MCP 서버들입니다.

터미널에서 다음 명령어를 실행합니다.

# 파일시스템 서버 (공식)
npx -y @modelcontextprotocol/server-filesystem /path/to/directory

# SQLite 서버 (공식)
npx -y @modelcontextprotocol/server-sqlite /path/to/database.db

# GitHub 서버 (공식)
npx -y @modelcontextprotocol/server-github

# PostgreSQL 서버
npx -y @modelcontextprotocol/server-postgres postgresql://user:pass@localhost/db

# Slack 서버
npx -y @modelcontextprotocol/server-slack

# Brave Search 서버
npx -y @modelcontextprotocol/server-brave-search

마치며

MCP는 AI 에이전트 생태계의 핵심 인프라로 빠르게 자리잡고 있습니다. 지금 배워두면 AI 에이전트 개발에서 큰 경쟁력을 가질 수 있습니다.

다음 단계:

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「MCP (Model Context Protocol) 완전 가이드 | AI 에이전트 표준 프로토콜」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「MCP (Model Context Protocol) 완전 가이드 | AI 에이전트 표준 프로토콜」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. MCP(Model Context Protocol)는 Anthropic이 만든 AI와 외부 도구 연결 표준입니다. Claude Desktop, Cursor, VS Code에서 MCP 서버를 만들고 연결하는 방법을 Py… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

MCP, Claude, AI, 에이전트, Python, TypeScript, 도구, Anthropic 등으로 검색하시면 이 글이 도움이 됩니다.