Building a Lightweight MCP Server with Rust and ZeroClaw: An Efficient Agent Communication Structure

Building a Lightweight MCP Server with Rust and ZeroClaw: An Efficient Agent Communication Structure

As evident in the recent article “Grit: Rewriting Git in Rust with Agents,” the Rust language is being re-evaluated for its value as a runtime for agent systems. Our team’s ZeroClaw runtime is also under development with high performance and safety as its goals, and with this in mind, we are increasingly focusing on how to efficiently build an MCP (Model Context Protocol) server.

Instead of traditional heavy Python-based servers or complex Node.js architectures, we propose a lightweight MCP server structure that leverages Rust’s concurrency and low-latency capabilities. In this article, we will write a Rust-based MCP server example that can be immediately applied in practice, while capitalizing on the advantages of the ZeroClaw architecture.

1. Combining ZeroClaw Architecture and MCP

The core of the ZeroClaw project lies in its file-based architectural design and the efficient implementation of its multi-agent communication protocol. The most crucial aspects when building an MCP server are reducing the overhead of data serialization/deserialization (Serde) and asynchronous request handling.

By utilizing Rust’s tokio runtime and serde_json, we can handle JSON-based MCP protocols with an overhead in the nanosecond range. This is a critical element, especially for large-scale log processing or monitoring systems (as experienced with [blog-api-server]).

2. Core Structure: Asynchronous Request Routing

We will build the skeleton of an MCP server using only the standard library and a few essential crates, without complex dependencies. This structure follows ZeroClaw’s agent-to-agent communication pattern and ensures safety in a multi-threaded environment, not just a single-threaded one.

2.1. Cargo.toml Configuration

First, add project dependencies.

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hyper = { version = "0.14", features = ["full"] }

2.2. Defining MCP Message Structure

MCP fundamentally has a structure similar to JSON-RPC. We actively utilize Rust’s type system to ensure data integrity during agent-to-agent communication.

// src/types.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct MCPRequest {
    pub jsonrpc: String, // Always "2.0"
    pub id: Option<String>,
    pub method: String,
    pub params: Option<serde_json::Value>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct MCPResponse {
    pub jsonrpc: String,
    pub id: Option<String>,
    pub result: Option<serde_json::Value>,
    pub error: Option<MCPError>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct MCPError {
    pub code: i32,
    pub message: String,
}

3. Implementing a Lightweight Request Handler

In line with ZeroClaw’s philosophy, we will remove unnecessary abstraction layers and write intuitive handlers. The following code demonstrates the flow of parsing incoming requests, performing appropriate processing based on the method name, and returning a response.

// src/main.rs
use hyper::{Body, Request, Response, Server, StatusCode};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
use types::{MCPRequest, MCPResponse};

async fn handle_mcp_request(req: MCPRequest) -> MCPResponse {
    match req.method.as_str() {
        "tools/list" => {
            // Example of returning a list of tools
            MCPResponse {
                jsonrpc: "2.0".to_string(),
                id: req.id,
                result: Some(serde_json::json!({
                    "tools": [
                        { "name": "get_weather", "description": "Get current weather" },
                        { "name": "calculate", "description": "Perform math operations" }
                    ]
                })),
                error: None,
            }
        },
        "tools/call" => {
            // Actual tool execution logic (calling ZeroClaw Agent, etc.)
            // Parameter validation logic goes here.
            println!("Agent executing with params: {:?}", req.params);
            MCPResponse {
                jsonrpc: "2.0".to_string(),
                id: req.id,
                result: Some(serde_json::json!({"status": "success"})),
                error: None,
            }
        },
        _ => {
            MCPResponse {
                jsonrpc: "2.0".to_string(),
                id: req.id,
                result: None,
                error: Some(types::MCPError {
                    code: -32601,
                    message: "Method not found".to_string(),
                }),
            }
        }
    }
}

async fn http_handler(req: Request<Body>) -> Result<Response<Body>, Infallible> {
    let whole_body = match hyper::body::to_bytes(req.into_body()).await {
        Ok(bytes) => bytes,
        Err(e) => {
            eprintln!("Error reading body: {}", e);
            return Ok(Response::builder()
                .status(StatusCode::INTERNAL_SERVER_ERROR)
                .body(Body::from("Internal Server Error"))
                .unwrap());
        }
    };

    // JSON Parsing
    let mcp_req: MCPRequest = match serde_json::from_slice(&whole_body) {
        Ok(req) => req,
        Err(_) => {
            return Ok(Response::builder()
                .status(StatusCode::BAD_REQUEST)
                .body(Body::from("Invalid JSON"))
                .unwrap());
        }
    };

    // MCP Logic Processing
    let mcp_res = handle_mcp_request(mcp_req).await;
    
    // Response Conversion
    let json_body = match serde_json::to_string(&mcp_res) {
        Ok(json) => json,
        Err(_) => "{}".to_string(),
    };

    Ok(Response::new(Body::from(json_body)))
}

#[tokio::main]
async fn main() {
    let make_svc = make_service_fn(|_conn| {
        async { Ok::<_, Infallible>(service_fn(http_handler)) }
    });

    let addr = ([127, 0 0, 1], 3000).into();
    let server = Server::bind(&addr).serve(make_svc);

    println!("MCP Server listening on http://{}", addr);

    if let Err(e) = server.await {
        eprintln!("Server error: {}", e);
    }
}

4. Advanced Optimization: ZeroClaw Integration

The code above is a basic HTTP server. To unleash the true power of ZeroClaw, this server needs to be integrated as a task within the agent runtime.

  • Channel-based Communication: Instead of directly handling HTTP requests, send requests through a tokio::sync::mpsc channel to the internal agent queue. This aligns with ZeroClaw’s asynchronous message passing structure.
  • Stealable Work Queue: Implement a work queue to allow agents to efficiently distribute tasks in a multi-core environment.

5. Conclusion

By writing an MCP server in Rust, you can fully leverage the benefits of high-performance runtimes like ZeroClaw. The key is not just changing the language, but designing an architecture that minimizes overhead in agent-to-agent communication. We encourage you to build upon the code provided and add your project-specific agent logic.

Note: This code focuses on practicality by simplifying error handling and logging, based on the experience gained from improving logging in blog-api-server.

Built with Hugo
Theme Stack designed by Jimmy