最近很多人把 MCP(Model Context Protocol) 当成“给大模型装插件”的标准接口。
你把自己的能力(工具、数据源、业务系统)封装成一个 MCP Server,模型侧(MCP Client,比如 Claude Desktop、Cursor 等)就能发现并调用你的工具。

这篇文章带你用 Rust 写一个最小可用 MCP Server:
- 支持基础握手/初始化
- 暴露一个
add 工具(可扩展成查数据库、发交易、查链上数据…)
- 让 MCP 客户端能调用它并拿到结果
本文尽量“工程化”:结构清晰、代码可复制、方便你后续塞进真实业务。
MCP 是什么:一句话搞懂
MCP = 一个标准化协议,让模型以“工具调用”的方式连接外部能力。
你可以把 MCP Server 理解成一个“工具盒服务”,它向外声明:
- 我有哪些 tools
- 每个 tool 的参数 schema 是啥
- 调用后返回什么结果
客户端会把模型的工具调用请求转成协议消息发给你,你返回结果即可。
Rust 适合写 MCP 吗?
适合,而且很香:
- 稳定:长时间运行不崩(工具服务常驻)
- 性能:高并发、低延迟(未来 tools 可能大量调用)
- 生态:
Tokio + serde_json + axum/warp 很成熟
- 可扩展:后续接数据库、RPC、链上节点、缓存、队列都容易
选型:MCP 的传输方式怎么做?
MCP 常见传输方式有两类(你可能在不同客户端看到不同配置):
- stdio:客户端启动你的进程,通过 stdin/stdout 交换 JSON 消息
- HTTP / SSE / WebSocket:以网络服务形式通信
为了让读者“马上跑起来”,本文用 stdio 做最小可用版本(最通用、部署最简单)。
项目结构
mcp-rust-demo/
Cargo.toml
src/
main.rs
protocol.rs
tools.rs
依赖(核心:tokio + serde):
# Cargo.toml
[package]
name = "mcp-rust-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
anyhow = "1"
定义协议消息(最小集合)
我们先不追求“覆盖协议全部字段”,而是实现 MCP 常用最小链路:
initialize:客户端初始化
tools/list:列出工具
tools/call:调用工具
src/protocol.rs
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcRequest {
pub id: Value,
pub method: String,
pub params: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcResponse {
pub id: Value,
pub result: Option<Value>,
pub error: Option<RpcError>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct RpcError {
pub code: i32,
pub message: String,
}
impl RpcResponse {
pub fn ok(id: Value, result: Value) -> Self {
Self { id, result: Some(result), error: None }
}
pub fn err(id: Value, code: i32, message: impl Into<String>) -> Self {
Self { id, result: None, error: Some(RpcError { code, message: message.into() }) }
}
}
写一个工具:add(但结构按真实业务来)
use anyhow::Result;
use serde_json::{json, Value};
pub fn tools_list() -> Value {
// MCP 工具通常需要:name / description / inputSchema
json!({
"tools": [
{
"name": "add",
"description": "Add two numbers and return the sum.",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
}
}
]
})
}
pub fn call_tool(name: &str, args: Value) -> Result<Value> {
match name {
"add" => {
let a = args.get("a").and_then(|v| v.as_f64()).unwrap_or(0.0);
let b = args.get("b").and_then(|v| v.as_f64()).unwrap_or(0.0);
Ok(json!({
"content": [
{ "type": "text", "text": format!("sum = {}", a + b) }
]
}))
}
_ => Ok(json!({
"content": [
{ "type": "text", "text": format!("unknown tool: {}", name) }
]
})),
}
}
你会发现返回结构是 content 数组,这样更贴近“模型可读输出”:
text:文本
- 后续你还可以扩展
image、json 等类型(看客户端支持)
MCP 主循环:读 stdin,写 stdout
src/main.rs
mod protocol;
mod tools;
use protocol::{RpcRequest, RpcResponse};
use serde_json::{json, Value};
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let stdin = io::stdin();
let mut reader = io::BufReader::new(stdin).lines();
let stdout = io::stdout();
let mut writer = io::BufWriter::new(stdout);
while let Some(line) = reader.next_line().await? {
if line.trim().is_empty() {
continue;
}
let req: RpcRequest = match serde_json::from_str(&line) {
Ok(v) => v,
Err(e) => {
// 解析失败就忽略或回错(这里选择忽略)
eprintln!("invalid json: {e}");
continue;
}
};
let id = req.id.clone();
let resp = handle(req).unwrap_or_else(|e| {
RpcResponse::err(id, -32000, format!("internal error: {e}"))
});
let out = serde_json::to_string(&resp)?;
writer.write_all(out.as_bytes()).await?;
writer.write_all(b"\n").await?;
writer.flush().await?;
}
Ok(())
}
fn handle(req: RpcRequest) -> anyhow::Result<RpcResponse> {
let id = req.id;
match req.method.as_str() {
"initialize" => {
// 初始化时返回 server capabilities / name 等
Ok(RpcResponse::ok(id, json!({
"protocolVersion": "0.1",
"serverInfo": { "name": "mcp-rust-demo", "version": "0.1.0" },
"capabilities": { "tools": {} }
})))
}
"tools/list" => {
Ok(RpcResponse::ok(id, tools::tools_list()))
}
"tools/call" => {
let params = req.params.unwrap_or(Value::Null);
let name = params.get("name").and_then(|v| v.as_str()).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(Value::Null);
let result = tools::call_tool(name, args)?;
Ok(RpcResponse::ok(id, result))
}
_ => Ok(RpcResponse::err(id, -32601, "Method not found")),
}
}
本地测试:不用客户端也能自测
编译运行:
cargo run
然后手动喂一条 initialize:
echo '{"id":1,"method":"initialize","params":{}}' | cargo run
你会看到 stdout 输出一个 JSON response。
再测工具列表:
echo '{"id":2,"method":"tools/list","params":{}}' | cargo run
测试工具调用:
echo '{"id":3,"method":"tools/call","params":{"name":"add","arguments":{"a":1,"b":2}}}' | cargo run
接入 Claude / MCP 客户端(思路)
不同客户端配置略有差异,但 stdio 方式基本都需要告诉客户端:
- 命令:运行你的二进制文件
- 参数:可选
- 环境变量:可选(比如 API KEY)
你把服务写好以后,往往只需要把 cargo build --release 生成的可执行文件路径填进去即可。
小技巧:真实业务里通常会加上日志(stderr),避免污染 stdout(stdout 必须保持协议消息)。
工程化建议:把 demo 变成可用服务
当你从 add 进化到真实工具(比如“查链上价格/下单/访问内部系统”)时,建议直接按下面做:
1. 工具分层
tools/:只做参数校验 + 路由
services/:业务逻辑(RPC、DB、缓存)
clients/:外部依赖封装(HTTP、gRPC、Sui/EVM RPC)
2. 并发与限流
Rust 配合 Tokio 很适合在 call_tool 内部:
- 用
Semaphore 控制并发
- 用
governor 做限流
- 对 RPC 节点做池化与熔断
3. schema 一定要写好
工具调用成功率=参数 schema 清晰度。
强烈建议:把 schema 生成/维护做成常量或用宏管理,避免漂移。
结语:Rust 写 MCP 的价值
MCP 本质是“模型工具调用的标准接口”。Rust 的优势在于:
- 你可以把它当成一个长跑型的工具服务
- 随时扩展:更多工具、更重的并发、更复杂的 IO
- 最终把它变成你团队的“模型能力入口”
如果你已经在做链上/日志/高并发任务系统,把这些能力封装成 MCP 工具,会非常自然:
模型负责“决策 + 调度”,MCP Server 负责“可靠执行”。
希望这篇从零开始的Rust实战指南能帮助你快速上手人工智能工具集成。如果你对构建更复杂的系统架构感兴趣,欢迎在云栈社区与更多开发者交流探讨。