MCP 工具注释与审批边界
MCP 工具注释可以描述预期行为,但只有对真实副作用的测试,才能决定哪些代理操作需要审批。

MCP 工具注释是很有用的文档,也很容易让不安全的审批系统看起来井然有序。如果客户端把 readOnlyHint、destructiveHint 或 idempotentHint 当成授权依据,服务器作者实际上就替用户写好了审批政策,却没有证明实现真的配得上这种信任。
这完全颠倒了顺序。注释可以帮助解释提示内容、整理工具列表,或为人工审核操作的人建议一个合理默认值。审批必须依据服务器将要执行的请求、使用的凭据、触达的目标,以及可能触发的副作用来决定。我见过太多集成调用名为 get 的端点,返回一个客气的 JSON 对象,却仍然在别处创建了任务。
代理场景下,这个区别尤其重要,因为代理会重试、组合工具,而且行动速度很快,一个小小的分类错误也可能带来高昂代价。某个工具单独调用一次是安全的,放进循环里就可能不安全。某个工具对一个 API 是只读的,对另一个 API 却可能成为数据导出通道。某个工具看起来具备幂等性,但超时掩盖第一次成功后,重试可能创建重复任务。
注释描述行为,但不授予权限
Model Context Protocol 工具规范把注释描述为关于工具行为的提示。这个措辞是有意为之:客户端可以用它们改进界面,却不能安全地把未经验证的声明当成安全决策依据。
这三个字段分别描述不同的主张:
readOnlyHint: true表示工具不会修改环境。destructiveHint: true表示工具可能执行破坏性更新。idempotentHint: true表示使用相同参数重复调用,不会对环境产生额外影响。
这些主张没有覆盖调用的全部风险。某个工具可能读取整个客户数据库,把结果发送给代理,同时诚实地将自己标记为只读。另一个工具可能只写入访问时间,这听起来影响很小,直到这个时间改变了数据保留、计费或事件记录。幂等性并不能说明第一次产生的影响是否可以接受。
规范还为这些字段设置了保守的默认值。readOnlyHint 默认值为 false。idempotentHint 默认值为 false。destructiveHint 默认值为 true,而且只有在工具不是只读工具时才有实际意义。不要用自定义规则替换这些默认值,例如“缺少元数据就算足够安全”。缺少元数据往往意味着服务器作者没有认真考虑过分类问题。
还有一个让人不太舒服的事实:善意的服务器也可能出错。开发者看到处理程序执行了 SELECT,就添加 readOnlyHint: true,但随后某个库层刷新了令牌、写入缓存,或调用了请求钩子。行为发生变化很久之后,注释仍然保持不变。没有人打算欺骗客户端,但如果客户端据此自动批准操作,仍然会做出错误决定。
读取不代表结果无害
只读操作可能泄露数据、消耗稀缺资源,或激活远程服务中的某些行为。把“不会写入”理解成“不需要审批”,是概念上的错误。
考虑一个名为 get_build_log、接受任务 ID 的工具。服务器从构建系统读取日志并返回输出,它完全可以正确声明 readOnlyHint: true。但日志中可能包含源代码、环境信息、签名下载 URL,或其他系统意外打印出的凭据。即使构建系统的数据库没有任何变化,把响应返回给自主代理也改变了谁可以使用这些信息。
管理 API 也会出现同样的问题。get_user 可能返回恢复代码。list_invoices 可能暴露银行信息。代理增大分页大小,或遍历每个前缀时,search_documents 可能变成批量提取工具。这里产生的副作用是信息披露,而注释中没有用于描述披露敏感度的字段。
读取操作还可能改变远程服务。有些 API 会更新 last_accessed_at、消耗一次性下载令牌、登记预览,或发起计量查询。缓存未命中可能会唤醒成本高昂的下游服务。这些影响并不意味着每次读取都很危险,但它们足以让不加限定的只读审批规则站不住脚。
从两个独立维度对调用分类:它是否会修改系统,以及它可能在系统之外暴露或引发什么。低风险状态探测和批量导出都可能不修改数据,但不应接受相同的审批处理。
实用的审核记录应使用通俗语言写明数据边界。“读取项目 A 的部署状态”可以审核。“调用 get_status”无法审核。后者隐藏了目标、范围、账户,也没有说明另一个服务器上名称相近的方法可能代表不同含义。
在一次性目标上测试处理程序
只看工具名称或输入模式,无法证明注释是安全的。应在能够观察请求、响应以及调用前后状态的环境中运行服务器。
先准备一个包含可丢弃记录的测试账户。为它配置独立的 API 凭据,并将通知 webhook 路由到捕获端点。记录服务器发出的请求、数据库状态(如果你能控制数据库)、审计事件、电子邮件、排队任务,以及计费或使用量计数器。响应内容是证据,但不是完整记录。
对所有可能影响审批的工具,使用一组小型测试矩阵:
- 使用普通的有效输入调用一次,保存完整的调用前后状态。
- 使用逐字节完全相同的输入再次调用,比较每一项可观察影响。
- 使用不存在的资源、已经完成的操作和无效字段进行调用。
- 服务器收到请求后中断客户端,然后重试相同调用。
- 如果代理可以并发发起调用,就同时运行两次相同调用。
超时场景可以捕获一种常见故障。假设 create_ticket 已经发送工单请求,但服务器返回前连接中断。代理看到错误后重试。如果工单系统没有幂等令牌,工具就会创建两张工单。处理程序的代码即使可以接受相同输入两次,也不能改变远程结果,不能因为这一点就把它标记为幂等。
记录一种迫使人们检查影响,而不是相信绿色响应的输出格式:
case: retry after response timeout
request: {"title":"rotate staging certificate","request_id":"test-104"}
first call: transport timeout after request received
second call: 201 {"ticket":"842"}
remote records: ["841", "842"]
result: not idempotent without a remote idempotency mechanism
示例中的 request_id 只有在远程 API 保存并强制执行它时才有用。服务器完全忽略的客户端生成标识符只是装饰。要验证远程系统是否会返回原操作而不是创建新操作,请使用完全相同的标识符重复调用,并检查它是否真正执行了约束。
将这些测试和服务器一起维护。注释漂移通常来自代码变更、依赖更新或新端点。一个检查声明提示是否符合可观察行为的通过测试,比工具定义旁边的一条注释更有价值。
只读声明会在边界处失效
最容易出现的错误 readOnlyHint,来自只查看主数据库查询。真正重要的边界包括处理程序调用的每个服务,以及响应引发的每个动作。
假设服务器有一个获取文档的工具。它的主要请求是 GET /documents/42,但处理程序可能先交换刷新令牌、生成临时下载 URL、更新本地缓存,并写入访问事件。每个操作都可能以不同方式失败,也可能使用不同凭据并遵循不同的审计要求。
不要接受“写入太小,不算写入”的说法。小型写入也会带来自己的故障模式。最后查看标记可能影响数据保留。缓存可能在访问本应结束后继续保存内容。访问事件可能通知所有者。使用量计数器可能让账户进入付费层级。要问的是,这次写入是否改变了另一个人、进程或账单能够观察到的事实。如果改变了,就记录下来。
同样要审查由响应触发的行为。返回签名链接的工具可能让代理稍后获取该链接。返回可执行命令的工具可能导致代理在另一个通道中运行它。第一个工具从狭义上看仍然是只读的,但审批界面如果显示“安全读取”,就会让人错误理解代理接下来可以采取的动作。
当风险不同时,优秀的服务器会拆分操作。get_document_metadata 可以继续作为范围明确的检查调用。create_download_link 应该成为独立工具,因为它会创建一个持有者即可使用的能力,即使底层文档字节没有变化。这样的划分能帮助代理做出正确选择,也让审核者看到一项真正可以批准的操作说明。
破坏性取决于可逆性,而不是动词列表
destructiveHint 应反映调用是否可能造成难以逆转的有害更新,而不是工具名称中是否包含 delete。团队在这两方面都经常判断错误。
有些常见动词在一个系统中可以逆转,在另一个系统中却是永久性的。archive 可能只是隐藏记录,也可能启动清除计时器。disable_user 可能保留所有权限和文件,也可能撤销访问权限,让自动化流程陷入停滞。replace_config 可能只更新草稿,也可能立即触发生产部署。处理程序需要了解目标,而单个布尔值无法表达这些信息。
有些名称无害的工具其实明显具有破坏性。sync_members 可能删除不在提交列表中的账户。apply_labels 可能覆盖精心维护的分类体系。reconcile 可能通过任何人都不应轻率创建的日记账分录,修正外部账本。服务器作者如果因为 API 理论上可以撤销这些操作就把它们标记为非破坏性,实际上是在掩盖修复所需的运营成本。
把可逆性当作一个过程,而不是一个复选框。要问谁可以撤销结果、需要什么证据、撤销在多长时间内有效,以及在任何人有机会逆转前,后续流程是否会先消费这项变更。如果人工必须根据日志重建批量更新的意图,即使 API 提供了反向方法,这项操作也应归为破坏性操作。
一种常见但糟糕的建议是,只对明确的删除操作要求审批。它之所以受欢迎,是因为能让代理持续运行,也能让演示看起来流畅。但在生产环境中,破坏性变更通常以替换、撤销、发送或对账的形式出现。应审批真正的状态变化,而不是审批描述它时所用的词汇。
对于影响集合的操作,要求审核记录包含选择规则和数量。“同步用户”太模糊。“删除根据所提供 ID 选出的 14 名非活跃承包商”才能让人判断范围。如果服务器无法在执行前报告这个范围,就没有为客户端提供足够信息来生成严肃的审批提示。
幂等性必须经得住重试和并发
idempotentHint 是一个范围很窄的主张:第一次调用之后,使用完全相同的参数不应产生额外影响。它并不意味着调用安全、成本低、可以逆转,或适合让代理无限重复。
如果两次设置 state=closed 都只让同一条记录保持关闭状态,状态更新可以是幂等的。但如果处理程序每次都会发送电子邮件、追加审计评论或增加版本计数器,它就不再幂等。人们常常只检查数据库行,忽略了用户最先感知到的次级影响。
还需要精确定义输入相等。JSON 对象的字段顺序不应产生影响。服务器如果区别处理省略的 note 和 note: "",就可能收到代理认为相同的请求,却执行两次不同的更新。时间值、自动生成的默认值,以及 tomorrow 这样的相对表达式,会让这个主张变得更弱,因为可见参数看起来没变,实际命令却发生了变化。
并发是随意宣称幂等性最容易崩溃的地方。两个工作进程都可能先检查对象不存在,然后同时创建对象。唯一约束、事务性 upsert 或远程幂等机制可以阻止这种情况。单个 MCP 服务器进程中的内存缓存无法保护一个运行多个进程的部署。
只有在定义好范围后,才使用幂等记录。将调用方提供的令牌与经过身份验证的身份、规范化后的请求正文、结果以及适合该操作的过期时间一起保存。如果重复使用的令牌对应不同的规范化输入,就拒绝请求。否则,代理可能意外地把旧令牌附加到新请求上,然后收到另一个操作的结果。
不要因为提示为 true 就自动重试。只有当你知道服务器是否收到调用时,才重试明确可判断的失败。如果无法确定,真正的解决办法是在实际发生变更的位置加入幂等机制,而不是依赖客户端提示。
根据实际执行的操作构建审批
审批系统应该回答这些问题:哪个进程发起请求,使用哪个凭据,哪个外部目标会收到请求,涉及哪些状态或数据,以及调用成功后会发生什么。工具注释可以让说明更简短,但无法提供服务器没有暴露的事实。
将会话审批与逐次调用审批分开。会话审批适合由已知代理进程使用普通能力执行范围明确的运行。逐次调用审批适合那些可以转移资金、改变生产访问权限、发送消息、披露敏感记录,或创建不可逆外部承诺的凭据。决定权属于凭据和操作上下文,而不是乐观的 readOnlyHint。
有用的提示会写出具体操作:“由此权限签名的代理将使用部署凭据,在账户 Y 中重启服务 X。”较弱的提示是:“允许工具 deploy 吗?”前者给审核者提供了可以评估的内容,后者却要求审核者相信某个实现细节。
Sallyport 将 API 和 SSH 凭据保存在加密保险库中,由自己执行 HTTP 或 SSH 操作,再把结果返回给代理,而不是返回凭据。它的会话授权会标识请求进程,同时允许按凭据设置每次使用都需要决定。与服务器提供的布尔值相比,把人工控制放在这里更合理。
即使凭据前面有审批闸门,也要保留记录最终请求目标和结果的活动记录。审批回答的是操作是否可以继续,审计记录回答的是操作继续后发生了什么。不要把这两个问题合并成一个含糊的“使用了工具”事件。
将注释检查纳入服务器维护
MCP 工具注释的正确用途,是用测试支持诚实的沟通。服务器作者应保守地设置注释,记录任何边界情况,并在行为变化时同步修改。客户端作者可以把注释作为界面设计的一个输入,但绝不能把它作为权限判断的唯一依据。
添加一些故意挑战声明属性的测试。对于只读声明,如果测试夹具发现写入、出站通知、凭据刷新,或创建了供稍后获取的能力,就让测试失败。对于破坏性声明,测试失败路径和撤销路径,包括下游任务消费变更后会发生什么。对于幂等性,在模拟超时后以及并发调用时运行相同的规范化请求。
不要通过更换测试目标直到测试通过来掩盖不匹配。可以缩小工具范围,让提示变得真实;也可以修改注释;还可以在审批详情中公开该影响。每个选择都能告诉下一位维护者代码实际做了什么。
如果 Sallyport 是操作网关,可以在事件复盘中运行 sp audit verify。它会离线验证加密哈希链,因此你无需打开保险库,就能检查记录的操作历史是否保持完整。这不能证明服务器的注释诚实,但能为跟随注释执行的调用提供一份防篡改记录。
实际标准很简单:注释应当经得住针对用户真正关心影响的对抗性测试。如果经不起测试,就保持保守,并把审批放在操作实际发生的地方。
常见问题
可以安全地自动批准带有 readOnlyHint 的 MCP 工具吗?
把它当作需要证据支持的声明,而不是权限授予。先检查服务器实现,再在一次性测试目标上运行工具,确认它可能造成的所有副作用。
idempotentHint 实际上保证了什么?
它表示作者认为,使用相同参数重复调用不会对环境产生额外影响。但外部系统、重试行为、时间戳、通知以及参数规范化仍需要单独测试。
MCP 工具注释是安全边界吗?
不能。Model Context Protocol 规范将这些字段描述为行为提示,不能替代客户端的安全决策。恶意、过时或仅仅存在错误的服务器,都可能发布误导性元数据。
哪些 MCP 工具仍应要求审批?
只要调用可能改变业务状态、暴露敏感数据、启动高成本任务,或触达受控测试目标之外的系统,就应保留审批。看似无害的名称和乐观的注释都不能消除这些风险。
如何测试工具是否具备幂等性?
使用一次性账户或本地测试夹具,记录初始状态,用相同参数调用工具两次,然后比较最终状态和外部证据。还要用格式错误的输入和中断请求重复测试,因为重试路径往往最容易暴露问题。
一个破坏性工具在什么都没有改变时,也可能安全吗?
删除工具对某条特定记录可能不会产生破坏性结果,因为该记录本来就不存在,但从整体能力看仍然具有破坏性。审批设计应根据工具能力和目标环境分类,而不能只看某一次调用的结果。
MCP 注释缺失时会怎样?
先按保守原则处理缺失信息,再了解字段语义,然后再编写自动化规则。在当前 MCP 工具注释中,缺失的 destructiveHint 默认值为 true,而缺失的 readOnlyHint 和 idempotentHint 默认值为 false。
只读 API 调用为什么仍可能产生副作用?
工具可能返回看似正常的响应,同时创建审计记录、更新最后访问时间、触发 webhook,或向账户收费,而明显的资源本身没有变化。检查下游记录、网络调用和系统日志,不要只看工具响应。
如何为自主编程代理设计审批?
先确认代码签名机构和预期范围,再批准代理运行。对于每次使用都需要人工决定的凭据或操作,保留逐次调用审批。元数据可以帮助生成审批说明,但不应决定审批结果。
Sallyport 如何帮助控制 MCP 操作?
Sallyport 将凭据留在代理之外,让人员批准一个会话,或要求每次使用特定凭据时都进行审批。这样,审批依据是实际执行的操作,而不是服务器提供的注释。