# 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 规范: - 看 OpenClaw 怎么实现 MCP client 的: - 配置和发现机制 - 何时连接、catalog 怎么暴露给模型 - 缓存、失效、安全细节 这部分在 OpenClaw 仓库的 `src/agents/agent-bundle-mcp-*.ts`, 以及 `notes/mcp-client-internals.md`。