MCP 기본개념 톺아보기
TL;DR
MCP(Model Context Protocol)는 LLM과 외부 데이터 소스를 연결하는 표준 프로토콜입니다. 클라이언트-서버 아키텍처로 구성되며, LLM(클라이언트)이 tool calling을 결정하면 MCP 서버가 외부 API를 호출하고 결과를 정규화하여 반환합니다. FastMCP를 사용하면 Python으로 간단하게 커스텀 MCP 서버를 구축할 수 있으며, stdio 기반 통신으로 다양한 LLM 환경과 통합 가능합니다.
Key Takeaways
- 표준화된 인터페이스: MCP는 LLM과 외부 데이터 소스 간의 표준 프로토콜을 제공하여, 각 LLM마다 다른 통합 로직을 작성할 필요가 없습니다.
- 명확한 책임 분리: 클라이언트(LLM)는 tool calling 결정과 응답 생성을, 서버는 외부 API 호출과 데이터 정규화를 담당하는 구조로 유지보수성이 높습니다.
- Pydantic 기반 타입 안정성: WebSearchArgs와 같이 Pydantic 모델로 입력 검증을 선언적으로 처리하여 런타임 오류를 사전에 방지할 수 있습니다.
- 비동기 I/O 최적화: httpx와 asyncio를 활용한 비동기 처리로 외부 API 호출 시 효율적인 리소스 활용이 가능합니다.
- stdio 기반 범용성: stdio 통신 방식을 사용하여 언어나 플랫폼에 구애받지 않고 다양한 LLM 클라이언트와 통합할 수 있습니다.
상세 내용
MCP 아키텍처 개요
MCP(Model Context Protocol)는 LLM 애플리케이션에서 외부 데이터 소스와의 통합을 표준화하는 프로토콜입니다. 전체 흐름은 다음과 같이 7단계로 구성됩니다:
- User Prompt: 사용자가 자연어로 요청
- Tool Calling 결정: LLM이 어떤 도구를 사용할지 판단
- MCP Request: 선택된 MCP 서버에 요청 전송
- External Source 호출: MCP 서버가 외부 API 호출 및 데이터 정규화
- MCP Response: 처리된 결과를 클라이언트에 반환
- 결과 분석: LLM이 반환된 데이터를 해석
- 자연어 응답 생성: 최종 사용자에게 답변 제공
이러한 구조는 관심사의 명확한 분리(Separation of Concerns)를 제공합니다. LLM은 의도 파악과 응답 생성에 집중하고, MCP 서버는 외부 시스템과의 통신과 데이터 변환을 담당합니다.
MCP 서버 구현
FastMCP 라이브러리를 사용하면 간결하게 MCP 서버를 구축할 수 있습니다:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("custom-web-search-mcp")
class WebSearchArgs(BaseModel):
query: str = Field(..., min_length=1)
count: int = Field(default=5, ge=1, le=10)
@mcp.tool()
async def web_search(args: WebSearchArgs) -> dict:
results = await brave_search(args.query, args.count)
return {"query": args.query, "results": results}
핵심 설계 포인트:
- Pydantic 스키마:
WebSearchArgs로 입력 검증을 선언적으로 정의합니다.min_length,ge,le같은 제약조건을 통해 유효하지 않은 요청을 사전에 차단합니다. - 데코레이터 패턴:
@mcp.tool()로 함수를 MCP 도구로 자동 등록하며, 타입 힌트를 기반으로 스키마를 자동 생성합니다. - 환경변수 관리: API 키는 환경변수로 관리하여 보안을 강화하고, 누락 시 명확한 에러를 발생시킵니다.
외부 API 통합
MCP 서버의 핵심 역할은 외부 데이터 소스를 LLM이 이해할 수 있는 형태로 변환하는 것입니다:
async def brave_search(query: str, count: int):
url = "https://api.search.brave.com/res/v1/web/search"
headers = {
"Accept": "application/json",
"X-Subscription-Token": BRAVE_API_KEY,
}
params = {"q": query, "count": count}
async with httpx.AsyncClient(timeout=15) as client:
r = await client.get(url, headers=headers, params=params)
r.raise_for_status()
data = r.json()
# 데이터 정규화
results = []
for item in (data.get("web", {}).get("results") or []):
results.append({
"title": item.get("title"),
"url": item.get("url"),
"snippet": item.get("description"),
})
return results
구현 Best Practices:
- 비동기 HTTP 클라이언트: httpx의 AsyncClient로 I/O 블로킹을 방지하고 동시성을 확보합니다.
- 타임아웃 설정: 15초 타임아웃으로 무한 대기를 방지합니다.
- 에러 핸들링:
raise_for_status()로 HTTP 에러를 명시적으로 처리합니다. - 데이터 정규화: 외부 API의 응답 구조를 일관된 형태(
title,url,snippet)로 변환하여 클라이언트의 부담을 줄입니다.
MCP 클라이언트 구현
LLM 애플리케이션에서 MCP 서버를 호출하는 클라이언트 측 코드입니다:
from mcp.client.stdio import StdioServerParameters, stdio_client
from mcp.client.session import ClientSession
async def main():
user_text = "웹에서 MCP Python 예제 찾아서 요약해줘"
server_params = StdioServerParameters(
command="python",
args=["mcp_server.py"],
env={**os.environ, "BRAVE_API_KEY": os.environ["BRAVE_API_KEY"]},
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# LLM이 tool calling 결정
tool_call = await llm_decide_tool(user_text)
# MCP 서버 호출
tool_resp = await session.call_tool(
tool_call["tool_name"],
tool_call["tool_args"],
)
tool_result = tool_resp.content[0].json
final_answer = await llm_finalize_answer(user_text, tool_result)
print(final_answer)
주요 특징:
- stdio 통신: 표준 입출력을 사용하여 프로세스 간 통신을 수행합니다. 이는 언어나 플랫폼에 독립적인 통합을 가능하게 합니다.
- 세션 관리:
ClientSession으로 연결 수명주기를 관리하며, context manager로 안전한 리소스 해제를 보장합니다. - 비동기 컨텍스트:
async with블록으로 여러 비동기 리소스를 안전하게 관리합니다.
Tool Calling 워크플로우
MCP의 핵심인 tool calling 과정을 상세히 살펴보면:
# (3) MCP 서버에 요청
tool_resp = await session.call_tool(
"web_search",
{"query": "MCP python tool calling example", "count": 3},
)
# (5) 결과 수신
tool_result = tool_resp.content[0].json
이 과정에서:
- 클라이언트는 도구 이름(
web_search)과 인자를 JSON-RPC 형태로 전송 - 서버는 Pydantic 모델로 검증 후 외부 API 호출
- 서버가 결과를 정규화하여 반환
- 클라이언트는
content[0].json으로 구조화된 데이터를 추출
이러한 구조는 LLM이 raw API 응답을 직접 파싱할 필요 없이, 이미 정제된 데이터를 받을 수 있게 합니다.
실무 활용 패턴
MCP를 프로덕션 환경에서 사용할 때 고려할 사항:
보안:
- API 키는 환경변수나 비밀 관리 시스템(AWS Secrets Manager 등)을 통해 관리
- MCP 서버는 신뢰할 수 있는 네트워크 내에서만 실행
확장성:
- 여러 MCP 서버를 병렬로 실행하여 다양한 데이터 소스 통합
- 각 서버는 독립적으로 배포/업데이트 가능
모니터링:
- 외부 API 호출 실패율, 응답 시간 등을 추적
- MCP 서버별 사용량 통계 수집
테스트:
- 외부 API를 mock하여 MCP 서버 단위 테스트 작성
- 다양한 에러 시나리오(타임아웃, 잘못된 응답 등) 검증
