1
00:00:00,100 --> 00:00:06,432
本内容改编自小山学堂《学 AI 产品，从入门到精通》，为二次演绎配音版。

2
00:00:06,382 --> 00:00:16,646
这一集讲两件事，一条 MCP 连接到底要承担多少协议外围的责任，以及一个插件从被看见到被执行要穿过哪几道门。

3
00:00:16,646 --> 00:00:28,016
两个话题看起来不相关，讲的其实是同一个道理，能力面可以做得很大，但真正决定系统稳不稳的，是你有没有把信任和执行边界收敛住。

4
00:00:28,016 --> 00:00:29,615
为什么要关心。

5
00:00:29,615 --> 00:00:33,846
很多人接 MCP，写完传输层、连上就算完事。

6
00:00:33,846 --> 00:00:37,067
跑十分钟挺好，跑三天就出问题。

7
00:00:37,067 --> 00:00:46,610
工具名撞车了，断线重连之后旧客户端的迟到事件把新连接删了，服务端一秒推二十次工具列表变更把界面冲垮。

8
00:00:46,610 --> 00:00:49,194
这些问题全在连上之后。

9
00:00:49,194 --> 00:00:51,935
插件这边也有同款误会。

10
00:00:51,935 --> 00:00:59,387
目录里出现一条记录，不代表插件会执行，中间隔着来源、启用状态和信任三道门。

11
00:00:59,387 --> 00:01:05,228
今天全程只讲源码能证实的事实，证不实的地方我会明确说不证实。

12
00:01:05,228 --> 00:01:06,887
顺序是这样。

13
00:01:06,887 --> 00:01:17,536
上半场从连接往外走，先看角色，再看授权落点，然后看工具怎么拿到模型可见性，最后看一条连接的生命周期和恢策略。

14
00:01:17,536 --> 00:01:25,985
下半场进插件，看目录、安装、运行时发现这三层的分工，以及从可见到可执行要过的三道门。

15
00:01:25,985 --> 00:01:27,295
开始。

16
00:01:27,440 --> 00:01:32,317
第一件事，把角色搞清楚，否则后面全是错的方向。

17
00:01:32,267 --> 00:01:35,537
判断方法很简单，看调用方向。

18
00:01:35,537 --> 00:01:40,957
谁发起初始化、谁拉工具列表、谁发起调用，谁就是客户端。

19
00:01:40,957 --> 00:01:50,777
源码里的 McpClient 启动 stdio 或者 Streamable HTTP 连接，执行初始化、拉工具列表、发起工具调用，所以 Grok Build 是客户端。

20
00:01:50,777 --> 00:01:58,541
另一处的 Computer Hub MCP Adapter，描述的是把外部 MCP Server 的工具桥接进 Hub 路由，方向也是往里接。

21
00:01:58,541 --> 00:02:01,090
这里必须说一句保守结论。

22
00:02:01,090 --> 00:02:08,722
当前源码快照里，没有找到把 Grok Build 自身通过 MCP 传输暴露给任意 MCP Client 的入口。

23
00:02:08,722 --> 00:02:18,013
仓库里确实有一个 Hub Server，但它属于 xAI Computer Hub 这套协议，不能拿来当作 Grok Build 也是通用 MCP 服务端的证据。

24
00:02:18,013 --> 00:02:20,308
这个区分不是抠字眼。

25
00:02:20,308 --> 00:02:27,712
你要是把它当服务端去对接下游工具链，做出来的东西没有源码支撑，一次版本更新就塌。

26
00:02:27,712 --> 00:02:32,796
拿不准的部分，就标注未证实，别拿它当设计依据。

27
00:02:32,940 --> 00:02:36,483
连上之后第一道真实复杂度是授权。

28
00:02:36,433 --> 00:02:38,885
源码把这件事拆成四步。

29
00:02:38,885 --> 00:02:41,097
第一步，复用或刷新。

30
00:02:41,097 --> 00:02:44,642
先读磁盘上的凭据，尝试刷新令牌。

31
00:02:44,642 --> 00:02:49,570
也就是说，绝大多数情况下用户不该被反复弹浏览器。

32
00:02:49,570 --> 00:02:51,890
第二步，浏览器授权。

33
00:02:51,890 --> 00:02:57,551
只有刷新走不通、确实需要用户参与的时候，才启动同意流程。

34
00:02:57,551 --> 00:02:59,931
第三步，回调换令牌。

35
00:02:59,931 --> 00:03:03,981
拿到授权码，换回访问令牌和刷新令牌。

36
00:03:03,981 --> 00:03:06,181
第四步，锁定写入。

37
00:03:06,181 --> 00:03:08,416
这一步最常被漏掉。

38
00:03:08,416 --> 00:03:13,645
令牌要写回磁盘，而写就意味着可能有多个进程同时写。

39
00:03:13,645 --> 00:03:20,628
源码的做法是文件锁配合原子保存，读取、插入、写回放在同一个临界区里。

40
00:03:20,628 --> 00:03:29,462
少了这一步，两个进程各刷一次令牌，后写的覆盖先写的，先那个进程手里就握着一张已经失效的令牌。

41
00:03:29,462 --> 00:03:40,940
配置侧只有三个字段需要你关心，oauth_client_id、oauth_client_secret_env_var、oauth_scopes。

42
00:03:40,940 --> 00:03:49,522
注意第二个字段名字里带了 env_var 后缀，意思是密钥本体不进配置文件，只放环境变量的名字。

43
00:03:49,522 --> 00:03:57,864
凭据本体落在一个本地 JSON 文件里，路径是 grok_home 下面的 mcp_credentials.json。

44
00:03:57,864 --> 00:04:00,460
再补一句实操上的判断。

45
00:04:00,460 --> 00:04:07,046
这四步里最容易出问题的是第一步和第四步，也就是刷新时机和写入并发。

46
00:04:07,046 --> 00:04:12,683
刷新太激进会被服务端限流，刷新太保守就整天弹授权页。

47
00:04:12,683 --> 00:04:19,498
真要调，先量一下你的令牌有效期和多进程启动频率，别凭感觉设阈值。

48
00:04:19,640 --> 00:04:22,739
工具进来之后，先解决命名。

49
00:04:22,689 --> 00:04:29,828
注册名是三段拼起来的，服务端名、分隔符、原始工具名，分隔符是两个下划线。

50
00:04:29,828 --> 00:04:35,645
源码在生成注册名的时候要求完整名称里这个分隔符恰好出现一次。

51
00:04:35,645 --> 00:04:37,364
为什么这么较真。

52
00:04:37,364 --> 00:04:44,455
因为出现两次你就没法确定从哪儿切，服务端自己名字里带的下划线会和分隔符打架。

53
00:04:44,455 --> 00:04:47,364
这个约定还顺手解决了一件事。

54
00:04:47,364 --> 00:04:55,369
两个不同服务端提供同名工具的时候，拼出来的完整名字不同，ToolId 也就不同，模型侧不会串。

55
00:04:55,369 --> 00:04:57,112
然后是可见性。

56
00:04:57,112 --> 00:05:00,886
工具不是进来就等于能用，它有三个去向。

57
00:05:00,886 --> 00:05:09,059
被禁用的工具，进一个叫 disabled_tool_registrations 的集合，保留元数据但不进执行链。

58
00:05:09,059 --> 00:05:16,583
model_visible 为真的那些，才进入模型侧的 Tool Bridge，也就是模型真正能调用的那批。

59
00:05:16,583 --> 00:05:22,640
还有一批带 ui.resourceUri 的工具，可以单独走 UI 通知给上层应用看。

60
00:05:22,640 --> 00:05:28,422
所以一个 MCP 工具有三个受众，模型、应用界面，以及谁都不给。

61
00:05:28,422 --> 00:05:31,823
把这当成单一开关，迟早会漏。

62
00:05:31,980 --> 00:05:33,817
再往上看一层。

63
00:05:33,767 --> 00:05:40,593
接了五六个 MCP 服务端之后，工具数轻易上百，全塞进提示词既贵又乱。

64
00:05:40,593 --> 00:05:54,043
源码的做法是维护一份工具元数据快照，里面存两样东西，工具列表和服务端列表，外加一个布尔标记，mcp_initialized，用来告诉搜索层能力发现到底做完了没有。

65
00:05:54,043 --> 00:06:01,291
这个标记很重要，没有它就分不清暂时搜不到和还没初始化这两种完全不同的状态。

66
00:06:01,291 --> 00:06:10,918
检索侧建的是一套 BM 二五,索引，支持两种命中方式，一个是拿完整限定名精确命中，一个是拿裸工具名命中。

67
00:06:10,918 --> 00:06:13,045
为什么要支持裸名。

68
00:06:13,045 --> 00:06:21,098
因为模型和用户不一定记得住完整前缀，裸名能先把候选捞出来，再由调用层解析到唯一的 ToolId。

69
00:06:21,098 --> 00:06:28,562
这样提示词里只需要放当前任务相关的少数工具，剩下的留在快照里按需检索。

70
00:06:28,562 --> 00:06:36,531
能力发现完成之前，搜索层知道自己处在半初始化状态，不会把还没发现误报成没有这个工具。

71
00:06:36,531 --> 00:06:39,055
这里有个取舍值得停一下。

72
00:06:39,055 --> 00:06:48,370
按需检索省下了提示词空间，代价是模型可能压根不知道某个能力存在，尤其是它在调用期才被发现的情况。

73
00:06:48,370 --> 00:06:54,704
源码选了这条路的另一面是，工具名要足够自解释，让裸名检索能命中。

74
00:06:54,704 --> 00:06:58,081
命名偷懒，检索就救不回来。

75
00:06:58,240 --> 00:07:01,014
接着看连接本身的生命周期。

76
00:07:00,964 --> 00:07:10,291
源码把连接分成五个状态，Initializing 开始握手，Ready 能力可用，NeedsAuth 等待授权，Unavailable 连接中断，Disabled 配置关闭。

77
00:07:10,291 --> 00:07:13,861
看着简单，难的是状态之间的事件流。

78
00:07:13,861 --> 00:07:16,277
第一个机制，事件合并。

79
00:07:16,277 --> 00:07:20,880
服务端可以高频推工具列表变更，一次刷新几十条。

80
00:07:20,880 --> 00:07:30,123
源码里的分发器以服务端名加事件类型作为键，在五十毫秒的滚动窗口内保留最新事件，也就是后写覆盖先写。

81
00:07:30,123 --> 00:07:34,293
所以一百条变更最终只往上推一次状态。

82
00:07:34,293 --> 00:07:40,135
这是典型的抖动吸收，没有它，界面会被每一次工具列表刷新闪一下。

83
00:07:40,135 --> 00:07:42,551
第二个机制，身份护栏。

84
00:07:42,551 --> 00:07:47,515
移除一个断线的客户端之前，要先比较 client_id。

85
00:07:47,515 --> 00:07:55,808
如果一个迟到的断线事件属于已经被替换掉的旧客户端，就保留当前客户端，丢弃那条过期状态。

86
00:07:55,808 --> 00:08:01,721
这条护栏防的是一类极难复现的 bug，旧连接的善后逻辑把新连接删了。

87
00:08:01,721 --> 00:08:05,387
第三个机制，重启策略按传输分开。

88
00:08:05,387 --> 00:08:17,899
stdio 子进程崩了要重启，退避是固定的一秒、四秒、十六秒，并且每次重启前检查三道护栏，是否正在关闭、是否已被禁用、配置是否已被移除。

89
00:08:17,899 --> 00:08:23,765
这三个检查不到位，你就能看到一个被用户禁用的服务在后台反复拉起。

90
00:08:23,765 --> 00:08:29,522
HTTP 这边不同，先在客户端内部尝试恢复，退避也是独立的一套。

91
00:08:29,522 --> 00:08:36,313
无论哪种传输，重连成功之后都要重新做能力发现和工具注册，再刷新快照。

92
00:08:36,313 --> 00:08:41,012
跳过这一步，连接是活的，工具列表却还是旧的。

93
00:08:41,150 --> 00:08:43,804
下半场换话题，看插件。

94
00:08:43,754 --> 00:08:54,235
一个 Marketplace 根目录下有个 .grok-plugin 目录，里面放 marketplace.json 和 plugin-index.json，再下面是 plugins 目录和 default-skills 目录。

95
00:08:54,235 --> 00:09:01,206
扫描器先读索引，索引缺失或者无效，才回退去逐个扫 plugins 下面的子目录。

96
00:09:01,206 --> 00:09:09,427
这个先索引后扫盘的顺序是有原因的，目录大的时候全盘扫很慢，索引是正常情况下的快路径。

97
00:09:09,427 --> 00:09:13,405
default-skills 会被当成虚拟插件加进结果里。

98
00:09:13,405 --> 00:09:22,672
单个插件这边，首选 manifest 是插件根下的 plugin.json，后备位置有两处，.grok-plugin 下面的和 .claude-plugin 下面的。

99
00:09:22,672 --> 00:09:30,124
manifest 可以覆盖六类路径，skills、commands、agents、hooks、MCP 配置以及 LSP。

100
00:09:30,124 --> 00:09:32,420
关键细节在后面一句。

101
00:09:32,420 --> 00:09:38,069
解析完这些覆盖之后，还要验证路径依然包含在插件根目录里面。

102
00:09:38,069 --> 00:09:44,126
少了这次校验，一个恶意 manifest 就能把某类资源的路径指到别的地方去。

103
00:09:44,126 --> 00:09:46,975
然后是必须分清的一条界线。

104
00:09:46,975 --> 00:09:52,888
Marketplace 这个包负责目录、扫描和安装，也就是有哪些东西可以装。

105
00:09:52,888 --> 00:10:01,194
另一个包负责运行时发现、去重、名称冲突、启用状态和信任，也就是哪些东西真的会被加载。

106
00:10:01,194 --> 00:10:06,098
目录里出现一条记录，不代表里面的组件会立刻执行。

107
00:10:06,098 --> 00:10:08,790
来源有优先级，从高到低。

108
00:10:08,790 --> 00:10:12,035
第一是命令行直接指定的插件目录。

109
00:10:12,035 --> 00:10:15,280
第二是项目级目录，兼容 .claude 的命名。

110
00:10:15,280 --> 00:10:18,550
第三是用户主目录下的已安装插件。

111
00:10:18,550 --> 00:10:23,465
第四是带 Marketplace 来源信息的安装记录，本地或 git 都算。

112
00:10:23,465 --> 00:10:26,614
第五是配置文件里写死的路径列表。

113
00:10:26,614 --> 00:10:31,422
来源不只决定谁覆盖谁，它直接影响初始信任判断。

114
00:10:31,422 --> 00:10:39,607
命令行 override 和用户范围在源码里标记为受信任，项目范围要求显式信任，因为项目是别人给你的代码。

115
00:10:39,607 --> 00:10:45,797
配置路径位于用户主目录下时才自动信任，其他位置仍然要授权。

116
00:10:45,797 --> 00:10:49,175
安装这一层还留了一条可追溯的记录。

117
00:10:49,175 --> 00:10:58,838
本地 Marketplace 通过受管的安装存储落盘，远程条目则通过 git 地址、分支引用、提交哈希以及子目录来定位内容。

118
00:10:58,838 --> 00:11:06,242
安装器会把这些来源信息写进一个安装注册表，也就是每个已装插件都带着一条 provenance。

119
00:11:06,242 --> 00:11:13,634
这条记录的价值在事后，出事的时候你能回答这个插件是从哪来的、锁在哪个提交上。

120
00:11:13,634 --> 00:11:15,473
最后说一句边界。

121
00:11:15,473 --> 00:11:23,333
源码能证明的是机制，官方源常量、多来源、目录索引、搜索和安装流程都写得很清楚。

122
00:11:23,333 --> 00:11:31,458
它证明不了生态规模，插件数量、活跃作者数量、审核覆盖率、增长速度，这些都推不出来。

123
00:11:31,458 --> 00:11:37,432
我不做这些推断，也不建议你拿机制的存在去反推生态的成熟。

124
00:11:37,570 --> 00:11:40,861
从可见到可执行，要过三道门。

125
00:11:40,811 --> 00:11:43,191
第一道门，来源与路径。

126
00:11:43,191 --> 00:11:49,885
相对路径类型会拒绝三种情况，绝对路径、父目录穿越、越界拼接。

127
00:11:49,885 --> 00:11:57,145
远程条目还可以用 git ref 或者提交哈希定位内容，这是为了可复现，建议默认锁哈希。

128
00:11:57,145 --> 00:11:59,320
第二道门，启用状态。

129
00:11:59,320 --> 00:12:03,179
发现配置里维护 enabled 和 disabled 两个列表。

130
00:12:03,179 --> 00:12:11,352
默认值是有偏向的，项目范围和用户范围默认进 disabled，命令行 override 和配置路径默认进 enabled。

131
00:12:11,352 --> 00:12:15,486
这个偏向有道理，越靠近别人的代码越保守。

132
00:12:15,486 --> 00:12:17,506
用户可以显式调整。

133
00:12:17,506 --> 00:12:19,945
第三道门，执行信任。

134
00:12:19,945 --> 00:12:27,758
粒度不是单个功能模块，而是单个插件根，授权记录写进用户主目录下的 trusted-plugins 文件。

135
00:12:27,758 --> 00:12:33,335
判定方式是先取插件根的规范路径，命中信任集合就放行。

136
00:12:33,335 --> 00:12:36,075
这里有一行逻辑值得单独讲。

137
00:12:36,075 --> 00:12:40,138
路径规范化返回错误的时候，结果是 false。

138
00:12:40,138 --> 00:12:44,417
也就是说失败指向不信任，而不是指向信任。

139
00:12:44,417 --> 00:12:47,097
这是 fail closed，必须这么写。

140
00:12:47,097 --> 00:12:53,359
要是任何异常都放行，攻击者只要制造一次解析失败就能绕过整道门。

141
00:12:53,359 --> 00:12:56,508
未信任的插件也不是完全看不见。

142
00:12:56,508 --> 00:13:00,943
skill 和 agent 可以列出元数据，保持元数据级发现。

143
00:13:00,943 --> 00:13:04,885
hook 能从 manifest 识别，但阻止加载执行。

144
00:13:04,885 --> 00:13:09,405
MCP 服务端能从配置路径识别，但阻止启动命令。

145
00:13:09,405 --> 00:13:13,046
脚本属于插件内容，一律阻止执行。

146
00:13:13,046 --> 00:13:18,167
这个矩阵的意思很明确，可读性保留，可执行性收走。

147
00:13:18,320 --> 00:13:21,635
最后收一下，六条可以带走的东西。

148
00:13:21,585 --> 00:13:23,640
第一，先定方向。

149
00:13:23,640 --> 00:13:27,703
判断谁是客户端，看谁发起初始化和调用。

150
00:13:27,703 --> 00:13:32,751
没有源码证据的部分就标注未证实，别拿它当架构依据。

151
00:13:32,751 --> 00:13:36,970
第二，凭据的存储方式要和它的生命周期匹配。

152
00:13:36,970 --> 00:13:44,193
令牌落盘的那一刻，就要同时解决并发写入，文件锁加原子保存，缺一不可。

153
00:13:44,193 --> 00:13:53,448
第三，任何会被多方提供的资源，都要有全局唯一的限定名，并且保证分隔符不会出现在参与者自己的名字里。

154
00:13:53,448 --> 00:13:57,390
第四，上游的高频事件一律做窗口合并。

155
00:13:57,390 --> 00:14:02,655
五十毫秒的后写覆盖，换来的稳定性远超那点延迟成本。

156
00:14:02,655 --> 00:14:07,282
第五，重连之后必须重新做能力发现和工具注册。

157
00:14:07,282 --> 00:14:10,683
连接活着不等于工具列表是最新的。

158
00:14:10,683 --> 00:14:13,796
第六，信任判定永远 fail closed。

159
00:14:13,796 --> 00:14:19,686
路径解析失败、规范路径取不到、来源说不清，统统落到不信任。

160
00:14:19,686 --> 00:14:23,340
把这六条摆在一起看，思路是一致的。

161
00:14:23,340 --> 00:14:30,131
能力面可以开放，发现链路可以宽松，但执行前那道门必须窄，并且默认关闭。

162
00:14:30,131 --> 00:14:35,010
前半段讲 MCP，后半段讲插件，说的是同一件事。

163
00:14:35,010 --> 00:14:37,114
这一集就到这里。

