Agent中MCP 协议详解:从“为什么要它“到“怎么用它“,最全最完整
MCP 协议详解:从"为什么要它"到"怎么用它",看完就会
没有 MCP 之前,每个 AI 工具都自己搞一套接口——工具 A 用 REST API,工具 B 用命令行,工具 C 用 gRPC。Claude Desktop 想用一个搜索工具,得单独写适配代码,换一个工具又得重写。
MCP(Model Context Protocol)是 Anthropic 定的"AI 工具通信标准",已成行业事实标准。就像 USB 是硬件接口标准,MCP 是 AI 应用的接口标准:所有工具走同一个协议说话,Claude Desktop / Cursor / 任何支持 MCP 的 host 都能直接用,零适配。
本文拆开 langchainrust 的 MCP 实现,每个概念都带例子,看完你就知道怎么用。
一、两个角色:Client 和 Server
先看一个生活中的类比:
你去餐厅吃饭。你是 Client,服务员是 Server。你看菜单(
tools/list),点菜(tools/call),服务员把菜端给你。
Claude Desktop / Cursor / 你的 Agent(Client)
│
│ MCP 协议(JSON-RPC 2.0)
│
▼
MCP Server(提供工具的一方)
├─ 文件读取工具
├─ 搜索工具
└─ 数据库查询工具
- MCP Server:我有工具,谁要来拿(餐厅有菜,谁来点)
- MCP Client:我要用你的工具(我要点菜)
langchainrust 两个都实现了——既能当 Client 去连别人的工具,也能当 Server 把自己的工具暴露给别人。
实际例子:
| 场景 | 你是 Client 还是 Server |
|---|---|
| 你的 Agent 要调 Claude Desktop 的文件系统工具 | Client——去连别人的 Server |
| 你写了一个 Rust 工具,想让 Claude Desktop 直接用 | Server——把工具暴露给别人 |
| 你有 3 个 Agent,想让它们共享同一套搜索工具 | 一个 Server + 三个 Client |
二、通信协议:JSON-RPC 2.0
MCP 底层是 JSON-RPC 2.0——每条消息就是一行 JSON。就像发微信,一条消息一个气泡,不会粘在一起。
三种消息
① 请求(Client 发,等回复——像点菜,等服务员确认):
{"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": null}
id 是请求编号,Server 回复时带上同一个 id,Client 才知道这条回复对应哪个请求。
② 响应(Server 回——像服务员端菜):
{"jsonrpc": "2.0", "id": 1, "result": {"tools": [...]}}
注意:响应的 id 和请求的 id 一致。
③ 通知(不等回复——像说"谢谢",不等服务员回"不客气"):
{"jsonrpc": "2.0", "method": "notifications/initialized"}
通知没有 id,发了就完了,不等回复。
完整的一次工具调用(从 JSON 角度看)
假设 Agent 要调用 read_file 工具读 /tmp/hello.txt,底层 JSON 长这样:
Client 发出请求:
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/hello.txt"}}}
Server 回复:
{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Hello, World!"}],"is_error":false}}
就这么简单——一行 JSON 过去,一行 JSON 回来。没有 HTTP 头、没有 WebSocket 帧、没有 gRPC 序列化,纯粹的文本行协议。
三、连接和握手
连接后不是直接干活,先握手确认双方都支持这个协议版本——就像打电话先说"喂,听得到吗",确认通了再说正事。
握手过程
Client Server
│ │
│── initialize ────────────────────→ │
│ "你好,我支持协议 2024-11-05" │
│ "我叫 langchainrust-mcp-client" │
│ │
│←── initialize response ───────────│
│ "你好,我也支持 2024-11-05" │
│ "我叫 langchainrust-mcp-server" │
│ "我能提供 tools" │
│ │
│── notifications/initialized ────→ │ "好的,开始干活"(不等回复)
对应的实际 JSON:
→ Client 发出:
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"langchainrust-mcp-client","version":"0.3.0"}}}
← Server 回复:
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"langchainrust-mcp-server","version":"0.5.0"}}}
→ Client 发出通知(无 id):
{"jsonrpc":"2.0","method":"notifications/initialized"}
代码在 client.rs:63-79:
// 1. 发 initialize 请求
let init_params = json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": { "name": "langchainrust-mcp-client", "version": "0.3.0" }
});
client.send_request("initialize", Some(init_params)).await?;
// 2. 发 initialized 通知(无 id,不等响应)
client.inner.transport.notify("notifications/initialized", None).await?;
30 秒超时:整个握手必须在 30 秒内完成,否则报错。防止 Server 卡住导致 Client 无限等待。
四、传输层:Stdio 和 SSE
两种连接方式,适用于不同场景。
Stdio — 子进程通信(最常见)
场景:你的 Rust Agent 要调一个 Node.js 写的文件系统工具。
Client 启动 Server 子进程,通过 stdin/stdout 交换 JSON——就像你启动一个命令行程序,给它输命令,它打印结果。
Client 进程 Server 子进程
│ │
│── 写 JSON 到 stdin ────────→ │ 子进程读到请求
│←── 从 stdout 读 JSON ────── │ 子进程处理后写回
具体例子:你用 npx 启动 Anthropic 的文件系统 MCP Server:
let client = MCPClient::connect(MCPConfig::stdio(
"npx", // 命令
vec![ // 参数
"@anthropic/mcp-server-filesystem".into(),
"/tmp".into(), // 允许访问的目录
],
)).await?;
底层发生了什么?
1. Rust 启动子进程:npx @anthropic/mcp-server-filesystem /tmp
2. 子进程运行起来,等待 stdin 输入
3. Rust 往子进程 stdin 写入:{"jsonrpc":"2.0","id":1,"method":"initialize",...}\n
4. 子进程从 stdout 输出:{"jsonrpc":"2.0","id":1,"result":{...}}\n
5. 握手完成,后续 tools/list、tools/call 同理
关键细节:request_lock
并发请求时,"写 stdin → 读 stdout"这个操作必须原子——不然两个请求同时写,回复错位就乱了。request_lock 就是保证这一点:
pub struct StdioTransport {
stdin: Arc<Mutex<ChildStdin>>, // 子进程的 stdin
stdout: Arc<Mutex<BufReader<ChildStdout>>>, // 子进程的 stdout
child: Arc<Mutex<Child>>, // 子进程句柄
request_lock: Arc<Mutex<()>>, // ← 请求级互斥锁
}
stderr 单独开个后台任务打印,不阻塞主通信。
SSE — HTTP 通信
场景:MCP Server 跑在远程机器上,你的 Agent 通过 HTTP 连过去。
- 先 GET
/sse端点,建立 SSE 长连接 - Server 推送一个
endpoint事件,告诉 Client “发消息用这个 URL” - Client 以后用 HTTP POST 往那个 URL 发请求
Client Server
│ │
│── GET /sse ──────────────────→ │ 建立 SSE 长连接
│←── event: endpoint ─────────── │ Server 推送 POST 地址
│ data: http://host/msg │
│ │
│── POST http://host/msg ──────→ │ 以后发请求走这个地址
│←── JSON-RPC response ───────── │
具体例子:
let client = MCPClient::connect(MCPConfig::sse(
"http://192.168.1.100:3001/sse" // 远程 MCP Server 的 SSE 端点
)).await?;
SSE 也有 30 秒发现超时(SSE_DISCOVER_TIMEOUT)——如果 30 秒内 Server 没推送 endpoint 事件,就报错。
两种传输层对比
| Stdio | SSE | |
|---|---|---|
| 通信方式 | 子进程 stdin/stdout | HTTP + SSE |
| 适用场景 | 本地工具(npm 包、命令行工具) | 远程服务(云端 MCP Server) |
| 启动方 | Client 启动 Server 子进程 | Server 独立运行,Client 连过去 |
| 延迟 | 低(进程间通信) | 较高(网络往返) |
| 部署 | 不用单独部署 Server | 需要部署 Server |
五、MCPClient:去连别人的工具(完整例子)
场景:你的 Agent 要读文件
// ① 连接:启动 MCP Server 子进程
let client = MCPClient::connect(MCPConfig::stdio(
"npx",
vec!["@anthropic/mcp-server-filesystem".into(), "/tmp".into()],
)).await?;
// ② 发现:问 Server 有哪些工具
let tools = client.list_tools().await?;
for tool in &tools {
println!("工具: {} - {}", tool.name, tool.description);
}
// 输出:
// 工具: read_file - Read the contents of a file
// 工具: write_file - Write content to a file
// 工具: list_directory - List directory contents
// 工具: search_files - Search for files matching a pattern
// ③ 调用:读一个文件
let result = client.call_tool("read_file", json!({"path": "/tmp/hello.txt"})).await?;
println!("文件内容: {}", result.text());
// 输出:文件内容: Hello, World!
// ④ 调用:写一个文件
let result = client.call_tool("write_file", json!({
"path": "/tmp/output.txt",
"content": "这是 MCP 写入的内容"
})).await?;
println!("写入结果: {}", result.text());
// ⑤ 关闭
client.close().await?;
底层 JSON 对应(③ 调用那步)
Client 发出:
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"read_file","arguments":{"path":"/tmp/hello.txt"}}}
Server 回复:
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"Hello, World!"}],"is_error":false}}
转为 Agent 可用的 BaseTool
as_tools() 把 MCP 工具自动适配为 BaseTool——这就是 MCP 的核心价值:
// 1. 连接 + 发现工具
let client = MCPClient::connect(MCPConfig::stdio("npx", vec![
"@anthropic/mcp-server-filesystem".into(), "/tmp".into(),
])).await?;
client.list_tools().await?;
// 2. 转为 BaseTool
let agent_tools: Vec<Arc<dyn BaseTool>> = client.as_tools().await;
// 3. Agent 直接用,和本地工具一模一样
let agent = Agent::new(llm)
.with_tools(agent_tools); // MCP 远程工具混在本地工具里,Agent 不知道也不关心
适配逻辑在 tool_adapter.rs——MCPToolAdapter 实现 BaseTool trait,run() 方法内部调 client.call_tool():
impl BaseTool for MCPToolAdapter {
fn name(&self) -> &str { &self.definition.name }
fn description(&self) -> &str { &self.definition.description }
async fn run(&self, input: String) -> Result<String, ToolError> {
let args: Value = serde_json::from_str(&input)?; // Agent 传来的 JSON 字符串
let result = self.client.call_tool(&self.definition.name, args).await?; // 远程调用
Ok(result.text()) // 返回文本结果
}
}
为什么说这是核心价值? 没有 MCP 之前,你想用一个远程工具,得自己写:
- 怎么连接(HTTP?gRPC?命令行?)
- 怎么序列化/反序列化参数
- 怎么处理错误
- 怎么把它适配成 Agent 能用的
BaseTool
有了 MCP,这些全部不用写——连接、序列化、适配全是框架的事。你只管 connect → list_tools → as_tools,三步完事。
六、MCPServer:把自己的工具暴露给别人(完整例子)
场景:你写了一个计算器工具,想让 Claude Desktop 直接调
第一步:定义你的工具(实现 BaseTool)
struct CalculatorTool;
#[async_trait]
impl BaseTool for CalculatorTool {
fn name(&self) -> &str { "calculator" }
fn description(&self) -> &str { "计算数学表达式,支持加减乘除" }
fn args_schema(&self) -> Option<Value> {
Some(json!({
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "数学表达式,如 '2 + 3 * 4'"
}
},
"required": ["expression"]
}))
}
async fn run(&self, input: String) -> Result<String, ToolError> {
let args: Value = serde_json::from_str(&input)
.map_err(|e| ToolError::ExecutionFailed(format!("参数解析失败: {}", e)))?;
let expr = args["expression"].as_str()
.ok_or_else(|| ToolError::ExecutionFailed("缺少 expression 参数".into()))?;
// 简化:这里用 meval 库计算表达式
let result = meval::eval_str(expr)
.map_err(|e| ToolError::ExecutionFailed(format!("计算失败: {}", e)))?;
Ok(format!("{} = {}", expr, result))
}
}
第二步:创建 MCP Server 并注册工具
let server = MCPServer::new()
.with_server_info("my-calculator", "1.0.0")
.with_tool(Arc::new(CalculatorTool));
// 启动:从 stdin 读请求,处理后写回 stdout
server.serve_stdio().await?;
第三步:Claude Desktop 配置
在 Claude Desktop 的 claude_desktop_config.json 里加:
{
"mcpServers": {
"calculator": {
"command": "your-calculator-binary",
"args": []
}
}
}
这样 Claude Desktop 启动时会自动启动你的二进制,走 MCP 协议通信。用户在 Claude Desktop 里问"123 * 456 等于多少",Claude 就会调你的计算器工具。
Server 收到请求后的处理
当 Claude Desktop 调你的计算器工具时,底层 JSON 流:
← 收到请求:
{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"calculator","arguments":{"expression":"123 * 456"}}}
→ 回复结果:
{"jsonrpc":"2.0","id":5,"result":{"content":[{"type":"text","text":"123 * 456 = 56088"}],"is_error":false}}
server.rs:59-93 的 handle_request() 分发逻辑:
match req.method.as_str() {
"initialize" => /* 返回协议版本 + 能力 + serverInfo */,
"tools/list" => /* 返回所有工具的定义 */,
"tools/call" => /* 找到工具,调 run(),返回结果 */,
_ => /* method_not_found 错误 */,
}
tools/call 的详细流程:
// 1. 从 params 取工具名和参数
let name = params.get("name").and_then(|v| v.as_str()); // "calculator"
let arguments = params.get("arguments").cloned().unwrap_or(Value::Null); // {"expression": "123 * 456"}
// 2. 找到工具
let tool = self.tools.iter().find(|t| t.name() == name); // 找到 CalculatorTool
// 3. 调用
match tool {
Some(t) => {
let result = t.run(input_str).await;
// 成功 → {"content": [{"type":"text","text":"123 * 456 = 56088"}], "is_error": false}
// 失败 → {"content": [{"type":"text","text":"计算失败: ..."}], "is_error": true}
}
None => /* 返回 "未知工具" 错误 */
}
serve_stdio:Server 的主循环
pub async fn serve_stdio(&self) -> Result<(), MCPError> {
loop {
// 从 stdin 读一行 JSON
let mut line = String::new();
reader.read_line(&mut line).await?;
// 通知(无 id)忽略,请求(有 id)处理
let msg: ServerMessage = serde_json::from_str(trimmed)?;
let id = match msg.id { Some(id) => id, None => continue };
// 处理请求,写回响应
let resp = self.handle_request(req).await;
let json = serde_json::to_string(&resp)?;
stdout.write_all(json.as_bytes()).await?;
stdout.write_all(b"\n").await?; // ← 每条响应后面跟一个换行
}
}
一行请求 → 一行响应,循环往复。
七、六大原语详解(每个都有例子)
MCP 协议定义了 6 种交互方式(叫"原语"),langchainrust v0.5.0 全部定义了类型,tools 完整实现,其余 handler 预留了 method_not_found。
1. tools — 调工具(✅ 完整实现)
最核心的原语,只有这个完整实现了。
tools/list:Server 告诉 Client 有哪些工具tools/call:Client 调用某个工具
实际例子:
Agent: "帮我查一下北京明天的天气"
│
├─ Agent 发现有个 weather 工具(来自 tools/list)
│
├─ tools/call: {"name": "weather", "arguments": {"city": "北京", "date": "明天"}}
│
└─ Server 返回: {"content": [{"type": "text", "text": "北京明天晴,15~28℃"}]}
这是 MCP 存在的理由——让 AI 能调用外部工具。
2. resources — 给 AI 读数据
Server 把文件、数据库表、API 响应等数据源暴露给 Client。和 tools 不同:resources 是"读"的,tools 是"做"的。
实际例子:
你的公司有个内部 wiki MCP Server,暴露了这些 resources:
wiki://project-architecture → 项目架构文档
wiki://api-spec → API 接口规范
wiki://onboarding-guide → 新人入职指南
Agent 不用调工具,直接读 resource:
resources/read: {"uri": "wiki://project-architecture"}
Server 返回:
项目架构文档的完整内容(纯文本或 Markdown)
和 tools 的区别:
| tools | resources | |
|---|---|---|
| 动作 | 做(搜索、计算、写文件) | 读(读文档、读数据) |
| 例子 | search("Rust 教程") | 读 wiki://rust-tutorial |
| 有副作用吗 | 可能有(写文件、发邮件) | 没有(只读) |
3. prompts — 预设 prompt 模板
Server 提供预写好的 prompt 模板,Client 可以直接用。
实际例子:
代码审查 MCP Server 暴露了这些 prompts:
code-review → 通用代码审查 prompt
security-review → 安全审查 prompt
performance-review → 性能审查 prompt
Agent 要做代码审查:
prompts/get: {"name": "code-review", "arguments": {"code": "fn main() { ... }"}}
Server 返回填好参数的 prompt:
"请审查以下代码,关注:1. 正确性 2. 可读性 3. 安全性\n代码:fn main() { ... }"
为什么需要 prompt 模板? 每次让 Agent 审查代码,你不用自己拼 prompt,Server 已经帮你写好了,填个参数就行——就像 Word 模板,不用每次从空白文档开始排版。
4. completion — 自动补全
Server 给 Client 提供输入补全建议。
实际例子:
你在 Claude Desktop 里搜内部知识库:
你输入: "张"
completion: {"ref": {"uri": "wiki://"}, "argument": {"name": "query"}, "value": "张"}
Server 返回补全建议:
"张三 - 工程师,负责后端"
"张伟 - 产品经理,负责用户端"
"张的项目 - 搜索引擎优化"
就像 Google 搜索框的自动补全,但补全内容来自 MCP Server 的内部数据。
5. elicitation — Server 反问用户
Server 执行任务时发现缺信息,反过来问 Client 要——就像你去餐厅点餐,服务员问"辣度要几级?"。
实际例子:
Agent 调"订机票"工具,只传了出发地:
tools/call: {"name": "book_flight", "arguments": {"from": "北京"}}
Server 发现缺目的地,通过 elicitation 反问:
elicitation: {
"message": "你要去哪个城市?",
"options": ["上海", "广州", "深圳", "成都"]
}
Client 弹出对话框让用户选 → 用户选"上海" → 传回去继续执行
本质:工具不只是被动执行,还能主动要信息——像真实的服务员,不只接单,还会追问。
6. roots — 文件访问范围声明
Client 告诉 Server:“你只能访问这些目录,别的别碰”——就像你请保洁阿姨打扫,你说"只打扫客厅和卧室,书房别进"。
实际例子:
你让 Claude Desktop 连了文件系统 MCP Server。
不设 roots(危险!):
Agent 可能读 /etc/passwd、写 /bin/ls → 安全事故
设了 roots:
roots: {"roots": [{"uri": "file:///home/user/project/"}]}
Agent 只能访问 /home/user/project/ 下的文件
请求读 /etc/passwd → Server 拒绝
本质是沙箱边界声明——在 AI 能动你的文件系统之前,先画个圈。
7. sampling — Server 反向调 LLM
Server 在执行任务时,反过来请求 Client 帮它调一次 LLM——就像你去修车,修车师傅发现需要用你的手机查个零件号,借用你的手机用一下。
实际例子:
数据分析 MCP Server,用户让它"分析这份数据":
1. Server 读完数据,发现需要 LLM 生成摘要
2. 但 Server 自己没模型(它只是个数据处理工具)
3. 通过 sampling 请求 Client:"帮我用你的 Claude 总结一下这些数据"
4. Client 用自己的 LLM 调完,返回摘要
5. Server 拿到摘要,继续工作
本质:Server 没模型时,借 Client 的模型用——避免每个 MCP Server 都要自己接 LLM。
六大原语一览
| 原语 | 方向 | 一句话 | 生活中的类比 | langchainrust 现状 |
|---|---|---|---|---|
| tools | Client → Server | 调工具 | 点菜 | ✅ 完整实现 |
| resources | Server → Client | 给 AI 读数据 | 看菜单 | 类型定义有,handler 留 method_not_found |
| prompts | Server → Client | 给 AI 预设 prompt | 点套餐 | 同上 |
| completion | Server → Client | 输入自动补全 | 搜索框补全 | 同上 |
| elicitation | Server → Client | 工具反问用户 | 服务员追问辣度 | 同上 |
| roots | Client → Server | 声明文件访问范围 | 说"只打扫客厅" | 同上 |
| sampling | Server → Client | Server 借 Client 的模型 | 借你手机查个号 | 同上 |
八、工具调用结果的三种内容类型
MCPToolResult 的 content 是 Vec<MCPContent>,每项可以是三种类型:
pub enum MCPContent {
Text { text: String }, // 文本(最常见)
Image { data: String, mime_type: String }, // 图片(base64)
Resource { uri: String, name: String }, // 资源引用(指向另一个 resource)
}
三种类型各什么时候出现:
| 类型 | 什么时候返回 | 具体例子 |
|---|---|---|
Text | 大多数工具 | 搜索工具返回 "Rust 是系统编程语言..." |
Image | 截图/图表工具 | 截图工具返回 base64 编码的 PNG |
Resource | 数据库/API 工具 | 数据库工具返回 db://users/table,让 Client 自己去读 |
实际例子:一个截图 + OCR 工具可能返回混合内容:
{
"content": [
{"type": "text", "text": "截图中检测到 3 个文本区域:"},
{"type": "image", "data": "iVBORw0KGgo...", "mime_type": "image/png"},
{"type": "text", "text": "1. 标题:季度报告\n2. 数据:收入增长 15%\n3. 备注:Q4 数据待更新"}
],
"is_error": false
}
代码里 text() 方法提取所有文本内容,用换行连接:
impl MCPToolResult {
pub fn text(&self) -> String {
self.content
.iter()
.filter_map(|c| c.as_text().map(|s| s.to_string()))
.collect::<Vec<_>>()
.join("\n")
}
}
九、错误处理
JSON-RPC 2.0 标准错误码,和 HTTP 状态码一个思路——用数字区分错误类型:
| code | 含义 | 什么时候出现 |
|---|---|---|
| -32700 | Parse error | 发了非法 JSON,比如少了个引号 |
| -32600 | Invalid Request | 请求格式不对,比如缺 method 字段 |
| -32601 | Method not found | 调了不存在的方法,比如 resources/list(还没实现) |
| -32602 | Invalid params | 参数无效,比如 tools/call 没传 name |
| -32603 | Internal error | Server 内部错误 |
实际例子:
Client 发了调了不存在的方法:
→ {"jsonrpc":"2.0","id":5,"method":"resources/list","params":null}
← {"jsonrpc":"2.0","id":5,"error":{"code":-32601,"message":"Method not found"}}
Client 调工具但没传工具名:
→ {"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"arguments":{}}}
← {"jsonrpc":"2.0","id":6,"error":{"code":-32602,"message":"缺少 name 参数"}}
Client 发了非法 JSON:
→ {jsonrpc: 2.0, id: 7} ← 少了引号,不是合法 JSON
← {"jsonrpc":"2.0","id":null,"error":{"code":-32700,"message":"解析请求失败: ..."}}
langchainrust 目前用了两个标准错误码:
pub fn method_not_found() -> Self { Self::new(-32601, "Method not found") }
pub fn invalid_params(msg: impl Into<String>) -> Self { Self::new(-32602, msg) }
十、完整流程一图流
Client 连 Server 调工具
以"Agent 要读文件"为例,完整过程:
你的代码
│
▼
MCPClient::connect(Stdio { "npx", ["@anthropic/mcp-server-filesystem", "/tmp"] })
│
├─ 启动子进程 npx @anthropic/mcp-server-filesystem /tmp
├─ 发 initialize 请求 ──→ Server 回 {"protocolVersion":"2024-11-05","capabilities":{"tools":{}}}
├─ 发 initialized 通知 ──→ (不等回复)
│
▼ 握手完成
│
MCPClient::list_tools()
│
├─ 发 tools/list ──→ Server 回 [read_file, write_file, list_directory, search_files]
│
▼
│
MCPClient::call_tool("read_file", {"path": "/tmp/hello.txt"})
│
├─ 发 tools/call ──→ Server 读文件 ──→ 回 {"content":[{"type":"text","text":"Hello, World!"}]}
│
▼
│
MCPClient::as_tools()
│
├─ read_file → MCPToolAdapter { name: "read_file", ... }
├─ write_file → MCPToolAdapter { name: "write_file", ... }
├─ list_directory → MCPToolAdapter { name: "list_directory", ... }
├─ search_files → MCPToolAdapter { name: "search_files", ... }
│
▼
│
Agent::new(llm).with_tools(client.as_tools().await)
│
└─ Agent 调 read_file 时,MCPToolAdapter.run() 内部调 client.call_tool()
Agent 不知道也不关心这是 MCP 远程工具还是本地工具
Server 把工具暴露给 Claude Desktop
以"计算器工具暴露给 Claude Desktop"为例:
Claude Desktop
│
│ MCP 协议(Stdio)
│
▼
MCPServer::new()
.with_server_info("my-calculator", "1.0.0")
.with_tool(Arc::new(CalculatorTool))
.serve_stdio()
│
├─ 从 stdin 读 JSON-RPC 请求
│
├─ "initialize" → 返回 {"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"my-calculator","version":"1.0.0"}}
│
├─ "tools/list" → 返回 {"tools":[{"name":"calculator","description":"计算数学表达式",...}]}
│
├─ "tools/call" →
│ 请求: {"name":"calculator","arguments":{"expression":"2+3*4"}}
│ 执行: CalculatorTool.run() → "2+3*4 = 14"
│ 回复: {"content":[{"type":"text","text":"2+3*4 = 14"}],"is_error":false}
│
├─ "resources/list" → method_not_found(-32601)
│
└─ 把响应写回 stdout + "\n"
十一、langchainrust MCP 实现的文件结构
src/mcp/
├── mod.rs # 模块入口,re-export 公开 API:MCPClient, MCPServer, MCPConfig 等
├── protocol.rs # JSON-RPC 2.0 类型:MCPRequest / MCPResponse / MCPError
├── types.rs # MCP 业务类型:MCPToolDefinition / MCPToolResult / MCPContent / MCPConfig
├── transport.rs # 传输层:MCPTransport trait + StdioTransport + SseTransport
├── client.rs # MCPClient:连接、握手、list_tools、call_tool、as_tools
├── server.rs # MCPServer:handle_request、serve_stdio
└── tool_adapter.rs # MCPToolAdapter:MCP 工具 → BaseTool 适配
各文件行数和职责:
| 文件 | 行数 | 一句话 |
|---|---|---|
protocol.rs | 95 | JSON-RPC 2.0 请求/响应/错误类型 |
types.rs | 173 | 工具定义、内容类型、配置枚举 |
transport.rs | 395 | Stdio 子进程通信 + SSE HTTP 通信 |
client.rs | 164 | 连接、握手、发现工具、调工具、转 BaseTool |
server.rs | 349 | 注册工具、处理请求、serve_stdio 主循环 |
tool_adapter.rs | 75 | MCP 工具 → BaseTool 的薄适配层 |
十二、和 LangChain Python 版的对比
| langchainrust | LangChain Python (langchain-mcp) | |
|---|---|---|
| Client | ✅ Stdio + SSE | ✅ Stdio + SSE |
| Server | ✅ Stdio | ✅ Stdio + SSE |
| tools | ✅ 完整 | ✅ 完整 |
| resources | 类型定义有,handler 未实现 | ✅ 完整 |
| prompts | 同上 | ✅ 完整 |
| completion | 同上 | 部分 |
| elicitation | 同上 | 部分 |
| roots | 同上 | 部分 |
| sampling | 同上 | 部分 |
langchainrust 的 tools 原语是完整的,其余原语的协议类型都定义好了,handler 预留了 method_not_found,后续补实现只需要在 handle_request() 里加 match arm 即可。
十三、什么时候该用 MCP
适合:
- 你想让 Claude Desktop / Cursor 直接调你的工具(最典型的场景)
- 你有多个 AI 应用,想共享同一套工具(一个 Server,多个 Client)
- 你在写工具给第三方用,MCP 是标准接口(npm 上已经很多 MCP Server)
- 你想用社区现成的 MCP Server(
npx @anthropic/mcp-server-filesystem一行搞定)
不适合:
- 单机应用,只用本地工具——直接
BaseTool就够了,多一层 MCP 协议是多余的 - 对延迟极度敏感——MCP 经过子进程/HTTP + JSON 序列化,比直接函数调用慢
- tools 之外的原语你都需要——langchainrust 目前只完整实现了 tools
十四、速查:从零到跑通的 3 段代码
Client 端:连别人的工具
use langchainrust::mcp::{MCPClient, MCPConfig};
// 连接
let client = MCPClient::connect(MCPConfig::stdio(
"npx", vec!["@anthropic/mcp-server-filesystem".into(), "/tmp".into()],
)).await?;
// 发现
let tools = client.list_tools().await?;
// 调用
let result = client.call_tool("read_file", json!({"path": "/tmp/hello.txt"})).await?;
println!("{}", result.text());
// 适配为 BaseTool(给 Agent 用)
let agent_tools = client.as_tools().await;
Server 端:暴露自己的工具
use langchainrust::mcp::MCPServer;
let server = MCPServer::new()
.with_server_info("my-tools", "1.0.0")
.with_tool(Arc::new(CalculatorTool))
.with_tool(Arc::new(SearchTool));
server.serve_stdio().await?;
Claude Desktop 配置
{
"mcpServers": {
"my-tools": {
"command": "/path/to/your/binary",
"args": []
}
}
}
更多推荐

所有评论(0)