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
| Feature | Description |
|---|---|
io-stdlib | Synchronous stdin/stdout transport |
io-tokio | Async stdin/stdout transport (Tokio) |
io-axum | HTTP transport (Axum) with session management |
jiff | Rfc3339 timestamp type using jiff (mutually exclusive with chrono) |
chrono | Rfc3339 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
ToolRegistryimplementation.
Structs§
- Client
- The connected MCP client.
- McpServer
- IO-less MCP server state machine.
- McpServer
Builder - Builder for constructing an
McpServer. - Outgoing
Message - Outgoing message that must be sent to the client.
- Parse
Error - Error returned when parsing a line fails.
- Responder
- Response builder for tool calls.
- Rfc3339
jifforchrono - RFC 3339 timestamp for MCP tool inputs.
- Tool
Definition - MCP tool definition for
tools/listresponses. - Tool
Definitions - Collection of tool definitions returned by
ToolRegistry::definitions. - Tool
Output - Successful output from a tool invocation.
- With
Source - Wrapper that formats the full error chain for tool responses.
Enums§
- Json
RpcError - Request-level JSON-RPC errors.
- NoTools
- Empty tool registry for servers that don’t expose tools.
- Output
- Output from handling a message.
- Protocol
Error - Protocol-level errors that terminate the connection.
Traits§
- Into
Tool Response - Converts a value into a tool response (
CallToolResult). - ToolDef
- Defines a tool’s input type and metadata.
- Tool
Registry - Registry of available tools.
Functions§
- parse_
line - Parses a line of input into a JSON-RPC message.