### Example: Running an MCP server over stdio with Tokio Source: https://docs.rs/mercutio/latest/mercutio/io/tokio/index.html This example demonstrates how to set up and run an MCP server using the `run_stdio` function with a custom tool handler. It defines a tool registry, a handler that increments a request counter, and a main function that builds the server and runs it. The example requires the `io-tokio` feature and uses `#[tokio::main]`. ```rust use std::convert::Infallible; use mercutio::{McpServer, ToolOutput, io::tokio::{run_stdio, MutToolHandler}}; mercutio::tool_registry! { enum MyTools { GetWeather("get_weather", "Gets weather") { city: String }, } } struct Handler { request_count: u32, } impl MutToolHandler for Handler { type Error = Infallible; async fn handle( &mut self, _session_id: Option, tool: MyTools, ) -> Result { self.request_count += 1; match tool { MyTools::GetWeather(input) => { Ok(format!("Weather in {}: sunny", input.city).into()) } } } } #[tokio::main] async fn main() -> Result<(), mercutio::io::tokio::IoError> { let server = McpServer::::builder() .name("my-server") .version("1.0.0") .build(); run_stdio(server, Handler { request_count: 0 }).await } ``` -------------------------------- ### Run MCP server over stdin/stdout with run_stdio Source: https://docs.rs/mercutio/latest/mercutio/io/stdlib/index.html Example of using run_stdio to start an MCP server over standard input/output. Requires the io-stdlib feature. The server is built with McpServer and handles tool calls, returning a string result. ```rust use std::convert::Infallible; use mercutio::{McpServer, io::stdlib::run_stdio}; mercutio::tool_registry! { enum MyTools { GetWeather("get_weather", "Gets weather") { city: String }, } } fn main() -> Result<(), mercutio::io::stdlib::IoError> { let server = McpServer::::builder() .name("my-server") .version("1.0.0") .build(); run_stdio(server, |_session_id, tool| -> Result { match tool { MyTools::GetWeather(input) => { Ok(format!("Weather in {}: sunny", input.city)) } } }) } ``` -------------------------------- ### Example: Using mcp_router with axum Source: https://docs.rs/mercutio/latest/mercutio/io/axum/index.html Shows how to create an MCP router using the mcp_router function and mount it in an axum application. The example defines a tool registry, configures the server, and handles a tool invocation. The router is then nested under the /mcp path. ```rust use std::convert::Infallible; use mercutio::{McpServer, io::axum::mcp_router}; mercutio::tool_registry! { enum MyTools { GetWeather("get_weather", "Gets weather") { city: String }, } } let mut builder = McpServer::::builder(); builder.name("my-server").version("1.0.0"); let router = mcp_router(builder, |_session_id, tool: MyTools| async move { match tool { MyTools::GetWeather(input) => { Ok::<_, Infallible>(format!("Weather in {}: sunny", input.city)) } } }); // Mount at your desired path let app = axum::Router::new().nest("/mcp", router); ``` -------------------------------- ### Example usage of Responder methods Source: https://docs.rs/mercutio/latest/mercutio/struct.Responder.html Shows how to use `respond` with direct values, `Result`, and `ToolOutput`, and how to use `rpc_error` for protocol-level errors. ```rust // Direct value responder.respond("Success!") // Result - Ok becomes success, Err becomes tool error responder.respond(std::fs::read_to_string(&path)) // With ToolOutput builder responder.respond(ToolOutput::json(&my_data)) // Rare: protocol-level error responder.rpc_error(JsonRpcError::InternalError { msg: "..." }) ``` -------------------------------- ### Usage Example Source: https://docs.rs/mercutio/latest/mercutio/struct.WithSource.html Shows how to use WithSource to wrap an error from an API call and get enriched error messages. ```APIDOC ## Usage Example ### Description Wrap an error with `WithSource` to include the underlying error source in the error message. ### Function `async fn handle_tool(tool: MyTools) -> Result>` ### Example ```rust use mercutio::WithSource; async fn handle_tool(tool: MyTools) -> Result> { let data = fetch_api().map_err(WithSource)?; Ok(format!("Got: {data}")) } ``` ### Behavior Without `WithSource`, an API failure might show: `"API request failed"` With `WithSource`, it shows: `"API request failed: HTTP error: 403 Forbidden"` ### Tuple Fields - `0: E` - The wrapped error. ``` -------------------------------- ### Test Server Initialization and Ping Source: https://docs.rs/mercutio/latest/src/mercutio/lib.rs.html Utility functions and tests for verifying server setup and ping handling during initialization. ```rust fn test_server() -> McpServer { McpServer::::builder() .name("test") .version("1.0") .build() } fn initialize_params() -> serde_json::Map { serde_json::from_str( r#"{ "protocolVersion": "2025-11-25", "capabilities": {}, "clientInfo": { "name": "test-client", "version": "1.0" } }"#, ) .expect("valid JSON") } #[test] fn ping_during_init() { let mut server = test_server(); let ping = JsonrpcMessage::Request(JsonrpcRequest::new( RequestId::String("1".into()), "ping".into(), None, )); let output = server.handle(ping); assert!(matches!(output, Output::Send(_))); assert!(!server.is_ready()); } ``` -------------------------------- ### Complete server supporting stdio and HTTP transports Source: https://docs.rs/mercutio/latest/index.html A complete server example using `McpServer` and `ToolHandler`. Supports both stdio (via `run_stdio`) and HTTP (via `mcp_router` and `axum::serve`) transports, selected by CLI subcommand. ```rust 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 for MyHandler { type Error = std::convert::Infallible; async fn handle(&self, _: Option, tool: MyTools) -> Result { 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::::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(()) } ``` -------------------------------- ### McpRouterBuilder storage and build methods Source: https://docs.rs/mercutio/latest/src/mercutio/io/axum.rs.html Sets a custom session storage and builds the axum Router with POST, GET, and DELETE routes. ```rust impl McpRouterBuilder where R: ToolRegistry + Send + Sync + 'static, H: ToolHandler + Clone + Send + Sync + 'static, S: SessionStorage, { /// Sets a custom session storage implementation. pub fn storage>(self, storage: S2) -> McpRouterBuilder { McpRouterBuilder { builder: self.builder, handler: self.handler, storage, } } /// Builds the axum [`Router`]. pub fn build(self) -> Router { let state = AppState { builder: Arc::new(self.builder), storage: Arc::new(self.storage), handler: self.handler, }; Router::new() .route("/", post(handle_post::)) .route("/", get(handle_get)) .route("/", delete(handle_delete::)) .with_state(state) } } ``` -------------------------------- ### tool_registry! usage example Source: https://docs.rs/mercutio/latest/mercutio/macro.tool_registry.html Example usage of the `tool_registry!` macro defining an enum `MyTools` with two tools: `GetWeather` and `SetReminder`. Doc comments on fields become JSON Schema descriptions sent to clients during `tools/list`. ```rust tool_registry! { enum MyTools { GetWeather("get_weather", "Gets weather for a city") { /// City name, e.g. "San Francisco" city: String, /// Temperature units (celsius or fahrenheit) units: Option, }, SetReminder("set_reminder", "Sets a reminder") { /// Reminder text text: String, }, } } ``` -------------------------------- ### McpServerBuilder::instructions Source: https://docs.rs/mercutio/latest/mercutio/struct.McpServerBuilder.html Sets instructions for the LLM on how to use this server. Sent to the client during initialization. ```APIDOC ## McpServerBuilder::instructions ### Description Sets instructions for the LLM on how to use this server. Sent to the client during initialization. The client may incorporate this text into the system prompt to help the LLM understand when and how to use the server's tools. Typical content includes tool selection guidance, required operation sequences, or domain-specific constraints. Note that client support for this field varies. ### Method pub fn instructions>(&mut self, instructions: S) -> &mut Self ### Parameters - **instructions** (S) - Required - The instructions text to set. ### Returns - **&mut Self** - The builder instance for chaining. ``` -------------------------------- ### Deserialize RFC 3339 timestamp example Source: https://docs.rs/mercutio/latest/mercutio/struct.Rfc3339.html Shows how to deserialize an RFC 3339 timestamp string into an Rfc3339 value using serde_json. The example uses the string "2024-03-11T10:00:00Z". ```rust use mercutio::Rfc3339; let ts: Rfc3339 = serde_json::from_str(r#""2024-03-11T10:00:00Z""#).expect("valid"); ``` -------------------------------- ### RFC 3339 parse error message example Source: https://docs.rs/mercutio/latest/src/mercutio/rfc3339.rs.html Shows the error message format when parsing an invalid RFC 3339 timestamp. The error includes the current time as an example to help models self-correct. ```text invalid RFC 3339 timestamp '2025-05-25 14:30:00': failed to find ... Example: current time is 2025-05-25T14:30:00+02:00 ``` -------------------------------- ### WithSource error chain example Source: https://docs.rs/mercutio/latest/src/mercutio/tools.rs.html Example usage of WithSource to wrap an error type and display the full error chain. The example is marked with ```ignore``` in the source, indicating it is not compiled. Without WithSource, an API failure might show "API request failed"; with it, it shows "API request failed: HTTP error: 403 Forbidden". ```rust use mercutio::WithSource; async fn handle_tool(tool: MyTools) -> Result> { let data = fetch_api().map_err(WithSource)?; Ok(format!("Got: {data}")) } ``` -------------------------------- ### McpServerBuilder::build Source: https://docs.rs/mercutio/latest/mercutio/struct.McpServerBuilder.html Builds the McpServer. ```APIDOC ## McpServerBuilder::build ### Description Builds the McpServer. ### Method pub fn build(&self) -> McpServer ### Returns - **McpServer** - The constructed McpServer instance. ``` -------------------------------- ### get_returns_405 test: GET request returns 405 Method Not Allowed Source: https://docs.rs/mercutio/latest/src/mercutio/io/axum.rs.html This test sends a GET request to the router and asserts the response status is 405 METHOD_NOT_ALLOWED. It uses the mcp_router with a test handler and an empty body. ```rust #[tokio::test] async fn get_returns_405() { let router = mcp_router(test_builder(), |_, t| async { test_handler(t) }); let response = router .oneshot( Request::builder() .method("GET") .uri("/") .body(Body::empty()) .unwrap(), ) .await .unwrap(); assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); } ``` -------------------------------- ### Test Full Server Initialization Source: https://docs.rs/mercutio/latest/src/mercutio/lib.rs.html Verifies the full initialization handshake process, including the initialize request and the initialized notification. ```rust #[test] fn full_initialization() { let mut server = test_server(); let init_req = JsonrpcMessage::Request(JsonrpcRequest::new( RequestId::String("1".into()), "initialize".into(), Some(initialize_params()), )); let output = server.handle(init_req); assert!(matches!(output, Output::Send(_))); assert!(!server.is_ready()); let initialized = JsonrpcMessage::Notification(JsonrpcNotification::new( "notifications/initialized".into(), None, )); let output = server.handle(initialized); assert!(matches!(output, Output::None)); assert!(server.is_ready()); } ``` -------------------------------- ### handle_get function Source: https://docs.rs/mercutio/latest/src/mercutio/io/axum.rs.html Handles GET requests by returning 405 Method Not Allowed since SSE streaming is not implemented. ```rust async fn handle_get() -> Response { StatusCode::METHOD_NOT_ALLOWED.into_response() } ``` -------------------------------- ### pub fn run_on(input: I, output: O, server: McpServer, handler: H) -> Result<(), IoError> Source: https://docs.rs/mercutio/latest/mercutio/io/stdlib/fn.run_on.html Runs an MCP server on arbitrary buffered input/output streams. Use this for custom transports or testing; prefer run_stdio for standard input/output. ```APIDOC ## Function: run_on ### Description Runs an MCP server on arbitrary buffered input/output streams. For most use cases, prefer `run_stdio` which handles stdin/stdout. Use this function for custom transports or testing. ### Signature ```rust pub fn run_on( input: I, output: O, server: McpServer, handler: H, ) -> Result<(), IoError> where R: ToolRegistry, T: Into, E: Display, H: FnMut(Option, R) -> Result, I: BufRead, O: Write, ``` ### Parameters - **input** (I) - Required - Buffered input stream (must implement `BufRead`). - **output** (O) - Required - Output stream (must implement `Write`). - **server** (McpServer) - Required - The MCP server instance. - **handler** (H) - Required - A closure that takes an optional session ID and a tool registry, returning a result. ### Return Value - **Result<(), IoError>** - Returns `Ok(())` on success, or an `IoError` on failure. ### Availability Available on **crate feature `io-stdlib`** only. ``` -------------------------------- ### Implement Display for ToolDefinitions Source: https://docs.rs/mercutio/latest/src/mercutio/tools.rs.html Implements Display for ToolDefinitions, writing a header "# Tools" followed by each tool definition separated by blank lines. Uses the Display implementation of each ToolDefinition. ```rust impl fmt::Display for ToolDefinitions { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "# Tools")?; writeln!(f)?; for (i, def) in self.0.iter().enumerate() { if i > 0 { writeln!(f)?; } write!(f, "{def}")?; } Ok(()) } } ``` -------------------------------- ### McpRouter::builder Source: https://docs.rs/mercutio/latest/mercutio/io/axum/struct.McpRouter.html Creates a builder with the required server builder and handler. This is the primary method to start building an MCP router. ```APIDOC ## McpRouter::builder ### Description Creates a builder with the required server builder and handler. This is the primary method to start building an MCP router. ### Method builder ### Signature pub fn builder(builder: McpServerBuilder, handler: H) -> McpRouterBuilder> where R: ToolRegistry + Send + Sync + 'static, H: ToolHandler + Clone + Send + Sync + 'static, ### Parameters - **builder** (McpServerBuilder) - Required - The server builder to use for the MCP router. - **handler** (H) - Required - The tool handler to use for the MCP router. ### Returns - **McpRouterBuilder>** - A builder for the MCP router with in-memory storage. ### Availability Available on crate feature `io-axum` only. ``` -------------------------------- ### Simple text output Source: https://docs.rs/mercutio/latest/mercutio/struct.ToolOutput.html Shows how to return a simple text response using `Ok` with a string literal or a formatted string. ```rust Ok("Operation completed") Ok(format!("Found {} items", count)) ``` -------------------------------- ### Implement run_stdio, run_on, and write_message Source: https://docs.rs/mercutio/latest/src/mercutio/io/tokio.rs.html Implementation of run_stdio, run_on, and write_message. run_stdio wraps stdin/stdout in a BufReader and calls run_on. run_on reads newline-delimited JSON-RPC lines, handles them via the server, and writes responses. write_message serializes an OutgoingMessage to JSON, appends a newline, and flushes. Includes warnings about not using println! and partial cancellation safety. ```rust use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; pub use super::{IoError, MutToolHandler, ToolHandler}; use crate::{McpServer, OutgoingMessage, Output, ToolRegistry}; /// Runs an MCP server over stdin/stdout asynchronously. /// /// Reads newline-delimited JSON-RPC messages from stdin and writes responses to stdout. Returns /// when stdin reaches EOF or a protocol error occurs. See the [module documentation](self) for a /// complete example. /// /// # Warning /// /// Do not use [`println!`] or other stdout-writing macros inside the handler - this will corrupt /// the JSON-RPC protocol stream. /// /// # Cancellation Safety /// /// Partially cancellation-safe. If cancelled mid-write, the client may receive a truncated /// response. pub async fn run_stdio(server: McpServer, handler: H) -> Result<(), IoError> where R: ToolRegistry, H: MutToolHandler, { let stdin = BufReader::new(tokio::io::stdin()); let stdout = tokio::io::stdout(); run_on(stdin, stdout, server, handler).await } /// Runs an MCP server on arbitrary async buffered input/output streams. /// /// For most use cases, prefer [`run_stdio`] which handles stdin/stdout. Use this function for /// custom transports or testing. pub async fn run_on( mut input: I, mut output: O, mut server: McpServer, mut handler: H, ) -> Result<(), IoError> where R: ToolRegistry, H: MutToolHandler, I: AsyncBufReadExt + Unpin, O: AsyncWriteExt + Unpin, { let mut line = String::new(); loop { line.clear(); let bytes = input.read_line(&mut line).await.map_err(IoError::Io)?; if bytes == 0 { break; } let msg = crate::parse_line(line.trim_end()).map_err(IoError::Parse)?; match server.handle(msg) { Output::Send(response) => { write_message(&mut output, response).await?; } Output::ToolCall { tool, responder } => { let response = responder.respond(handler.handle(None, tool).await); write_message(&mut output, response).await?; } Output::ProtocolError(e) => { return Err(IoError::Protocol(e)); } Output::None => {} } } Ok(()) } /// Writes a JSON-RPC message followed by a newline. async fn write_message( w: &mut (impl AsyncWriteExt + Unpin), msg: OutgoingMessage, ) -> Result<(), IoError> { let mut json = serde_json::to_vec(msg.as_inner()).map_err(IoError::Serialize)?; json.push(b'\n'); w.write_all(&json).await.map_err(IoError::Io)?; w.flush().await.map_err(IoError::Io) } ``` -------------------------------- ### run_on Source: https://docs.rs/mercutio/latest/mercutio/io/stdlib/index.html Runs an MCP server on arbitrary buffered input/output streams. ```APIDOC ## run_on ### Description Runs an MCP server on arbitrary buffered input/output streams. ### Function Signature `run_on(input, output, server, handler) -> Result<(), IoError>` ### Parameters - **input** (impl BufRead) - The buffered input stream. - **output** (impl Write) - The output stream. - **server** (McpServer) - The MCP server instance to run. - **handler** (FnMut) - A closure that handles tool calls, receiving a session ID and a tool instance, returning a Result. ### Returns - **Result<(), IoError>** - Ok if the server runs successfully, or an IoError on failure. ``` -------------------------------- ### Test error message format for invalid RFC 3339 timestamp Source: https://docs.rs/mercutio/latest/src/mercutio/rfc3339.rs.html Attempts to deserialize an invalid timestamp string and asserts the error message contains the expected text and an example hint. ```rust let err = serde_json::from_str::(r#""2025-05-25 14:30:00""#).unwrap_err(); let msg = err.to_string(); assert!(msg.contains("invalid RFC 3339 timestamp '2025-05-25 14:30:00'")); assert!(msg.contains("Example: current time is")); ``` -------------------------------- ### Recommended text vs JSON output Source: https://docs.rs/mercutio/latest/mercutio/struct.ToolOutput.html Shows the recommended approach of returning plain text for LLM reasoning, and using `ToolOutput::json` only when structured parsing is needed downstream. ```rust // Recommended: human-readable text Ok(format!("Temperature: {}F\nConditions: {}", temp, conditions)) // Use only when structured parsing is needed downstream Ok(ToolOutput::json(&data)) ``` -------------------------------- ### Using Rfc3339 in tool_registry macro Source: https://docs.rs/mercutio/latest/src/mercutio/rfc3339.rs.html Example of using the Rfc3339 type in a tool registry macro to define a tool input field. The field is annotated with a doc comment that becomes part of the schema. ```rust use mercutio::Rfc3339; mercutio::tool_registry! { enum Tools { Schedule("schedule", "Schedule a meeting") { /// Meeting start time. start: Rfc3339, }, } } ``` -------------------------------- ### Snapshot Testing with ToolOutput Source: https://docs.rs/mercutio/latest/mercutio/struct.ToolOutput.html Demonstrates using insta to perform snapshot testing on a ToolOutput instance. The output is rendered as a string, with binary content represented by placeholders. ```rust #[test] fn weather_tool_output() { let output = get_weather_handler(input).await?; insta::assert_snapshot!(output.to_string(), @r" Temperature: 72F Conditions: Sunny "); } ``` -------------------------------- ### Test module with test_server and full_session Source: https://docs.rs/mercutio/latest/src/mercutio/io/tokio.rs.html Test module for the Tokio transport. Defines a test_server helper that builds an McpServer with NoTools and starts a tokio::test async function named full_session. The test body is truncated in the source. ```rust #[cfg(test)] mod tests { use std::io::Cursor; use super::{IoError, run_on}; use crate::{McpServer, NoTools}; fn test_server() -> McpServer { McpServer::builder().name("test").version("1.0").build() } #[tokio::test] async fn full_session() { ``` -------------------------------- ### Test: builder sets fields Source: https://docs.rs/mercutio/latest/src/mercutio/config.rs.html Builds a server with name, version, title, and instructions, and asserts the corresponding fields are set on the config info. ```rust #[test] fn builder_sets_fields() { let server = McpServer::::builder() .name("test-server") .version("1.2.3") .title("Test Server") .instructions("Use this server for testing.") .build(); assert_eq!(server.config.info.name, "test-server"); assert_eq!(server.config.info.version, "1.2.3"); assert_eq!(server.config.info.title.as_deref(), Some("Test Server")); assert_eq!( server.config.instructions.as_deref(), Some("Use this server for testing.") ); } ``` -------------------------------- ### ToolOutput::new Source: https://docs.rs/mercutio/latest/mercutio/struct.ToolOutput.html Creates an empty ToolOutput for building. ```APIDOC ## ToolOutput::new ### Description Creates an empty output for building. ### Method Constructor ### Signature `pub fn new() -> Self` ### Parameters None ### Returns - `Self` - A new empty `ToolOutput` instance. ``` -------------------------------- ### Test handler with fixtures and insta snapshot Source: https://docs.rs/mercutio/latest/index.html Constructs a handler with test fixtures and calls `handle` directly. Uses `ToolRegistry` macro to define tools. Asserts output text and uses `insta::assert_snapshot!` to manage large text outputs. ```rust use mercutio::{ToolOutput, ToolRegistry, io::ToolHandler}; mercutio::tool_registry! { enum Tools { Greet("greet", "Greets someone") { name: String, }, } } struct Handler; impl ToolHandler for Handler { type Error = std::convert::Infallible; async fn handle(&self, _: Option, tool: Tools) -> Result { 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!"); ``` -------------------------------- ### Test: no tools Source: https://docs.rs/mercutio/latest/src/mercutio/config.rs.html Builds a server without a tool registry and asserts the tools capability is none. ```rust #[test] fn tools_not_enabled_without_registry() { let server = McpServer::::builder().build(); assert!(server.config.capabilities.tools.is_none()); } ``` -------------------------------- ### Set up HTTP transport with io-axum Source: https://docs.rs/mercutio/latest/index.html Creates an axum router with MCP endpoints. For custom session storage, use McpRouter::builder() with .storage(). ```rust let mut builder = McpServer::::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); ``` -------------------------------- ### build Source: https://docs.rs/mercutio/latest/mercutio/io/axum/struct.McpRouterBuilder.html Builds the axum Router from the configured MCP router builder. ```APIDOC ## build ### Description Builds the axum Router from the configured MCP router builder. ### Method build ### Signature pub fn build(self) -> Router ### Returns - **Router** - The constructed axum Router. ``` -------------------------------- ### run_stdio Source: https://docs.rs/mercutio/latest/mercutio/io/stdlib/fn.run_stdio.html Documentation for the run_stdio function, which runs an MCP server over stdin/stdout. ```APIDOC ## Function: run_stdio ### Description Runs an MCP server over stdin/stdout. Reads newline-delimited JSON-RPC messages from stdin and writes responses to stdout. Returns when stdin reaches EOF or a protocol error occurs. ### Signature ```rust pub fn run_stdio( server: McpServer, handler: H, ) -> Result<(), IoError> where R: ToolRegistry, T: Into, E: Display, H: FnMut(Option, R) -> Result, ``` ### Parameters - **server** (McpServer) - Required - The MCP server instance to run. - **handler** (H) - Required - A closure that handles each request. It receives an optional session ID and the tool registry, and returns a result. ### Returns - **Result<(), IoError>** - Returns `Ok(())` on successful completion, or an `IoError` if an I/O error occurs. ### Availability Available on crate feature `io-stdlib` only. ### Deadlock Warning Stdout is locked for the duration of this call. Using `println!` or other stdout-locking macros inside the handler will deadlock. ``` -------------------------------- ### ServerConfig struct and McpServerBuilder implementation Source: https://docs.rs/mercutio/latest/src/mercutio/config.rs.html Defines the `ServerConfig` struct holding implementation info, capabilities, and optional instructions, and the `McpServerBuilder` with methods for setting name, version, title, and instructions, plus a `build` method that constructs an `McpServer` with appropriate capabilities. ```rust 1//! MCP server configuration and builder. 2 3use std::marker::PhantomData; 4 5use rust_mcp_schema::{Implementation, ServerCapabilities, ServerCapabilitiesTools}; 6 7use crate::{McpServer, Phase, ToolRegistry}; 8 9/// Server configuration for MCP initialization. 10pub(crate) struct ServerConfig { 11 /// Server implementation info sent during initialization. 12 pub info: Implementation, 13 /// Server capabilities advertised to the client. 14 pub capabilities: ServerCapabilities, 15 /// Optional LLM instructions sent during initialization. 16 pub instructions: Option, 17} 18 19/// Builder for constructing an [`McpServer`]. 20pub struct McpServerBuilder { 21 /// Server name. 22 name: String, 23 /// Server version. 24 version: String, 25 /// Optional human-readable title. 26 title: Option, 27 /// Optional LLM instructions. 28 instructions: Option, 29 /// Tool registry marker. 30 _marker: PhantomData, 31} 32 33impl McpServerBuilder { 34 /// Creates a new builder with default values. 35 pub(crate) fn new() -> Self { 36 Self { 37 name: "unnamed-mcp-server".into(), 38 version: "0.0.0".into(), 39 title: None, 40 instructions: None, 41 _marker: PhantomData, 42 } 43 } 44 45 /// Sets the server name sent to clients during initialization. 46 pub fn name>(&mut self, name: S) -> &mut Self { 47 self.name = name.into(); 48 self 49 } 50 51 /// Sets the server version sent to clients during initialization. 52 pub fn version>(&mut self, version: S) -> &mut Self { 53 self.version = version.into(); 54 self 55 } 56 57 /// Sets a human-readable title sent to clients during initialization. 58 pub fn title>(&mut self, title: S) -> &mut Self { 59 self.title = Some(title.into()); 60 self 61 } 62 63 /// Sets instructions for the LLM on how to use this server. 64 /// 65 /// Sent to the client during initialization. The client may incorporate this text into the 66 /// system prompt to help the LLM understand when and how to use the server's tools. Typical 67 /// content includes tool selection guidance, required operation sequences, or domain-specific 68 /// constraints. Note that client support for this field varies. 69 /// 70 /// See for 71 /// examples. 72 pub fn instructions>(&mut self, instructions: S) -> &mut Self { 73 self.instructions = Some(instructions.into()); 74 self 75 } 76 77 /// Builds the [`McpServer`]. 78 pub fn build(&self) -> McpServer { 79 let capabilities = ServerCapabilities { 80 tools: if R::ENABLED { 81 Some(ServerCapabilitiesTools { 82 list_changed: Some(false), 83 }) 84 } else { 85 None 86 }, 87 completions: None, 88 experimental: None, 89 logging: None, 90 prompts: None, 91 resources: None, 92 tasks: None, 93 }; 94 95 McpServer { 96 config: ServerConfig { 97 info: Implementation { 98 description: None, 99 icons: Vec::new(), 100 name: self.name.clone(), 101 title: self.title.clone(), 102 version: self.version.clone(), 103 website_url: None, 104 }, 105 capabilities, 106 instructions: self.instructions.clone(), 107 }, 108 phase: Phase::WaitingForInitialize, 109 _marker: PhantomData, 110 } 111 } 112} ``` -------------------------------- ### Multiple content blocks with builder Source: https://docs.rs/mercutio/latest/mercutio/struct.ToolOutput.html Shows how to combine multiple content blocks using the builder API with `.text()` calls. ```rust Ok(ToolOutput::new() .text("Query results:") .text(format!("Found {} matches", results.len()))) ``` -------------------------------- ### McpRouter builder method Source: https://docs.rs/mercutio/latest/src/mercutio/io/axum.rs.html Creates a builder with the required server builder and handler, using InMemoryStorage with default capacity and eviction age. ```rust impl McpRouter { /// Creates a builder with the required server builder and handler. pub fn builder( builder: McpServerBuilder, handler: H, ) -> McpRouterBuilder> where R: ToolRegistry + Send + Sync + 'static, H: ToolHandler + Clone + Send + Sync + 'static, { McpRouterBuilder { builder, handler, storage: InMemoryStorage::new(DEFAULT_CAPACITY, DEFAULT_MIN_EVICTION_AGE), } } } ``` -------------------------------- ### Display implementation for ToolOutput Source: https://docs.rs/mercutio/latest/src/mercutio/tools.rs.html Formats ToolOutput for display. Iterates over content blocks, writing text or placeholders for images, audio, resource links, and embedded resources. Appends structured content as pretty-printed JSON if present. ```rust impl fmt::Display for ToolOutput { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for (i, block) in self.content.iter().enumerate() { if i > 0 { writeln!(f)?; writeln!(f)?; } match block { ContentBlock::TextContent(text) => { write!(f, "{}", text.text)?; } ContentBlock::ImageContent(img) => { write!(f, "[Image: {}, {} bytes]", img.mime_type, img.data.len())?; } ContentBlock::AudioContent(audio) => { write!( f, "[Audio: {}, {} bytes]", audio.mime_type, audio.data.len() )?; } ContentBlock::ResourceLink(link) => { write!(f, "[Resource: {} ({})]", link.name, link.uri)?; } ContentBlock::EmbeddedResource(res) => { use rust_mcp_schema::EmbeddedResourceResource; match &res.resource { EmbeddedResourceResource::TextResourceContents(text) => { write!(f, "[Embedded Text: {}]", text.uri)?; } EmbeddedResourceResource::BlobResourceContents(blob) => { let mime = blob.mime_type.as_deref().unwrap_or("unknown"); write!( f, "[Embedded Blob: {}, {}, {} bytes]", blob.uri, mime, blob.blob.len() )?; } } } } } if let Some(structured) = &self.structured_content { if !self.content.is_empty() { writeln!(f)?; writeln!(f)?; } writeln!(f, "Structured Content:")?; let json = serde_json::to_string_pretty(structured).unwrap_or_default(); write!(f, "{}", json)?; } Ok(()) } } ``` -------------------------------- ### Rust test: create_and_access_session Source: https://docs.rs/mercutio/latest/src/mercutio/io/axum.rs.html Creates a session with a test server and verifies that with_session can access it, returning the closure's value. ```rust #[tokio::test] async fn create_and_access_session() { let storage = InMemoryStorage::new(10, Duration::from_secs(0)); let server = test_builder().build(); let id = storage.create(server).await.unwrap(); let result = storage .with_session(id, |_server| "accessed") .await .unwrap(); assert_eq!(result, Some("accessed")); } ``` -------------------------------- ### McpServerBuilder::title Source: https://docs.rs/mercutio/latest/mercutio/struct.McpServerBuilder.html Sets a human-readable title sent to clients during initialization. ```APIDOC ## McpServerBuilder::title ### Description Sets a human-readable title sent to clients during initialization. ### Method pub fn title>(&mut self, title: S) -> &mut Self ### Parameters - **title** (S) - Required - The human-readable title to set. ### Returns - **&mut Self** - The builder instance for chaining. ``` -------------------------------- ### Rust tests for tool registry and output Source: https://docs.rs/mercutio/latest/src/mercutio/tools.rs.html Unit tests covering NoTools behavior, ToolDefinition::from_tool, field docstring conversion to schema descriptions, and ToolOutput conversions. Verifies empty definitions, MethodNotFound errors, schema properties, and structured content presence. ```rust #[cfg(test)] mod tests { use super::{IntoToolResponse, NoTools, ToolDefinition, ToolOutput, ToolRegistry}; use crate::JsonRpcError; #[test] fn no_tools_definitions_empty() { assert!(NoTools::definitions().is_empty()); } #[test] fn no_tools_parse_returns_error() { let result = NoTools::parse("anything", serde_json::Value::Null); assert!(result.is_err()); let err = result.unwrap_err(); assert!(matches!(err, JsonRpcError::MethodNotFound { .. })); } #[test] fn tool_definition_from_tool() { #[allow(dead_code)] #[derive(Debug, schemars::JsonSchema, serde::Deserialize)] struct TestInput { value: String, } impl super::ToolDef for TestInput { const NAME: &'static str = "test_tool"; const DESCRIPTION: &'static str = "A test tool"; } let def = ToolDefinition::from_tool::(); assert_eq!(def.name, "test_tool"); assert_eq!(def.description, "A test tool"); assert_eq!(def.input_schema.type_(), "object"); assert!(def.input_schema.properties.is_some()); } #[test] fn field_docstrings_become_schema_descriptions() { #[allow(dead_code)] #[derive(Debug, schemars::JsonSchema, serde::Deserialize)] struct TestInput { /// The city to look up. city: String, /// Temperature unit preference. units: Option, } impl super::ToolDef for TestInput { const NAME: &'static str = "test"; const DESCRIPTION: &'static str = "Test"; } let def = ToolDefinition::from_tool::(); let props = def.input_schema.properties.expect("properties"); let city_prop = props.get("city").expect("city property"); let city_desc = city_prop.get("description").and_then(|v| v.as_str()); assert_eq!(city_desc, Some("The city to look up.")); let units_prop = props.get("units").expect("units property"); let units_desc = units_prop.get("description").and_then(|v| v.as_str()); assert_eq!(units_desc, Some("Temperature unit preference.")); } #[test] fn tool_output_from_string() { let result = "hello".into_tool_response(); let json = serde_json::to_value(&result).expect("serialize"); let content = json.get("content").expect("content field"); assert!(content.is_array()); assert_eq!(content.as_array().expect("array").len(), 1); assert_eq!(json.get("isError").and_then(|v| v.as_bool()), Some(false)); } #[test] fn tool_output_from_owned_string() { let result = String::from("hello").into_tool_response(); let json = serde_json::to_value(&result).expect("serialize"); let content = json.get("content").expect("content field"); assert!(content.is_array()); } #[test] fn tool_output_json_sets_structured_content() { #[derive(serde::Serialize)] struct Data { value: i32, } let result = ToolOutput::json(&Data { value: 42 }).into_tool_response(); let json = serde_json::to_value(&result).expect("serialize"); assert!(json.get("structuredContent").is_some()); assert!(json.get("content").expect("content").is_array()); } } ``` -------------------------------- ### Test module with TestTools registry and unit tests Source: https://docs.rs/mercutio/latest/src/mercutio/config.rs.html Contains a minimal `TestTools` registry implementing `ToolRegistry` and two unit tests: `default_values` checks that a built server is not ready, and `tools_disabled_for_no_tools` verifies tool capabilities are disabled when no tools are present. ```rust 114#[cfg(test)] 115mod tests { 116 use crate::{JsonRpcError, McpServer, ToolDefinitions, ToolRegistry}; 117 118 /// Minimal tool registry for testing builder behavior. 119 enum TestTools {} 120 121 impl ToolRegistry for TestTools { 122 fn parse(name: &str, _arguments: serde_json::Value) -> Result { 123 Err(JsonRpcError::MethodNotFound { 124 msg: format!("unknown tool: {name}"), 125 }) 126 } 127 128 fn definitions() -> ToolDefinitions { 129 ToolDefinitions::new(vec![]) 130 } 131 } 132 133 #[test] 134 fn default_values() { 135 let server = McpServer::::builder().build(); 136 assert!(!server.is_ready()); 137 } 138 139 #[test] 140 fn tools_disabled_for_no_tools() { 141 use crate::NoTools; 142 ``` -------------------------------- ### Sans-IO usage with McpServer and Output Source: https://docs.rs/mercutio/latest/index.html Shows the core state machine loop: parse lines, handle messages, and match on Output variants to send responses or process tool calls. ```rust use mercutio::{McpServer, Output}; let mut server = McpServer::::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 => {} } } ```