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 连过去。

  1. 先 GET /sse 端点,建立 SSE 长连接
  2. Server 推送一个 endpoint 事件,告诉 Client “发消息用这个 URL”
  3. 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 事件,就报错。

两种传输层对比

StdioSSE
通信方式子进程 stdin/stdoutHTTP + 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-93handle_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 的区别

toolsresources
动作做(搜索、计算、写文件)读(读文档、读数据)
例子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 现状
toolsClient → Server调工具点菜✅ 完整实现
resourcesServer → Client给 AI 读数据看菜单类型定义有,handler 留 method_not_found
promptsServer → Client给 AI 预设 prompt点套餐同上
completionServer → Client输入自动补全搜索框补全同上
elicitationServer → Client工具反问用户服务员追问辣度同上
rootsClient → Server声明文件访问范围说"只打扫客厅"同上
samplingServer → ClientServer 借 Client 的模型借你手机查个号同上

八、工具调用结果的三种内容类型

MCPToolResultcontentVec<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含义什么时候出现
-32700Parse error发了非法 JSON,比如少了个引号
-32600Invalid Request请求格式不对,比如缺 method 字段
-32601Method not found调了不存在的方法,比如 resources/list(还没实现)
-32602Invalid params参数无效,比如 tools/call 没传 name
-32603Internal errorServer 内部错误

实际例子

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.rs95JSON-RPC 2.0 请求/响应/错误类型
types.rs173工具定义、内容类型、配置枚举
transport.rs395Stdio 子进程通信 + SSE HTTP 通信
client.rs164连接、握手、发现工具、调工具、转 BaseTool
server.rs349注册工具、处理请求、serve_stdio 主循环
tool_adapter.rs75MCP 工具 → BaseTool 的薄适配层

十二、和 LangChain Python 版的对比

langchainrustLangChain 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": []
    }
  }
}

仓库:github.com/atliliw/langchainrust

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐