Designing and Implementing a Communication Protocol for MCP-based Multi-Agent Systems in Rust

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:

  1. Asynchrony: If Agent A delegates a task to Agent B and Agent A stops execution until B completes, the overall system throughput decreases.
  2. Event-Driven Interaction: Some agents need to monitor system state changes (e.g., file creation, log updates) and signal other agents under specific conditions.
  3. 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
Built with Hugo
Theme Stack designed by Jimmy