数据截至 (上游 commit fd0b7e1d9ed9)
第 4 章:声明式响应装配
这章讲什么: 为什么每个工具的返回值看起来那么一致、输出体积怎么被控制住,以及同一批数据怎么同时喂给"读文本的模型"和"读 JSON 的程序"。
4.1 问题:每个工具都想附带同一堆上下文
点了一次按钮,模型接下来大概率想知道:页面跳了吗?控制台报错了吗?现在页面结构长什么样?有没有弹对话框?
如果每个 handler 自己去取这些、自己拼文本,会有三个后果:56 份重复代码、输出格式各不相同、并且没人能统一控制体积。
本项目的解法:handler 只表达意图,不生产文本。
4.2 声明式的收集器
handler 侧长什么样
看 click 的 handler(src/tools/input.ts:102-133),它对输出做的全部事情是:
response.appendResponseLine(`Successfully clicked on the element`);
response.attachWaitForResult(result);
if (request.params.includeSnapshot) {
response.includeSnapshot();
}
三句话都是打标记:加一行文字、挂上等待结果、要求附带快照。至于快照怎么生成、放在响应哪个位置、要不要转 JSON,handler 一概不管。
标记 → 输出段落
McpResponse(src/McpResponse.ts:70)的接口就是一张标记表:
| 方法 | 打的标记 | 最终产出 |
|---|---|---|
appendResponseLine | 一行自由文本 | 排在最前的说明文字 |
includeSnapshot(params?) | 要页面快照 | ## Latest page snapshot + 树 |
setIncludePages(true) | 要页面列表 | ## Pages + 每行 id: 标题 (url) [selected] |
setIncludeNetworkRequests(true, opts) | 要网络清单 | ## Network requests + 分页信息 |
setIncludeConsoleData(true, opts) | 要控制台清单 | ## Console messages + 分页信息 |
attachNetworkRequest(reqid) | 要某条请求的详情 | 完整头/体(可落盘) |
attachConsoleMessage(msgid) | 要某条消息的详情 | 堆栈/关联请求 |
attachTraceSummary(trace) | 要 trace 摘要 | 语义化性能结论 |
attachImage(data) | 挂一张图 | content 里的 image 项 |
setError(err) | 出错了 | 末尾 Error: … |
为什么这样更好
三个直接收益:
- 一致性由结构保证。 所有工具的输出顺序完全相同,模型不用适应 56 种格式。
- 横切能力只写一遍。 分页、脱敏、压缩编码、落盘,全部在这一层加。
- 数据获取可以并行。 见下。
4.3 handle():七件事并行做
// src/McpResponse.ts:678-694
const [snapshot, detailedNetworkRequest, detailedConsoleMessage,
thirdPartyDeveloperTools, webmcpTools, consoleMessages, networkRequests
] = await Promise.all([
this.#handleSnapshot(context),
this.#handleAttachedNetworkRequest(context),
this.#handleAttachedConsoleMessage(),
this.#handleThirdPartyDevelopeTools(),
this.#handleWebMCP(),
this.#handleConsoleList(context),
this.#handleNetworkRequestList(context),
]);
每个 #handleXxx 都先看自己的标记,没打就立刻返回 undefined。所以没被要的东西一分钱也不花,被要的几件并发跑。
其中 #handleSnapshot(:450)还顺带做两件事:如果要页面列表就先刷新页面快照;如果快照带了 filePath 就写文件并只返回文件名,不把整棵树塞进响应。