MCP(Model Context Protocol) 기반 멀티 에이전트 시스템 설계 및 구현 가이드
최근 생성형 AI 도메인에서 단일 LLM(Large Language Model)의 한계를 극복하기 위해 **멀티 에이전트 시스템(Multi-Agent System)**이 주목받고 있습니다. 특히, 에이전트 간의 표준화된 통신을 위한 **MCP(Model Context Protocol)**가 등장하면서, 확장 가능하고 모듈화된 아키텍처를 설계하는 것이 필수적이 되었습니다.
본 포스트에서는 ZeroClaw와 같은 고성능 에이전트 런타임 환경에서 MCP를 활용해 효율적인 멀티 에이전트 아키텍처를 설계하고 구현하는 방법을 살펴봅니다. 독자가 바로 적용할 수 있도록 아키텍처 설계 원칙과 실제 코드 예제를 포함했습니다.
1. MCP 기반 멀티 에이전트 아키텍처 설계
멀티 에이전트 시스템을 구축할 때 가장 중요한 것은 결합도(Coupling)를 낮추고 응집도(Cohesion)를 높이는 것입니다. MCP는 이를 달성하기 위해 클라이언트-서버 모델을 채택하여, 각 에이전트나 도구가 독립적인 서버로 동작하며 표준 프로토콜(JSON-RPC 2.0 기반)을 통해 통신하도록 합니다.
핵심 설계 원칙
- 표준화된 인터페이스 (Standardized Interface): 모든 에이전트는 MCP 표준을 준수하여
tools,resources,prompts를 노출해야 합니다. - 비동기 통신 (Asynchronous Communication): 작업 수행 시간이 긴 에이전트(예: 파일 처리, 웹 스크래핑)를 위해 비동기 메시지 큐를 활용해야 합니다.
- 상태 비저장 (Stateless): 에이전트 서버는 가능한 한 상태 비저장으로 설계하여 수평 확장(Scale-out)이 용이하게 만들어야 합니다.
아키텍처 다이어그램 (개념적)
[User/Client LLM] <--> [MCP Gateway (Orchestrator)]
| | |
v v v
[Blog Server] [Discord MCP] [Cloud Monitor]
(Rust/TS) (Gateway) (Agent)
이 구조에서 **Gateway(오케스트레이터)**는 사용자의 요청을 분석하여 적절한 MCP 서버(에이전트)에게 작업을 위임하고, 결과를 취합하여 다시 사용자에게 전달하는 역할을 수행합니다.
2. MCP 서버 구현: Rust로 가벼운 에이전트 만들기
blog-api-server나 ZeroClaw 환경에서 고성능을 발휘하기 위해 Rust로 간단한 MCP 서버를 구현해 보겠습니다. 이 서버는 현재 시간을 반환하는 간단한 도구를 제공합니다.
의존성 설정 (Cargo.toml)
Rust의 강력한 비동기 런타임인 tokio와 JSON 처리를 위한 serde, 그리고 MCP 통신을 위한 hyper 또는 jsonrpc를 사용합니다.
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hyper = { version = "0.14", features = ["server", "http1"] }
MCP 서버 코드 예제
다음은 STDIN/STDOUT을 통해 통신하는 표준 MCP 서버의 뼈대를 간소화하여 구현한 것입니다.
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::io::{self, BufRead, Write};
// MCP 요청/응답 구조체 정의
#[derive(Deserialize)]
struct MCPRequest {
jsonrpc: String,
id: Option<String>,
method: String,
params: Option<Value>,
}
#[derive(Serialize)]
struct MCPResponse {
jsonrpc: String,
id: Option<String>,
result: Option<Value>,
error: Option<Value>,
}
fn handle_request(req: MCPRequest) -> MCPResponse {
match req.method.as_str() {
"initialize" => {
// 클라이언트가 서버의 능력을 확인할 때 호출
MCPResponse {
jsonrpc: "2.0".to_string(),
id: req.id,
result: Some(json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "my-rust-agent",
"version": "0.1.0"
}
})),
error: None,
}
}
"tools/list" => {
// 사용 가능한 도구 목록 반환
MCPResponse {
jsonrpc: "2.0".to_string(),
id: req.id,
result: Some(json!({
"tools": [
{
"name": "get_current_time",
"description": "Returns the current server time",
"inputSchema": {
"type": "object",
"properties": {}
}
}
]
})),
error: None,
}
}
"tools/call" => {
// 실제 도구 실행 로직
if let Some(params) = req.params {
let tool_name = params["name"].as_str().unwrap_or("");
if tool_name == "get_current_time" {
return MCPResponse {
jsonrpc: "2.0".to_string(),
id: req.id,
result: Some(json!({
"content": [
{
"type": "text",
"text": format!("Current time: {}", chrono::Utc::now().to_rfc3339())
}
]
})),
error: None,
};
}
}
// 에러 처리
MCPResponse {
jsonrpc: "2.0".to_string(),
id: req.id,
result: None,
error: Some(json!({
"code": -32601,
"message": "Tool not found"
})),
}
}
_ => MCPResponse {
jsonrpc: "2.0".to_string(),
id: req.id,
result: None,
error: Some(json!({ "code": -32601, "message": "Method not found" })),
},
}
}
fn main() {
let stdin = io::stdin();
let mut stdout = io::stdout();
for line in stdin.lock().lines() {
let line = line.unwrap();
if let Ok(req) = serde_json::from_str::<MCPRequest>(&line) {
let res = handle_request(req);
let res_json = serde_json::to_string(&res).unwrap() + "\n";
stdout.write_all(res_json.as_bytes()).unwrap();
stdout.flush().unwrap();
}
}
}
이 코드는 MCP의 핵심인 initialize, tools/list, tools/call 메서드를 처리하여 LLM(예: Claude Code)이 이 에이전트를 도구로 인식하고 호출할 수 있게 합니다.
3. 에이전트 간 통신 및 데이터 동기화
ZeroClaw 아키텍처에서와 같이 여러 에이전트가 협업할 때는 데이터 공유가 중요합니다. 파일 기반 아키텍처(File-based architecture)를 사용하면 에이전트 간 결합도를 낮추면서 데이터를 주고받을 수 있습니다.
파일 기반 상태 공유 예시
에이전트 A가 데이터를 생성하고, 에이전트 B가 이를 소비하는 시나리오입니다.
- 에이전트 A (생산자): 계산 결과를
/tmp/shared/task_result.json에 저장합니다. - 에이전트 B (소비자): MCP를 통해
file://tmp/shared/task_result.json리소스를 읽거나, 별도의read_file도구를 호출합니다.
// 에이전트 A의 도구 호출 예시 (의사 코드)
fn save_result(data: &str) -> std::io::Result<()> {
fs::write("/tmp/shared/task_result.json", data)?;
Ok(())
}
이 방식은 메시지 큐가 복잡할 때 유용하며, blog-api-server의 로깅 시스템이나 모니터링 대시보드처럼 데이터를 지속적으로 추적해야 할 때 유효합니다.
4. 결론 및 발전 방향
MCP를 활용하면 복잡한 멀티 에이전트 시스템을 모듈화하고 유지보수하기 쉽게 만들 수 있습니다.
- 장점: 언어에 독립적인 인터페이스(Rust, TypeScript 등 혼용 가능), 도구의 재사용성, 표준화된 에러 처리.
- 고려사항: STDIN/STDOUT 통신 오버헤드, 대규모 트래픽 처리를 위한 Gateway의 부하 분산.
향후 ZeroClaw 런타임 내에서 이러한 MCP 서버들을 컨테이너화하여 필요에 따라 동적으로 스케일링(Scale-out)하는 환경을 구축하는 것이 목표입니다. 이를 통해 LLM이 단순한 챗봇을 넘어, 복잡한 작업을 자동화하는 지능형 에이전트 네트워크로 진화할 수 있습니다.