最近很多人把MCP(ModelContextProtocol)当成“给大模型装插件”的标准接口:你把自己的能力(工具、数据源、业务系统)封装成一个MCPServer,模型侧(MCPClient,比如ClaudeDesktop、Cursor等)就能发现并调用你的工具。这篇文
最近很多人把 MCP(Model Context Protocol) 当成“给大模型装插件”的标准接口:
你把自己的能力(工具、数据源、业务系统)封装成一个 MCP Server,模型侧(MCP Client,比如 Claude Desktop、Cursor 等)就能发现并调用你的工具。
这篇文章带你用 Rust 写一个最小可用 MCP Server:
add 工具(可扩展成查数据库、发交易、查链上数据…)本文尽量“工程化”:结构清晰、代码可复制、方便你后续塞进真实业务。
MCP = 一个标准化协议,让模型以“工具调用”的方式连接外部能力。
你可以把 MCP Server 理解成一个“工具盒服务”,它向外声明:
客户端会把模型的工具调用请求转成协议消息发给你,你返回结果即可。
适合,而且很香:
MCP 常见传输方式有两类(你可能在不同客户端看到不同配置):
为了让读者“马上跑起来”,本文用 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.rsuse 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() }) }
}
}
src/tools.rsuse 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 等类型(看客户端支持)src/main.rsmod 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
不同客户端配置略有差异,但 stdio 方式基本都需要告诉客户端:
你把服务写好以后,往往只需要把 cargo build --release 生成的可执行文件路径填进去即可。
小技巧:真实业务里通常会加上日志(stderr),避免污染 stdout(stdout 必须保持协议消息)。
当你从 add 进化到真实工具(比如“查链上价格/下单/访问内部系统”)时,建议直接按下面做:
tools/:只做参数校验 + 路由services/:业务逻辑(RPC、DB、缓存)clients/:外部依赖封装(HTTP、gRPC、Sui/EVM RPC)Rust + Tokio 很适合在 call_tool 内部:
Semaphore 控制并发governor 做限流工具调用成功率=参数 schema 清晰度。 强烈建议:把 schema 生成/维护做成常量或用宏管理,避免漂移。
MCP 本质是“模型工具调用的标准接口”。Rust 的优势在于:
如果你已经在做链上/日志/高并发任务系统,把这些能力封装成 MCP 工具,会非常自然:
模型负责“决策 + 调度”,MCP Server 负责“可靠执行”。
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!