MCP权限分级:按真实动作决定是否审批
作者修复了因信任MCP服务而导致邮件发送绕过审批的问题,将工具调用划为自动允许、首次询问、始终询问和禁止四级。权限匹配按黑名单、工具规则、应用规则、应用默认值及启发式判断依次执行。
AI 深度解读
桌面 AI 接上 Gmail 后,最危险的故障可能不是调用失败,而是邮件已经发出,用户却从未点过确认。开源 AI 伴侣 Ankita 的作者就修过这个问题:旧规则信任了 Composio,连带放行了它代理的发信操作。
作者的改法是把权限判断从「哪个工具、哪个服务器」推进到「这次到底做什么」。这套方案值得接入 MCP 的开发者参考,尤其是批量调用和工具套工具的场景。但它仍依赖工具声明与动作名称,不能替代真正的执行隔离。
信任服务器,为什么会变成默认允许发信
Ankita 通过 Composio,把 Gmail、日历等外部应用接入 MCP。旧审批规则只有两档:带有 readOnlyHint: true 的工具直接执行,其余工具请求确认。另外还有一个优先放行条件:可信服务器可以跳过审批。
Composio 恰好属于可信服务器。结果是,连接好的 Gmail 可以未经确认就发送邮件。作者认为,这是最不该误放行的操作之一:邮件发出去无法撤回,而且收件人会直接看到。
问题出在授权范围。用户信任一个连接,并不等于同意它执行所有动作。
作者在 src/integrations/mcp-tiers.mjs 中改成四级策略:
- Tier 0 自动允许,用于只读工具。
- Tier 1 首次询问,是未分类工具的默认级别。
- Tier 2 每次询问,用于破坏性动作。
- Tier 3 拒绝执行,只能由显式黑名单触发。
规则按固定顺序解析:黑名单、显式的单工具规则、单应用规则、应用默认值,最后才是启发式判断。
这里有个有意保留的边界:启发式判断不能把工具升到 Tier 3。作者希望猜错的代价是多一次确认,而不是莫名其妙阻断用户的工作。
可信连接只解决接入问题,具体动作仍然需要单独授权。
工具名藏住了动作,就要拆开参数再审批
Composio 的 COMPOSIO_MULTI_EXECUTE_TOOL 是一个批量执行入口,真正要调用的工具放在参数里。审批框如果只显示这个名字,用户根本不知道自己批准的是查邮件还是发邮件。
作者用 enclosedActions() 拆开内部动作,再由 splitSlug() 解析标识。例如,GMAIL_SEND_EMAIL 会被拆成应用 gmail 和动作 send email。最终展示的是 describeCall() 生成的「gmail: send email」,而不是外层包装工具名。
批量调用取内部动作的最高风险级别。十个动作里,只要一个需要每次确认,整个调用就进入 Tier 2。原文用「九次读取加一次发送」说明这种取舍:规则容易理解,但审批粒度比较粗。
纯发现工具则保持自动允许,包括 COMPOSIO_SEARCH_TOOLS 和 COMPOSIO_GET_TOOL_SCHEMAS。作者的理由是,它们只读取工具元数据,不执行应用操作,没有实际动作需要用户批准。
连接管理也按操作区分:新增连接首次询问,因为用户正在参与 OAuth 流程;移除连接每次询问,因为它会破坏已经可用的配置。
这套设计最有价值的地方,是让审批内容与用户能理解的后果对应起来。一个外层函数名,通常不足以承担这个责任。
默认询问之外,还要防止策略被悄悄降级
启发式规则用子串匹配识别危险动作,词表包括 send、delete、publish、pay、transfer、remove、destroy、revoke。因此,payment 和 resend 也会命中。
作者接受误报:多弹一次确认,可以接受;漏掉不可逆动作,代价更大。不过,这只是作者的风险取舍,原文没有提供误报率或漏报率。
新建 Composio 连接时,Gmail 默认进入每次询问,而不是继承通用默认值。这是对原始发信漏洞的直接修补。原文同时将只读工具列为自动允许,但没有完整展开 Gmail 默认值与只读声明重叠时的具体判定细节。
显式策略保存在独立的 mcp-tiers.json,没有塞进 mcp.json。原因与现有架构有关:Composio 是合成连接,不出现在 MCP 服务器列表里;强行加入一个没有启动命令的记录,会让协调器尝试启动不存在的进程。
策略文件还有三层保护。每次写入前重新读取,减少 CLI 与运行中 daemon 各持一份旧状态、互相覆盖的问题;文件缺失或格式错误时退回启发式规则,未知动作仍默认询问;降低保护级别本身也要审批。
其中,lowersProtection() 把未设置级别视为首次询问,所以把未分类工具改成自动允许,也算降级。每次决策还会更新 allowed、asked、denied 审计计数。不过,原文没有展示完整记录结构,不能仅凭计数就认定它能还原每次调用的全部经过。
我的判断
我认同它把动作放在连接身份之前,也认同审批框应展示真实动作。尤其是批量工具,只审核外层名字,很容易让用户批准自己没有看见的行为。
但作者也明确承认:这是分类机制,不是沙箱。Tier 0 仍然依赖服务器提供的 readOnlyHint。如果服务器把写操作谎报为只读,这套设计并没有补上缺口。危险词表也存在盲区:一个没有命中词表的新破坏性动作,只会落到首次询问,之后可以继续运行。
相比「每次调用都询问」,分级方案试图减少无意义确认;相比「整台服务器都信任」,它缩小了直接放行的范围。但作者没有提供对照实验,无法证明提示疲劳减少了多少,也无法证明危险操作拦截率提高了多少。这些仍是实现经验。
批量审批同样没有标准答案。整批取最高风险,规则简单;逐动作审批更精确,却可能产生更多提示。审批次数少不等于设计更好,用户能看懂并作出有效判断才是关键。
能拿来做什么
- 如果你的 Node 服务或桌面助手接入了 MCP,先检查可信服务器是否能够绕过写操作审批,尤其关注发信、删除、发布和支付路径。
- 如果工具支持批量执行,就解析参数里的实际动作,并在审批界面展示应用与操作;可以参考作者的做法,让整批调用采用最高风险级别。
- 把权限降级也纳入审批,并检查策略缺失、解析失败时的默认行为,确保未知动作会请求确认。
- 为危险词表之外的新动作补充显式规则;同时单独评估
readOnlyHint的可信程度,避免把声明当成执行隔离的保证。