学 AI 产品 · 专业 AI 产品经理播客第 2 章 · T2 解剖 Grok Build:Rust 写的生产级 Coding Agent · EP 07
第 2 章 · EP 07

MCP 连接、发现与恢复 · Plugin Marketplace 的发现与信任

时长 14:37音色 云健 · 男声

同步字幕

章节导航(点击跳转)

0:00开场 · 连上那一刻,工程才刚开始1:27
1:27角色落位 · 先看谁是客户端1:05
2:32授权与凭据 · 密钥放 Env,令牌加锁1:46
4:19命名与可见性 · 两个服务端的同名工具怎么共存1:12
5:31搜索快照 · 工具不必全部常驻提示词1:26
6:58状态机与恢复 · 五十毫秒合并与身份护栏1:42
8:41插件结构 · 目录负责有哪些,运行时负责能用什么2:56
11:37三道门 · 从可见到可执行1:40
13:18可带走的设计原则 · 六条1:18
解读全文

grok07 · MCP 连接、发现与恢复 · Plugin Marketplace 的发现与信任

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版

本集定位

本集属于技术侧(S2)模块 T2「解剖 Grok Build:Rust 写的生产级 Coding Agent」,取材自 2 节课:

  1. MCP 连接、发现与恢复
  2. Plugin Marketplace 的发现与信任

这两条内容放在一起不是凑数。它们各自解决一个不同的外部扩展面:MCP 解决工具从哪来,插件市场解决能力包怎么安装和获准执行。但把源码摆平看,两边的工程重心完全同构——都不在传输或解析本身,而在连接之后的纪律:命名、可见性、身份、状态合并、恢复退避,以及来源、启用、信任三道门。

一句话概括:连上是起点,不是终点;出现在目录里是可发现,不是可执行。


一、能力地图

读完本集(听完音频),你应该能够:

能力具体表现
判定协议角色从「谁发起初始化 / 工具列表 / 调用」判断客户端与服务端,不把内部 Hub Server 当成通用 MCP Server
复述授权链路说清「复用刷新 → 浏览器授权 → 回调换令牌 → 锁定写入」四步,以及每步失败后的走向
处理凭据并发知道多进程场景下令牌为何必须用文件锁 + 原子保存写回
设计工具命名用 服务端名 + 保留分隔符 + 原始工具名 构造全局唯一名,并理解「分隔符恰好出现一次」的原因
区分三类受众说清 disabled / model-visible / UI-only 三条去向各自的落点
解释按需检索说明为什么要维护元数据快照,以及 mcp_initialized 标记解决了什么歧义
画出恢复状态机列出 Initializing / Ready / NeedsAuth / Unavailable / Disabled,并标出转换条件
设计抖动吸收用五十毫秒窗口做 last-write-wins 合并,估算高频事件的实际推送次数
防串连接用 client_id 护栏丢弃迟到事件,避免旧连接的善后逻辑删掉新连接
分开重启策略说明 stdio 与 HTTP 两种不同的退避与护栏检查
拆开 Marketplace 与运行时分清「哪些可以装」与「哪些会被加载」两条链
排序发现来源按优先级复述五种来源,并说出来源如何影响初始信任
走完三道门逐项检查来源与路径、启用状态、执行信任
读懂未信任矩阵知道 skill / agent / hook / MCP / script 在未信任状态下的处理差异
坚持 fail closed能解释为什么路径规范化失败必须落到「不信任」,而不是放行

二、MCP:连接只是起点

2.1 角色:先把方向定下来

SOURCE CONFIRMED —— Grok Build 是 MCP 客户端。

判定依据只有一条:看调用方向。McpClient 启动 stdio 或 Streamable HTTP 连接,执行初始化、list_tools 与 call_tool。Computer Hub 的 MCP Adapter 的描述也是「把 MCP Server 的工具桥接进 Hub 路由」——方向同样是往里接。

保守口径(重要):当前源码快照没有找到把 Grok Build 自身通过 MCP 传输暴露给任意 MCP Client 的入口。仓库里的 Hub Server 属于 xAI Computer Hub 协议,不能作为「Grok Build 也是通用 MCP Server」的证据。

这条结论听着像抠字眼,实际影响很大。如果你按「它同时也是服务端」去做对接设计,做出来的东西没有源码支撑,一次版本更新就可能无声崩塌。

判断项源码事实证据强度
Grok Build 发起 list_tools / call_tool是源码确认
Computer Hub Adapter 桥接外部 MCP 工具进 Hub 路由是源码确认
Grok Build 自身作为通用 MCP Server 对外暴露未发现入口未证实
Hub Server 可当作通用 MCP Server 证据不可以协议不同

2.2 授权链路:四步走,最后一步最容易被漏


1 · 复用或刷新   先读磁盘凭据,尝试 token refresh
2 · 浏览器授权   需要交互时启动用户同意流程
3 · 回调换令牌   授权码交换访问令牌与刷新令牌
4 · 锁定写入     文件锁 + 原子保存,支持多进程

配置侧只有三个字段需要关心:

  • oauth_client_id
  • oauth_client_secret_env_var —— 注意后缀是 env_var,存的是环境变量名字,不是密钥本体
  • oauth_scopes

凭据落点是本地 JSON 文件,路径为 grok_home 下的 mcp_credentials.json。读写顺序为 lock + load + insert + atomic save。

第 4 步「锁定写入」是工程上最容易省略的一步。它的必要性不在单进程,而在多进程:两个进程各自刷新令牌,后写的覆盖先写的,前一个进程手里就握着一张已经失效的令牌,而错误往往要等到下一次调用才暴露——排查成本极高。

提示一 · 给刷新阈值定标的方式

不要凭感觉设 refresh 提前量。先量两个数:令牌有效期和本进程的多实例启动频率。刷新太激进会被服务端限流,太保守就整天弹授权页。这两个数取到之后,阈值是算出来的,不是猜出来的。同理,「文件锁 + 原子保存」不是一个可选的优化项,而是多进程写入的前提条件——只要你的架构里可能出现两个进程同时操作同一份凭据文件,这一步就是必做。

2.3 命名与可见性:同一件事的两个切面

命名空间(NAMESPACE)

注册名由三段构成:服务端名 + 保留分隔符 __ + 原始工具名。源码要求完整名称中分隔符恰好出现一次(into_registration)。

为什么这么严?因为一旦分隔符出现两次,切分位置就产生了歧义——服务端自己的名字里如果带下划线,就会和分隔符打架,解析器无法判断从哪儿切开。

这条约定顺带解决了冲突问题:两个不同 Server 提供同名工具时,拼出的完整名不同,ToolId 也不同,模型侧不会串。

两类受众(TWO AUDIENCES)

工具不是「进来就等于能用」,它有三个去向:

  1. 被禁用 → 存入 disabled_tool_registrations,保留元数据,不进执行链
  2. model_visible 为真 → 进入模型侧 Tool Bridge,模型真正可调用
  3. 带 ui.resourceUri → 可单独进入 UI 通知,给上层应用看

常见错误是把它当成单一开关。三者是正交的:一个工具可以「对模型不可见、对 UI 可见、且未被禁用」。

2.4 搜索快照:工具不必全部常驻提示词

接了五六个 MCP Server 之后,工具数轻易上百,全塞进提示词既贵又乱。源码的做法是维护一份 ToolMetadataSnapshot:


pub struct ToolMetadataSnapshot {
    pub tools: Vec<ToolMetadata>,
    pub servers: Vec<ServerMetadata>,
    pub mcp_initialized: bool,
}

其中 mcp_initialized 这个布尔标记是关键。没有它,搜索层分不清两种完全不同的状态:「暂时搜不到」和「还没初始化」。前者是结果,后者是状态,二者给出不同的用户提示与重试策略。

检索侧建的是 BM25 索引,支持两种命中方式:

  • 按 qualified name(完整限定名)精确命中
  • 按 裸工具名命中,再由调用层 resolve 到唯一 ToolId

支持裸名的理由很实际:模型和用户不一定记得住完整前缀,裸名先把候选捞出来,再由调用层消歧。

取舍提示:按需检索省下提示词空间,代价是模型可能压根不知道某个能力存在(尤其是调用期才被发现的能力)。这套设计隐含一个要求——工具名必须足够自解释,否则裸名检索救不回来。命名偷懒,检索就是摆设。

2.5 状态机与恢复:难的是事件流,不是状态枚举

连接状态共五个:

状态含义
Initializing开始握手
Ready能力可用
NeedsAuth等待授权
Unavailable连接中断
Disabled配置关闭

枚举本身很简单,复杂度全在状态之间的事件流。三个机制必须配套:

① 五十毫秒合并(50 MS COALESCE)

mcp_dispatcher 以 (server_name, event_kind) 为键,在 50 ms tumbling window 内 last-write-wins。

课堂中的估算练习:模拟 100 条 tools/list_changed,写出合并后的预期通知数。答案不是 100,也不是固定常数——它取决于这批事件分布在多少个 50 ms 窗口内。理解这一点比记住数字重要:合并的收益来自「同一窗口内重复事件的折叠率」。

② 身份护栏(IDENTITY GUARD)

移除 dead client 之前比较 client_id。如果断线事件属于已被替换的旧客户端,则保留当前客户端,丢弃过期状态。

这条护栏防的是一类极难复现的线上 bug:旧连接的善后逻辑把新连接删了。表现为「偶发自动断线」,日志里看不出因果,因为没有 client_id 比较就无法区分事件的归属代际。

③ 重启策略(RESTART POLICY)

两种传输不同处理:

传输恢复动作退避护栏检查
stdio自动重启子进程固定 1s → 4s → 16s正在关闭 / 已被禁用 / 配置已被移除
HTTP先尝试客户端内恢复独立退避——

三道护栏检查不到位,最典型的症状就是:一个已被用户禁用的服务在后台反复拉起。

无论哪种传输,重连成功后都必须重新做能力发现与工具注册,随后刷新快照。 跳过这一步,连接是活的,工具列表却是旧的——这类不一致最难定位。


三、Plugin Marketplace:发现、安装、执行分层

3.1 真实结构


marketplace-root/
├── .grok-plugin/
│   ├── marketplace.json
│   └── plugin-index.json
├── plugins/
│   └── sample-plugin/
└── default-skills/

扫描器先读索引,索引缺失或无效时才回退去逐个扫 plugins/*/。这个顺序有性能原因:目录大时全盘扫很慢,索引是正常路径上的快通道。default-skills 可作为虚拟插件加入结果。

单个插件的结构:


sample-plugin/
├── plugin.json        # 首选 manifest
├── skills/*/SKILL.md
├── commands/
├── agents/
├── hooks/hooks.json
├── .mcp.json
└── scripts/

plugin.json 是首选 manifest,后备位置两处:.grok-plugin/plugin.json 与 .claude-plugin/plugin.json。PluginManifest 可覆盖 skills、commands、agents、hooks、MCP 与 LSP 六类路径。

关键实现细节:解析完 override 之后,还要验证这些路径仍包含在插件根目录内。少了这次校验,一个恶意 manifest 能把某类资源的路径指到别处去(例如把 skills 指向 .ssh)。

3.2 两条发现链,别混为一谈

职责归属包回答的问题
目录、扫描、安装xai-grok-plugin-marketplace有哪些东西可以装
运行时发现、去重、名称冲突、启用、信任xai-grok-agent::plugins哪些东西真的会被加载
核心区分:Marketplace 里出现一条记录,不代表组件会立即执行。 中间隔着来源判定、启用状态与信任三重检查。

发现来源按优先级从高到低:

  1. CLI override --plugin-dir —— 最高来源优先级
  2. Project .grok/plugins(兼容 .claude)
  3. User $GROK_HOME/plugins —— 已安装插件
  4. Registry marketplace provenance —— git / local 来源
  5. Config path [plugins].paths —— 位置影响信任

来源不只决定谁覆盖谁,它直接决定初始信任判断。

3.3 从可见到可执行的三道门

第一道门 · 来源与路径

MarketplaceRelativePath 拒绝三类输入:绝对路径、父目录穿越、越界 join。远程条目可用 git ref 或 SHA 定位内容。

提示二 · 远程插件默认锁 SHA

远程条目既然支持 SHA 定位,就默认锁 SHA 而不是锁分支。分支是可变的,意味着你今天审计过的插件,明天内容可能已经不同。锁 SHA 换来的是可复现性——这是插件生态里最值得花的五个字符。

第二道门 · 启用状态

发现配置维护 enabled 与 disabled 两个列表。默认值有明确偏向:

来源范围默认进入
项目范围disabled
用户范围disabled
CLI overrideenabled
Config pathenabled

这个偏向是有设计意图的:越靠近「别人给你的代码」,默认越保守。用户可显式调整。

第三道门 · 执行信任

粒度不是单个功能模块,而是单个插件根(canonical plugin root)。信任记录写入 ~/.grok/trusted-plugins。

判定逻辑如下:


match dunce::canonicalize(plugin_root) {
    Ok(path) => self.trusted.contains(&path),
    Err(_)   => false,          // ← FAIL CLOSED
}

Err(_) => false 这一行是本集最值得记住的一行代码。 路径解析失败 → 落到「不信任」,而不是「信任」。若反过来写(Err 也放行),攻击者只需制造一次解析失败就能绕过整道门——这道门就彻底失效了。

初始信任的来源影响:

来源初始判断
CLI override源码中标记为 trusted
用户范围源码中标记为 trusted
项目范围要求显式信任
Config path位于用户 home 下自动信任,其他位置仍需授权

3.4 未信任插件的组件矩阵

未信任并不等于完全不可见。源码保留了元数据级发现,只在执行面收口:

组件发现未信任时的执行
Skills / Agents可列出元数据保持元数据级发现
Hooks可从 manifest 识别阻止加载执行
MCP Servers可从配置路径识别阻止启动命令
Scripts属于插件内容阻止执行

一句话:可读性保留,可执行性收走。

提示三 · 安装来源必须可追溯

安装器会为每个已安装插件写入一条 provenance:本地 Marketplace 通过 managed install storage 安装,远程条目通过 Git URL、ref、SHA 与 subdir 定位,InstallRegistry 记录下来。

这条记录的价值体现在事后:出事时你能回答两个问题——这个插件从哪来,以及它锁在哪个提交上。没有 provenance,事故复盘就只能停在「装过一个插件」。


四、审查清单

落地走查时,按顺序逐项打勾:

MCP 侧

  • 角色已确认:客户端身份有源码依据,服务端角色区分 Hub Server 与通用 MCP Server
  • OAuth 四步齐全,第 4 步(文件锁 + 原子保存)未省略
  • 密钥本体不入配置文件,只存环境变量名(oauth_client_secret_env_var)
  • 工具注册名满足「分隔符恰好出现一次」
  • 三个去向(disabled / model-visible / UI-only)已分别配置,未用单一开关代替
  • ToolMetadataSnapshot 含 mcp_initialized,搜索层能区分「未初始化」与「无结果」
  • 恢复状态机的五个状态与转换条件已画清楚
  • client_id 护栏已实现,旧连接事件不会删掉新连接
  • 重启退避具备三道护栏(关闭中 / 已禁用 / 配置已移除)
  • 重连后重新执行能力发现与工具注册,并刷新快照

插件侧

  • Manifest override 的路径已校验仍位于插件根内
  • 五类来源优先级了然于胸,未跨来源混用
  • 远程插件锁 SHA 而非分支
  • enabled / disabled 默认偏向符合要求(项目/用户范围默认禁用)
  • 信任判定采用 fail closed(canonicalize 失败 → 不信任)
  • 未信任矩阵逐项验证:hook 不加载、MCP 不启动、script 不执行
  • 每个已安装插件都有 provenance 记录

提示四 · 给 teammates / CI 的最小实践包

如果你要在一个团队里落地上面的清单,三件事投入产出比最高:① 把 client_id 护栏和重连后重新发现写成两条必须通过的集成测试(分别覆盖 stdio 与 HTTP);② 在 CI 里加一个路径约束测试:构造含绝对路径、.. 穿越、越界 join 的 manifest,断言全部被拒;③ 信任相关测试必须包含错误分支——刻意让 canonicalize 失败,断言结果是拒绝而非放行。这三条覆盖的正是最贵、最难事后补的三类缺陷。


五、约束说明

本页严格遵守以下约束,也请你在引用时保持同样的克制:

  1. 取材范围:仅使用本集 2 节课的素材,不跨集取材,不引入外部数据或未在源码中出现的案例。
  2. 代码截取:所有代码片段均为源码快照中的教学截取,用于说明结构与语义,不代表可直接编译的完整实现。
  3. 保守结论:Grok Build 的 MCP 服务端角色未获源码证实,本页不作此推断;内部 Hub Server 属于 xAI Computer Hub 协议,不作为通用 MCP Server 的证据。
  4. 生态规模不可推断:源码能证明的是机制——官方源常量、多来源、目录索引、搜索与安装流程。它无法单独证明插件数量、活跃作者、审核覆盖率或增长速度,本页不提供这些数字。
  5. 目录树合并:文中目录树合并了源码默认约定用于教学表达;来源优先级、路径约束、启用配置与信任行为保持源码语义。
  6. 克制建议:文中所有「建议」(如锁 SHA、见于上文几处提示项)为工程取向建议,不等同于源码既定行为。

六、本集不建议做的事

  • 不要把机制的存在反推生态成熟度——插件市场有完整的目录与信任机制,不等于市场已经繁荣或经过审核。
  • 不要用「连上了」作为 MCP 集成完成的判据。七项外围责任(配置合并、OAuth、能力发现、命名隔离、可见性控制、状态推送、断线恢复)任何一项缺失都不算完成。
  • 不要把 disabled 与 model-invisible 当成同一件事:前者是不让它执行,后者是不让模型看见,UI 仍可能需要它。
  • 不要把信任判定的失败分支写成「乐观放行」。一行代码的差异,是整个信任体系的有无。

来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)
本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版