Recently, while working on the ‘ZeroClaw’ project and MCP (Model Context Protocol), I’ve keenly felt the necessity of a Multi-Agent System that goes beyond a single agent. To build a structure where agents can exchange meaningful data and collaborate, rather than simply sending prompts to an LLM, a robust Communication Protocol is essential.
In this post, we’ll explore the design process for multi-agent communication based on the MCP architecture and how to implement it on ZeroClaw (Rust), a high-performance runtime.
1. Problem Definition: Why a Communication Protocol?
In previous implementations like [blog-api-server] or [Discord MCP], the ‘Request-Response’ structure was primarily followed. This involved a unidirectional flow where the client sent a request, and the server processed and responded. However, in a multi-agent environment, this structure alone is insufficient for the following reasons:
- Asynchrony: If Agent A delegates a task to Agent B and Agent A stops execution until B completes, the overall system throughput decreases.
- Event-Driven Interaction: Some agents need to monitor system state changes (e.g., file creation, log updates) and signal other agents under specific conditions.
- Reliability: Messages must not be lost in situations of network instability or temporary agent failures.
2. Protocol Design: Request, Task, Completion
To address these issues, we designed a Task-Based Messaging Protocol. This protocol extends the MCP standard message format and consists of three main message types:
- TaskRequest: Initiates a task. Used when Agent A delegates a specific task to Agent B.
- TaskUpdate: Reports progress. Conveys intermediate results for long-running tasks.
- TaskResult: Final outcome. Returns data along with a success or failure status.
3. Implementing the ZeroClaw Communication Layer in Rust
ZeroClaw implements this protocol in a type-safe manner using Rust’s powerful type system and its asynchronous runtime (Tokio). Below is a simplified message definition and handler structure.
3.1. Message Definition (Based on JSON-RPC)
MCP is fundamentally based on JSON-RPC 2.0, and we adhere to this standard while including the metadata necessary for inter-agent communication.
// src/protocol/message.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMessage {
pub id: String, // Unique message ID
pub sender: String, // Sender agent ID
pub target: String, // Recipient agent ID
pub timestamp: i64, // Timestamp
pub payload: Payload, // Actual data
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Payload {
#[serde(rename = "task/request")]
TaskRequest {
task_id: Uuid,
description: String,
context: serde_json::Value,
},
#[serde(rename = "task/result")]
TaskResult {
task_id: Uuid,
status: TaskStatus,
data: Option<serde_json::Value>,
},
#[serde(rename = "system/ping")]
SystemPing,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum TaskStatus {
Success,
Failure(String), // Reason for failure
Pending,
}
3.2. Agent Runtime Structure
In ZeroClaw, each agent runs as an independent Tokio task. Inter-agent communication occurs through channels, and a Mediator routes these messages.
// src/runtime/agent.rs
use tokio::sync::mpsc;
use crate::protocol::message::{AgentMessage, Payload};
pub struct Agent {
id: String,
tx: mpsc::Sender<AgentMessage>, // Sender for outgoing messages
rx: mpsc::Receiver<AgentMessage>, // Receiver for incoming messages
}
impl Agent {
pub fn new(id: String) -> Self {
let (tx, rx) = mpsc::channel(100);
Self { id, tx, rx }
}
// The agent's main execution loop
pub async fn run(mut self) {
println!("[{}] Agent started", self.id);
while let Some(msg) = self.rx.recv().await {
if let Err(e) = self.handle_message(msg).await {
eprintln!("[{}] Error handling message: {:?}", self.id, e);
}
}
}
async fn handle_message(&self, msg: AgentMessage) -> Result<(), Box<dyn std::error::Error>> {
match msg.payload {
Payload::SystemPing => {
// Logic to respond to ping with pong
// In a real environment, this would use the client's tx to send a response
println!("[{}] Received Ping from {}", self.id, msg.sender);
}
Payload::TaskRequest { task_id, description, context } => {
println!("[{}] Received Task {}: {}", self.id, task_id, description);
// LLM calls or tool execution logic would go here.
// Example: Result return logic (simulated)
// self.send_result(task_id, ...).await;
}
_ => {
println!("[{}] Received unhandled message type", self.id);
}
}
Ok(())
}
}
4. Conclusion and Next Steps
Through the structured communication layer described above, we achieve an architecture capable of task-unit collaboration, moving beyond simple text generation. The ‘high-performance agent runtime’ goal for the [ZeroClaw] project in the first half of 2026 will be realized by integrating LLM inference logic on this solid foundation.
The next post will delve into the implementation details of a ‘Planner’ agent that uses tools by calling an LLM over this communication protocol.
References
- [ZeroClaw] Multi-Agent Architecture Design Proposal
- [Claude Code] Team Agent Communication Architecture
- Rust Tokio Documentation