MCP 审批门对齐指南(SDK 非规范性共同依据)¶
定位:MCP Server 审批门(approval gate)是 SDK 自留地——协议规范(specification/)不定义本地 settings 形态,仅在 runtime-contract §5 item 10 规定两套开关正交等硬约束。但双 SDK(python-sdk / rust-sdk)必须行为一致,故以本指南作为共同对齐锚点(替代此前各 SDK 引用自家私有设计文档的做法)。本文非 conformance 要求,第三方实现 MAY 不提供审批门;一旦提供,SHOULD 按本文对齐。
语义整体对标 Claude Code settings(deniedMcpServers / allowedMcpServers / disabledMcpjsonServers / enabledMcpjsonServers / enableAllProjectMcpServers),键语义按 A2C 身份模型调整如下。
1. 名单键 = bundle_id¶
四个名单数组(denied / allowed / disabledMcpjson / enabledMcpjson)的元素一律为 bundle_id(解析后运行期身份),不是 display name:
- 为什么:按 name 授予信任会让「同名两条 server 共用一份审批」——你批准了 plugin 带的
filesystem,你自己那个恰好也叫filesystem的 server 自动免批准(真实信任泄漏)。bundle_id 键与get_config.serverskey、工具前缀{bundle_id}__、错误码meta.mcp_server同一身份空间,零桥接。 - hash-fallback 形态的免费收益:bundle_id 为连接身份摘要形态时,改连接参数 ⇒ bundle_id 变 ⇒ 旧审批自然失效 ⇒ TOFU 语义。
2. 档位表(安全层 vs 信任层,顺序即优先级,先到先决)¶
审批门拆为两层,MUST 分别判定(与 runtime-contract §5 item 10 对齐):
| 层 | 范围 | 方向 | 适用对象 |
|---|---|---|---|
| 安全层 | Gate 1-3(denied / allowed / disabledMcpjson) | DENY / 白名单收窄 / 用户侧禁用 | 所有 server(含 plugin baseline) |
| 信任层 | Gate 4-7(trusted_origin / enabledMcpjson / enableAllProject / PENDING) | ENABLE | 仅声明 server + project-enable bundled server |
plugin 受信 scope enable 的 bundled server 免批准——免的是信任层,安全层仍必经(见 §2.3)。信任层输入为 (bundle_id, settings, trusted_origin),安全层输入为 (bundle_id, policy_settings):
档位表(对进入对应层的 server 判定;安全层 ∀ server,信任层仅声明 server + project-enable bundled):
| 档 | 判据 | 结果 |
|---|---|---|
| 1 | deniedMcpServers 含该 bundle_id(企业拒绝名单) |
DISABLED |
| 2 | allowedMcpServers 非空且不含该 bundle_id(企业白名单) |
DISABLED |
| 3 | disabledMcpjsonServers 含该 bundle_id(disabled 优先于 enabled) |
DISABLED |
| 4 | trusted_origin(user / flag / embed / policy 声明) |
ENABLED |
| 5 | enabledMcpjsonServers 含该 bundle_id(判据仅取自受信 scope,见 §2.1) |
ENABLED |
| 6 | enableAllProjectMcpServers == true(判据仅取自受信 scope,见 §2.1) |
ENABLED |
| 7 | 否则(工作区共享且未决) | PENDING(弹批准框) |
历史档位「bundled 名集免批准」已删除,MUST NOT 以任何形状复活
旧实现在档 3 与 4 之间存在「bundled.contains(name) → ENABLED」档位。它是安全缺陷:真正的 plugin bundled server 从不进入 mcp.json 解析层(bundled 走 enable→mount,不落 mcp.json),因此该档唯一可达路径是「project/local 声明的 server 借用了某已装 plugin 的 server 名」——即 100% 借名跳过批准门(攻击者只需猜中受害者装过的任一插件的 server 名)。
- plugin 声明的 server,当其 enable 判据来自受信 scope 时,MUST 不进入信任层迭代(Gate 4-7)——在迭代层过滤(
origin == plugin的条目不进信任层),但 MUST 仍经过安全层(Gate 1-3)。禁止写成门内新档位(「进门后豁免」形状即旧档位复发,即便判据换成 origin 也重新违反 item 10「两套开关分别应用」)。其可信性由 install ∧ 受信 scope enable 门保证(runtime-contract §2.5)。project scope 供给enabledPlugins=true时相反:该 bundled server MUST 进门迭代落 PENDING(见 §2.2)。 - 审批门实现 MUST NOT 依赖物化账本 / bundled 名集。
- 可验收信号:门函数签名不含
bundled入参;代码库中不存在「读账本聚合 bundled 名集」的函数(如bundled_mcp_server_names())——「函数不存在」比「文档说别用」可靠。 - 上述是下方 §2.1 通则的一个具体形状。只钉形状不钉通则会被同构路径绕过——见 §2.1。
2.1 判据来源的信任约束(通则)¶
档位表规定了判据是什么,本节规定判据可由哪些 scope 供给。核心通则:
审批门的输入 MUST 来自比被判定 server 更高信任的来源;任何 scope 都不得为「自身是否受信」提供判据。
审批门存在的全部目的就是把关 project scope(不受信、随 git 分发)声明的 server。若放任 project scope 自己供给「我受信」的判据,则整个门形同虚设(自我批准闭环)。据此,按判据的方向分别约束:
本通则不限于下表 5 个门字段——任何为 server 受信供给判据的路径均受其约束,含 plugin-enable 路径(
enabledPlugins),见 §2.2。
| 档 | 字段 | 方向 | 可供给 scope | 理由 |
|---|---|---|---|---|
| 1 | deniedMcpServers |
DENY | policy only | 企业拒绝名单,已有 POLICY_ONLY_FIELDS 约束 |
| 2 | allowedMcpServers |
ENABLE(白名单收窄) | policy only | 同上 |
| 3 | disabledMcpjsonServers |
DENY | 任意 scope(含 project) | fail-safe——仓库禁自己的 server 无安全影响,更严格永远安全 |
| 5 | enabledMcpjsonServers |
ENABLE | user / local / flag / policy(拒 project) |
project 供给 = 自我批准 = 档④ 同构 |
| 6 | enableAllProjectMcpServers |
ENABLE | user / local / flag / policy(拒 project) |
同上,且更易达成(无需任何 server 名,一个 bool 即绕过全门) |
约束(MUST):档⑤ enabledMcpjsonServers / 档⑥ enableAllProjectMcpServers 由 project scope 供给时 MUST 被过滤,并进入既有 settings 校验错误通道(响亮失败,不静默忽略,同 §3 姿态)。这几个字段本就是个人决定——三个批准写助手(approve / deny-all / approve-all)只写 local scope(个人决定不污染共享层),读面 MUST 与之对称、不接受 project 供给。
project settings.json 入 git —— 与档④ 同构的自我批准面
.tfrobot/settings.json(project scope)与 mcp.json 一样入 git、随仓库分发。若 gate 接受它供给档⑤/⑥,被 clone 的仓库携一份 {"enableAllProjectMcpServers": true} 即可让其 mcp.json 里的任意 server 启动期无提示直挂——比档④更易达成(档④尚需猜中某已装插件的 bundled server 名;本路径无需装任何插件、无需任何名字)。
落地(SDK 自治,双端一致):在既有 POLICY_ONLY_FIELDS 过滤通道旁增一个 TRUSTED_SCOPE_ONLY_FIELDS(= enable 方向 gate 判据,拒 project)类目,复用现成的过滤 + 记错管线即可。可验收信号:以 project scope settings.json 供给 enableAllProjectMcpServers: true + project mcp.json 声明未批准 server → 该 server verdict MUST 为 PENDING(非 ENABLED),且 project 供给该字段产生一条 settings 校验错误。
2.2 plugin-enable 路径的同构约束(enabledPlugins)¶
§2.1 通则不限于上表 5 个门字段——任何为 server 受信供给判据的路径都受其约束。plugin bundled server 的免批准特权(runtime-contract §5 item 10 / §2.5 第 3 条「origin=plugin 的可信性由 install ∧ enable 门保证」)也不例外:「enable」这一半判据来自 enabledPlugins,而 enabledPlugins 有 per-scope 三态合并(user < project < local,runtime-contract §2.4)。当 project scope 供给 enabledPlugins=true 时,project scope 正在为 bundled server 是否受信提供判据——即通则所禁的自我批准闭环。
enabledPlugins 供给 scope |
该 plugin 的 bundled server |
|---|---|
user / local / flag / policy(受信 scope) |
免批准——免信任层(滤出 Gate 4-7 迭代),安全层(Gate 1-3)仍必经(见 §2.3) |
project(入 git、随仓库分发) |
回落审批门 —— 进门迭代落 PENDING(与 project-scope 声明 server 同视) |
约束(MUST):project scope 供给的 enabledPlugins=true 不构成 bundled server 的免批准判据。这与档⑤/档⑥ 拒 project scope 同构(self-approval 闭环):被 clone 的仓库携一份 enabledPlugins: {"evil@mp": true} 即可让其 bundled server 启动期免提示直挂——攻击者无需猜任何 server 名(marketplace 须已在受害者本地 known_marketplaces.json,非零点击)。
方向性:仅 ENABLE 方向受限。project scope 的 enabledPlugins: false(显式禁用)保留有效——与档③ disabledMcpjsonServers「DENY 方向任意 scope、fail-safe 永远更安全」同姿;不应把 enabledPlugins 加入 TRUSTED_SCOPE_ONLY_FIELDS 整字段过滤(会破坏 runtime-contract §2.4 三态合并语义,project 的 false 被吞,且殃及 disable 方向)。
落地(SDK 自治,双端一致):bundled server 采集逻辑须感知激活 plugin 的 enable 判据来源 scope(仅 project 供给 → 投影为 PENDING 而非 ENABLED)。实现 MUST NOT 以新增审批门档位达成(「origin==plugin ∧ project-enable → PENDING」是 §2 danger note 明禁的「进门后豁免/进门后降级」形状变体)——project-enabled bundled server 落 PENDING 是「它没获得免批准特权、作为未决 server 自然落档⑦」,不是「进门后改判」。TOFU 审批一次后写 local scope,与既有批准写助手(只写 local)对称。
可验收信号:project scope settings.json 供给 enabledPlugins: {"x@mp": true} + 该 plugin 声明某 bundled server 且 plugin 已 installed → 该 bundled server verdict MUST 为 PENDING(非 ENABLED);同一 plugin 改由 user scope enabledPlugins 激活 → verdict ENABLED(免批准保持)。
2.3 安全层对 plugin baseline 的生效语义¶
plugin 受信 scope enable 的 bundled server 免批准——免的是信任层(Gate 4-7),安全层(Gate 1-3)仍必经(runtime-contract §5 item 10)。安全层判定是纯函数 (bundle_id, policy_settings) → {PASS, DISABLED},在 reconcile 阶段 collect_enabled_bundled_servers 采集之后、挂载之前执行。
生效时机:
enable → reconcile →
1. collect_enabled_bundled_servers(采集完整意图清单)
2. 安全层判定:逐 bundle_id 过 Gate 1-3
- DISABLED → 不进投影(continue),产出结构化诊断(类别 policy)
- PASS → 继续
3. 信任层判定(仅声明 server + project-enable bundled 进门)
4. 挂载
半态澄清:安全层按 bundle_id 全局拒绝不产生 runtime-contract §2.4 所禁半态。理由:安全层过滤发生在「skill + bundled server 原子投影」之前,被拒绝的 server 与同 plugin 的 skill 一并不入投影——这与 policy enabledPlugins: false 整体关停 plugin 是同构的「整包不投影」,而非「只打掉 server、skill 仍在」的半态。单独打掉某 bundled server(通过非 policy 手段,如在 mcp.json 中覆盖)仍属半态禁令。
实现约束(MUST):
- 安全层判定 MUST NOT 限定"声明 server"范围——collect 采集到的 plugin bundled server 与声明 server 同一判定通道。
disabledMcpjsonServers(Gate 3)虽在安全层,其判据来自任意 scope(含 project)——DENY 方向 fail-safe,"更严格永远安全"(§2.1 通则)。- 被安全层 DISABLED 的 bundled server MUST NOT 出现在
pending_bundled_servers——避免"待批准一个已被 policy 拒绝的 server"的 UX 陷阱。 list_pending_bundled_approvals类查询接口 MUST 同步过滤安全层 Disabled。
可验收信号:
- policy
deniedMcpServers含bundle_id_X→ user scope enabled plugin 声明依赖 X → X 不挂载、不出现在pending_bundled_servers、产出policy类别诊断 - 同 plugin 的 skill 正常投影(server 被安全层拒绝 ≠ 整 plugin 被关停,但 skill 投影因安全层在原子投影前发生,skill 也一并被抑制——与被 policy
enabledPlugins: false整体关停在投影面的效果一致) - Gate 3(
disabledMcpjsonServers)由 project scope 供给 → 对 plugin bundled server 同样生效(fail-safe)
3. settings 校验 fail-fast¶
加载 settings 时 MUST 校验四个名单数组的每个条目是合法 bundle_id;不合法条目进入既有 settings 校验错误通道(响亮失败,不静默忽略),报错信息 SHOULD 附带提示该 name 对应的 bundle_id:
❌ policy/settings.json
deniedMcpServers[0]="my.server" 不是合法 bundle_id(含 '.')
提示: 该 server 的 bundle_id 为 "my_server"
这把「黑名单静默不生效 = 安全事故」变成响亮失败。policy 是企业下发的,用户本地改不了——但至少可见、可反馈给 admin。
4. 批准框 UX:name 与 bundle_id 双显示¶
PENDING 弹框 MUST 同时显示 display name(给人看)与 bundle_id(消歧 + 供日后手写 policy),批准写入的是 bundle_id:
╔══════════════════════════╗
║ 发现新的 MCP Server ║
║ filesystem ║ ← name
║ bundle_a3f9c2e1 ║ ← bundle_id
║ npx @mcp/fs ║
║ 信任? [y]es [n]o [a]ll ║
╚══════════════════════════╝
↓ y
enabledMcpjsonServers: ["bundle_a3f9c2e1"]
理由:同名两条 server 分别弹框时,只显示 name 会出现两个一模一样的框,用户分不清在批哪一个。
5. 写入面守卫(与 runtime-contract §2.5 优先序一致)¶
- upsert(写 mcp.json)MUST NOT 因「同 bundle_id 已由 plugin 提供」而拒写——用户在 user scope 声明同 bundle_id 正是优先序赋予的覆盖权(user > plugin 声明)。SDK MAY 提示「该 bundle_id 已由 plugin
<x>提供,你的声明将覆盖它」。 - remove 守卫按
origin判定,不按账本名集:origin == plugin ∧ 无用户侧声明⇒ 提示改用plugin uninstall;用户侧存在声明 ⇒ 放行(删的是用户自己那条声明,plugin 基线保留)。
6. 双端一致性¶
本指南所有判定为纯函数(settings + origin → status),SHOULD 以共享测试向量对拍(姿态同 bundle_id 一致性向量);夹具 MUST 遵循「display name 与 bundle_id 取值分叉」条款。