Rust SDK (RMCP)
Создание высокопроизводительных MCP серверов на Rust с RMCP
RMCP — официальный Rust SDK для MCP с процедурными макросами для минимального бойлерплейта.
Установка
Заголовок раздела «Установка»Добавьте в Cargo.toml:
[dependencies]rmcp = { version = "0.1", features = ["server", "transport-io"] }tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }serde_json = "1"[dependencies]rmcp = { version = "0.1", features = ["server", "transport-sse-server"] }tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }serde_json = "1"Требования:
- Rust 1.75+
- Cargo
Базовый сервер
Заголовок раздела «Базовый сервер»use rmcp::{Server, ServerHandler, model::ServerInfo};use rmcp::transport::io::stdio;
struct MyServer;
impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo { name: "rust-server".into(), version: "1.0.0".into(), ..Default::default() } }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let server = Server::new(MyServer); let transport = stdio(); server.run(transport).await?; Ok(())}Инструменты (Tools)
Заголовок раздела «Инструменты (Tools)»Макрос #[tool]
Заголовок раздела «Макрос #[tool]»use rmcp::tool;use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, schemars::JsonSchema)]struct AddParams { /// Первое число a: f64, /// Второе число b: f64,}
#[tool( name = "add", description = "Сложение двух чисел")]async fn add(params: AddParams) -> String { format!("Результат: {}", params.a + params.b)}Tool Router для группировки
Заголовок раздела «Tool Router для группировки»use rmcp::{tool, tool_router};
#[tool_router]impl Calculator { #[tool(description = "Сложение")] async fn add(&self, a: f64, b: f64) -> f64 { a + b }
#[tool(description = "Умножение")] async fn multiply(&self, a: f64, b: f64) -> f64 { a * b }
#[tool(description = "Деление")] async fn divide(&self, a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err("Деление на ноль".into()) } else { Ok(a / b) } }}Регистрация инструментов
Заголовок раздела «Регистрация инструментов»use rmcp::{Server, ServerHandler};
struct MyServer { calculator: Calculator,}
impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new("calc-server", "1.0.0") }
fn list_tools(&self) -> Vec<Tool> { self.calculator.list_tools() }
async fn call_tool(&self, name: &str, params: Value) -> ToolResult { self.calculator.call_tool(name, params).await }}Инструмент с HTTP запросами
Заголовок раздела «Инструмент с HTTP запросами»use rmcp::{tool_router, tool};use reqwest::Client;
struct HttpTools { client: Client,}
#[tool_router]impl HttpTools { #[tool(description = "Загрузка содержимого URL")] async fn fetch_url(&self, url: String) -> Result<String, String> { self.client .get(&url) .send() .await .map_err(|e| e.to_string())? .text() .await .map_err(|e| e.to_string()) }
#[tool(description = "POST запрос")] async fn post_json(&self, url: String, body: String) -> Result<String, String> { self.client .post(&url) .header("Content-Type", "application/json") .body(body) .send() .await .map_err(|e| e.to_string())? .text() .await .map_err(|e| e.to_string()) }}Инструмент с файловой системой
Заголовок раздела «Инструмент с файловой системой»use rmcp::{tool_router, tool};use std::path::Path;use tokio::fs;
struct FileTools { allowed_path: String,}
#[tool_router]impl FileTools { #[tool(description = "Чтение файла")] async fn read_file(&self, path: String) -> Result<String, String> { let full_path = Path::new(&self.allowed_path).join(&path);
// Проверка безопасности if !full_path.starts_with(&self.allowed_path) { return Err("Доступ запрещён".into()); }
fs::read_to_string(&full_path) .await .map_err(|e| format!("Ошибка чтения: {}", e)) }
#[tool(description = "Запись файла")] async fn write_file(&self, path: String, content: String) -> Result<String, String> { let full_path = Path::new(&self.allowed_path).join(&path);
if !full_path.starts_with(&self.allowed_path) { return Err("Доступ запрещён".into()); }
fs::write(&full_path, content) .await .map_err(|e| format!("Ошибка записи: {}", e))?;
Ok(format!("Файл сохранён: {}", path)) }}Ресурсы (Resources)
Заголовок раздела «Ресурсы (Resources)»use rmcp::{resource, Resource, ResourceContent};
#[resource( uri = "config://app", name = "App Configuration", description = "Конфигурация приложения", mime_type = "application/json")]async fn app_config() -> ResourceContent { let config = serde_json::json!({ "version": "1.0.0", "environment": "production" }); ResourceContent::text(config.to_string())}Динамический ресурс с шаблоном
Заголовок раздела «Динамический ресурс с шаблоном»use rmcp::{resource_template, ResourceContent};
#[resource_template( uri_template = "file://{path}", name = "File Reader", description = "Чтение файлов")]async fn read_file(path: String) -> Result<ResourceContent, String> { let content = tokio::fs::read_to_string(&path) .await .map_err(|e| e.to_string())?;
Ok(ResourceContent::text(content))}Список ресурсов
Заголовок раздела «Список ресурсов»impl ServerHandler for MyServer { fn list_resources(&self) -> Vec<Resource> { vec![ Resource::new("config://app", "Конфигурация"), Resource::template("file://{path}", "Файлы"), ] }
async fn read_resource(&self, uri: &str) -> ResourceResult { match uri { "config://app" => app_config().await, uri if uri.starts_with("file://") => { let path = uri.strip_prefix("file://").unwrap(); read_file(path.to_string()).await } _ => Err("Resource not found".into()), } }}Промпты (Prompts)
Заголовок раздела «Промпты (Prompts)»use rmcp::{prompt, Prompt, PromptMessage};
#[prompt( name = "code_review", description = "Промпт для код-ревью")]async fn code_review(language: String, code: String) -> Vec<PromptMessage> { vec![ PromptMessage::user(format!( "Проведи код-ревью следующего {} кода:\n\n```{}\n{}\n```", language, language, code )) ]}
#[prompt( name = "debug_helper", description = "Помощь с отладкой")]async fn debug_helper(error: String, context: String) -> Vec<PromptMessage> { vec![ PromptMessage::system("Ты опытный отладчик. Анализируй ошибки и предлагай решения."), PromptMessage::user(format!( "Ошибка: {}\n\nКонтекст:\n{}", error, context )) ]}Обработка ошибок
Заголовок раздела «Обработка ошибок»use mcp_rust_sdk::error::McpError;use thiserror::Error;
#[derive(Error, Debug)]pub enum ServerError { #[error("Файл не найден: {0}")] FileNotFound(String),
#[error("Доступ запрещён: {0}")] AccessDenied(String),
#[error("Неверные параметры: {0}")] InvalidParams(String),}
impl From<ServerError> for McpError { fn from(err: ServerError) -> Self { match err { ServerError::FileNotFound(msg) => McpError::not_found(msg), ServerError::AccessDenied(msg) => McpError::forbidden(msg), ServerError::InvalidParams(msg) => McpError::invalid_params(msg), } }}Логирование
Заголовок раздела «Логирование»use tracing::{info, error, debug};use tracing_subscriber;
fn setup_logging() { tracing_subscriber::fmt() .with_env_filter("mcp=debug,my_server=info") .init();}
impl ToolHandler for MyTool { async fn execute(&self, params: Value) -> ToolResult { info!("Выполнение инструмента с параметрами: {:?}", params);
match self.do_work(params).await { Ok(result) => { debug!("Успешное выполнение"); Ok(result) } Err(e) => { error!("Ошибка: {:?}", e); Err(e) } } }}Транспорты
Заголовок раздела «Транспорты»Stdio (стандартный)
Заголовок раздела «Stdio (стандартный)»use rmcp::transport::io::stdio;
let transport = stdio();server.run(transport).await?;SSE Server
Заголовок раздела «SSE Server»use rmcp::transport::sse_server::SseServerTransport;use axum::Router;
let (transport, router) = SseServerTransport::new("/mcp");
let app = Router::new().merge(router);let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
tokio::spawn(async move { axum::serve(listener, app).await.unwrap();});
server.run(transport).await?;Полный пример сервера
Заголовок раздела «Полный пример сервера»use rmcp::{Server, ServerHandler, tool_router, tool};use rmcp::model::ServerInfo;use rmcp::transport::io::stdio;
struct Calculator;
#[tool_router]impl Calculator { #[tool(description = "Сложение")] async fn add(&self, a: f64, b: f64) -> f64 { a + b }
#[tool(description = "Умножение")] async fn multiply(&self, a: f64, b: f64) -> f64 { a * b }}
struct MyServer { calc: Calculator,}
impl ServerHandler for MyServer { fn get_info(&self) -> ServerInfo { ServerInfo::new("calc-server", "1.0.0") }
fn list_tools(&self) -> Vec<rmcp::model::Tool> { self.calc.list_tools() }
async fn call_tool(&self, name: &str, params: serde_json::Value) -> rmcp::ToolResult { self.calc.call_tool(name, params).await }}
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { tracing_subscriber::fmt().init();
let server = Server::new(MyServer { calc: Calculator }); server.run(stdio()).await?;
Ok(())}Структура проекта
Заголовок раздела «Структура проекта»my-mcp-server/├── src/│ ├── main.rs│ ├── lib.rs│ ├── tools/│ │ ├── mod.rs│ │ ├── calculator.rs│ │ └── filesystem.rs│ ├── resources/│ │ ├── mod.rs│ │ └── config.rs│ └── prompts/│ ├── mod.rs│ └── templates.rs├── Cargo.toml├── Dockerfile└── README.md# Build stageFROM rust:1.75 as builderWORKDIR /appCOPY . .RUN cargo build --release
# Runtime stageFROM debian:bookworm-slimRUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*COPY --from=builder /app/target/release/mcp-server /usr/local/bin/CMD ["mcp-server"]Конфигурация Claude Desktop
Заголовок раздела «Конфигурация Claude Desktop»{ "mcpServers": { "rust-server": { "command": "/path/to/target/release/mcp-server" } }}Cargo.toml
Заголовок раздела «Cargo.toml»[package]name = "mcp-server"version = "1.0.0"edition = "2021"
[dependencies]rmcp = { version = "0.1", features = ["server", "transport-io"] }tokio = { version = "1", features = ["full"] }serde = { version = "1", features = ["derive"] }serde_json = "1"schemars = "0.8"tracing = "0.1"tracing-subscriber = "0.3"
# Для HTTPaxum = "0.7"rmcp = { version = "0.1", features = ["server", "transport-sse-server"] }