AI 에이전트를 만들다 보면 반드시 이런 순간이 옵니다: "이 에이전트가 우리 데이터베이스에 접근할 수 있으면 좋겠는데." 그러면 LangChain 툴을 하나 만들고, OpenAI function calling 형식으로 래핑하고, Claude에는 또 다른 방식으로 연결하고... AI 모델마다 통합 코드를 따로 작성하는 악순환에 빠집니다.
MCP는 이 문제를 해결하기 위해 Anthropic이 2024년에 발표한 오픈 표준입니다.
MCP란 무엇인가?
MCP(Model Context Protocol)는 AI 모델과 외부 도구/데이터 소스를 연결하는 **표준화된 프로토콜**입니다. 컴퓨터에서 USB-C가 어떤 장치든 하나의 포트로 연결하는 것처럼, MCP는 어떤 AI 모델이든 어떤 외부 도구든 하나의 인터페이스로 연결합니다.
핵심 특징:
- **오픈 표준**: Anthropic이 만들었지만 특정 회사에 종속되지 않음
- **양방향**: 클라이언트(AI)와 서버(도구) 모두 메시지를 보낼 수 있음
- **JSON-RPC 기반**: 단순하고 검증된 프로토콜
MCP 이전의 세상 (문제점)
왜 이런 표준이 필요했을까요? 상황을 보면 명확합니다.
**기존 방식의 M×N 문제:**
모델 수 × 도구 수 = 통합 코드 수
Claude + (Slack + GitHub + DB + Notion) = 4개의 Claude 통합
GPT-4 + (Slack + GitHub + DB + Notion) = 4개의 GPT 통합
Gemini + (Slack + GitHub + DB + Notion) = 4개의 Gemini 통합
총 12개의 서로 다른 통합 코드
MCP 이후:
도구 수 = MCP 서버 수 (모델과 무관하게 재사용)
Slack MCP 서버 1개 → Claude, GPT-4, Gemini 모두 사용 가능
GitHub MCP 서버 1개 → 모든 모델에서 사용 가능
한번 만든 MCP 서버는 MCP를 지원하는 모든 AI 클라이언트에서 동작합니다.
MCP 아키텍처
┌─────────────────────┐ ┌──────────────────────┐
│ MCP Client │◄───────►│ MCP Server │
│ (Claude, Cursor, │ JSON │ (your tools, DBs, │
│ IDE, 등) │ RPC │ APIs, files, etc.) │
└─────────────────────┘ └──────────────────────┘
MCP Server가 노출하는 3가지:
├── Resources (읽기 전용 데이터)
│ └── 파일, DB 쿼리 결과, API 응답 등
│
├── Tools (실행 가능한 액션)
│ └── 파일 쓰기, API 호출, DB 수정 등
│
└── Prompts (재사용 가능한 프롬프트 템플릿)
└── "코드 리뷰해줘" 같은 구조화된 템플릿
MCP는 서버-클라이언트 모델을 따릅니다:
- **MCP Client**: Claude Desktop, Cursor, 직접 만든 AI 앱 등
- **MCP Server**: 여러분이 만드는 도구 서버 (데이터베이스, API, 파일 시스템 등)
- **Transport**: stdio (로컬) 또는 HTTP/SSE (원격)
MCP Server 만들기 (Python)
실제로 동작하는 MCP 서버를 만들어봅시다. 고객 데이터베이스에 접근하는 서버입니다.
from mcp.server import Server
from mcp.server.models import InitializationOptions
app = Server("my-database-server")
Resources 등록: 읽기 전용 데이터 노출
@app.list_resources()
async def list_resources() -> list[types.Resource]:
return [
types.Resource(
uri="db://customers",
name="Customer Database",
description="Access customer records. Returns up to 100 records.",
mimeType="application/json"
),
types.Resource(
uri="db://orders",
name="Order History",
description="Access order history for all customers",
mimeType="application/json"
)
]
@app.read_resource()
async def read_resource(uri: str) -> str:
if uri == "db://customers":
실제 DB에서 데이터를 가져옴
customers = await db.fetch_all(
"SELECT id, name, email, created_at FROM customers LIMIT 100"
)
return json.dumps([dict(c) for c in customers])
elif uri == "db://orders":
orders = await db.fetch_all(
"SELECT id, customer_id, total, status, created_at FROM orders LIMIT 100"
)
return json.dumps([dict(o) for o in orders])
raise ValueError(f"Unknown resource: {uri}")
Tools 등록: 실행 가능한 액션 노출
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="create_customer",
description="Create a new customer record in the database",
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Customer's full name"
},
"email": {
"type": "string",
"description": "Customer's email address"
},
"phone": {
"type": "string",
"description": "Customer's phone number (optional)"
}
},
"required": ["name", "email"]
}
),
types.Tool(
name="search_customers",
description="Search customers by name or email",
inputSchema={
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (name or email)"
}
},
"required": ["query"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "create_customer":
result = await db.execute(
"INSERT INTO customers (name, email, phone) VALUES (:name, :email, :phone)",
{
"name": arguments["name"],
"email": arguments["email"],
"phone": arguments.get("phone", None)
}
)
return [types.TextContent(
type="text",
text=f"Customer created successfully. ID: {result.lastrowid}"
)]
elif name == "search_customers":
query = f"%{arguments['query']}%"
customers = await db.fetch_all(
"SELECT id, name, email FROM customers WHERE name LIKE :q OR email LIKE :q",
{"q": query}
)
if not customers:
return [types.TextContent(type="text", text="No customers found")]
result = "\n".join([f"ID: {c.id}, Name: {c.name}, Email: {c.email}" for c in customers])
return [types.TextContent(type="text", text=result)]
raise ValueError(f"Unknown tool: {name}")
서버 실행
async def main():
from mcp.server.stdio import stdio_server
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-database-server",
server_version="0.1.0"
)
)
if __name__ == "__main__":
asyncio.run(main())
Claude Desktop에서 MCP 서버 사용하기
서버를 만들었으면 Claude Desktop에 등록합니다:
// macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
// Windows: %APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"my-database": {
"command": "python",
"args": ["/path/to/your/mcp_server.py"],
"env": {
"DATABASE_URL": "postgresql://user:pass@localhost/mydb"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/username/projects"]
}
}
}
설정 후 Claude Desktop을 재시작하면 Claude가 여러분의 데이터베이스에 직접 접근할 수 있습니다. "고객 목록 보여줘", "홍길동이라는 고객 추가해줘" 같은 자연어 명령이 동작합니다.
기존 통합 방식과의 차이점
OpenAI Function Calling
OpenAI function calling — OpenAI API에 종속
tools = [{"type": "function", "function": {"name": "search", ...}}]
response = openai.chat.completions.create(tools=tools, ...)
특정 모델 API에 묶여 있습니다. Claude에서 쓰려면 다시 구현해야 합니다.
LangChain Tools
LangChain — 프레임워크에 종속
from langchain.tools import tool
@tool
def search(query: str) -> str:
"""Search the web"""
return web_search(query)
LangChain을 쓰지 않는 환경에서는 동작하지 않습니다.
MCP
MCP — 어떤 MCP 클라이언트에서도 동작
@app.list_tools()
async def list_tools():
return [types.Tool(name="search", ...)]
Claude Desktop, Cursor, 직접 만든 앱 — MCP를 지원하는 어디서든 동작합니다.
| 특성 | OpenAI Function Calling | LangChain Tools | MCP |
|------|------------------------|-----------------|-----|
| 모델 독립성 | X (OpenAI 전용) | 부분적 | O |
| 표준화 | X | X | O (오픈 표준) |
| 재사용성 | 낮음 | 중간 | 높음 |
| 설정 복잡도 | 낮음 | 중간 | 중간 |
| 생태계 | OpenAI 생태계 | LangChain 생태계 | 성장 중 |
MCP 생태계 현황 (2025-2026)
공식적으로 제공되는 MCP 서버들이 빠르게 늘고 있습니다:
**공식 서버 (Anthropic 제공):**
- `@modelcontextprotocol/server-filesystem` — 로컬 파일 시스템
- `@modelcontextprotocol/server-github` — GitHub 저장소
- `@modelcontextprotocol/server-postgres` — PostgreSQL 데이터베이스
- `@modelcontextprotocol/server-brave-search` — Brave Search API
- `@modelcontextprotocol/server-slack` — Slack 메시지
**커뮤니티 서버:**
- Notion, Jira, Linear 연동
- Docker, Kubernetes 관리
- AWS, GCP 리소스 접근
설치는 간단합니다:
npm으로 설치 (Node.js 서버의 경우)
npx -y @modelcontextprotocol/server-github
pip으로 설치 (Python 서버의 경우)
pip install mcp
실전 팁: MCP 서버를 잘 만드는 법
**1. Tool description을 구체적으로 작성하라**
나쁜 예
types.Tool(name="query_db", description="Query database")
좋은 예
types.Tool(
name="query_customers",
description=(
"Search customer records. Use this when you need to find customers "
"by name, email, or ID. Returns customer ID, name, email, and creation date. "
"Use search_orders for order-related queries."
)
)
**2. 에러를 툴 결과로 반환하라**
@app.call_tool()
async def call_tool(name: str, arguments: dict):
try:
result = await execute_operation(arguments)
return [types.TextContent(type="text", text=json.dumps(result))]
except Exception as e:
에러도 정상 응답으로 반환 — LLM이 에러를 보고 대응할 수 있음
return [types.TextContent(
type="text",
text=f"Error: {str(e)}. Please check the input and try again."
)]
**3. 읽기와 쓰기를 명확히 분리하라**
Resources = 읽기 전용 (DB SELECT, 파일 읽기, API GET)
Tools = 쓰기 작업 (DB INSERT/UPDATE/DELETE, 파일 쓰기, API POST)
이 구분이 명확할수록 AI가 안전하게 판단합니다. "이 작업은 데이터를 변경하는 건가?" 같은 의사결정이 아키텍처 수준에서 명확해집니다.
마치며
MCP는 아직 성숙 중인 표준입니다. 하지만 방향은 분명합니다 — AI 생태계의 공통 언어가 될 가능성이 높습니다. Cursor, Zed, VS Code 같은 IDE들이 MCP 지원을 추가하고 있고, 서드파티 서버 생태계가 빠르게 성장하고 있습니다.
지금 AI 앱을 만들고 있다면, 툴 통합을 MCP 형식으로 구현해두는 것을 추천합니다. 나중에 다른 AI 모델을 지원할 때 재사용할 수 있습니다.
다음 글에서는 여러 에이전트를 조율하는 **Multi-Agent 시스템** — AutoGen, CrewAI, LangGraph를 비교합니다.
현재 단락 (1/230)
AI 에이전트를 만들다 보면 반드시 이런 순간이 옵니다: "이 에이전트가 우리 데이터베이스에 접근할 수 있으면 좋겠는데." 그러면 LangChain 툴을 하나 만들고, OpenAI...