**🦀 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.