前言

在大概半年前的文章里,我介绍了 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 后,会带来几个问题:

  1. 上下文膨胀:工具名称、描述、参数 schema 会占据每一次模型请求的空间。
  2. 选择干扰:很多相似工具同时出现,模型更容易选错或难以发现真正相关的工具。
  3. 暴露面过大:即使某个工具在当前任务中无关,它仍出现在模型可调用的工具集合里。

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 加入后续请求

更具体地说:

  1. mcp/index.ts 在 session_start 读取配置,并在后台启动 server 连接。
  2. 连接完成后,MCP 扩展注册各个 Pi 工具。mcp/tools.ts 把参数 schema、输出 schema、MCP 调用和返回结果包装成 Pi 的 ToolDefinition。
  3. 如果工具是间接暴露的,MCP 扩展仍把它保留在 Pi 的内部工具目录中。Codemode 会从当前可调用工具创建脚本侧的 tools 接口;searchTools() 使用 BM25 排序工具信息,describeTool() 返回详细声明。
  4. 嵌套工具调用经 ctx.executeTool() 进入 Pi 的工具执行流程,因此参数校验和扩展的 tool_call / tool_result 钩子仍然适用。
  5. 如果走 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. 使用时的选择原则

  1. 工具少且经常使用:使用 direct,模型可直接选择和调用。
  2. 调用步骤明确,需要批量处理或过滤大量结果:使用默认 codemode,由脚本组合调用。
  3. 工具很多,但需要模型理解 schema 后自行规划调用:使用 deferred,让模型通过 tool_search 按需加载。
  4. 某些工具不应该被模型接触:设为 hidden。这是一种可用性控制,不是操作系统级的安全沙箱。

用一句话总结:**MCP 决定工具从哪里来,Codemode 决定脚本怎样使用工具,Tool Search 决定哪些工具声明按需交给模型。**这组设计既支持工具互通,又避免把所有工具目录都永久塞进模型上下文。

参考资源