MCP + Multi-Agent — 에이전트가 도구를 공유하고 협업하는 법

MCP + Multi-Agent — 에이전트가 도구를 공유하고 협업하는 법
에이전트 하나는 강력합니다. 하지만 현실 세계의 복잡한 태스크는 하나의 에이전트로 해결하기 어렵습니다. 리서치, 코딩, 리뷰를 동시에 해야 한다면? 여러 에이전트가 각자의 역할을 맡아 협업하는 게 답입니다.
이번 글에서는 MCP(Model Context Protocol)로 도구 통합을 표준화하고, CrewAI로 멀티 에이전트 팀을 구성하며, A2A(Agent-to-Agent) 패턴으로 에이전트끼리 대화하는 방법을 다룹니다.
시리즈: Part 1: ReAct 패턴 | Part 2: LangGraph + Reflection | Part 3 (이 글) | Part 4: 프로덕션 배포
N×M 통합 문제
에이전트를 만들다 보면 금방 벽에 부딪힙니다. Agent가 3개, Tool이 5개라면?
직접 통합 방식에서는 Agent마다 Tool별 커넥터를 각각 만들어야 합니다. 3 × 5 = 15개의 커넥터. Agent가 늘거나 Tool이 바뀔 때마다 수정해야 할 코드가 기하급수적으로 늘어납니다.
이게 바로 N×M 통합 문제입니다. Agent N개와 Tool M개를 연결하려면 N×M개의 통합이 필요하죠. MCP는 이 문제를 N+M으로 줄여줍니다.
MCP(Model Context Protocol)란?
MCP는 Anthropic이 만든 오픈 표준으로, LLM이 외부 시스템에 연결하는 보편적인 방법을 제공합니다.
가장 좋은 비유는 USB-C입니다. USB-C가 나오기 전에는 기기마다 다른 충전기가 필요했죠. MCP도 마찬가지입니다. 각 LLM마다 도구 연결 방식이 제각각이던 것을 하나의 표준 프로토콜로 통일합니다.
MCP의 3가지 핵심 요소(Primitives)
- Resources — 서버가 노출하는 데이터 (파일, DB 레코드, 로그 등)
- Prompts — 구조화된 요청을 위한 미리 정의된 템플릿
- Tools — 실제 작업을 수행하는 실행 가능한 함수
아키텍처: Server + Client
MCP는 서버-클라이언트 구조입니다.
- MCP Server: 도구를 등록하고 노출합니다
- MCP Client: 서버를 발견하고 도구를 호출합니다
- Host: 클라이언트를 내장한 LLM 앱 (Claude Desktop, VS Code 등)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MCP Client │────▶│ MCP Server │────▶│ External │
│ (LLM App) │◀────│ (Tools) │◀────│ Service │
└──────────────┘ └──────────────┘ └──────────────┘MCP 서버 만들기
실제로 MCP 서버를 만들어 봅시다. 사내 데이터베이스 검색과 Slack 메시지 전송, 두 가지 도구를 노출하는 서버입니다.
from mcp import Server, Tool
server = Server("my-tools")
@server.tool()
def search_database(query: str) -> str:
"""Search the company database for relevant information."""
results = db.search(query)
return format_results(results)
@server.tool()
def send_slack(channel: str, message: str) -> str:
"""Send a message to a Slack channel."""
slack.post(channel, message)
return f"Sent to #{channel}"
server.run()핵심은 @server.tool() 데코레이터입니다. 함수 이름, docstring, 타입 힌트가 자동으로 도구의 메타데이터가 됩니다. LLM은 이 정보를 보고 어떤 도구를 언제 호출할지 판단합니다.
서버가 노출하는 것
서버의 list_tools() 메서드를 호출하면 이런 스키마가 반환됩니다:
[
{
"name": "search_database",
"description": "Search the company database for relevant information.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"}
},
"required": ["query"]
}
},
{
"name": "send_slack",
"description": "Send a message to a Slack channel.",
"input_schema": {
"type": "object",
"properties": {
"channel": {"type": "string"},
"message": {"type": "string"}
},
"required": ["channel", "message"]
}
}
]MCP 클라이언트로 도구 연결
클라이언트는 세 단계로 동작합니다:
1단계: Discovery (발견)
# 서버에 어떤 도구가 있는지 물어보기
tools = await client.list_tools()
print(f"Available tools: {[t.name for t in tools]}")
# => Available tools: ['search_database', 'send_slack']2단계: Conversion (변환)
MCP 도구 정의를 LLM이 이해하는 형태(예: OpenAI function calling 형식)로 변환합니다.
def mcp_to_openai_functions(mcp_tools):
"""MCP 도구 스키마를 OpenAI function 형식으로 변환"""
return [
{
"type": "function",
"function": {
"name": tool.name,
"description": tool.description,
"parameters": tool.input_schema,
}
}
for tool in mcp_tools
]
functions = mcp_to_openai_functions(tools)3단계: Execution (실행)
LLM이 도구 호출을 결정하면 클라이언트가 실행합니다.
# LLM 응답에서 tool_call 추출 후 실행
result = await client.call_tool(
name="search_database",
arguments={"query": "Q4 revenue report"}
)이 3단계 덕분에 어떤 LLM이든 동일한 MCP 서버의 도구를 사용할 수 있습니다. Claude, GPT, Gemini 모두 같은 서버에 붙으면 됩니다.
실전 설정: claude_desktop_config.json
Claude Desktop에서 MCP 서버를 연결하려면 설정 파일에 추가합니다:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/Users/me/projects"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "ghp_xxx" }
},
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "postgresql://localhost/mydb"]
}
}
}Filesystem, GitHub, Slack, PostgreSQL 등 이미 만들어진 MCP 서버가 풍부합니다. 직접 만들 필요 없이 갖다 쓰면 됩니다.
CrewAI: 역할 기반 멀티 에이전트
도구 통합을 MCP로 해결했으니, 이제 여러 에이전트를 조율하는 방법을 봅시다.
CrewAI는 역할극(role-playing) 기반의 멀티 에이전트 프레임워크입니다. 가상의 팀을 구성하고, 각 멤버에게 역할과 목표를 부여해서 협업하게 합니다.
3가지 핵심 구성 요소
전체 예제: 리서치 → 블로그 작성
from crewai import Agent, Task, Crew
# 1. Agent 정의 — 역할, 목표, 배경을 명확히
researcher = Agent(
role="Senior Research Analyst",
goal="Find comprehensive information about {topic}",
backstory=(
"10년 경력의 리서치 전문가. 복잡한 기술 주제도 "
"핵심을 빠르게 파악하고 구조화하는 능력이 탁월하다."
),
allow_delegation=False, # 다른 에이전트에 위임 안 함
)
writer = Agent(
role="Technical Writer",
goal="Create engaging blog posts from research",
backstory=(
"수상 경력의 기술 블로거. 어려운 개념을 쉽게 풀어쓰되 "
"정확성을 절대 타협하지 않는다."
),
allow_delegation=False,
)
# 2. Task 정의 — 무엇을, 누가, 결과는?
research_task = Task(
description="Research {topic} thoroughly. Include latest trends and key statistics.",
agent=researcher,
expected_output="Detailed research report with sources",
)
writing_task = Task(
description="Write a blog post based on the research. Make it engaging and informative.",
agent=writer,
expected_output="Complete blog post in markdown format",
context=[research_task], # 핵심: 리서치 결과가 입력으로 들어감
)
# 3. Crew 실행
crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process="sequential", # 순차 실행
)
result = crew.kickoff(inputs={"topic": "AI Agents in 2026"})
print(result)context=[research_task]가 포인트입니다. research_task의 결과가 writing_task의 입력으로 자동 전달됩니다. 에이전트 간 데이터 흐름이 명시적으로 정의되는 거죠.
backstory가 중요한 이유
CrewAI에서 backstory는 장식이 아닙니다. LLM은 부여된 페르소나에 맞춰 행동합니다. "10년 경력의 리서치 전문가"라는 배경을 주면, 더 체계적이고 깊이 있는 리서치 결과를 생성합니다. 잘 작성된 backstory = 더 나은 결과물.
Process 타입
Sequential (순차형)
Researcher → Writer → Editor앞 태스크 출력이 뒤 태스크 입력이 됩니다. 파이프라인 워크플로우에 적합합니다.
Hierarchical (계층형)
Manager
/ | \
Researcher Writer Editor매니저 에이전트가 동적으로 태스크를 할당하고 조율합니다. 복잡하지만 유연합니다.
도구 연결
에이전트에 도구를 붙이는 건 간단합니다:
from crewai_tools import SerperDevTool, WebsiteSearchTool
researcher = Agent(
role="Research Analyst",
tools=[SerperDevTool(), WebsiteSearchTool()],
verbose=True, # 추론 과정 출력
)MCP 서버의 도구를 CrewAI 에이전트에 연결하면 표준화된 도구 + 역할 기반 협업의 조합이 완성됩니다.
A2A: 에이전트 간 통신
MCP가 "에이전트 ↔ 도구" 연결이라면, A2A(Agent-to-Agent)는 "에이전트 ↔ 에이전트" 연결입니다.
핵심 아이디어: 에이전트 자체를 도구로 노출합니다.
class ResearchAgent:
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o")
def research(self, topic: str) -> str:
"""Perform deep research on a topic."""
return self.llm.invoke(f"Research: {topic}")
def as_tool(self):
"""에이전트를 도구로 변환 — 다른 에이전트가 호출 가능"""
return Tool(
name="research_agent",
description="Delegate research tasks to a specialized research agent",
func=self.research,
)
# Manager가 ResearchAgent를 도구처럼 사용
research_agent = ResearchAgent()
manager = Agent(
role="Project Manager",
tools=[research_agent.as_tool()],
)위임 토폴로지
Supervisor (감독자) 패턴
하나의 관리자가 하위 에이전트에게 태스크를 위임합니다.
Supervisor
/ | \
Agent A Agent B Agent C- 장점: 중앙 제어, 태스크 추적 용이
- 단점: Supervisor가 병목이 될 수 있음
- 예시: 고객 지원 시스템 (라우터가 전문 에이전트에 분배)
Peer-to-Peer (동등 협업) 패턴
에이전트끼리 직접 소통하고 협업합니다.
Agent A ←→ Agent B
↕ ↕
Agent C ←→ Agent D- 장점: 병목 없음, 유연한 협업
- 단점: 조율이 복잡, 무한 루프 위험
- 예시: 코드 리뷰 팀 (개발자들이 서로 리뷰)
CrewAI vs LangGraph: 언제 뭘 쓸까?
Part 2에서 다룬 LangGraph와 이번 글의 CrewAI, 어떻게 선택할까요?
경험 법칙: 빠르게 멀티 에이전트를 돌려보고 싶다면 CrewAI, 상태 관리와 분기를 세밀하게 제어해야 한다면 LangGraph.
아키텍처 선택 가이드
실제 프로젝트에서 어떤 패턴을 선택할지 정리합니다.
시작은 Single Agent + MCP로 하세요. 충분히 강력합니다. 하나의 에이전트로 부족해지는 순간이 멀티 에이전트를 도입할 때입니다.
핵심 정리
- MCP는 N×M 통합 문제를 N+M으로 줄여주는 표준 프로토콜입니다
- MCP 서버는 도구를 등록하고, 클라이언트는 발견 → 변환 → 실행 3단계로 사용합니다
- CrewAI는 역할 기반 멀티 에이전트 프레임워크로, Agent + Task + Crew로 구성됩니다
- A2A 패턴은 에이전트를 도구로 노출해서 에이전트 간 위임을 가능하게 합니다
- Supervisor vs Peer-to-Peer 토폴로지를 태스크 특성에 맞게 선택하세요
전체 실습은 Agent Cookbook에서
이 글의 코드를 직접 실행해보고 싶다면 Jupyter 노트북으로 준비된 실습을 활용하세요.
- Week 3: MCP & A2A 노트북 — MCP 서버/클라이언트 구현, A2A 위임 패턴
- Week 3: CrewAI 노트북 — 역할 기반 에이전트 팀 구성
- Weekend Project — 이번 주 배운 것을 통합하는 실전 프로젝트
다음 편 예고
Part 4에서는 Agent를 프로덕션에 배포합니다. 아무리 잘 만든 에이전트도 배포하지 않으면 의미가 없으니까요.
- Guardrails — 에이전트가 엉뚱한 짓을 하지 않도록 안전장치 설정
- Human-in-the-Loop — 중요한 결정은 사람이 확인
- FastAPI + Docker — API 서버로 배포하기
- DSPy — 프롬프트 최적화 자동화