3c0cd86352
- notes/mcp-overview.md: MCP 协议概览,跟 tool use 的分层差异 - notes/mcp-client-internals.md: MCP client 内部实现(发现/连接/catalog) - notes/plugin-tools.md: plugin tool 与 native/MCP 的对比 - notes/lsp-tools.md: LSP 协议科普 + OpenClaw LSP tool 包装 - notes/rag-overview.md: RAG 与向量化(向量只用来检索,prompt 用原文) - notes/blog/mcp-101.md: 面向开发者的 MCP 科普,LSP 类比引入 - README 索引更新
371 lines
14 KiB
Markdown
371 lines
14 KiB
Markdown
# MCP 是什么?为什么 Function Call 之后还需要它
|
||
|
||
> 本文以 [OpenClaw](https://github.com/openclaw/openclaw) 开源源码为例
|
||
> (基于 2026.6.2 版本),结合实际代码说明 MCP 的工作原理和定位。
|
||
> 涉及源码路径均可在仓库中直接查阅。
|
||
|
||
如果你看过上一篇 [Function Call 101](./function-call-101.md),应该已经
|
||
知道 LLM 怎么"用工具"了:开发者把工具的 JSON Schema 塞进请求,模型决定
|
||
调哪个、传什么参数,代码负责真正执行。
|
||
|
||
那为什么 2024 年 Anthropic 又搞了一个叫 **MCP(Model Context Protocol)**
|
||
的东西?它跟 Function Call 是同一个东西吗?还是说 Function Call 不够用?
|
||
|
||
本文回答这两个问题。
|
||
|
||
---
|
||
|
||
## 从一个老问题说起
|
||
|
||
先看一个跟 AI 没关系的故事。
|
||
|
||
2016 年微软在做 VSCode 的时候,碰到一个头疼的事:
|
||
|
||
```
|
||
编辑器(M 个) 语言(N 种)
|
||
┌────────────┐ ┌──────────┐
|
||
│ VSCode │ ↔↔↔↔ │ Python │
|
||
│ Vim │ ↔↔↔↔ │ Go │
|
||
│ Emacs │ ↔↔↔↔ │ TypeScript│
|
||
│ Sublime │ ↔↔↔↔ │ Rust │
|
||
│ JetBrains │ ↔↔↔↔ │ Java │
|
||
└────────────┘ └──────────┘
|
||
```
|
||
|
||
每个编辑器都要给每种语言写一遍"hover 显示类型"、"跳转到定义"、
|
||
"查找所有引用"、"实时报错"...M 个编辑器 × N 种语言 = M × N 份
|
||
重复实现。这显然不可持续。
|
||
|
||
微软的解法是 **LSP(Language Server Protocol)**:把语言能力抽出来
|
||
变成一个独立的服务器进程,编辑器跟服务器用一套标准协议通信:
|
||
|
||
```
|
||
┌────────┐ LSP ┌──────────────┐
|
||
│ VSCode │ ←──JSON-RPC→│ tsserver │ ← TypeScript
|
||
└────────┘ └──────────────┘
|
||
┌────────┐ ┌──────────────┐
|
||
│ Vim │ ←──────────│ rust-analyzer│ ← Rust
|
||
└────────┘ └──────────────┘
|
||
```
|
||
|
||
现在变成 M + N:每个编辑器实现一次 LSP client,每种语言实现一次
|
||
LSP server。**`rust-analyzer` 一份能给所有支持 LSP 的编辑器用。**
|
||
|
||
记住这个故事。
|
||
|
||
---
|
||
|
||
## AI 工具集成的同样问题
|
||
|
||
现在回到 2024 年。当时已经有不少 AI 应用支持 Function Call,但每个
|
||
应用都在重复造轮子:
|
||
|
||
```
|
||
AI 应用(M 个) 工具(N 种)
|
||
┌────────────────┐ ┌──────────────┐
|
||
│ Claude Desktop │ ↔↔↔│ 读文件 │
|
||
│ ChatGPT App │ ↔↔↔│ 查数据库 │
|
||
│ Cursor │ ↔↔↔│ 调 GitHub API │
|
||
│ Codex │ ↔↔↔│ 操作 Notion │
|
||
│ 你的 agent │ ↔↔↔│ 控制浏览器 │
|
||
└────────────────┘ └──────────────┘
|
||
```
|
||
|
||
每个 AI 应用都要给每种工具写一遍 Function Call 的接入代码:
|
||
读文件、调 API、解析响应、错误处理。M × N 又来了。
|
||
|
||
你想要的是这样:把"读文件"这个能力写一次,Claude Desktop、Cursor、
|
||
你的 agent 都能用。
|
||
|
||
Anthropic 给的答案就是 MCP——**它对 AI 工具集成做的事,跟 LSP 对
|
||
编辑器语言支持做的事是一模一样的**。
|
||
|
||
---
|
||
|
||
## MCP 是什么
|
||
|
||
一句话:**MCP 是一个开放协议,让任何 AI 应用都能用一套标准的方式
|
||
接入任何外部工具。**
|
||
|
||
具体来说,MCP 定义了两个角色之间怎么通信:
|
||
|
||
- **Host**(主机):跑 LLM 的那一侧——Claude Desktop、Cursor、你写的
|
||
agent 框架
|
||
- **Server**(服务器):提供工具的那一侧——可以是本地子进程
|
||
(读文件、控浏览器),也可以是远端 HTTP 服务(Notion 集成、
|
||
GitHub 集成)
|
||
|
||
通信用 JSON-RPC 2.0,跑在 stdio 或 HTTP 之上。MCP server 暴露三类能力:
|
||
|
||
- **tools**:模型可以调的函数
|
||
- **resources**:可读的数据源
|
||
- **prompts**:可被引用的预制 prompt 模板
|
||
|
||
我们今天主要讲 tools。
|
||
|
||
跟 LSP 长得很像对吧?**stdio + JSON-RPC + 服务器自报能力**——某种意义上
|
||
MCP 就是 LSP 思路在 AI 时代的延续。
|
||
|
||
---
|
||
|
||
## 但是……Function Call 不是已经能干这件事了吗?
|
||
|
||
这是新手最容易卡住的地方。我们仔细看看。
|
||
|
||
### Function Call 是什么层面
|
||
|
||
Function Call 是**模型和应用之间的约定**:模型怎么"申请"调用一个
|
||
函数,应用怎么"执行"它再把结果回传。
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ LLM (Claude / GPT 等) │
|
||
│ 输出 tool_use 请求 │ ← Function Call 层
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 你的应用 │
|
||
│ 接到请求,跑函数,回传结果 │
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
这一层只回答了"模型怎么表达调用意图"。**它没规定函数本身是怎么
|
||
来的、是谁写的、怎么真正执行**。
|
||
|
||
### MCP 是什么层面
|
||
|
||
MCP 是**应用和工具实现之间的协议**:这个函数从哪儿来,代码在哪个
|
||
进程里跑,参数怎么传过去。
|
||
|
||
```
|
||
┌─────────────────────────────┐
|
||
│ LLM │
|
||
│ 输出 tool_use 请求 │ ← Function Call 层(没变)
|
||
└──────────────┬──────────────┘
|
||
│
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ 你的应用 │
|
||
│ - 收 tool_use │
|
||
│ - 转成 MCP 调用 │
|
||
└──────────────┬──────────────┘
|
||
│ JSON-RPC over stdio/HTTP
|
||
▼
|
||
┌─────────────────────────────┐
|
||
│ MCP server (子进程/远端) │ ← MCP 层
|
||
│ 跑函数,回结果 │
|
||
└─────────────────────────────┘
|
||
```
|
||
|
||
注意:**模型完全不知道 MCP 的存在**。它看到的还是一个普通的 tool 定义,
|
||
还是按 Function Call 的方式输出 `tool_use`。MCP 是中间那一层应用代码
|
||
的事——它把 tool 的"实现"从应用进程里挪到了独立的 MCP server 里。
|
||
|
||
### 一个类比
|
||
|
||
- Function Call ≈ "我要打个电话"——这是个抽象动作
|
||
- MCP ≈ "电话怎么拨通对方公司的总机,问到分机号,再接到具体的人"
|
||
——这是个具体协议
|
||
|
||
或者:
|
||
|
||
- Function Call ≈ 函数调用这个概念本身
|
||
- MCP ≈ gRPC / OpenAPI——让函数能跨进程跨语言被发现和调用的具体协议
|
||
|
||
**MCP 不替代 Function Call,它给 Function Call 加了一个"工具供应链"。**
|
||
|
||
---
|
||
|
||
## MCP 实际带来了什么
|
||
|
||
讲了这么多概念,看看实际效果。
|
||
|
||
### 1. 工具一次写好,所有 AI 应用都能用
|
||
|
||
`@modelcontextprotocol/server-filesystem` 是社区的一个 MCP server,
|
||
让 AI 能读文件、列目录、搜文件。装一次:
|
||
|
||
```bash
|
||
npx @modelcontextprotocol/server-filesystem ~/Documents
|
||
```
|
||
|
||
然后 Claude Desktop、Cursor、OpenClaw 都能直接用上它,不用各自实现
|
||
一遍文件操作工具。**这就是 M + N 而不是 M × N。**
|
||
|
||
### 2. 工具的提供者跟 AI 应用作者解耦
|
||
|
||
Notion 想给 AI 助手提供集成?以前要等每家 AI 应用主动接入,或者发
|
||
SDK 给开发者集成。现在 Notion 自己跑一个 MCP server,任何支持 MCP
|
||
的应用都能用。
|
||
|
||
### 3. 工具能跨语言、跨进程
|
||
|
||
MCP server 可以是 Python、Go、Rust、任何能讲 JSON-RPC 的语言写的。
|
||
你的 AI 应用是 Node.js,但工具实现可以是 Python——只要双方说 MCP 就行。
|
||
|
||
### 4. 故障隔离
|
||
|
||
MCP server 跑在独立进程里(或者远端),一个 server 崩了不会拖死整个
|
||
AI 应用。可以单独重启、单独限流、单独配置超时。
|
||
|
||
---
|
||
|
||
## 看看 OpenClaw 怎么做的
|
||
|
||
OpenClaw(一个开源 AI agent 框架)的代码可以当作 MCP 的具体落地示例。
|
||
|
||
### MCP server 怎么"加进来"
|
||
|
||
OpenClaw 不去网络扫描发现 MCP server,**全靠声明式 config**。两个来源:
|
||
|
||
**1. 用户写在 `~/.openclaw/openclaw.json` 里:**
|
||
|
||
```json
|
||
{
|
||
"mcp": {
|
||
"servers": {
|
||
"filesystem": {
|
||
"command": "npx",
|
||
"args": ["@modelcontextprotocol/server-filesystem", "/home/user"]
|
||
},
|
||
"github": {
|
||
"url": "https://mcp.github.com",
|
||
"transport": "streamable-http",
|
||
"auth": "oauth"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**2. 插件作者预置:** 装一个 OpenClaw plugin 时,它可能自带几个
|
||
MCP server 配置(`src/plugins/bundle-mcp.ts`)。
|
||
|
||
两个来源在 `src/agents/bundle-mcp-config.ts` 里合并,**用户 config
|
||
能覆盖 plugin 默认**。
|
||
|
||
### 什么时候真的连接 MCP server?
|
||
|
||
这是个有意思的设计选择。OpenClaw 的策略:**进程启动时不连接、每次
|
||
模型轮次开始时连接**。
|
||
|
||
为什么?因为模型必须在请求里就看到全部 tool 才能决定调哪个,所以
|
||
连接不能拖到模型真的调用某个 tool 时再做(那时候 tool 列表都没
|
||
组装好)。但又没必要在 OpenClaw 启动时就把所有 MCP server 都拉起来
|
||
(可能配了 20 个,这次对话其实只用 2 个)。
|
||
|
||
折衷点是 **agent 一轮模型请求开始时**:这时候必须把所有 MCP server
|
||
都 spawn、`tools/list`、拿到工具目录,然后把工具塞进发给模型的
|
||
tool 列表里。
|
||
|
||
```ts
|
||
// 简化版,真实代码在 src/agents/embedded-agent-runner/run/attempt.ts
|
||
const mcpRuntime = await materializeBundleMcpToolsForRun({...});
|
||
// 这里 MCP server 已经 spawn 完了,tool 也都拿到了
|
||
|
||
const tools = [...nativeTools, ...mcpRuntime.tools];
|
||
// 发给模型
|
||
sendRequest({ tools, ... });
|
||
```
|
||
|
||
### 模型看到的是什么
|
||
|
||
**一个扁平的 tool 数组,跟 native tool 长得完全一样:**
|
||
|
||
```json
|
||
[
|
||
{ "name": "Read", "description": "...", "input_schema": {...} },
|
||
{ "name": "Bash", "description": "...", "input_schema": {...} },
|
||
{ "name": "filesystem__read_file", "description": "...", "input_schema": {...} },
|
||
{ "name": "github__list_issues", "description": "...", "input_schema": {...} }
|
||
]
|
||
```
|
||
|
||
注意 `filesystem__read_file` 这种 `serverName__toolName` 的命名是为了
|
||
防止重名。模型完全不知道哪些来自 MCP、哪些是 OpenClaw 写死的;它只
|
||
根据 `description` 和 schema 做语义匹配,挑一个最合适的。
|
||
|
||
### 模型调用时发生什么
|
||
|
||
模型返回 `tool_use: filesystem__read_file`,OpenClaw 在 tool 列表里
|
||
找到对应条目,它的 `execute` 函数会调 `client.callTool("read_file", input)`,
|
||
通过早就建立好的 stdio 连接发给 MCP server,server 跑完返回结果,
|
||
OpenClaw 再把结果回传给模型。
|
||
|
||
整个过程模型一无所知——它只觉得自己"调了个工具,得到了结果"。
|
||
|
||
---
|
||
|
||
## 关键澄清:几个常见误解
|
||
|
||
### 误解 1:"MCP 是给模型用的协议"
|
||
|
||
**不是。MCP 是给应用用的协议。** 模型那一侧永远是 Function Call,
|
||
看到的就是普通 tool 定义。MCP 是应用怎么"获取这些 tool 定义、怎么
|
||
真正执行 tool"的事。
|
||
|
||
### 误解 2:"用了 MCP 就不用 Function Call 了"
|
||
|
||
**Function Call 永远在用。** MCP server 提供的 tool,最终还是要包装
|
||
成 Function Call 的格式发给模型。两者不是 A 替代 B,而是 A 之上又加
|
||
了一层 B。
|
||
|
||
### 误解 3:"native tool 落后了,MCP 才是未来"
|
||
|
||
**不一定。** 跟应用深度耦合的工具(比如 OpenClaw 的 `Read`、`Bash`)
|
||
做成 native tool 启动快、故障少、能直接用 host 内部能力;MCP 的优势在
|
||
**跨应用复用** 和 **可独立分发**。两者各有适用场景。
|
||
|
||
OpenClaw 自己就同时有 native tool、plugin tool、MCP tool,模型看见
|
||
的是同一个扁平列表,不区分。
|
||
|
||
---
|
||
|
||
## 那 MCP 现在能用来干什么
|
||
|
||
一些实际的应用方向:
|
||
|
||
- **本地工具**:文件系统、Git、Docker、SQLite——`@modelcontextprotocol/server-*`
|
||
系列已经有不少现成的
|
||
- **远端 SaaS 集成**:Notion、Linear、Slack、GitHub——越来越多 SaaS
|
||
在做官方 MCP server
|
||
- **企业内部工具**:把内部数据库查询、运维脚本包装成 MCP server,
|
||
团队所有 AI 助手都能用
|
||
- **跨工具协作**:同一个 agent 同时接文件系统 + GitHub + Notion 的 MCP server,
|
||
可以读本地代码、查 issue、写文档
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
回到开头的问题:**MCP 跟 Function Call 是什么关系?**
|
||
|
||
```
|
||
Function Call:模型 ←→ 应用 之间怎么调函数
|
||
MCP: 应用 ←→ 工具 之间怎么提供函数
|
||
```
|
||
|
||
Function Call 解决"模型怎么用工具"。MCP 解决"工具从哪儿来、怎么跨应用
|
||
复用"。两者各管一层,组合起来才是完整的故事。
|
||
|
||
LSP 当年解决了编辑器 × 语言的 M × N 问题,让 `rust-analyzer` 这种
|
||
高质量语言服务器能服务所有编辑器。MCP 试图解决 AI 应用 × 工具的同样
|
||
问题,让一个 MCP server 能服务所有 AI 助手。
|
||
|
||
它能不能像 LSP 那样成为事实标准,还要看生态——但思路是清晰的:
|
||
**把工具实现从单个应用里抽出来,变成可复用的独立组件**。
|
||
|
||
---
|
||
|
||
## 想深入了解?
|
||
|
||
- MCP 规范:<https://modelcontextprotocol.io>
|
||
- 看 OpenClaw 怎么实现 MCP client 的:
|
||
- 配置和发现机制
|
||
- 何时连接、catalog 怎么暴露给模型
|
||
- 缓存、失效、安全细节
|
||
|
||
这部分在 OpenClaw 仓库的 `src/agents/agent-bundle-mcp-*.ts`,
|
||
以及 `notes/mcp-client-internals.md`。
|