콘텐츠로 이동

Model Context Protocol (MCP) 개요

AI 에이전트가 외부 시스템(데이터베이스, API, 파일 시스템)과 통합될 때, 기존에는 각 LLM 제공자마다 서로 다른 방식으로 도구를 정의해야 했습니다. OpenAI 방식으로 작성한 도구는 Anthropic 모델에서 그대로 쓸 수 없고, 새 모델이 나올 때마다 통합 코드를 다시 작성해야 했습니다.

**MCP(Model Context Protocol)**는 Anthropic이 2024년 11월 공개한 오픈 표준입니다. LLM과 외부 도구 사이의 공통 언어를 정의하여, 한 번 만든 MCP 서버를 모든 호환 클라이언트에서 사용할 수 있습니다.

“MCP is to AI tools what USB-C is to device charging.” — 다양한 기기를 하나의 표준 포트로 연결하듯, MCP는 다양한 AI 클라이언트를 하나의 서버 인터페이스로 연결합니다.

기존 function calling 방식에서는 OpenAI와 Anthropic이 서로 다른 스키마를 요구했습니다.

// OpenAI function calling 스키마
{
"type": "function",
"function": {
"name": "read_file",
"parameters": {
"type": "object",
"properties": {
"path": { "type": "string" }
}
}
}
}
// Anthropic tool_use 스키마
{
"name": "read_file",
"input_schema": {
"type": "object",
"properties": {
"path": { "type": "string" }
}
}
}

MCP를 사용하면 서버에 도구를 한 번 정의하면, 호환 클라이언트는 모델에 관계없이 동일한 서버를 사용할 수 있습니다. 통합 코드를 모델마다 작성하는 N×M 문제가 N+M 문제로 줄어듭니다.

┌─────────────────────────────────────────────────────┐
│ Host Application │
│ (Claude Desktop, Cursor, 커스텀 에이전트 등) │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ MCP Client 1 │ │ MCP Client 2 │ │
│ └──────┬───────┘ └────────────┬─────────────┘ │
└─────────│────────────────────────│─────────────────┘
│ JSON-RPC 2.0 │ JSON-RPC 2.0
▼ ▼
┌─────────────────┐ ┌──────────────────────────┐
│ MCP Server A │ │ MCP Server B │
│ (파일 시스템) │ │ (GitHub, Slack, DB 등) │
└─────────────────┘ └──────────────────────────┘

각 MCP 클라이언트는 단일 MCP 서버와 1:1로 연결됩니다. 하나의 호스트 애플리케이션은 여러 클라이언트를 통해 여러 서버에 동시에 연결할 수 있습니다.

통신은 JSON-RPC 2.0 프로토콜을 사용합니다. 전송 계층은 연결 방식에 따라 다릅니다.

| 전송 방식 | 사용 시나리오 | 특징 | |----------|------------|------| | Stdio | 로컬 프로세스 | 클라이언트가 서버를 자식 프로세스로 실행 | | Streamable HTTP | 원격·로컬 HTTP 서버 | 단일 MCP 엔드포인트에서 POST/GET을 사용하고, 필요 시 SSE로 서버 메시지를 스트리밍 | | Custom transport | 특수 런타임 | JSON-RPC 메시지와 라이프사이클 요구사항을 보존하는 범위에서 구현 |

MCP의 모든 통신은 JSON-RPC 2.0 형식을 따릅니다. 요청, 응답, 알림 세 종류의 메시지가 있습니다.

요청 (Request): 클라이언트가 서버에 작업을 요청할 때

{
"jsonrpc": "2.0",
"id": "req-001",
"method": "tools/call",
"params": {
"name": "read_file",
"arguments": {
"path": "/workspace/src/main.ts"
}
}
}

응답 (Response): 서버가 요청에 대한 결과를 반환할 때

{
"jsonrpc": "2.0",
"id": "req-001",
"result": {
"content": [
{
"type": "text",
"text": "import { createServer } from 'http';\n..."
}
]
}
}

알림 (Notification): 응답을 기다리지 않는 단방향 메시지 (예: 진행 상황 업데이트)

{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": {
"progressToken": "task-001",
"progress": 45,
"total": 100
}
}

클라이언트와 서버는 연결 시 핸드셰이크를 통해 서로의 기능을 협상합니다.

클라이언트 → 서버: initialize (프로토콜 버전, 클라이언트 기능)
서버 → 클라이언트: initialize 응답 (서버 기능, 사용 가능한 프리미티브 목록)
클라이언트 → 서버: initialized (핸드셰이크 완료 확인)
---
이후 정상 통신 시작

MCP 기능은 누가 기능을 제공하느냐에 따라 나누어 이해하면 쉽습니다. 서버 기능은 MCP 서버가 클라이언트에 제공하는 기능이고, 클라이언트 기능은 서버가 작업 중 호스트 애플리케이션 쪽 능력을 요청하는 기능입니다.

| 구분 | 기능 | 핵심 역할 | |------|------|----------| | 서버 기능 | Tools | 모델이 실행할 수 있는 함수 | | 서버 기능 | Resources | 모델이나 사용자가 읽을 수 있는 데이터 | | 서버 기능 | Prompts | 재사용 가능한 워크플로우 템플릿 | | 클라이언트 기능 | Sampling | 서버가 클라이언트를 통해 LLM 생성을 요청 | | 클라이언트 기능 | Roots | 서버가 접근 가능한 파일·URI 경계를 확인 | | 클라이언트 기능 | Elicitation | 서버가 사용자에게 추가 정보를 요청 |

LLM이 호출할 수 있는 함수입니다. 타입이 명시된 입력/출력 스키마를 가집니다. 기존의 function calling과 가장 유사한 개념입니다.

{
"name": "read_file",
"description": "파일 내용을 읽습니다",
"inputSchema": {
"type": "object",
"properties": {
"path": { "type": "string", "description": "읽을 파일 경로" }
},
"required": ["path"]
}
}

실제 GitHub MCP 서버의 Tool 예시입니다.

{
"name": "create_pull_request",
"description": "GitHub 저장소에 PR을 생성합니다",
"inputSchema": {
"type": "object",
"properties": {
"owner": { "type": "string" },
"repo": { "type": "string" },
"title": { "type": "string" },
"head": { "type": "string", "description": "소스 브랜치" },
"base": { "type": "string", "description": "대상 브랜치" },
"body": { "type": "string" }
},
"required": ["owner", "repo", "title", "head", "base"]
}
}

LLM이 읽을 수 있는 데이터입니다. 파일, 데이터베이스 레코드, API 응답 등을 URI로 노출합니다. 도구와 달리 부작용(side effect) 없이 데이터를 제공합니다.

{
"uri": "file:///project/src/main.ts",
"name": "main.ts",
"mimeType": "text/typescript"
}

Resources는 정적 리소스와 동적 리소스 두 종류로 나뉩니다.

정적: file:///docs/api-spec.md (고정된 파일)
동적: db://customers/{id}/orders (파라미터 기반 조회)
github://repos/{owner}/{repo}/issues (실시간 데이터)

동적 리소스는 URI 템플릿을 통해 파라미터를 받아 실시간으로 데이터를 생성합니다.

재사용 가능한 프롬프트 템플릿입니다. 서버가 클라이언트에게 제안하는 프롬프트 구조로, 일관된 방식으로 특정 작업을 요청할 때 사용합니다.

{
"name": "code_review",
"description": "코드 리뷰 요청 템플릿",
"arguments": [
{ "name": "language", "required": true },
{ "name": "focus", "required": false }
]
}

클라이언트가 이 프롬프트를 호출하면 서버는 채워진 메시지 배열을 반환합니다.

{
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "다음 TypeScript 코드를 리뷰해 주세요. 보안 측면에 집중하세요:\n\n{{code}}"
}
}
]
}

서버가 클라이언트를 통해 LLM을 호출하는 역방향 메커니즘입니다. MCP 서버가 복잡한 작업 처리 중 LLM의 추론을 활용해야 할 때 사용합니다.

일반적 흐름: 클라이언트 → LLM → 서버 도구 호출
Sampling: 서버 → 클라이언트 → LLM → 서버로 결과 반환

Sampling 요청 메시지 형식입니다.

{
"method": "sampling/createMessage",
"params": {
"messages": [
{
"role": "user",
"content": {
"type": "text",
"text": "이 함수의 의도를 한 문장으로 설명해 주세요:\n\nfunction calcExpiry(t) { return t + 86400; }"
}
}
],
"maxTokens": 200
}
}

Roots는 서버가 작업 가능한 URI나 파일 시스템 경계를 클라이언트에게 확인하는 메커니즘입니다. 파일 시스템 서버처럼 로컬 경로를 다루는 MCP 서버에서는 “어디까지 읽고 써도 되는가”를 명확히 하는 데 중요합니다.

Elicitation은 서버가 작업 도중 사용자 입력이 더 필요할 때 클라이언트를 통해 추가 정보를 요청하는 기능입니다. 예를 들어 CRM MCP 서버가 고객 레코드를 수정하기 전에 “어느 고객을 의미하는가”를 사용자에게 확인하도록 요청할 수 있습니다.

| 상황 | 사용할 프리미티브 | |------|----------------| | 파일 읽기, 검색, 조회 | Resources | | 파일 쓰기, API 호출, 상태 변경 | Tools | | 표준화된 워크플로우 요청 | Prompts | | 서버에서 LLM 추론이 필요 | Sampling | | 서버의 파일·URI 접근 경계 확인 | Roots | | 사용자에게 추가 입력 요청 | Elicitation |

로컬 파일 시스템을 MCP로 노출하는 가장 기본적인 서버입니다. Claude Desktop에 다음과 같이 등록합니다.

{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/Users/user/Documents"
]
}
}
}

등록 후 Claude는 /Users/user/Documents 하위 파일을 읽고 쓸 수 있습니다.

데이터베이스를 MCP Resource로 노출합니다. LLM이 직접 SQL을 실행하는 대신, 서버가 안전하게 쿼리를 중개합니다.

{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"POSTGRES_URL": "postgresql://localhost/mydb"
}
}
}
}

이 서버는 postgres://mydb/tables 같은 Resource URI로 스키마 정보를 노출하고, query Tool로 SELECT 쿼리를 실행합니다.

MCP 서버는 강력한 기능을 제공하는 만큼, 보안 설계에 주의가 필요합니다.

MCP 서버가 제공하는 도구는 필요한 최소 권한만 가져야 합니다. 읽기 전용 작업에 쓰기 도구를 함께 노출하지 않습니다.

# 잘못된 설계: 모든 권한을 하나의 서버에 노출
tools = [read_file, write_file, delete_file, exec_shell]
# 올바른 설계: 역할별로 서버를 분리
read_only_server_tools = [read_file, list_directory, search_files]
write_server_tools = [write_file, create_directory]
exec_server_tools = [run_test, run_lint] # 별도 승인 필요

Tool의 inputSchema만으로는 악의적인 입력을 막기 어렵습니다. 서버 내부에서 추가 검증을 수행합니다.

def read_file(path: str) -> str:
# Path traversal 공격 방지
resolved = os.path.realpath(path)
allowed_base = os.path.realpath("/workspace")
if not resolved.startswith(allowed_base):
raise PermissionError(f"허용되지 않는 경로: {path}")
with open(resolved) as f:
return f.read()

Stdio 방식은 로컬 프로세스이므로 신뢰도가 높습니다. 그러나 Streamable HTTP로 원격 MCP 서버에 연결할 때는 추가 보안 조치가 필요합니다.

| 위협 | 완화 전략 | |------|---------| | 중간자 공격 | TLS 적용, 서버 인증서 검증 | | 무단 접근 | API 키 또는 OAuth 토큰 인증, Resource Indicator 사용 | | 서버 사칭 | 서버 신원 검증 (핀닝 또는 CA 검증) | | 과도한 권한 요청 | Tool 목록 검토 후 선택적 허용 |

MCP는 2025년 이후 빠르게 발전하고 있습니다.

  • 전송 계층: 새 구현은 Streamable HTTP와 협상 헤더, 목표 명세 버전을 함께 점검
  • 상태와 라우팅: stateless core와 헤더 라우팅을 고려해 프록시·로드밸런서 경계를 설계
  • 인증과 확장: 원격 인증·권한 부여와 확장 기능은 서버가 지원하는 범위만 노출
  • 인증/인가: OAuth Resource Server, Resource Indicator, Protected Resource Metadata 등 원격 서버 보안 요구사항 강화
  • 사용자 확인 흐름: Elicitation처럼 서버가 추가 정보를 요청하는 패턴이 표준 기능으로 정리
  • 구조화된 결과: 도구 결과를 단순 텍스트가 아니라 구조화된 출력·리소스 링크와 함께 전달하는 방향으로 확장

MCP는 LLM과 외부 도구 통합의 공통 표준입니다. JSON-RPC 2.0 기반으로 동작하며 표준 전송 계층은 Stdio와 Streamable HTTP입니다. 서버는 Tools, Resources, Prompts를 제공하고, 클라이언트는 Sampling, Roots, Elicitation 같은 기능을 제공할 수 있습니다. 보안 측면에서는 최소 권한 원칙, 입력 검증, 원격 서버 신뢰 검토가 필수입니다. 한 번 작성한 MCP 서버는 모든 호환 AI 클라이언트에서 재사용할 수 있습니다.