하나의 AI 도구 서버를 구축하고 세 가지 서로 다른 에이전트에서 호출하기 (MCP 설명)
요약
Model Context Protocol(MCP)을 사용하여 하나의 Python 서버가 Claude Code, Python 에이전트, Rust CLI 등 서로 다른 세 가지 환경에서 공통적으로 작동하는 도구를 제공하는 방법을 설명합니다. MCP를 AI 도구의 표준 규격인 'USB-C'에 비유하며 서버와 클라이언트 간의 통신 구조를 다룹니다.
핵심 포인트
- MCP는 다양한 AI 앱에서 공통으로 사용할 수 있는 표준 프로토콜임
- MCP 서버는 도구를 제공하고, 클라이언트는 이를 AI 모델에 연결함
- stdio를 통해 JSON 메시지로 클라이언트와 서버가 통신함
- 서버 구현 시 stdout은 통신용이므로 로그는 stderr를 사용해야 함
AI 어시스턴트에게 이미지 생성과 같은 새로운 능력을 부여하고, 그 능력이 단 하나의 도구가 아니라 당신이 사용하는 어떠한 AI 도구에서도 작동하게 만들고 싶었던 적이 있나요?
이 프로젝트가 바로 그 일을 수행하며, 그 마법의 핵심 요소는 **Model Context Protocol (MCP)**입니다. 이 글에서는 하나의 작은 Python 서버가 완전히 다른 세 가지 프로그램에 이미지 생성 초능력을 부여하는 실제 작동하는 리포지토리(repo)를 살펴보겠습니다.
- 🖥️ Claude Code (Anthropic의 AI 코딩 어시스턴트)
- 🐍 Python으로 작성된 Google ADK 에이전트
- 🦀 Rust 커맨드 라인 앱
이들은 단 한 줄의 코드도 공유하지 않습니다. 어떻게 가능한지 알아봅시다.
🤔 첫째: MCP란 무엇인가?
MCP를 AI 도구를 위한 USB-C라고 생각하세요.
USB-C가 나오기 전에는 모든 장치마다 각자의 전용 케이블이 필요했습니다. MCP가 나오기 전에는 모든 AI 앱마다 각자의 전용 플러그인 형식이 필요했습니다. ChatGPT 플러그인은 Claude에서 작동하지 않았고, Claude 도구는 당신의 Python 에이전트에서 작동하지 않는 식이었죠.
MCP는 다음과 같은 단순한 분리를 통해 이 문제를 해결합니다:
- **MCP 서버 (MCP server)**는 도구를 제공하는 작은 프로그램입니다. 각 도구는 이름, 설명, 그리고 AI가 읽을 수 있는 함수 시그니처(function signature)와 같은 타입화된 파라미터(typed parameters)를 가집니다.
- **MCP 클라이언트 (MCP client)**는 AI 앱 내부에 존재합니다. 클라이언트는 서버에 "어떤 도구들을 가지고 있나요?"라고 묻고, 이를 AI 모델에게 보여준 뒤, 모델의 도구 호출(tool calls)을 다시 서버로 전달합니다.
두 측면은 JSON 메시지로 대화합니다. 이들이 연결되는 가장 간단한 방식은 stdio라고 불립니다. 클라이언트가 서버를 자식 프로세스(child process)로 실행하기만 하면 되며, 이들은 echo hi | grep h를 실행할 때 사용하는 것과 동일한 표준 입력/출력(standard input/output) 파이프를 통해 대화합니다.
💡 재미있는 결과: stdout이 통신 채널 _그 자체_이기 때문에, MCP 서버는 절대 stdout으로
print()를 해서는 안 됩니다. 대신 우리 서버는 stderr로 로그를 남깁니다. 잘못된 print 문 하나가 프로토콜을 망가뜨릴 수 있기 때문입니다!
🤖 그렇다면 "에이전트 (agent)"란 무엇인가?
**에이전트 (agent)**는 도구와 함께 루프(loop) 안에 있는 AI 모델입니다. 모델은 당신의 요청을 읽고, 도구가 도움이 될 것이라고 결정하고, 도구를 호출하고, 결과를 읽으며, 작업이 완료될 때까지 이 과정을 반복합니다. AI가 두뇌라면, MCP 도구는 손입니다.
🗺️ 프로젝트 개요
레포지토리(repo) 레이아웃은 다음과 같습니다:
nb2lite-agent-claude/
├── MCP/ ← 핵심: Gemini의 이미지 모델을 래핑(wrapping)하는 MCP 서버
│ └── server.py
...
그리고 각 구성 요소가 연결되는 방식은 다음과 같습니다:
Claude Code ──┐
ADK agent ──┼── stdio를 통한 MCP (MCP over stdio) ──► MCP/server.py ──► Gemini image API
Rust CLI ──┘ │
...
세 개의 화살표가 들어와 하나의 서버를 거쳐 하나의 API로 나갑니다. Gemini 전용 코드는 정확히 단 하나의 파일에 존재합니다.
🎨 서버: 약 300줄로 구현한 4개의 도구
이 서버는 공식 mcp Python 패키지에 포함된 FastMCP를 사용하여 구축되었습니다. 도구를 작성하는 것은 함수에 데코레이터(decorator)를 붙이는 것만큼 간단합니다:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("NB2Lite Agent")
...
이것이 전부입니다. FastMCP는 함수의 시그니처(signature)와 독스트링(docstring)을 읽어 연결된 모든 AI에게 자동으로 다음과 같이 알립니다: "generate_image라는 도구가 있습니다. 이 도구의 역할은 다음과 같으며, 파라미터(parameter)는 다음과 같습니다." 즉, 여러분의 코드가 곧 AI가 보는 문서(documentation)가 됩니다.
서버는 네 가지 도구를 노출합니다:
| 도구 | 기능 |
|---|---|
generate_image | 텍스트 프롬프트(Text prompt) → 완전히 새로운 이미지 |
| ... |
내부적으로 이 도구들은 모두 Interactions API라고 불리는 것을 통해 Google의 gemini-3.1-flash-lite-image 모델(빠른 이미지 모델)을 호출합니다.
🔁 핵심적인 부분: 기억하는 편집
대부분의 이미지 API는 금붕어와 같습니다. 모든 요청이 제로(zero) 상태에서 시작되기 때문입니다. 하지만 Interactions API는 다릅니다. 이 API는 상태 유지(stateful) 방식입니다. 모든 생성 작업은 interaction_id를 반환하며, 이 ID를 다시 전달하여 세션을 계속 이어갈 수 있습니다:
interaction = ai_client.interactions.create(
model=MODEL_NAME,
previous_interaction_id=previous_interaction_id, # 👈 "여기서부터 계속하세요"
...
실제로 대화는 다음과 같은 방식으로 진행됩니다:
- 사용자 (You): "Generate a cyberpunk ramen kitchen, 16:9"
- **에이전트 (Agent)**가
generate_image(...)를 호출 → "Saved to gen...png • Interaction ID:int_abc"_를 반환받음 - 사용자 (You): "Nice — add a neon sign that says RAMEN"
- **에이전트 (Agent)**가
edit_image(previous_interaction_id="int_abc", edit_prompt="add a neon RAMEN sign")를 호출 - 모델이 스타일과 디테일을 일관되게 유지하면서 _정확히 그 이미지_를 편집합니다 🎉
무엇을 누가 기억하는지 주목하세요: Google의 서버가 이미지 세션(session)을 저장하고, **에이전트의 대화 메모리 (conversation memory)**가 ID를 보유합니다. MCP 서버 자체는 상태를 유지하지 않는 스테이트리스 (stateless) 방식으로 동작하므로, 언제든 재시작할 수 있습니다.
📦 도구가 이미지가 아닌 파일 경로를 반환하는 이유
도구는 이미지 바이트 (image bytes)를 AI에게 직접 보낼 수도 있습니다. 하지만 이 서버는 의도적으로 그렇게 하지 않습니다. 대신 파일을 디스크에 저장하고 다음과 같은 짧은 메시지를 반환합니다:
🟢 Image successfully saved!
• Saved to: /home/you/images/gen_1780123456_a3b2c1d0.png
• Interaction ID: int_abc
여기에는 초보자에게 유용한 두 가지 교훈이 담겨 있습니다:
- 토큰 경제성 (Token economy). Base64로 인코딩된 PNG 파일은 매우 큽니다. 이를 AI의 컨텍스트 (context)에 집어넣는 것은 아무런 이득 없이 수천 개의 토큰을 낭비하는 일이 됩니다. AI는 가공되지 않은 픽셀 (raw pixels)로 할 수 있는 일이 많지 않지만, 파일 경로를 알려주는 것은 완벽하게 수행할 수 있습니다.
- 친절한 에러 메시지 (Friendly errors). 모든 도구는 예외 (exceptions)를 포착하여, 시스템이 충돌하는 대신 읽기 쉬운
🔴 Image generation failed: ...문자열을 반환합니다. AI는 이 에러를 읽고 스스로의 실수(잘못된 종횡비 등)를 수정할 수 있습니다 (유효한 종횡비로 다시 시도할 것입니다).
🔌 소비자 1: Claude Code (코드 작성 불필요!)
서버를 Claude Code에 연결하는 데는 설정 파일인 .mcp.json 하나면 충분합니다:
{
"mcpServers": {
"nb2lite-agent": {
...
Claude Code는 서버를 실행하고 4개의 도구를 발견하며, 그 이후부터는 코딩 세션에서 _"generate a 16:9 image of a mountain sunrise"_라고 입력하기만 하면 됩니다.
🐍 소비자 2: Google ADK 에이전트
Agent Development Kit (ADK)는 Google이 제공하는 자신만의 에이전트를 구축하기 위한 프레임워크입니다. 이 프레임워크의 MCPToolset은 서버 실행, 핸드셰이크 (handshake) 수행, 발견된 모든 도구를 LLM이 호출할 수 있는 형태로 변환하는 등 모든 MCP 배관 작업 (plumbing)을 처리합니다.
root_agent = LlmAgent(
name="nb2lite_adk_agent",
model="gemini-2.5-flash",
...
주목할 만한 두 가지 사항이 있습니다:
- 이 파일에서
generate_image를 정의하지 않습니다. 도구 세트(toolset)가 시작 시점에 프로토콜을 통해 도구들을 가져옵니다 (imports the tools over the protocol). instruction은 LLM에게 상호작용 ID (interaction IDs)를 추적하도록 명시적으로 지시합니다. 프로토콜이 ID를 전달하면, LLM의 메모리가 이를 유지합니다.
터미널에서 채팅을 하려면 adk run nb2lite_adk_agent를 실행하고, 브라우저 UI를 사용하려면 adk web을 실행하세요.
🦀 소비자 3: Rust CLI
"어떤 언어든 가능하다"는 주장을 증명하기 위해, 이 저장소에는 공식 Rust MCP SDK인 rmcp를 사용하는 Rust 클라이언트가 포함되어 있습니다. 이 클라이언트는 동일한 Python 서버를 자식 프로세스로 실행합니다:
let service = ()
.serve(TokioChildProcess::new(Command::new("python3").configure(
|cmd| { cmd.arg(&server); },
...
이 바이너리에는 AI 모델이 전혀 포함되어 있지 않습니다. 도구를 직접 호출하는 일반적인 프로그램일 뿐입니다:
cargo run -- tools # 도구 목록 표시
cargo run -- generate "a cyberpunk ramen kitchen" 16:9 high # 이미지 생성
cargo run -- edit int_abc123 "add a neon RAMEN sign" # 이미지 수정
마지막으로 정리하기 좋은 개념 모델이 있습니다: MCP 도구 호출은 단지 파이프 (pipe)를 통한 함수 호출일 뿐입니다. LLM이 이를 수행할 수 있는 것처럼, 여러분의 셸 스크립트 (shell script)도 수행할 수 있습니다.
🧠 핵심 요약 (The takeaway)
MCP가 없다면, 이 세 가지 소비자를 지원하기 위해 **세 번의 통합 (three integrations)**이 필요합니다. 즉, Claude 전용 설정, ADK 래퍼 (wrapper), 그리고 Gemini 클라이언트의 Rust 포팅 버전이 각각 필요합니다. 이는 API가 변경될 때마다 세 곳을 모두 업데이트해야 함을 의미합니다.
MCP를 사용하면 해당 기능이 단 하나의 파일에 존재하며, 각 소비자(consumer)는 약 30줄 정도의 설정(config) 또는 상용구(boilerplate) 코드만 있으면 됩니다. 내일 LangChain이나 에디터 플러그인 등 네 번째 소비자를 추가하더라도 비용은 거의 동일합니다.
도구를 한 번만 작성하세요. 모든 에이전트가 이를 호출하게 하세요.
🚀 직접 시도해 보세요
서버는 즉시 실행 가능한 Docker 이미지로 배포되므로, 리포지토리(repo)가 전혀 필요하지 않습니다. 어떤 MCP 클라이언트든 해당 이미지를 가리키도록 설정하면 됩니다 (다음은 Claude Code를 위한 .mcp.json 예시입니다):
{
"mcpServers": {
"nb2lite-agent": {
...
환경 변수에 GEMINI_API_KEY를 설정하고, 에이전트에게 이미지를 생성하도록 요청한 뒤 마운트된 images/ 폴더를 확인하세요. (주의: -i는 사용하되 -t는 절대 사용하지 마세요. TTY는 프로토콜 스트림(protocol stream)을 손상시킵니다!)
MCP, ADK, 또는 Rust 측면에 대해 궁금한 점이 있으신가요? 댓글로 남겨주세요! 👇
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기