**🦀 MCP in Rust: A Practical Guide using `rmcp`** This guide demonstrates how to use the Model Context Protocol (MCP) in Rust with the `rmcp` crate. MCP allows AI systems to interact with tools via JSON-RPC 2.0. Rust's performance and safety make it a great choice for building these tools. We'll build several example MCP servers, from simple file operations to more complex KYC tasks. **1. Environment Setup** First, add the necessary dependencies to your `Cargo.toml`: ```toml [dependencies] rmcp = { version = "0.1", features = ["server", "client"] } # Core MCP library for server/client tokio = { version = "1", features = ["full"] } # Async runtime serde = { version = "1", features = ["derive"] } # Serialization/Deserialization schemars = "0.8" # JSON Schema generation for tool inputs anyhow = "1.0" # Flexible error handling reqwest = { version = "0.11", features = ["json"] } # HTTP client (for examples) serde_json = "1.0" # JSON handling (for examples) # Note: For OCR example, ensure 'tesseract' CLI tool is installed and in PATH ``` **2. General MCP Server Examples** Let's create some basic tools exposed via MCP. **2.1. File Explorer** This tool provides functions to list directory contents and read files. ```rust use rmcp::{tool, tool_box}; use serde::Deserialize; use schemars::JsonSchema; use anyhow::Result; // Using anyhow for convenient error handling #[derive(Clone)] struct FileServer; // Define the structure for requests needing a path #[derive(Deserialize, JsonSchema)] struct PathRequest { path: String } #[tool_box] // Exposes methods within this impl block as MCP tools impl FileServer { #[tool(description = "List directory contents")] // MCP description for this tool/method fn list_dir(&self, #[tool(aggr)] req: PathRequest) -> Result<Vec<String>> { // #[tool(aggr)] indicates the 'req' struct aggregates all parameters Ok(std::fs::read_dir(&req.path)? .filter_map(Result::ok) // Iterate, filter out errors .map(|e| e.file_name().to_string_lossy().to_string()) // Get filenames .collect()) // Collect into a Vec<String> } #[tool(description = "Read a file")] fn read_file(&self, #[tool(aggr)] req: PathRequest) -> Result<String> { Ok(std::fs::read_to_string(&req.path)?) // Read file content to string } } // Note: You'd need to add server setup code (e.g., using rmcp::Server) to run this. // See rmcp documentation for server initialization examples. ``` * `#[tool_box]` marks the `impl` block, making its methods discoverable MCP tools. * `#[tool(description = "...")]` provides a human-readable description for the AI model. * `#[derive(Deserialize, JsonSchema)]` on `PathRequest` allows `serde` to parse incoming JSON into the struct and `schemars` to generate a schema for validation/description. * `#[tool(aggr)]` tells `rmcp` to map the incoming JSON parameters object directly to the fields of the `PathRequest` struct. * Standard Rust `std::fs` functions are used for the core logic. `anyhow::Result` simplifies error handling. **2.2. Currency Converter** This tool uses an external API to convert currencies. ```rust use rmcp::{tool, tool_box}; use serde::Deserialize; use schemars::JsonSchema; use anyhow::Result; use reqwest; // HTTP client #[derive(Clone)] // Added Clone trait struct CurrencyServer; #[derive(Debug, Deserialize, JsonSchema)] struct ConvertRequest { from: String, to: String, amount: f64, } // Structure to deserialize the relevant part of the API response #[derive(Debug, Deserialize)] struct ApiResponse { result: f64, } #[tool_box] impl CurrencyServer { #[tool(description = "Convert currency using live exchange rates from api.exchangerate.host")] async fn convert(&self, #[tool(aggr)] req: ConvertRequest) -> Result<String> { // This method is async because it performs a network request let url = format!( "https://api.exchangerate.host/convert?from={}&to={}&amount={}", req.from, req.to, req.amount ); // Use reqwest to make the HTTP GET request and await the response let resp = reqwest::get(&url).await? .json::<ApiResponse>().await?; // Parse JSON response into ApiResponse // Format the result nicely Ok(format!("{:.2} {}", resp.result, req.to)) } } // Requires a tokio runtime to execute async functions. ``` * Uses `async fn` because `reqwest::get` is an asynchronous operation. Requires an async runtime like `tokio`. * Makes an HTTP GET request to an external currency API. * Uses `serde` to deserialize the JSON response from the API into the `ApiResponse` struct. **2.3. Task Manager** A stateful tool to manage a simple list of tasks. ```rust use rmcp::{tool, tool_box}; use serde::{Deserialize, Serialize}; use schemars::JsonSchema; use anyhow::Result; use std::sync::{Arc, Mutex}; // For shared mutable state #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] struct Task { title: String, done: bool, } #[derive(Debug, Deserialize, JsonSchema)] struct TaskRequest { title: String, done: bool, // Assuming we want to set status on add } #[derive(Clone)] // Clone is needed for the server state to be shared across requests struct TaskManager { // Use Arc<Mutex<T>> for thread-safe shared mutable state tasks: Arc<Mutex<Vec<Task>>>, } impl TaskManager { // Constructor to initialize the state fn new() -> Self { TaskManager { tasks: Arc::new(Mutex::new(Vec::new())), } } } #[tool_box] impl TaskManager { #[tool(description = "Add a new task")] fn add_task(&self, #[tool(aggr)] req: TaskRequest) -> Result<String> { // Lock the mutex to safely access the shared task list let mut tasks = self.tasks.lock().unwrap(); // .unwrap() is used for simplicity here tasks.push(Task { title: req.title, done: req.done }); Ok("Task added.".to_string()) } #[tool(description = "List all tasks")] fn list_tasks(&self) -> Result<Vec<Task>> { // Lock, clone the current list, and return it let tasks = self.tasks.lock().unwrap(); Ok(tasks.clone()) // Clone the data to release the lock quickly } } ``` * Demonstrates state management using `Arc<Mutex<Vec<Task>>>` for sharing the task list safely across concurrent requests (if the server runs multithreaded). * `list_tasks` returns the list of `Task` structs directly. `rmcp` handles serializing this into the JSON-RPC response. **2.4. Weather Forecast** Fetches weather from the `wttr.in` service. ```rust use rmcp::{tool, tool_box}; use serde::Deserialize; use schemars::JsonSchema; use anyhow::Result; use reqwest; #[derive(Clone)] // Added Clone trait struct WeatherServer; #[derive(Debug, Deserialize, JsonSchema)] struct CityRequest { city: String, } #[tool_box] impl WeatherServer { #[tool(description = "Get city weather from wttr.in")] async fn get_forecast(&self, #[tool(aggr)] req: CityRequest) -> Result<String> { // Async again due to network call let url = format!("https://wttr.in/{}?format=3", req.city); // format=3 gives concise output let forecast = reqwest::get(&url).await? .text().await?; // Get response body as plain text Ok(forecast) } } ``` * Similar to the currency converter, uses `async fn` and `reqwest` for an external HTTP request. * Fetches plain text data from `wttr.in`. **2.5. JSON Formatter** A simple tool to prettify a JSON string. ```rust use rmcp::{tool, tool_box}; use anyhow::Result; use serde_json; // Use serde_json for JSON manipulation #[derive(Clone)] // Added Clone trait struct JsonServer; #[tool_box] impl JsonServer { #[tool(description = "Prettify a JSON string")] fn format_json(&self, #[tool(param)] raw: String) -> Result<String> { // #[tool(param)] indicates 'raw' is the single, unnamed parameter // expected in the JSON-RPC request's "params" field. // Parse the raw string into a generic JSON value let value: serde_json::Value = serde_json::from_str(&raw)?; // Serialize the value back to a prettified string Ok(serde_json::to_string_pretty(&value)?) } } ``` * Uses `#[tool(param)]` for methods expecting a single unnamed parameter in the `params` field of the JSON-RPC request (e.g., `"params": "{\"key\":\"value\"}"`). * Leverages `serde_json` to parse and then re-serialize the JSON data with pretty printing. **3. KYC Tool Set — Servers & Client Interaction** Now, let's look at more specialized tools, common in KYC (Know Your Customer) processes. We'll show both the server-side tool definition and how a client might call it. *(Note: These KYC examples are illustrative and simplified.)* **3.1. Identity OCR Tool** **Server:** Extracts text from an image file using the external `tesseract` OCR tool. ```rust # // --- Server Code --- # use rmcp::{tool, tool_box}; # use serde::Deserialize; # use schemars::JsonSchema; # use anyhow::Result; # use std::process::Command; // To run external processes # # // Assuming KycServer is defined elsewhere or combined # #[derive(Clone)] struct KycServer; # # #[derive(Debug, Deserialize, JsonSchema)] # struct OcrRequest { # image_path: String, // Path to the image file for OCR # } # # #[tool_box] # impl KycServer { // Assuming methods are added to a common KycServer #[tool(description = "Extract text from an ID document image using Tesseract OCR")] async fn extract_id_text(&self, #[tool(aggr)] req: OcrRequest) -> Result<String> { // Run the tesseract command externally // Assumes 'tesseract' executable is in the system's PATH let output = Command::new("tesseract") .arg(&req.image_path) // Input image file .arg("stdout") // Output result to stdout .output()?; // Execute and wait for completion if output.status.success() { // Convert stdout bytes to a UTF-8 string (lossy conversion handles potential errors) Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { // If tesseract failed, return an error let stderr = String::from_utf8_lossy(&output.stderr); Err(anyhow::anyhow!("Tesseract failed: {}", stderr)) } } # } ``` * Uses `std::process::Command` to execute the `tesseract` command-line tool. * Requires `tesseract` to be installed on the server machine where this Rust code runs. * Handles potential errors from the external process. **Client:** Example of calling this tool using `rmcp` client (raw request). ```rust # // --- Client Code Example --- # use rmcp::client::Client; // Assuming 'Client' is set up # use serde_json::json; # use anyhow::Result; # # async fn call_ocr_tool(client: &Client) -> Result<()> { let request_payload = json!({ "jsonrpc": "2.0", "id": 1, // Request ID "method": "extract_id_text", // Matches the server function name "params": { // Corresponds to #[tool(aggr)] req: OcrRequest "image_path": "./path/to/your/id_photo.png" } }); // Assuming 'client' is an initialized rmcp::client::Client let result = client.send_raw(request_payload).await?; println!("OCR Result: {:?}", result); // Prints the raw JSON-RPC response # Ok(()) # } ``` * Constructs a standard JSON-RPC 2.0 request payload using `serde_json::json!`. * Sends the raw request using `client.send_raw`. The `result` will be the full JSON-RPC response `{ "jsonrpc": "2.0", "id": 1, "result": "extracted text..." }` or an error object. **3.2. Bank Account Verification** **Server:** A simplified check matching name and IBAN details. ```rust # // --- Server Code --- # use rmcp::{tool, tool_box}; # use serde::Deserialize; # use schemars::JsonSchema; # use anyhow::Result; # # // Assuming KycServer is defined elsewhere or combined # #[derive(Clone)] struct KycServer; # #[derive(Debug, Deserialize, JsonSchema)] struct BankCheckRequest { name: String, iban: String, } #[tool_box] impl KycServer { // Assuming methods are added to a common KycServer #[tool(description = "Verify bank holder name and IBAN match (simplified check)")] async fn verify_bank(&self, #[tool(aggr)] req: BankCheckRequest) -> Result<String> { // Basic placeholder logic: Checks if IBAN starts with 'FR' and name contains 'test' (case-insensitive) // Replace with actual validation logic (e.g., API call to a verification service) if req.iban.starts_with("FR") && req.name.to_lowercase().contains("test") { Ok("Match confirmed (Test Logic).".to_string()) } else { Ok("No match found (Test Logic).".to_string()) } } } ``` * Implements placeholder logic for demonstration. A real-world tool would integrate with a banking API or database. **Client:** Example call. ```rust # // --- Client Code Example --- # use rmcp::client::Client; // Assuming 'Client' is set up # use serde_json::json; # use anyhow::Result; # # async fn call_bank_verify_tool(client: &Client) -> Result<()> { let request_payload = json!({ "jsonrpc": "2.0", "id": 2, // Different request ID "method": "verify_bank", "params": { // Corresponds to #[tool(aggr)] req: BankCheckRequest "name": "Test User", "iban": "FR7612345678901234567890123" } }); let result = client.send_raw(request_payload).await?; println!("Bank Verification Result: {:?}", result); # Ok(()) # } ``` * Similar structure to the OCR client call, targeting the `verify_bank` method with appropriate parameters. **4. Conclusion** You've seen how to create various MCP tools in Rust using the `rmcp` crate, covering stateless and stateful servers, interaction with external APIs, and running external processes. The `#[tool_box]` and `#[tool]` attributes, combined with `serde` and `schemars`, provide a powerful way to expose Rust functions to AI models or other systems speaking JSON-RPC 2.0.