Перейти к содержимому

Rust SDK (RMCP)

Создание высокопроизводительных MCP серверов на Rust с RMCP

RMCP — официальный Rust SDK для MCP с процедурными макросами для минимального бойлерплейта.

Добавьте в Cargo.toml:

Cargo.toml
[dependencies]
rmcp = { version = "0.1", features = ["server", "transport-io"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Требования:

  • Rust 1.75+
  • Cargo
src/main.rs
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(())
}
src/tools.rs
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)
}
src/calculator.rs
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)
}
}
}
src/server.rs
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
}
}
src/http_tools.rs
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())
}
}
src/file_tools.rs
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))
}
}
src/resources.rs
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())
}
src/resources.rs
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))
}
src/server.rs
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()),
}
}
}
src/prompts.rs
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
))
]
}
src/error.rs
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),
}
}
}
src/logging.rs
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)
}
}
}
}
src/main.rs
use rmcp::transport::io::stdio;
let transport = stdio();
server.run(transport).await?;
src/main.rs
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?;
src/main.rs
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
Dockerfile
# Build stage
FROM rust:1.75 as builder
WORKDIR /app
COPY . .
RUN cargo build --release
# Runtime stage
FROM debian:bookworm-slim
RUN 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_config.json
{
"mcpServers": {
"rust-server": {
"command": "/path/to/target/release/mcp-server"
}
}
}
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"
# Для HTTP
axum = "0.7"
rmcp = { version = "0.1", features = ["server", "transport-sse-server"] }