Building an MCP Server with Rust: Utilizing the ZeroClaw Architecture

Building an MCP Server with Rust: Utilizing the ZeroClaw Architecture

Recently, Model Context Protocol (MCP) has become a standard interface in the AI agent ecosystem. When building an MCP server in high-performance runtime environments like our team’s ZeroClaw project, Rust’s safety and speed are essential. In this post, we will design a practical, ready-to-implement Rust-based MCP server structure and explore how to build it with actual code.

1. Core Requirements of an MCP Server

To allow AI models to access data, an MCP server must perform the following three core functions:

  1. Resource Discovery: Provide a list of data (files, APIs, etc.) that the model can use.
  2. Resource Access: Efficiently return the actual content of requested data.
  3. Tool Execution: Perform actions within the system based on the model’s instructions.

Rust is ideal for MCP servers handling large-scale requests, especially due to its Zero-cost abstractions and robust asynchronous runtime (tokio).

2. Architecture Design: ZeroClaw Style

Beyond simple RPC calls, we adopt an event-driven architecture for scalability. The structure is as follows:

  • Transport Layer: Sending and receiving messages via stdio or WebSocket (JSON-RPC 2.0)
  • Handler Layer: Layer for routing and validating requests
  • Core Logic: Actual business logic (file system access, DB queries, etc.)

3. Practical Code Examples

We will write a simple MCP server based on stdio (standard input/output), the most common transport. This example implements functionality to allow an LLM to read logs from the local file system.

3.1. Dependency Setup (Cargo.toml)

We add crates for serialization and asynchronous processing to leverage Rust’s ecosystem.

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
async-trait = "0.1"

3.2. Handler Trait Definition

Following the MCP standard, we define a common interface for handling Tool and Resource. This makes it easier to integrate with the ZeroClaw runtime later.

use serde_json::Value;
use async_trait::async_trait;

#[async_trait]
pub trait McpHandler {
    async fn handle(&self, params: Value) -> Result<Value, String>;
    fn name(&self) -> &str;
}

3.3. Implementing the Log Reading Tool

Now, we implement the logic to actually read files within the server and provide them to the AI model.

use std::fs;
use std::path::Path;

pub struct LogReaderTool;

#[async_trait]
impl McpHandler for LogReaderTool {
    fn name(&self) -> &str {
        "read_logs"
    }

    async fn handle(&self, params: Value) -> Result<Value, String> {
        // 1. Parameter validation and extraction
        let file_path = params.get("path")
            .and_then(|v| v.as_str())
            .ok_or("Missing 'path' parameter".to_string())?;

        // 2. Safeguard: Prevent path traversal (Sandboxing)
        // In a production environment, chroot or a dedicated virtual directory should be used.
        if !file_path.ends_with(".log") {
            return Err("Only .log files are allowed".into());
        }

        // 3. File system access (wrapping synchronous I/O in an asynchronous context)
        let content = fs::read_to_string(file_path)
            .map_err(|e| format!("Failed to read file: {}", e))?;

        // 4. Result formatting
        Ok(serde_json::json!({
            "status": "success",
            "content": content,
            "lines": content.lines().count()
        }))
    }
}

3.4. Main Loop and JSON-RPC Server

This is the main loop responsible for communication with the LLM. It reads JSON-RPC requests from standard input and outputs the processing results to standard output.

use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::io::{AsyncWriteExt, BufWriter};
use std::sync::Arc;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let stdin = tokio::io::stdin();
    let stdout = tokio::io::stdout();
    let mut reader = BufReader::new(stdin).lines();
    let mut writer = BufWriter::new(stdout);

    let tool = Arc::new(LogReaderTool);

    // Process requests as they arrive from the LLM or host
    while let Ok(Some(line)) = reader.next_line().await {
        if line.trim().is_empty() { continue; }

        if let Ok(json_req) = serde_json::from_str::<Value>(&line) {
            let method = json_req.get("method").and_then(|m| m.as_str()).unwrap_or("");
            let id = json_req.get("id");

            if method == "tools/call" || method == "tools/invoke" {
                let params = json_req.get("params").cloned().unwrap_or(Value::Null);
                
                // Execute business logic
                let result = tool.handle(params).await;
                
                let response = match result {
                    Ok(data) => serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": id,
                        "result": data
                    }),
                    Err(e) => serde_json::json!({
                        "jsonrpc": "2.0",
                        "id": id,
                        "error": { "code": -32000, "message": e }
                    })
                };
                
                writer.write_all(response.to_string().as_bytes()).await?;
                writer.write_all(b"\n").await?;
                writer.flush().await?;
            }
        }
    }
    Ok(())
}

4. Optimization and Deployment Considerations

The code above provides a basic framework, but for production environments like ZeroClaw, consider the following:

  1. Concrete Error Types: Use specific thiserror or anyhow-based error types instead of String to facilitate debugging.
  2. Security Sandboxing: Use a virtual file system (VFS) library instead of std::fs to prevent agents from accessing the entire system.
  3. Graceful Shutdown: Utilize tokio’s signal module to handle interrupt signals (SIGTERM) and safely terminate ongoing file operations.

5. Conclusion

Rust is the ideal language for building reliable MCP servers due to its type safety and memory management capabilities. Based on the code above, by integrating with a blog-api-server or monitoring system, you can build a fully automated cycle where AI models directly analyze system logs and suggest patches.

In the next post, we will cover how to agentize this MCP server on the ZeroClaw runtime and communicate with other services.

Built with Hugo
Theme Stack designed by Jimmy