Python에서의 Model Context Protocol
요약
Model Context Protocol(MCP)의 개념과 Python SDK를 활용한 구현 방법을 다룹니다. MCP는 AI 애플리케이션과 도구 서버 간의 표준화된 와이어 포맷을 제공하여, 프레임워크나 언어에 상관없이 도구를 재사용할 수 있게 합니다.
핵심 포인트
- MCP는 도구 통합의 M×N 문제를 M+N 문제로 단순화함
- 표준 JSON-RPC 2.0 프로토콜을 사용하여 도구, 리소스, 프롬프트 노출
- stdio 및 Streamable HTTP 전송 방식을 지원
- 한 번 작성된 MCP 서버는 다양한 호스트 애플리케이션에서 즉시 사용 가능
서론
모든 에이전트(Agent)는 도구(Tools)가 필요하며, 모든 도구는 모델에 도달할 수 있는 방법이 필요합니다. Building Agentic Workflows in Python에서는 수동으로 작성된 JSON 스키마와 block.name에 따라 디스패치(dispatch)하는 루프를 통해 그 연결을 직접 구현했습니다. LLM Frameworks vs. the Raw SDK in Python에서는 LangChain의 @tool이 bind_tools를 통해 일반 함수를 동일한 스키마로 변환하는 과정을 보여주었습니다. 두 방식 모두 여전히 _맞춤형(bespoke)_입니다. 즉, 도구가 하나의 프로세스 내에 존재하며, 하나의 에이전트에 연결되어 있고, 하나의 언어로 작성됩니다.
Model Context Protocol (MCP)는 다른 문제를 해결합니다. MCP는 AI 애플리케이션과 도구 서버(tool server) 사이의 _와이어 포맷(wire format)_을 표준화하여, 서버를 에이전트별, 프레임워크별, 또는 언어별로 매번 다시 작성할 필요가 없도록 만듭니다. 이 포스트에서는 MCP를 통해 얻을 수 있는 이점이 무엇인지 다루고, 공식 Python SDK를 사용하여 최소 기능의 MCP 서버와 이를 소비하는 클라이언트를 구축하며, 직접적인 도구 호출(direct tool call) 대신 프로토콜을 사용하는 것이 언제 가치가 있는지에 대해 솔직한 답변을 제공합니다.
MCP가 해결하는 문제
공유된 프로토콜이 없다면, _에이전트 프레임워크(agent framework)_와 _도구(tool)_의 모든 조합마다 고유한 글루 코드(glue code)가 필요합니다. LangChain의 @tool 래퍼(wrapper), Raw SDK를 위한 수동 스키마, 혹은 동료가 다음에 선택할 프레임워크를 위한 또 다른 래퍼 등, 프레임워크와 도구마다 통합 작업이 필요하게 됩니다. 이는 M×N 문제(M×N problem)가 됩니다.
MCP는 이를 M+N으로 단순화합니다. **서버(server)**는 표준 JSON-RPC 프로토콜을 통해 도구, 리소스(resources), 프롬프트(prompts)를 한 번만 노출합니다. Claude Code, Claude Desktop, VS Code 또는 여러분 자신의 에이전트와 같은 어떤 호스트(host) 애플리케이션이라도, 호스트를 구축한 프레임워크와 상관없이 동일한 프로토콜을 사용하는 MCP **클라이언트(client)**를 생성할 수 있습니다. 서버를 한 번만 작성하면, MCP를 인식하는 모든 호스트가 새로운 통합 코드 없이 이를 사용할 수 있습니다.
프로토콜 자체는 의도적으로 지루하게 설계되었습니다. 수명 주기 협상 (lifecycle negotiation), 도구 검색 (tool discovery), 그리고 도구 실행 (tool execution)을 위해 JSON-RPC 2.0 메시지를 사용합니다. 이 포스트에서 중요한 두 가지 호출은 검색 (tools/list)과 실행 (tools/call)입니다.
// tools/list 응답 (축약됨)
{
"jsonrpc": "2.0", "id": 2,
...
두 가지 전송 방식 (transports)이 대부분의 사례를 다룹니다: stdio (로컬 서브프로세스, 서버당 하나의 클라이언트 — 이 포스트에서 사용하는 방식)와 Streamable HTTP (원격 서버, 다수의 클라이언트, 표준 베어러 토큰 인증 (bearer-token auth)). 출처: MCP architecture overview.
최소 기능의 MCP 서버 구축하기
공식 Python SDK의 안정화 버전(stable line)은 v1.x입니다. v2는 알파(alpha) 버전이며 프로덕션 환경에서는 명시적으로 권장되지 않으므로 (python-sdk README), 이 포스트에서는 해당 버전을 고정하여 사용합니다:
uv add "mcp[cli]"
FastMCP는 타입 힌트 (type-hinted)가 지정되고 독스트링 (docstring)으로 설명된 함수를 도구 (tool)로 변환합니다. 이는 JSON 스키마 (JSON Schema)를 직접 작성할 필요가 없음을 의미하며, SDK 자체의 quickstart에서 제시하는 "두 개의 타입 힌트가 지정된 함수"라는 핵심 개념과 일치합니다. 이 도구는 읽기 전용 계좌 잔액 조회 기능이며, 다음 섹션에서 구체적으로 다룰 이유 때문에 자유 형식의 쿼리 대신 의도적으로 ID를 키로 사용합니다:
import re
from mcp.server.fastmcp import FastMCP
...
읽기 전용 리소스 (resource) — 모델이 특정 동작을 수반하지 않고 컨텍스트 (context)에 로드할 수 있는 데이터를 위한 MCP의 두 번째 기본 요소 (primitive) — 는 또 다른 데코레이터 (decorator)로 구현됩니다:
@mcp.resource("docs://refund-policy")
def refund_policy() -> str:
"""현재 환불 정책 텍스트."""
...
FastMCP, @mcp.tool(), 그리고 @mcp.resource()에 대한 소스: python-sdk v1.x README 및 공식 Build an MCP Server 튜토리얼. stdio 로깅 경고는 언어에 구애받지 않습니다: print()는 기본적으로 stdout(표준 출력)에 기록되는데, 이는 프로토콜이 사용하는 것과 동일한 스트림입니다. 대신 sys.stderr나 로깅 라이브러리(logging library)를 사용하여 로그를 남기세요.
신뢰 경계(Trust Boundary)는 이동하지 않습니다
도구(tool) 함수가 전달받는 account_id 인자는 원시 SDK 루프(post 15)에서의 block.input과 동일하게 신뢰할 수 없는, 모델이 제공한 데이터입니다. 또한 @tool 데코레이터가 지정된 함수가 bind_tools/create_agent로부터 전달받는 인자와도 같습니다 (post 27). MCP는 전송 형식(wire format)을 표준화할 뿐, 신뢰 모델(trust model)을 표준화하는 것이 아닙니다. FastMCP는 함수가 실행되기 전에 타입 힌트(type hints)로부터 유도된 스키마(schema)를 바탕으로 인자의 *형태(shape)*를 검증하지만, 형태 검증은 account_id가 문자열(string)임을 확인할 뿐, 그것이 안전한 문자열임을 확인하는 것은 아닙니다. 위에서 언급한 정규 표현식(regex) 체크는 여전히 함수 본문 내부에서 수행되어야 합니다.
타입 힌트만 믿고 두 번째 체크를 절대 건너뛰지 마세요:
# 위험함 — 절대 이렇게 하지 마세요: 도구 인자를 쿼리에 문자열 보간(string-interpolating)하는 경우
sql = f"SELECT balance FROM accounts WHERE id = '{account_id}'" # SQL 인젝션 (SQL injection)
...
post 15의 execute_validated_tool에서 얻은 교훈은 변함없이 적용됩니다: 예상하는 형태를 화이트리스트(whitelist)로 지정하고, 그 외의 것은 거부하며, 도구 인자가 결합(concatenation)을 통해 셸 명령(shell command), 파일 경로(file path), 또는 SQL 문자열에 도달하게 하지 마세요.
서버를 소비하는 클라이언트 구축하기
클라이언트는 동일한 stdio 전송(transport)을 통해 연결하고, 도구를 발견하며, 이를 호출합니다. 서버가 어떻게 구현되었는지에 대한 지식은 필요하지 않습니다:
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
...
ClientSession, StdioServerParameters, stdio_client, 그리고 session.call_tool에 대한 소스: python-sdk v1.x docs/client.md. 이는 modelcontextprotocol.io의 자체 Build an MCP Client 튜토리얼에서 사용하는 것과 동일한 패턴입니다. 클라이언트는 전송 계층(transport) 반대편에서 어떤 SDK나 언어로 서버를 구축했는지 상관하지 않습니다.
포스트 15로부터 에이전트 루프(Agent Loop)에 MCP 연결하기
에이전트 루프 자체는 변경되지 않으며, 도구 목록(tool list)과 도구 실행(tool execution)이 어디에서 오는지만 달라집니다. 수동으로 작성된 TOOLS 목록과 로컬 execute_validated_tool 함수 대신, MCP 세션에서 도구를 나열하고 tool_use 블록을 session.call_tool(...)을 통해 라우팅합니다:
tools_response = await session.list_tools()
anthropic_tools = [{
"name": t.name,
...
포스트 15의 다른 모든 요소들 — MAX_ITERATIONS 제한, thinking={"type": "adaptive"}가 적용된 claude-opus-4-8, 메시지 기록 관리(message bookkeeping) — 는 변경되지 않았습니다. 오직 도구의 _구현(implementation)_만이 별도의 재사용 가능한 프로세스로 이동했을 뿐이며, t.inputSchema는 수동 변환이 필요하지 않습니다. MCP 도구 스키마는 이미 tools=가 기대하는 것과 동일한 JSON 스키마(JSON Schema) 형태를 갖추고 있기 때문입니다.
MCP vs 프레임워크 도구 추상화 (포스트 27)
LangChain의 @tool/bind_tools/create_agent (포스트 27)는 사용자의 Python 프로세스 내에 존재하는 함수를 래핑(wrap)합니다. 이는 편리하지만, 해당 도구는 오직 그 하나의 애플리케이션 내부에서만 존재합니다. 반면 MCP는 별도의 프로세스나 서비스를 표준 프로토콜 뒤에 래핑합니다. 따라서 위에서 언급한 동일한 계정 서버(accounts server)를 Claude Desktop에서 실행할 수도 있고, Claude Code에서 호출할 수도 있으며, 이 포스트의 일반 클라이언트에서 사용할 수도 있습니다. 즉, 세 개의 서로 관련 없는 호스트가 존재하지만, 재작성해야 하는 통합 코드는 전혀 없습니다 (이는 modelcontextprotocol.io가 광고하는 ["광범위한 생태계 지원("broad ecosystem support"]") 모델입니다).
이 두 가지는 상호 배타적이지 않습니다. LangChain은 MCP 세션의 도구(tools)를 LangChain 도구로 직접 변환하는 langchain-mcp-adapters를 제공하므로, create_agent가 수동적인 session.call_tool 루프 없이도 이들을 구동할 수 있습니다:
from langchain_mcp_adapters.tools import load_mcp_tools
from langchain.agents import create_agent
...
출처: langchain-mcp-adapters README. 프레임워크의 도구 추상화(tool abstractions)와 MCP는 동일한 문제의 서로 다른 계층을 해결합니다. 하나는 함수를 프롬프트(prompt)에 연결하고, 다른 하나는 해당 함수의 _서버(server)_가 특정 프레임워크의 수명보다 더 오래 지속될 수 있도록 합니다.
MCP가 제 역할을 하는 때 — 그리고 과한 때
도구 또는 데이터 소스가 독립적으로 구축된 여러 AI 애플리케이션 간에 공유되어야 할 때 MCP를 사용하세요. 예를 들어, 지원 인력을 위한 Claude Desktop, 엔지니어를 위한 Claude Code, 그리고 커스텀 내부 에이전트가 모두 동일한 계정 조회 기능을 사용하는 경우입니다. 또는 호스트마다 통합 코드를 작성하지 않고도 타인의 서버(Sentry, GitHub, 파일 시스템 등)를 사용할 때도 적합합니다. 도구가 단일 에이전트의 사적인 구현 세부 사항이 아니라, 그 자체로 하나의 제품일 때 MCP를 선택하는 것이 옳습니다.
반면, 도구를 호출하는 주체가 단 하나의 코드베이스에 딱 하나뿐이라면 MCP는 불필요한 절차(ceremony)가 됩니다. 별도의 프로세스, stdio 또는 HTTP 전송(transport), 그리고 프로토콜 핸드셰이크(protocol handshake)는 인터프리터를 벗어나지 않는 @tool 데코레이터가 적용된 함수(27번 포스트)나 직접 작성한 JSON 스키마(15번 포스트)에 비해 순수한 오버헤드일 뿐입니다. 먼저 직접 호출하는 방식을 구축하고, 두 번째의 독립적인 호스트가 실제로 이를 호출해야 할 때 비로소 MCP 서버로 격상시키세요.
실무 체크리스트
| 관행 | 중요한 이유 |
|---|---|
| 직접적인 도구 호출(15/27번 포스트)로 시작하고, 두 번째 호스트가 필요할 때만 MCP로 격상할 것 | MCP의 비용(프로세스, 전송 방식, 프로토콜)은 단일 호출자에게는 아무런 이득을 주지 못함 |
| ... |
마치며
MCP는 15번 포스트의 에이전틱 루프 (agentic loop)나 27번 포스트의 프레임워크 도구 추상화 (framework tool abstractions)를 대체하는 것이 아닙니다. 대신 그 이면에 있는 것, 즉 호스트별 글루 코드 (glue code) 없이도 규격을 준수하는 어떤 클라이언트든 발견하고 호출할 수 있는 도구 서버 (tool server)를 표준화합니다. 이러한 표준화는 하나의 도구가 둘 이상의 애플리케이션에 서비스를 제공해야 할 때 실제 엔지니어링 비용을 들일 만한 가치가 있습니다. 호출자가 하나뿐인 도구의 경우에는, 이 시리즈에서 이미 다룬 직접적인 접근 방식들이 여전히 더 단순하고 올바른 기본값으로 남습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기