MCP 工具的有界 I/O
解释 MCP 工具为什么必须对扫描时间、返回数量、文件大小和 stdout/stderr 建立显式预算,并通过分页与 truncation 把大数据面拆成可控的小响应。
[!info] related notes
- 所属 MOC: mcp-moc
- 前置概念:
- 并列概念:
- 易混淆概念:
- 关系笔记: mcp-in-agent-harness、[private note]
MCP 工具的有界 I/O
一句话定义
有界 I/O 是指每个 MCP 工具都必须明确自己最多扫描多久、最多读取多少、最多返回多少,并在达到预算后返回可继续的状态,而不是无限递归或把完整结果一次塞进 response。
核心原则:
任何一次 tools/call 都应有可预测的最坏成本。
为什么文件工具也会拖垮 Agent
一个看起来很普通的:
search_files("/home/user/projects", "foo")
如果没有边界,可能递归进入:
.gitnode_modules.cache.pnpm-storedistbuild.venv- 大型生成目录
最终扫描几十万文件。
同样,一个:
cat huge.log
可能产生几 MB 到几百 MB stdout。
这些都不是“文件系统功能更强”,而是把不可预测的数据面塞进单次 RPC。
四类预算
1. 时间预算
递归 search/tree 必须有 deadline,例如:
default: 15s
max: 30s
达到上限后返回:
{
"truncated": true,
"reason": "timeout"
}
而不是继续扫。
2. 扫描预算
例如设置:
max scanned entries = 50,000
这样即使目录结构异常,也存在明确上限。
3. 结果数量预算
例如:
search default = 100
search max = 500
“扫描多少”和“返回多少”应分别限制;否则即使扫描很快,结果本身也可能淹没模型上下文。
4. 单次 payload 预算
当前 broad-agent 实践中:
stdout <= 64 KiB
stderr <= 64 KiB
文件读取也采用分段上限,而不是整文件无条件返回。
这些数字是当前部署基线,不是 MCP 协议要求。
默认跳过重目录
对代码 Agent,通常可以默认跳过:
.git
node_modules
.cache
.pnpm-store
.yarn
.next
.nuxt
.turbo
dist
build
coverage
.venv
venv
target
.gradle
.idea
这不是因为这些目录永远没有价值,而是因为它们不应成为“普通源代码搜索”的默认成本。
如果任务确实需要,应通过显式参数扩大范围,而不是默认全扫。
Pagination 是大结果的正确协议形状
文件读取:
read(offset, max_bytes)
→ data
→ next_offset
→ truncated
Job 日志:
job_output(offset, limit)
→ chunk
→ next_offset
→ eof
相关链接:
get_related_notes(limit)
→ first N
→ total_count
→ truncated
它们本质上都是同一个设计:
单次 response 只返回一个有界窗口,并显式告诉调用者是否还有数据。
为什么 truncation 必须是协议字段
错误做法:
返回前 500 条,但不告诉调用者还有更多
这样模型会把“不完整数据”误认为“完整事实”。
正确做法至少返回:
truncated
total
next_offset / cursor
reason
scanned_count
elapsed_ms
具体字段可因工具不同而变化,但必须能表达“不完整”。
有界 I/O 与 LLM context 的关系
MCP 不只是操作系统接口,它最终服务于模型。
一个 5 MB 的日志即使网络传输成功,也会带来:
- 序列化成本;
- Tunnel 传输成本;
- hosted tool result 处理成本;
- LLM 上下文成本;
- 关键信息被大量噪声淹没。
所以 Bounded I/O 同时是:
- reliability 策略;
- performance 策略;
- context engineering 策略。
设计检查
给新工具增加以下问题:
- 最坏情况下会扫描多少对象?
- 最长允许执行多久?
- 最多返回多少字节/条目?
- 达到上限时如何表示 truncation?
- 调用者如何继续读取?
- 是否有默认应排除的重路径?
- 返回值是否仍足够支持模型下一步决策?
只要其中一个答案是“无限”,这个工具就还不是一个成熟的 Agent Tool。
学习检查
- 为什么“限制返回 500 条”仍然不能替代“限制扫描 50,000 项”?
- 为什么 truncation 必须显式返回?
- Bounded I/O 为什么既是系统可靠性问题,也是上下文工程问题?
- 哪些情况下应该允许显式覆盖默认 skip 目录?
- Pagination 与 Job Manager 的日志读取为什么属于同一设计思想?