Skip to main content

Crate mercutio

Crate mercutio 

Source
Expand description

§mercutio

A Rust library for building MCP servers. In MCP, clients are LLM host applications (IDEs, chat interfaces) that connect to servers to give models access to tools. mercutio handles the server-side protocol (parsing messages, managing the initialization handshake, dispatching tool calls), while you handle the transport. The core is a pure state machine: feed it JSON-RPC messages, and it returns what to send back.

This sans-io design means you can run it over stdio, HTTP, WebSockets, or anything else without fighting the library.

§Defining Tools

Use tool_registry! to define your tools. Field doc comments become JSON Schema descriptions that the LLM sees:

mercutio::tool_registry! {
    enum MyTools {
        GetWeather("get_weather", "Gets current weather for a city") {
            /// City name, e.g. "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch".
            city: String,
        },
        SetReminder("set_reminder", "Sets a reminder") {
            /// What to remind about.
            message: String,
            /// When to trigger the reminder.
            at: mercutio::Rfc3339,
            /// Minutes to wait before reminding again.
            snooze_minutes: u32,
        },
    }
}

Rfc3339 requires either the jiff or chrono feature. It emits format: "date-time" in JSON Schema, and deserialization errors include the current time as an example to help models self-correct.

§Sans-IO Usage

The core API is a state machine. Pass in parsed messages, match on the output:

use mercutio::{McpServer, Output};

let mut server = McpServer::<MyTools>::builder()
    .name("my-server")
    .version("1.0")
    .build();

loop {
    let line = read_line_somehow();
    let msg = mercutio::parse_line(&line)?;

    match server.handle(msg) {
        Output::Send(response) => send(response.into_inner()),
        Output::ToolCall { tool, responder } => {
            let result = match tool {
                MyTools::GetWeather(input) => format!("Weather in {}: sunny", input.city),
                MyTools::SetReminder(input) => format!("Reminder set: {}", input.message),
            };
            send(responder.respond(Ok::<_, std::convert::Infallible>(result)).into_inner());
        }
        Output::ProtocolError(_) => break,
        Output::None => {}
    }
}

§Transports

If you’d rather not wire up I/O yourself, the io-* feature flags provide ready-made transports. These use handler traits to process tool calls:

use mercutio::{ToolOutput, io::{McpSessionId, ToolHandler}};

struct MyHandler;

impl ToolHandler<MyTools> for MyHandler {
    type Error = std::convert::Infallible;

    async fn handle(
        &self,
        _session_id: Option<McpSessionId>,
        tool: MyTools,
    ) -> Result<ToolOutput, Self::Error> {
        match tool {
            MyTools::GetWeather(input) => {
                Ok(format!("Weather in {}: sunny", input.city).into())
            }
            MyTools::SetReminder(input) => {
                Ok(format!("Reminder set: {}", input.message).into())
            }
        }
    }
}

ToolHandler takes &self for concurrent contexts; MutToolHandler takes &mut self for exclusive access. The session ID is Some for HTTP (multiple clients share one server), None for stdio (one process = one session). Closures work via blanket impl: |_session_id, tool| async move { ... }.

§io-tokio

Async stdin/stdout using Tokio:

let server = McpServer::<MyTools>::builder().name("my-server").version("1.0").build();
mercutio::io::tokio::run_stdio(server, MyHandler).await?;

§io-stdlib

Synchronous stdin/stdout (no async runtime):

let server = McpServer::<MyTools>::builder().name("my-server").version("1.0").build();
mercutio::io::stdlib::run_stdio(server, |_session_id, tool| handle_tool(tool))?;

§io-axum

HTTP transport with session management:

let mut builder = McpServer::<MyTools>::builder();
builder.name("my-server").version("1.0");

let router = mercutio::io::axum::mcp_router(builder, MyHandler);
let app = axum::Router::new().nest("/mcp", router);

For custom session storage, use McpRouter::builder() with .storage().

§Testing

To test your handler, construct it with test fixtures and call handle directly:

use mercutio::{ToolOutput, ToolRegistry, io::ToolHandler};

mercutio::tool_registry! {
    enum Tools {
        Greet("greet", "Greets someone") {
            name: String,
        },
    }
}

struct Handler;

impl ToolHandler<Tools> for Handler {
    type Error = std::convert::Infallible;

    async fn handle(&self, _: Option<mercutio::io::McpSessionId>, tool: Tools) -> Result<ToolOutput, Self::Error> {
        match tool {
            Tools::Greet(g) => Ok(format!("Hello, {}!", g.name).into()),
        }
    }
}

let handler = Handler;
let tool = Tools::Greet(Greet { name: "Alice".into() });

let output = handler.handle(None, tool).await.expect("handler failed");
assert_eq!(output.as_text(), Some("Hello, Alice!"));

// Tool outputs are text blocks that can grow large; insta snapshots help manage them:
insta::assert_snapshot!(output, @"Hello, Alice!");

To test that invalid inputs produce useful error messages, use ToolRegistry::parse:

use mercutio::ToolRegistry;

mercutio::tool_registry! {
    enum Tools {
        Greet("greet", "Greets someone") { name: String },
    }
}

let err = Tools::parse("greet", serde_json::json!({})).err().expect("should fail");
assert!(err.to_string().contains("name"));

§Example

A complete server supporting both transports:

use clap::{Parser, Subcommand};
use mercutio::{McpServer, ToolOutput, io::{McpSessionId, ToolHandler}};

mercutio::tool_registry! {
    enum MyTools {
        Greet("greet", "Greets someone") { name: String },
    }
}

struct MyHandler;

impl ToolHandler<MyTools> for MyHandler {
    type Error = std::convert::Infallible;

    async fn handle(&self, _: Option<McpSessionId>, tool: MyTools) -> Result<ToolOutput, Self::Error> {
        match tool {
            MyTools::Greet(input) => Ok(format!("Hello, {}!", input.name).into()),
        }
    }
}

#[derive(Parser)]
struct Args {
    #[command(subcommand)]
    command: Command,
}

#[derive(Subcommand)]
enum Command {
    Mcp,
    McpHttp { bind: std::net::SocketAddr },
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let mut builder = McpServer::<MyTools>::builder();
    builder.name("greeter").version("1.0");

    match args.command {
        Command::Mcp => {
            mercutio::io::tokio::run_stdio(builder.build(), MyHandler).await?;
        }
        Command::McpHttp { bind } => {
            let router = mercutio::io::axum::mcp_router(builder, MyHandler);
            let listener = tokio::net::TcpListener::bind(bind).await?;
            axum::serve(listener, router).await?;
        }
    }
    Ok(())
}

§Feature Flags

FeatureDescription
io-stdlibSynchronous stdin/stdout transport
io-tokioAsync stdin/stdout transport (Tokio)
io-axumHTTP transport (Axum) with session management
jiffRfc3339 timestamp type using jiff (mutually exclusive with chrono)
chronoRfc3339 timestamp type using chrono (mutually exclusive with jiff)

Re-exports§

pub use rust_mcp_schema;

Modules§

io
I/O transports for MCP servers.

Macros§

tool_registry
Generates tool input structs, a dispatch enum, and ToolRegistry implementation.

Structs§

Client
The connected MCP client.
McpServer
IO-less MCP server state machine.
McpServerBuilder
Builder for constructing an McpServer.
OutgoingMessage
Outgoing message that must be sent to the client.
ParseError
Error returned when parsing a line fails.
Responder
Response builder for tool calls.
Rfc3339jiff or chrono
RFC 3339 timestamp for MCP tool inputs.
ToolDefinition
MCP tool definition for tools/list responses.
ToolDefinitions
Collection of tool definitions returned by ToolRegistry::definitions.
ToolOutput
Successful output from a tool invocation.
WithSource
Wrapper that formats the full error chain for tool responses.

Enums§

JsonRpcError
Request-level JSON-RPC errors.
NoTools
Empty tool registry for servers that don’t expose tools.
Output
Output from handling a message.
ProtocolError
Protocol-level errors that terminate the connection.

Traits§

IntoToolResponse
Converts a value into a tool response (CallToolResult).
ToolDef
Defines a tool’s input type and metadata.
ToolRegistry
Registry of available tools.

Functions§

parse_line
Parses a line of input into a JSON-RPC message.