外部工具怎么接进来?MCP 放在哪一层?
本课一问:到上一课为止,工具都是 harness 自己写的。用户还想让 agent 操作浏览器、查数据库、读公司的工单系统——这些外部工具怎么接进来?业界的答案叫 MCP,它该放在 harness 的哪一层?
这一课先不碰:外部工具执行危险操作谁来拦(A3、A4);让模型写脚本批量调工具(岔路课 Code Mode);技能文件 SKILL.md 经 MCP 分发(A6)。
从已有出发
每一轮请求都带着全部工具定义:本课项目 miniagent 的 6 个工具,在第 1 轮请求里占 685 个 token,而 system prompt 加任务描述加模板标记一共只有 214 个(岔路课 B1 逐轮拆过)。
贯穿任务本身用不上外部工具——修 dateparse.py 只要读、改、跑测试。所以这一课换一个例子:假设用户在同一个项目里还接了浏览器调试工具(Chrome DevTools)、网页自动化(Playwright)和 MCP 官方的参考 server。这三个 server 都是真实的 npm 包,本课在本机实际启动它们、拿到了它们的工具清单(2026-09-26)。
先自己设计:接进来的工具,全部塞进每一轮
最省事的接法:启动时把每个 server 的工具清单拿来,原样加进工具区,每一轮都发。
- ① 朴素方案
- ② 它在哪失败
- ③ 改进方案
在下面的条上,把三个 server 都切到「全量加载」。每一段是一个工具,悬停看它是谁、多少 token。
口径:token 数用项目录制模型 Qwen3-8B 的 tokenizer 与 chat template 算——miniagent 的 6 个工具合计 685 token,第 1 轮完整 prompt 899 token,与 trace 里服务端返回的 usage.prompt_tokens 一致;换成别家模型,tokenizer 与模板不同,数会变。MCP 工具定义来自本机实跑 tools/list(@modelcontextprotocol/server-everything 2026.8.31、@playwright/mcp 0.0.80、chrome-devtools-mcp 1.10.1,2026-09-26);「deferred」按工具名文本计,不含 server instructions。OpenCode:只计 .txt 描述原文;参数 JSON schema 未计入。bash 的描述在运行时由 shell/prompt.ts 按平台渲染,这里计的是模板 shell.txt;task 的描述运行时会追加可用子 agent 列表。websearch 只对 opencode 自家 provider 或开了 exa/parallel 开关时出现(registry.ts:58-65),question 只在 app/cli/desktop 客户端出现(:207)。「缓存」提示依据 Claude Code 官方 prompt-caching 文档(2026-09-26 核对)。
把三个 server 切到「deferred 只放名字」:上下文里只放 67 个工具名,一共 247 个 token;模型需要某个工具时,先调一个搜索工具把它的完整定义取回来。Claude Code 现在默认就是这样:官方文档写的是「Tool search is enabled by default: MCP tools are deferred and discovered on demand」 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方一手的产品行为说明;非第一方的 ANTHROPIC_BASE_URL 下会自动关闭。它是设计选择,文档没有给出节省比例。 出处Claude Code 文档:Scale with MCP tool search(2026-09-26 核对) ↗需联网 。
Codex 的工具有一个「暴露方式」字段:直接给模型、延迟(要搜才见)、只在 Code Mode 里可见、隐藏(spec_plan.rs:233-245)。
还有第二笔账:缓存。每一轮的请求前缀如果和上一轮一样,服务端可以复用上一轮算过的部分(A8 细讲)。工具定义就在前缀里,所以中途增删一个完整的工具定义,前缀就变了。Claude Code 的缓存文档把两种情况写得很清楚:「Tools loaded into the prefix: adding a definition invalidates the cache」;而 deferred 的工具只在末尾追加名字,「doesn’t disturb anything already cached」 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条官方一手的机制说明;前缀缓存本身的原理在 A8 讲。 出处Claude Code 文档:Connecting or removing an MCP server(2026-09-26 核对) ↗需联网 。在上面的条上切换时,条下方会提示这次切换属于哪一种。
规模再大一档,Anthropic 的工程博客《Code execution with MCP》给过一个量级:把工具定义与中间结果都塞进上下文的做法,和让模型写代码按需调工具相比,token 从 150,000 降到 2,000,「a time and cost saving of 98.7%」 证据等级 · 实验验证 由对比实验得出的经验结论,只在出处写明的模型、数据和规模下成立。换了条件可能不再成立,引用时要带上实验设置。 本条博客里的示例场景,不是本课测的;只作量级参考线。 出处Anthropic 工程博客 2025-11-04(官方自报) ↗需联网 。那是岔路课 Code Mode 的话题。
改进:MCP 到底是什么,放在哪一层
先把位置钉死。MCP(Model Context Protocol)是 harness 和外部工具之间的协议:harness 里有一个 MCP client,它通过 stdio 或 HTTP 和各个 MCP server 说 JSON-RPC,拿到工具清单、替模型发起调用、拿回结果。模型从来不直接说 MCP:它看到的仍然是 harness 发来的工具定义,它写出的仍然是 A1 里那种 tool_call 文本。
下面是一次真实的往返,三条泳道:模型 API、harness(MCP client)、MCP server。
轨道 ①:本机实跑 @modelcontextprotocol/server-everything 2026.8.31(serverInfo mcp-servers/everything 2.0.0),stdio,2026-09-26;客户端是 gen_a02_tools_data.py 里的最小 JSON-RPC 客户端,消息为原始字节(只格式化了缩进)。轨道 ②:modelcontextprotocol.io 的 2026-07-28 规范原文示例(server/discover 页、schema 示例 list-tools-request.json、server/tools 页),2026-09-26 核对。
试三件事:① 在轨道 ① 的前两步看「新客户端遇到旧 server」:先问 server/discover,被回 -32601 Method not found,于是回落到旧握手——这是 2026-09 的真实现状;② 走到第 9 步「harness 翻译」:MCP 的 inputSchema 被改名成 parameters,annotations 等字段被丢掉,这才是模型看到的样子;③ 切到轨道 ②,对比 2026-07-28 版:没有 initialize,每个请求自带 _meta。
2026-07-28 版改了什么
截至 2026-09-26,MCP 的最新规范是 2026-07-28 版,之后没有新版本 证据等级 · 有理论动机 有数学或机制上的理由说明它可能有效,但没有证明它一定更好。最后是否采用,仍然要看实验。 本条规范变更本身是官方一手、多方核对一致的事实;下面的落地状态是各家的官方发布记录,其中一处是推断,已标出。 出处MCP 规范 changelog(官方一手,2026-09-26 核对) ↗需联网 。和本课有关的四条:
- 无状态:去掉了
initialize/notifications/initialized握手和Mcp-Session-Id会话头;每个请求在_meta里自带io.modelcontextprotocol/protocolVersion、clientCapabilities(以及建议带上的clientInfo);新增必须实现的server/discover。 - 废弃 Sampling、Roots、Logging:Sampling 让 server 反过来请 client 替它调模型,规范给的迁移建议是「integrate directly with LLM provider APIs instead of Sampling」。
- 废弃不等于移除:规范的功能生命周期规定废弃窗口「at least twelve」个月,所以这三项最早要到 2027-07-28 之后才可能被移除;
ping、logging/setLevel等几项则这一版就直接删了。 - 工具清单要稳定排序:server「SHOULD return tools from tools/list in a deterministic order」,理由正是提高客户端缓存和 prompt 缓存的命中率——和上面那笔缓存账是同一件事。
落地是渐进的(2026-09-26,各家官方记录):
| 客户端 | 对 2026-07-28 的支持 | 依据 |
|---|---|---|
| Claude Code | v2 MCP client 默认只和 HTTP server 协商新版;stdio server 仍走旧握手,要设 MCP_PROTOCOL_NEGOTIATION=auto | 官方文档与 CHANGELOG |
| Codex | v0.147.0 起可选开启,feature flag mcp_2026_07_28;是否已默认开启未核实 | 官方 release notes |
| OpenCode | 依赖的 @modelcontextprotocol/sdk 是 1.29.0 | package.json;推断尚未支持(新版随 TS SDK 2.0 提供) |
| 本课实测的 server-everything | SDK 1.30.0,回 server/discover 为 -32601 | 本机实跑,见上面轨道 ① |
CoreCoder v0.7.0 的 MCP client 是一个 208 行的最小实现,只说 stdio、协议版本写死 2025-06-18,并把远程工具统一命名成 mcp__<server>__<tool>,让权限、hook 和主循环把它们当内置工具一样对待:
把外部工具接进自己的 prompt,工业实现还要处理名字。Codex 把 MCP 工具名限制在 64 字节,重名或超长时补一段 12 位的 SHA-1 后缀消歧:
工具集本身也随模型变。OpenCode 给 GPT 系模型用 apply_patch、拿掉 edit 与 write,给其他模型反过来(registry.ts:297-300):在上面的条上切到「OpenCode」,再换模型试试。
不是。MCP 连接的是 harness 与工具、资源。唯一一个「server 反过来请 client 调模型」的功能是 Sampling,2026-07-28 版已经把它废弃,官方建议直接对接各家模型 API。模型调用归 A8 的模型接入层,工具接入归本章。
废弃窗口至少 12 个月,Sampling、Roots、Logging 在窗口内仍然可用,Claude Code 在旧连接上仍会响应 roots/list。至于 Codex:pin 8cabf5a 上还有一个自称「Prototype」的 codex mcp-server,但它已于 2026-09-05 在上游删除(#42993,勘误表 X5),讲义里不再这么说。
极小真数字:一轮的工具区
把上面的账写成一行:
证据三问
| 问 | 本课的回答 |
|---|---|
| ① 它解决什么问题? | 外部工具的接入要有统一协议;接进来的工具不能无限撑大每一轮的上下文 |
| ② 为什么可能有效? | 协议把「工具在哪、怎么调」与「模型看到什么」分开,harness 可以决定只放名字 证据等级 · 数学可证明 在写明的前提下可以严格推导出来,不依赖实验。只要前提成立,换数据、换规模仍然成立。 本条算法性质:deferred 时工具区只含名字,token 数与工具定义的长度无关;全量加载时随定义线性增长。本课组件按项目模型 tokenizer 现算。 出处待补 |
| ③ 实验上呢? | 本机实测三个真实 server 的工具区大小(项目实测);Anthropic 自报的 150,000 → 2,000(官方自报,场景不同);deferred 能让任务成功率提高多少,本轮没有找到公开对照 |
演进地图:你在这里
- 解决了模型不用复述原文,只报行号,输出最短。卡在行号是「上一次读文件时」的快照:第一处修复只要多插 2 行,第二次按旧行号 41 改,就落在
if m:上(第 1 课现算);CoreCoder README 的原话是「Line numbers are a trap」。 - 解决了不需要定位,格式最不容易错:aider 同一个 Qwen2.5-Coder-32B,whole 格式合规 99.6%,diff 格式只有 71.6%。卡在只改 1 行,也要把 44 行的文件整个再写一遍:参数 487 token,片段替换只要 54(9.0 倍,项目模型 tokenizer 实数);还会覆盖别人在此期间的改动。
- 解决了按内容定位,不怕行号过期;0 处或多处都报错、文件不动,让模型自己补上下文。卡在真实 trace pd-truncate-s1:第 4 轮 old_string 在文件里 2 处、第 5 轮缩进多 4 格 0 处,两次报错多跑了两轮,花掉 8,481 token、109.5 秒;唯一也不等于对:pd-v2-unlimited-s1 第 7 轮的 old_string 恰好是已修好那一行的前半截,计数 = 1,于是
+ 1变成了+ 1 + 1。 - 解决了模型复述时的小漂移不再白费一轮:上面第 5 轮那条缩进多 4 格的 edit,OpenCode 直接接住(LineTrimmed 级)。卡在接住 ≠ 改对:多转义一层的引号,OpenCode 的 EscapeNormalized 级照样接住,写回去的是
\",模块导入即 SyntaxError;OpenCode 的 v2(packages/core)眼下只有精确匹配,「edit fuzzy parity」还列在待移植清单里。 - 解决了语法由解码器保证,不再靠 prompt 教。对照:靠 prompt 教格式时,aider 榜上 47 个 diff 格式条目的「格式正确」比例最低只有 64.4%(官方自报)。卡在语法只管格式,定位仍是 seek_sequence:它不检查唯一、取第一处——两处相同的
return datetime.date(year, month, day)会改到第 31 行 ISO 分支,test_parse_iso 跟着坏;整行比较还会把没写缩进的 new 行原样写进去,得到 IndentationError。 - 解决了一套协议接任意外部工具;同一个 harness 给 GPT 换上 apply_patch、给 Claude 用 edit。卡在接进来的每个工具每轮都要付上下文:chrome-devtools-mcp 的 30 个工具在项目模型的 chat template 里占 7,047 token,是 miniagent 自己 6 个工具(685)的 10.3 倍。
- 解决了N 次「模型 → 工具 → 模型」往返压成一段脚本。卡在省的是往返,不省判断:真实 trace 里模型每一步都要看上一步的结果才知道下一步做什么(岔路课逐轮算)。
一句话
先停在这里:记住位置(harness 与工具之间)和账单(每轮都付)两件事。
再深一层:去岔路课 Code Mode:如果让模型写一段脚本、在脚本里调工具,往返次数和中间结果都不必进上下文。
下一个缺口
工具能执行 rm -rf,谁来拦? → A3 第 1 课:拦危险命令,写个黑名单行不行?
这一章给了模型改文件、跑命令、接外部系统的能力。能力越大,越需要一道闸:A3 从最朴素的黑名单开始。本课 CoreCoder 把 MCP 工具命名成 mcp__server__tool,就是为了让那道闸能按名字认出它们。
你可能会问
- 「让模型写代码来调工具,是不是比 MCP 更好?」 两者不在一层:Code Mode 里调的往往正是 MCP 工具。见岔路课 让模型写代码来调工具,行不行?
- 「MCP 现在归谁管?」 Anthropic 于 2025-12-09 把 MCP 捐给了 Linux 基金会旗下的 Agentic AI Foundation(AAIF);截至 2026-09-26,AAIF 有 6 个项目,MCP 与 Google 的 A2A 同在其中(均为当事方官方自报)。
- 「技能(SKILL.md)能经 MCP 分发吗?」 能:SEP-2640 Skills Extension 已于 2026-09-13 合并为 Final,但各语言 SDK 的实现还在进行中——这是 A6 的话题。