前言
在大概半年前的文章里,我介绍了 Pi 的 Agent Loop、上下文、工具和扩展机制,当时Pi还专门说明为什么没有把MCP作为内建的能力,但是最近 Pi 又加入了一组值得单独拆解的能力:MCP、Codemode 和 Tool Search。
我觉得这不是Pi的设计理念突然就变化了,是他们找到了一套比较好的MCP设计方式。
MCP作为Agent标准化的能力,适用性方面不必多阐述,但是如果把所有的工具都注入,就会占用不少的上下文,比如Playwright MCP就有几十个工具。
Pi 的处理方式不是把 MCP 当成 pi-agent-core 的一部分,而是把它和 Codemode、Tool Search 一起作为内置扩展加入:MCP 提供工具,Codemode 提供脚本式编排,Tool Search 则按需把某些工具声明给模型。它们共同解决的不是“能不能连 MCP”,而是如何在工具很多时,仍然让模型只看到当前需要的那一小部分。
1. MCP 接进来以后,真正的问题是工具目录
一个 MCP server 可以提供很多工具。比如网页检索、抓取、爬站、监控、结构化抽取等能力,都可能来自同一个 server。Pi 把工具名称规范化为 mcp__<server>__<tool>,随后把每个工具包装成 Pi 的工具定义。
最直接的做法是将所有工具声明都放进模型请求。小型工具集这样很方便,但接入多个 server 后,会带来几个问题:
- 上下文膨胀:工具名称、描述、参数 schema 会占据每一次模型请求的空间。
- 选择干扰:很多相似工具同时出现,模型更容易选错或难以发现真正相关的工具。
- 暴露面过大:即使某个工具在当前任务中无关,它仍出现在模型可调用的工具集合里。
Pi 因而把“工具已连接”和“工具声明已提供给模型”分开。MCP server 可以已经连接、工具也已经在 Pi 内部注册,但具体工具仍不必直接出现在模型的工具列表中。
2. 三个组件各自负责什么
- **MCP:**从外部 server 发现和调用工具。
- **Codemode:**由一段 JavaScript 脚本组合工具,并处理调用结果。
- **Tool Search:**根据模型的需要找到工具,再把它的声明放进后续模型请求。
它们不是三个重复的搜索入口,而是工具调用链上的不同角色。
2.1 MCP:连接和调用协议,不负责 Agent 思考
Pi 内置的 MCP 支持负责读取 mcp.json、连接 stdio 或 Streamable HTTP server、发现工具,以及将调用和结果转换为 Pi 的工具格式。需要认证的 HTTP server 还可以通过 OAuth 登录。
MCP 本身不会替模型规划任务。它提供的是一个标准化的“工具来源”:Pi 连接以后,把 firecrawl_search 或 github_search_code 这样的 server 工具包装成 Pi 工具;接下来由模型直接调用,或由 Codemode 脚本调用。
2.2 Codemode:让模型把工具编排写成脚本
Codemode 是模型可调用的一个元工具。模型提交一段 JavaScript,在受限的 QuickJS 沙箱中运行;脚本通过 tools.<name>(args) 调用 Pi 工具,能够并行执行独立任务、按结果分支、过滤大结果,再只把整理后的输出返回给模型。
MCP 工具太多、schema 没有全部放在 Codemode 的静态描述中时,脚本可以使用:
searchTools(query, options):按名称和描述对当前可调用的工具做 BM25 相关性排序,返回名称和描述。describeTool(name):查询一个具体工具的详细声明,包括参数定义。describeNamespace(name):查看某个 MCP server 的名称、说明、instructions 和工具名称。ALL_TOOLS:枚举当前可调用工具的名称和描述。
例如,第一段脚本可以先找到网页搜索工具,再把参数声明返回给模型:
const matches = await searchTools("web search", {
namespace: "mcp__firecrawl",
limit: 5,
});
const match = matches[0];
if (!match) return "没有找到匹配的工具";
return {
tool: match,
declaration: await describeTool(match.name),
};
模型看到返回的名字和 schema 后,再写下一段脚本调用它:
const result = await tools["mcp__firecrawl__firecrawl_search"]({
query: "Pi Coding Agent Codemode MCP",
sources: ["web"],
limit: 3,
});
return result.structuredContent ?? result.content;
这两段也可以合成一次脚本执行,但前提是脚本已经知道要如何根据工具定义构造参数。**searchTools() 不会在脚本运行中途再叫一次主模型,替它理解 schema 并生成参数。**如果模型需要先看到搜索结果和参数定义再做决定,就要让脚本返回发现结果,随后由模型继续生成调用;或者改用 Tool Search。
另一个重要边界是:Codemode 沙箱不能直接访问 Node.js、文件系统或网络。它只能使用 Pi 注入的工具和模型接口。不过,通过工具调用 MCP 仍会产生真实的外部操作;沙箱限制脚本本身,并不会撤销工具的能力。
2.3 Tool Search:让主模型按需加载工具声明
Tool Search 是另一个顶层模型工具。模型描述自己要找什么,Pi 在尚未直接提供给模型的工具中做相关性排序,并把匹配工具加入当前工具集。接下来的模型请求就会带上这些工具的完整声明,模型可以像调用普通工具一样直接调用它们。
模型调用 tool_search("搜索产品文档并读取指定页面");
Pi 找到匹配的 MCP 工具并加入工具集;
下一次模型请求收到这些工具的名称、描述和参数 schema;
模型决定调用哪个工具,以及传入什么参数。
这里的重点是:搜索结果不只是留在脚本里,而是被 Pi 用来改变下一次模型请求的工具声明。被加载的工具会记录在当前会话分支上,不只是脚本内部临时可见。
3. MCP 工具的四种暴露方式
Pi 把 MCP 工具的暴露方式作为配置项。常见的四种模式是:
exposure | 工具如何到达模型 | 适合的场景 |
|---|---|---|
direct | 每个工具都作为直接工具声明给模型;Codemode 脚本也能调用 | 数量少、经常使用,且希望模型随时直接调用 |
codemode(默认) | 不逐个声明给模型;脚本通过 searchTools()、describeTool() 等发现并调用 | 工具较多、希望脚本编排或压缩结果 |
deferred | 默认不直接声明;由 tool_search 搜索并加载选中的工具,之后模型直接调用;Codemode 启用时脚本也能调用 | 工具较多,但希望模型自己看 schema、逐步决策 |
hidden | 注册了,但不让模型或 Codemode 调用 | 明确不希望暴露的工具 |
可以在整个 server 上设置 exposure,也可以用 toolExposure 对个别工具覆盖。例如,把少数常用读取工具设为 direct,把长尾工具留在 deferred,把不希望模型调用的操作设为 hidden。codemode 和 deferred 不是互斥的调用权限:两者都可由已启用的 Codemode 脚本调用,也都能被 Tool Search 搜索加载;配置值主要决定 Pi 默认启用哪种入口。
一个远程 server 可以这样配置(用户级配置位于 ~/.pi/agent/mcp.json;项目级 .pi/mcp.json 需要项目通过 trust):
{
"mcpServers": {
"docs": {
"url": "https://docs.example.com/mcp",
"description": "搜索和读取产品文档",
"exposure": "codemode"
}
}
}
codemode 本身是默认值,省略 exposure 也可以。若改成 "deferred",Pi 会改为自动启用 Tool Search;direct 则让工具声明直接进入模型请求。
Pi 还会把 MCP server 的名称和一行简介放进 mcp_servers 系统提示词段,告诉模型有哪些间接暴露的 server、应该通过 Codemode 还是 Tool Search 使用它们。这个段落不是所有 MCP 工具的完整清单,也不包含每个工具的参数 schema;简介来自配置的 description,若没有配置,则来自 server instructions 的第一行。完整工具定义仍按需查找或加载。
一个容易混淆的源码细节是:配置中的 codemode 和 deferred 都不会直接声明 MCP 工具;内部都会映射成可间接使用的工具暴露类型。之后 MCP 扩展再根据原始配置选择启用哪个入口:codemode exposure 自动启用 Codemode,deferred exposure 自动启用 Tool Search。因而它们在底层都“先不直接暴露”,但面向模型的使用流程不同。
4. 一次调用在源码里怎样走完
把配置和运行时串起来看,大致是下面这条路径:
mcp.json
↓ session_start
MCP 扩展连接 server,并发现工具
↓
Pi 为每个工具注册 mcp__server__tool 定义
├─ direct:放入模型请求的工具声明
├─ codemode:由脚本搜索、查看声明并调用
└─ deferred:由 tool_search 加入后续请求
更具体地说:
mcp/index.ts在session_start读取配置,并在后台启动 server 连接。- 连接完成后,MCP 扩展注册各个 Pi 工具。
mcp/tools.ts把参数 schema、输出 schema、MCP 调用和返回结果包装成 Pi 的ToolDefinition。 - 如果工具是间接暴露的,MCP 扩展仍把它保留在 Pi 的内部工具目录中。Codemode 会从当前可调用工具创建脚本侧的
tools接口;searchTools()使用 BM25 排序工具信息,describeTool()返回详细声明。 - 嵌套工具调用经
ctx.executeTool()进入 Pi 的工具执行流程,因此参数校验和扩展的tool_call/tool_result钩子仍然适用。 - 如果走 Tool Search,搜索器会把匹配的工具加入 active tool set;AgentSession 在下一次模型请求中提供相应的工具声明,并把工具集变化保存在会话分支中。
MCP 连接也不一定阻塞用户的第一条消息。直接暴露的 MCP 工具必须尽早声明,Pi 会在首个 prompt 上等待一段有限时间;Codemode 或 Tool Search 暴露的 server 则可以在后台连接,需要它们时再等待。这让“配置了一个 MCP server”不意味着每次启动都要先同步等待所有工具服务。
5. 为什么把三者一起设计
5.1 MCP 放在扩展层,不塞进 Agent 核心
MCP 是一种工具互通协议,但不是 Agent Loop 本身。Pi 的核心需要定义工具调用、参数校验、工具执行事件和上下文等通用机制;至于某个工具来自内置实现、MCP server 还是第三方扩展,尽量交给扩展层处理。
这样既可以有官方维护、开箱可用的 MCP 支持,也不必让核心 Agent 引擎依赖每一种外部工具协议。官方 MCP、Codemode 和 Tool Search 是内置扩展,但内置不等于不可替换;第三方扩展可以接管同名入口。
5.2 渐进式披露,让模型只看当前需要的工具
direct 适合小而常用的工具集;codemode 适合脚本通过程序逻辑组合和压缩结果;deferred 则适合让模型先找到工具、查看完整 schema,再自己决定调用方式。
这三种选择比“全部工具每次都直接暴露”更灵活:工具目录可以很大,但模型上下文中实际声明的工具集仍然可以保持小而相关。
5.3 同一套工具既能被脚本编排,也能被模型逐步推理
Codemode 和 Tool Search 代表两种不同的控制方式:
- 让脚本决定如何串联工具:效率高,中间数据留在脚本里。
- 让主模型决定如何调用工具:每一步结果都能进入上下文,适合需要继续推理的工作。
MCP 工具不必只能选一种交互方式。Pi 可以按 server 或单个工具设置暴露方式,让常用工具直接可见、批处理工具走 Codemode、长尾工具留给 Tool Search。
6. 使用时的选择原则
- 工具少且经常使用:使用
direct,模型可直接选择和调用。 - 调用步骤明确,需要批量处理或过滤大量结果:使用默认
codemode,由脚本组合调用。 - 工具很多,但需要模型理解 schema 后自行规划调用:使用
deferred,让模型通过tool_search按需加载。 - 某些工具不应该被模型接触:设为
hidden。这是一种可用性控制,不是操作系统级的安全沙箱。
用一句话总结:**MCP 决定工具从哪里来,Codemode 决定脚本怎样使用工具,Tool Search 决定哪些工具声明按需交给模型。**这组设计既支持工具互通,又避免把所有工具目录都永久塞进模型上下文。