第 2 章 · 工具与 MCP 扩展
这一章讲 goose 的「插槽标准」:工具不是写死在代码里的,而是由一个个 MCP 扩展提供。本章解释扩展有哪几种、工具怎么命名、调用时 goose 怎么从一个工具名找到该派给谁。
2.1 直觉:扩展是「插槽」,MCP 是「插头规格」
MCP(Model Context Protocol,模型上下文协议)是一个开放标准,规定了「一个能提供工具/资源的 server 长什么样、怎么和 host 通信」。goose 是 MCP 的 host:它能同时插上很多个 MCP server(在 goose 里叫 extension / 扩展),把它们各自的工具汇总成一张大列表喂给模型。
类比:goose 主机是一台带很多 USB 口的电脑,每个扩展是一个 USB 设备(键盘、硬盘、网卡),MCP 就是 USB 这个统一规格。你不需要改主机就能加设备。
2.2 扩展有七种类型
ExtensionConfig(agents/extension.rs:163)是个 enum,枚举了 goose 支持的所有接入方式:
| 类型 | 怎么跑 | 典型用途 |
|---|---|---|
Stdio | 启一个子进程,用 stdin/stdout 通信 | 本地命令行 MCP server(最常见) |
StreamableHttp | 连一个 HTTP MCP server | 远程 / 云端工具 |
Sse | Server-Sent Events(代码里标注为已不支持,extension.rs:423) | 旧式远程 |
Builtin | goose 自带、随二进制分发的扩展 | goose-mcp 里的 computercontroller / memory 等 |
Platform | 直接在 agent 进程内、能访 问 agent 本身 | 需要操作 agent 状态的特权工具(如管理扩展) |
Frontend | 工具由前端(桌面 App / CLI)执行 | 需要 UI 介入的工具 |
InlinePython | 内联一段 Python 代码作为扩展直接执行(可声明依赖、超时) | 配方/会话里临时注入的轻量自定义工具 |
注意 Frontend 和 Platform 是特殊路径:前端工具不走 MCP client,而是把请求 yield 回 UI 让前端跑(agents/agent.rs:1140);platform 工具如调度管理在 dispatch_tool_call 里被直接拦截处理(agents/agent.rs:1094)。
2.3 工具命名:扩展名__工具名
多个扩展可能各有一个叫 read 的工具,怎么不打架?goose 给每个工具名加扩展名前缀,分隔符是双下划线 __。
聚合工具时拼前缀(agents/extension_manager.rs:1395):
// agents/extension_manager.rs:1395 — fetch_all_tools 给每个工具打上「归属扩展」前缀
format!("{}__{}", name, tool.name)
于是模型看到的是 developer__shell、memory__remember 这样的全名。这一步既消歧,也让 goose 之后能从工具名反推它属于哪个扩展。
2.4 调用时怎么找到对的扩展:resolve_tool + dispatch_tool_call
模型点了 developer__shell,goose 要把它送到 developer 这个扩展的 MCP client。这一步是 resolve_tool(agents/extension_manager.rs 内,核心片段在 :1676-1726):
- 先精确查:在汇总的工具表里找到这个全名,通过
get_tool_owner拿到归属扩展(extension_manager.rs:1677)。 - 拿不到 owner 就按前缀拆:用
split_once("__")把prefix__actual拆开,前缀就是扩展名(extension_manager.rs:1714)。 - 去掉前缀还原真实工具名:
strip_prefix("{owner}__")(extension_manager.rs:1692)——因为真正发给 MCP server 的应该是它自己认识的短名shell,而不是带前缀的全名。 - 找到对应 client,返回
ResolvedTool。
然后 dispatch_tool_call(agents/extension_manager.rs:1744)用这个解析结果:
// agents/extension_manager.rs:1751 — 先解析归属,再校验可用性,再真正调用
let resolved = self.resolve_tool(&ctx.session_id, &tool_name_str).await?;
// ... 校验 is_tool_available ...
// 订阅该扩展的通知流,把工具执行期间的 MCP notification 也带回来
let notifications_receiver = client.subscribe().await;
注意它在调用前还订阅了该 client 的通知流——这就是第 1 章里 McpNotification 事件的来源:工具跑的过程中,MCP server 可以推进度/日志,goose 把它们一路流回 UI。
2.5 工具列表怎么组装(给模型看的那张表)
prepare_tools_and_prompt(agents/reply_parts.rs:147)负责每圈开始前准备工具列表 + 系统提示:
list_tools汇总所有扩展的前缀工具 + 前端工具 + (有调度服务时)平台调度工具 + final_output 工具(agents/agent.rs:1425)。- 过滤掉「对模型不可见」的工 具(MCP Apps 可见性规范,
reply_parts.rs:211)。 - 按名字排序(
reply_parts.rs:214)——注释点明这是为了多会话 prompt 缓存的稳定性:工具顺序固定,prompt 前缀才能命中缓存。
工具列表有缓存(get_all_tools_cached,extension_manager.rs:1292),用一个版本号 + lock 做无效化(invalidate_tools_cache_and_bump_version,extension_manager.rs:1356),避免每圈都去问所有扩展。
2.6 系统提示是「拼」出来的
PromptManager(agents/prompt_manager.rs:22)用一个 builder 拼系统提示,模板是 crates/goose/src/prompts/system.md(Jinja 风格)。拼进去的有:
- 当前启用的扩展列表 + 各自的 instructions(
system.md的{% for extension in extensions %}段)。 - 工具/扩展数量超限提醒(默认上限 5 扩展 / 50 工具,
prompt_manager.rs:19-20):超了就建议用户关掉一些以提高选工具准确率。 - 当前
goose_mode、是否启用子 agent、工作目录下的 hints 文件(.goosehints等)。
2.7 子 agent(subagent)
goose 允许主 agent 派生一个子 agent 去独立完成一个子任务。run_subagent_task(agents/subagent_handler.rs:48)的做 法很直白——子 agent 就是一个全新的 Agent 实例:
// agents/subagent_handler.rs:141 — 子 agent = 新建一个 Agent,配独立 provider/扩展,再调它的 reply
let agent = Arc::new(Agent::with_config(config));
agent.update_provider(task_config.provider.clone(), task_config.model_config.clone(), &session_id).await?;
for extension in &task_config.extensions { agent.add_extension(extension.clone(), &session_id).await?; }
// ... 然后 agent.reply(user_message, ...) 跑它自己的循环
子任务用 recipe(可复用任务模板,recipe/mod.rs:43 的 struct Recipe)描述 instructions 和 prompt。子 agent 跑完后,把它的文本输出(或 final_output)抽出来作为一条工具结果返回给主 agent(extract_response_text,subagent_handler.rs:65)。这让「主 agent 调度、子 agent 干活」成为可能,且子 agent 的上下文与主 agent 隔离。
2.8 小结
- 工具全 部来自 MCP 扩展;七种接入类型覆盖本地子进程、远程 HTTP、内置、平台特权、前端执行、内联 Python。
- 工具名 =
扩展名__工具名,既消歧又能反推归属;resolve_tool靠这个前缀路由。 - 工具列表稳定排序是为了 prompt 缓存命中;列表有版本化缓存。
- 子 agent 就是「再 new 一个 Agent 跑独立 reply 循环」,用 recipe 描述任务。
下一章看工具在执行前必须先过的安检与权限 → 03-safety-chain.md。