Rust MCP Server(rmcp 3.x)
用 rmcp crate 把现有 Rust 工具/CLI 暴露为 MCP tools,让协议系 agent 能调用。MCP 是 CLI 的薄适配层——core 只暴露 CLI/函数,server 只做包装。
何时使用
- 已有 Rust 后端/CLI,想让 Claude Desktop、Cursor、Codex 等 agent 能调用
- 用户说"做 MCP server"、"让 AI agent 能调用我们的工具"
- 已有 Python MCP server(见 mcp-python-patterns),需要 Rust 侧对应物
Cargo.toml 依赖
[dependencies]
rmcp = { version = "3.1", features = ["server", "transport-io", "macros"] }
schemars = { version = "1.0", features = ["derive"] } # ← 必须 1.0,见坑表
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
features 名:server + transport-io(stdio)。没有 server-stdio feature(不存在,写了会 resolve 冲突)。查真实 feature 列表:crates.io/api/v1/crates/rmcp/<version> 的 JSON。
最小 server(宏模式)
use rmcp::{
ServerHandler, ServiceExt,
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
tool, tool_handler, tool_router,
};
use rmcp::schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
pub struct MyServer { tool_router: ToolRouter<Self> }
impl MyServer {
pub fn new() -> Self { Self { tool_router: Self::tool_router() } }
}
#[tool_handler(router = self.tool_router)]
impl ServerHandler for MyServer {}
#[derive(Serialize, Deserialize, JsonSchema)]
struct ListParam { #[serde(default)] category: Option<String> }
#[tool_router(router = tool_router)]
impl MyServer {
/// 工具描述(agent 看到的就是这个)。
#[tool(name = "list_items", description = "List items. Returns JSON array.")]
pub async fn list_items(&self, p: Parameters<ListParam>) -> String {
// 返回 String(JSON 文本),MCP 会包成 text content
serde_json::to_string(&vec![1, 2, 3]).unwrap()
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let running = MyServer::new().serve(rmcp::transport::io::stdio()).await?;
running.waiting().await?; // ← 必须!否则进程 init 后立即退出
Ok(())
}
验证(python 裸客户端,不用 MCP SDK)
import subprocess, json, select
p = subprocess.Popen(['./target/debug/my-mcp'], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
def send(o): p.stdin.write(json.dumps(o) + '\n'); p.stdin.flush()
def recv(t=3):
r,_,_ = select.select([p.stdout], [], [], t)
return p.stdout.readline().strip() if r else None
send({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}}})
print("init:", recv())
send({"jsonrpc":"2.0","method":"notifications/initialized"})
send({"jsonrpc":"2.0","id":2,"method":"tools/list"})
print("tools:", recv(5))
stdio 传输是 JSON-Lines(每行一个 JSON + \n),不是 Content-Length 帧。
详细坑位
见 references/rmcp-3.1-pitfalls.md。高频要点:
- schemars 版本必须和 rmcp 匹配:rmcp 3.1 依赖 schemars 1.0。用 0.8 derive 的
JsonSchema和 rmcp 的Parameters<T>类型不兼容 →trait bound unsatisfied。要么rmcp::schemars::JsonSchema(derive 宏要独立 schemars 开 derive feature),要么独立 schemars 1.0 + derive。 RunningService::waiting().await必须调用:serve()返回后 main 一结束RunningService被 drop → 连接关闭 → 进程退出。症状:init 响应正常,但 tools/list 时 BrokenPipe。这是本会话最隐蔽的坑。- 没有
#[tool(aggr)]属性:参数就是Parameters<T>类型,直接作为 fn 参数,不要加任何 attribute。 - 写工具(提权/确认)也走同一 server:把写操作委托给共享 launcher(helper 弹窗),AI 无法绕过确认——MCP tool 只是调用入口,硬确认在运行时。
参考文件
references/rmcp-3.1-pitfalls.md— rmcp 3.1 完整坑位(版本、宏、stdio、进程生命周期)