第十一章 外部工具与代码模式
11.1 MCP 把外部能力接入助手
如果工具都写在助手内部,每连接一个文档系统、数据库或业务服务,都可能需要一套专门的接入方式。MCP 即 Model Context Protocol,中文可以理解为模型上下文协议。它约定客户端怎样向服务器了解能力、调用工具和读取资源,让不同服务使用相近的交互方式。Pi 在这里主要扮演客户端,不是通用 MCP 服务器。
packages/mcp 提供独立客户端,并不依赖 Pi 的模型包或官方 MCP SDK。SDK 是供程序接入能力的软件开发工具包。客户端管理初始化、请求编号、响应配对、超时、取消和通知。传输层负责实际收发。把这两层拆开,就能在不改工具调用逻辑的情况下,选择本机进程、远程 HTTP 服务或测试时的内存连接。
stdio 传输启动一个子进程,用它的标准输入输出交换协议消息。配置里的 command 是可执行程序,args 是参数,不是一整段交给 shell 任意解释的命令。Streamable HTTP 传输通过网络与远程地址通信,可以带请求头或使用鉴权提供者。当前包不支持旧式 HTTP 加 SSE 传输,不能因为地址里有事件流就把它们视为同一种协议。
连接后,客户端先协商协议与能力,才能列出工具并调用。工具返回可以带文本、图片和结构化内容。协议错误与业务工具错误也要区分:请求本身可能成功到达服务器,但工具结果里的 isError 表示业务失败。上层包装工具时需要保留这一语义,否则模型可能把“调用成功送达”误读成“任务已经完成”。
11.2 登录和工具可见性由宿主安排
远程 MCP 服务有的接受固定令牌,有的要求 OAuth。OAuth 是一种通过授权流程取得访问凭证的机制。独立 MCP 包提供发现授权服务器、授权码流程与令牌刷新等能力,但不会自己替应用打开浏览器或选择持久化位置。这些交互和存储决定属于宿主。Pi 的命令行程序(CLI)在其 MCP 扩展里才把配置、登录界面和凭证文件连起来。
CLI 的用户配置通常在 ~/.pi/agent/mcp.json,项目配置在 .pi/mcp.json。项目配置需通过项目信任流程才读取。启用服务器并不意味着要把所有工具的详细说明一次性塞给模型。工具暴露方式可以选择直接声明、代码模式、延迟搜索或隐藏。暴露方式决定“模型怎样发现与到达工具”,不等于远端服务授予了哪些账户权限。
当前默认的 MCP 暴露方式是 codemode:工具能从脚本调用,但不会逐个直接声明给模型。direct 则直接声明;deferred 可以通过工具搜索加载;hidden 让工具不可达。这些都是当前 coding-agent 集成层的策略,不是 MCP 协议规定所有客户端必须采用的默认行为。
还应分清 CLI 与 SDK。CLI 会装配 MCP、codemode、tool-search 等内置扩展,但 codemode 工具注册时默认不激活。默认允许自动启用时,带 codemode 暴露工具的服务器连接会激活它;deferred 工具对应工具搜索。SDK 默认创建会话时不会自动加载这组 CLI 内置扩展,需要开发者显式加入所需工厂,不能仅复制 MCP 配置就假定已经接好。
11.3 代码模式怎样减少中间上下文
codemode 即代码模式:让模型写一小段 JavaScript,程序在受控环境运行它,脚本再调用已注入的工具。packages/codemode 使用编译为 WebAssembly 的 QuickJS。WebAssembly 是一种可在不同宿主中运行的二进制执行格式;QuickJS 是 JavaScript 引擎。这里的脚本不直接运行在宿主 Node 环境中。
脚本通过 tools 调用宿主提供的函数,参数与结果经过 JSON 往返;可用 text 输出文字、image 输出图片,用 return 返回值。工具列表还可以通过 ALL_TOOLS 了解。Node 与 Bun 的 CodemodeSandbox 通常为每次执行启动工作线程与新的虚拟机;可移植和远程方案采用其他承载方式。不同方案共享脚本接口,但执行位置和限制并不完全相同。
看一个假设任务:“从三百条工单中统计本周各优先级数量,再列出五条未解决的高优先级工单。”普通逐次调用可以把三百条完整结果交回模型,再让模型筛选。代码模式可以让脚本调用工单工具,在脚本中按日期与状态过滤,再输出统计和五个标题。中间结果留在工具与脚本之间,模型上下文只收到脚本输出和返回值。
这不是自动消除成本。工具仍然发起了真实请求,脚本仍消耗运行资源,错误的筛选条件仍会给出错误统计。收益来自减少模型需要读取的中间材料,以及把简单循环和计算交给程序。若脚本把完整三百条结果全部打印出来,上下文节省也会消失。因此应按任务选择保留哪些证据,不能为了短而丢掉判断所需的信息。
在 Pi 的 MCP 集成里,脚本收到完整 CallToolResult,包括 content、structuredContent 和 isError;具体使用哪个字段应依据服务实际返回。不能假定调用结果永远是一个直接可筛选的业务数组。工具可能返回错误,也可能把数据包装在结构化字段里。章中工单流程是伪代码思路,没有声称任何真实 MCP 服务已提供同名接口。
11.4 虚拟机隔离不等于工具没有权限
虚拟机脚本没有直接的 fetch、process、require、模块或任意文件系统接口,能接触宿主的主要路径是注入函数。这样可以限制脚本自行打开网络或进入宿主对象的机会。但如果宿主注入了“写文件”“执行 shell”或“发送请求”的工具,脚本仍可通过这些工具产生对应副作用。能力边界取决于注入了什么,以及工具实现如何检查请求。
例如只注入“读取指定文档”函数,脚本不能凭空获得删除服务器文件的函数。若注入的读取函数任意接受路径、还使用宿主全部文件权限,那么限制就不能只写在工具描述里。描述用于告诉模型怎样调用,真正约束需要由执行代码落实。独立 codemode 包中的 JSON Schema,即描述参数字段和类型的格式说明,主要用于生成工具声明,不会自动据此校验每个工具值。
与 Agent 集成时,直接调用工具的 execute 会绕过 Agent 的前后调用钩子。若需要嵌套调用也经过同一管线,应使用 runToolCall 等相应入口。Pi 的 MCP 集成会让调用经过其工具管线,权限扩展可以据此处理。不能把“存在钩子入口”说成“默认已为所有外部操作配置了某种确认规则”。
执行也需要边界。超时、内存限制和取消信号控制脚本运行,但默认值随沙箱方案不同,不应混用。取消结果返回后,某些环境中的工作线程或远程虚拟机仍可能短暂计算;工具也需要响应自己的取消信号。已经成功提交到外部系统的修改不会因虚拟机停止而自动撤销。隔离、终止和业务回滚是不同职责。
11.5 小结与练习
MCP 解决外部能力怎样连接,代码模式解决怎样组合调用并整理中间数据。两者结合可以让模型专注目标与关键证据,但真正的服务权限、工具校验和运行限制仍由宿主与服务共同实现。
练习一:MCP 工具没有直接出现在模型声明里,就一定不能调用吗?参考答案:不一定。当前 Pi 可以通过 codemode 或工具搜索间接到达,具体取决于暴露设置;hidden 工具则不可达。
练习二:QuickJS 没有文件 API,是否保证脚本无法改文件?参考答案:不能。宿主若注入可写文件的工具,脚本仍可通过工具修改;隔离约束的是访问路径,不会消除已授予的能力。
练习三:为什么在脚本里过滤三百条工单可能节省上下文?参考答案:中间工具结果不逐条进入模型对话,只输出完成任务所需的统计与条目;若全部打印,收益就会减小。