Menu

AI

How to Build an MCP Server in Rust with rmcp (2026)

Build an MCP server in Rust with the official rmcp 3.8.0 crate. Use tool macros, serve stdio or stateless Streamable HTTP with axum, and test with MCP Inspector.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server in Rust with rmcp (2026)

This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.

You build an MCP server in Rust with rmcp, the official Rust SDK from the modelcontextprotocol organization. You write a struct, mark its methods with the #[tool] macro, and serve it over stdio or Streamable HTTP on the tokio async runtime. The macros generate the tool list and JSON Schema from your parameter types.

This guide covers install, a minimal server, adding a tool with proper error handling, testing with MCP Inspector, HTTP hosting with axum and the version traps that break older tutorials. It assumes you know Rust and async/await.

Code here was checked against the rmcp 3.8.0 README, crate metadata and examples on 2 November 2026. eSEOspace did not compile or run it, because no Rust toolchain was available for this article. Run cargo build and the Inspector checks below before you rely on it.

Key Takeaways

  • The official crate is rmcp (with rmcp-macros); as of November 2026 the latest release is 3.8.0, published 26 October 2026.
  • rmcp 3.x needs Rust 1.88 or later, per its crates.io metadata.
  • Default features are base64, macros and server; add transport-io for stdio and transport-streamable-http-server for HTTP.
  • #[tool_router(server_handler)] turns a plain impl block into a tools-only server with no extra handler code.
  • Return CallToolResult::error(...) for failures the user should see; return Err(McpError) only for protocol-level problems.
  • The official build-a-server tutorial's Rust tab still pins rmcp 0.3, so do not copy its Cargo.toml version.

Prerequisites

  • Rust 1.88 or newer. The crates.io record for rmcp lists rust_version 1.88 for 3.8.0, and the 3.x migration guide says the minimum supported Rust version is now 1.88.
  • Node.js to run MCP Inspector through npx.
  • An MCP host to try the finished server, such as Claude Desktop or Claude Code.

New to MCP? Read how to build your first MCP server for the concepts. This page covers only what is specific to Rust.

Which crate and version

The Rust SDK README describes rmcp as "an official Rust Model Context Protocol SDK implementation with tokio async runtime." The repo ships two crates: rmcp for the protocol and rmcp-macros for the tool and prompt macros.

The README says the SDK "implements the stable MCP 2026-07-28 specification while remaining fully compatible with the 2025-11-25 release and earlier versions." The official SDK page lists Rust as Tier 1 as of November 2026. See our official SDKs overview for what that tier promises.

On 2 November 2026 crates.io showed 3.8.0 as the latest stable version, licensed Apache-2.0.

Install

Create a project and add dependencies. The README's install command is cargo add rmcp --features server. For a stdio server you also need the transport-io feature, per the README's transport table:

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

[dependencies]
rmcp = { version = "3.8", features = ["server", "macros", "transport-io"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] }
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }

The tokio and tracing lines mirror the repo's example servers Cargo.toml. The rmcp Cargo.toml sets default = ["base64", "macros", "server"], so listing server and macros is explicit rather than required.

A minimal working server

This is the README's tools-only example. #[tool_router(server_handler)] generates both the tool router and the ServerHandler implementation for you:

use rmcp::{handler::server::wrapper::Parameters, schemars, tool, tool_router, ServiceExt, transport::stdio};

#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct AddParams {
    a: i32,
    b: i32,
}

#[derive(Clone)]
struct Calculator;

#[tool_router(server_handler)]
impl Calculator {
    #[tool(description = "Add two numbers")]
    fn add(&self, Parameters(AddParams { a, b }): Parameters<AddParams>) -> String {
        (a + b).to_string()
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let service = Calculator.serve(stdio()).await?;
    service.waiting().await?;
    Ok(())
}

What each part does:

  • AddParams derives Deserialize for incoming arguments and JsonSchema so the macro can publish an input schema.
  • Parameters<T> is the wrapper that tells rmcp to deserialize the tool arguments into T.
  • #[tool(description = ...)] registers the method as a tool. The method name becomes the tool name.
  • serve(stdio()) connects the service to stdin and stdout, and waiting() keeps it running until the client disconnects.

The README adds a detail worth knowing: the generated schema uses only field names, field types and field documentation. The type name and doc comment on the struct itself are ignored, so put your descriptions on the fields.

Add a tool with real error handling

When you need a server name, version and instructions, switch to the explicit #[tool_handler] form from the README. This version adds a stock lookup and uses rmcp's two error paths:

use rmcp::{
    handler::server::wrapper::Parameters,
    model::{CallToolResult, ContentBlock},
    schemars, tool, tool_handler, tool_router, ErrorData as McpError, ServerHandler,
};

#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct StockArgs {
    /// The product SKU, for example TSHIRT-M
    sku: String,
}

#[derive(Clone)]
struct Inventory;

#[tool_router]
impl Inventory {
    #[tool(description = "Return the number of units in stock for one product SKU")]
    async fn check_stock(&self, Parameters(args): Parameters<StockArgs>) -> Result<CallToolResult, McpError> {
        if args.sku.is_empty() {
            return Err(McpError::invalid_params("sku must be non-empty", None));
        }
        let qty = match args.sku.as_str() {
            "TSHIRT-M" => 42,
            "MUG" => 17,
            _ => {
                return Ok(CallToolResult::error(vec![ContentBlock::text(
                    format!("unknown SKU '{}'", args.sku),
                )]));
            }
        };
        Ok(CallToolResult::success(vec![ContentBlock::text(format!("{qty} in stock"))]))
    }
}

#[tool_handler(name = "inventory", version = "1.0.0", instructions = "Look up product stock levels")]
impl ServerHandler for Inventory {}

The README explains when to use which path. Ok(CallToolResult::error(...)) is for a tool that ran but failed, and "the client renders your content, so the message reaches the user." Err(McpError) is for requests the server cannot process at all, and "clients render these opaquely." Use the first for almost everything.

Tool names and descriptions are what the model reads when it chooses a tool, so keep them specific.

Log to stderr, not stdout

A stdio server uses stdout for JSON-RPC messages. The official build-a-server tutorial says never use println!() or print!() in one, and suggests eprintln!() or a logging library such as tracing.

The repo's counter_stdio example sends tracing output to stderr like this:

tracing_subscriber::fmt()
    .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
    .with_writer(std::io::stderr)
    .with_ansi(false)
    .init();

Put that at the top of main, with use tracing_subscriber::EnvFilter; among your imports.

Test with MCP Inspector

Build a release binary first so the Inspector does not time out waiting for cargo to compile:

cargo build --release
npx @modelcontextprotocol/inspector --cli ./target/release/inventory-mcp --method tools/list
npx @modelcontextprotocol/inspector --cli ./target/release/inventory-mcp \
  --method tools/call --tool-name check_stock --tool-arg sku=MUG

These flags come from the Inspector CLI README. Add --protocol-era legacy and then --protocol-era modern to confirm your server handles both the old handshake and the 2026-07-28 model. Drop --cli to open the web UI. Our Inspector lesson covers the UI.

Connect it to a host

The official tutorial's Rust tab points Claude Desktop at the release binary with an absolute path:

{
  "mcpServers": {
    "inventory": {
      "command": "/ABSOLUTE/PATH/TO/inventory-mcp/target/release/inventory-mcp"
    }
  }
}

On Windows, use the .exe path with escaped backslashes. Quit Claude Desktop fully and reopen it after editing. The MCP config generator produces the same entry for Cursor, VS Code and other hosts.

Serve over Streamable HTTP

For a remote server, enable the transport-streamable-http-server feature and add axum. The README's stateless example mounts rmcp's Tower service on an axum router:

use rmcp::transport::streamable_http_server::{
    StreamableHttpService, StreamableHttpServerConfig,
    session::local::LocalSessionManager,
};

let config = StreamableHttpServerConfig::default()
    .with_legacy_session_mode(false)
    .with_json_response(true);

let service = StreamableHttpService::new(
    || Ok(Inventory),
    LocalSessionManager::default().into(),
    config,
);

let router = axum::Router::new().nest_service("/mcp", service);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8000").await?;
axum::serve(listener, router).await?;

Per the README, rmcp serves 2026-07-28 clients statelessly "automatically." The with_legacy_session_mode(false) call also makes older clients stateless. Because there is no per-session state, the factory closure "runs per request," so keep database pools and caches in a cloneable handle captured by the closure.

Add OAuth before you expose the endpoint publicly; the README has an OAuth section and an auth feature.

Common pitfalls

  • Copying rmcp 0.3 from the official tutorial. On 2 November 2026 the Rust tab of the build-a-server tutorial still listed rmcp = { version = "0.3", ... }. The current line is 3.x, with many renamed types.
  • Old type names. The 3.x migration guide lists renames such as Error to ErrorData and with_stateful_mode to with_legacy_session_mode. Manual ServerHandler implementations also need new return types; macro users mostly do not.
  • Expecting the legacy SSE transport. The README calls the 2024-11-05 HTTP+SSE transport "a deliberate non-goal." Use Streamable HTTP.
  • Forgetting transport-io. Without it, rmcp::transport::stdio is not available.
  • Struct-level docs. Doc comments on the parameter struct are ignored; document fields.
  • Printing to stdout. One stray println! corrupts a stdio session.

Next step

Compare this with the Go guide or the C# and .NET guide, or follow the free MCP course from the start. If you need a production Rust server built and secured for your team, talk to eSEOspace about MCP development.

More in the MCP Guide

Free tools: server.json Validator · MCP Error Code Lookup

Latest research: State of MCP: October 2026

Frequently asked questions

What is rmcp?

rmcp is the official Rust SDK for the Model Context Protocol, maintained in the modelcontextprotocol GitHub organization. It runs on tokio and comes with a companion macros crate, rmcp-macros.

What Rust version does rmcp need?

rmcp 3.8.0 lists Rust 1.88 as its minimum version on crates.io. The 3.x migration guide gives the same number.

Does rmcp support the 2026-07-28 spec?

Yes. The README says it implements the stable 2026-07-28 specification and stays compatible with 2025-11-25 and earlier versions.

Should my tool return Err or CallToolResult::error?

Return CallToolResult::error when the tool ran and failed in a way the user should see. Return Err(McpError) only when the request itself is unusable, because clients show protocol errors opaquely.

Can rmcp build MCP clients too?

Yes. Enable the client feature, plus transport-child-process to launch a server or transport-streamable-http-client-reqwest to reach a remote one, per the README's transport table.

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.