Minimal mcp server using rmcp 0.3

rmcp was updated July 16 2025 – this article looks as how to use the latest custom procedural macro attributes and build a minimalist example:

#[tool_router]

#[tool_handler]

Note : They replace #[tool(tool_box)] from earlier versions
Build using the official Rust SDK for the Model

Let’s build using the official Rust SDK for the Model Context Protocol


[package]
name = "mcp-calculator-2"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0.98"
rmcp = { version = "0.3.0", features = ["server", "transport-io", "transport-sse-server", "transport-streamable-http-server"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
tokio = { version = "1.46.1", features = ["full"] }
tracing = "0.1.41"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
/// main.rs 
/// ProtocolVersion::V_2025_03_26
/// rmcp = { version = "0.3.0", features = ["server", "transport-io", "transport-sse-server", "transport-streamable-http-server"] }

use rmcp::{
    ServerHandler, ServiceExt,
    handler::server::router::tool::ToolRouter,
    model::{ProtocolVersion, ServerCapabilities, ServerInfo}, tool, tool_handler, tool_router,
    transport::stdio,
};
use std::sync::Arc;
use tokio::sync::Mutex;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let service = HelloWorld::new()
        .serve(stdio())
        .await
        .inspect_err(|e| eprintln!("{e}"))?;
    service.waiting().await?;
    Ok(())
}

#[derive(Debug, Clone)]
pub struct HelloWorld {
    counter: Arc<Mutex<i32>>,
    tool_router: ToolRouter<Self>,
}

/// not #[tool(tool_box)] !! as previously used
#[tool_router]
impl HelloWorld {
    pub fn new() -> Self {
        Self {
            counter: Arc::new(Mutex::new(0)),
            tool_router: Self::tool_router(),
        }
    }

    #[tool(description = "Increment the counter")]
    async fn increment(&self) -> String {
        let mut counter = self.counter.lock().await;
        *counter += 1;
        counter.to_string()
    }

    #[tool(description = "Get the current counter value")]
    async fn get_value(&self) -> String {
        let counter = self.counter.lock().await;
        counter.to_string()
    }

    #[tool(description = "Say Something")]
    fn echo(&self) -> String {
        "hello from your first MCP server".to_string()
    }
}

/// not #[tool(tool_box)] !! as previously used
#[tool_handler]
impl ServerHandler for HelloWorld {
    fn get_info(&self) -> ServerInfo {
        ServerInfo {
            protocol_version: ProtocolVersion::V_2025_03_26,
            instructions: Some(
                "This server provides a counter tool that can increment and get current value"
                    .to_string(),
            ),
            capabilities: ServerCapabilities::builder().enable_tools().build(),
            ..Default::default()
        }
    }
}
inspector

Why tool_router: ToolRouter<Self>??