跳到主要内容

MCP 工具列表在会话中途增加,你的智能体调用了一个它从未被告知过的工具

· 阅读需 11 分钟
Tian Pan
Software Engineer

一场安全事件回顾以一个团队无法回答的问题开始:智能体是如何知道它刚刚调用的工具名称的?审计追踪显示了一个 tools/call 请求,但该工具的名称并未出现在 harness 记录的任何 tools/list 响应中。MCP 服务器欣然接受并执行了该调用。在事后分析中,当被要求解释工具名称来源时,模型给不出答案,因为根本没有答案 —— 它猜中了,而且这个猜测恰好命中了一个真实的操作。

这是两个在理论上看起来兼容的假设之间产生的失效模式。客户端将工具列表视为一份契约,界定了它被授予的权限范围。服务器则将工具列表视为当前可用工具的快照,可以随着环境的变化自由增长。在这两种观点之间,LLM 是一座不知道二者差异的桥梁。

MCP 助长的“仅列表一次”习惯

大多数智能体 harness 在每个会话中仅调用一次 tools/list。原因很务实。工具描述通常很大;将它们序列化到每个提示词中会消耗大量 token,成本昂贵。在单次对话中,工具列表很少发生变化,因此缓存它是显而易见的优化方案。几个流行的客户端 SDK 在发布时都默认设置了 cache_tools_list=True,大多数生产环境中的智能体也保留了这一设置。

MCP 规范承认列表可能会发生变化。声明了 listChanged 能力的服务器应在可用工具集发生变化时发出 notifications/tools/list_changed,而收到通知的客户端应当重新获取。协议图示看起来很整洁:发现、调用、变更通知、重新发现。

图示中没有显示的是,当服务器添加了工具而客户端没有重新获取时会发生什么。也许客户端正处于回合之间,而通知在模型调用期间到达。也许 harness 是一个无状态的包装器,它忽略了通知,因为它根据 TTL 将 tools/list 视为可缓存的。也许服务器是众多将 listChanged 能力标志设为 true 但从未真正触发通知的实现之一,因为底层集成是后来强行加入的,没人编写相关的事件逻辑。在这三种情况下,客户端的结果都是一样的:它正基于对服务器权限的陈旧视图进行操作。

为什么 LLM 的臆测能命中真实的工具

关于 LLM 幻觉出工具名称的幼稚想法是模型发明了一个不存在的字符串。随后模型会被协议错误纠正,智能体使用存在的工具重试,审计追踪保持干净。

危险的情况是模型发明了一个确实存在的字符串。工具名称往往趋同。连接到项目追踪器的空间集成极有可能暴露名为 create_issueupdate_issuelist_issues 之类的工具。如果模型已经在足够多的公开 MCP 服务器上进行过训练(事实确实如此),那么当上下文暗示已连接问题追踪器时,它就会猜测其中一个名称。如果服务器在 5 秒前刚刚添加了该集成,而客户端尚未看到新的工具列表,模型的猜测就会命中。

服务器没有理由拒绝。从它的角度来看,工具存在,调用者已通过身份验证,调用格式有效。在 tools/call 请求中没有哪个字段可以断言“我被告知过这个工具的存在”。即使有,服务器也无法验证该声明,因为协议中不包含针对过去 tools/list 响应的签名收据。

在追踪记录中看起来像是智能体在调用一个工具,实际上是智能体在调用一个名称。而这个名称恰好解析为一个工具。审计追踪失去了这种区分。

并不总是奏效的通知通道

规范对会话中变更的对策是 notifications/tools/list_changed。在一个运行良好的系统中,这应该能弥合差距:服务器在界面变化时告知客户端;客户端重新获取;智能体的下一回合看到新列表,LLM 的选择建立在真实的基础之上。

在实践中,通知通道是设计中最薄弱的部分。原因如下:

  • 传输歧义。 2025-06-18 规范假设了一个可以推送通知的持久连接(stdio、SSE)。2026-07-28 发布候选版转向了可在普通基础设施上扩展的无状态 HTTP 核心。在无状态模式下,服务器无处推送通知,因此客户端必须轮询或依赖缓存头。许多实际部署混合了传输方式 —— 在有状态服务器前放置无状态网关 —— 导致通知在衔接处丢失。
  • 能力宣告沦为“表演”。 服务器声明 listChanged: true 只是因为 SDK 模板中有该字段。没有任何机制测试当列表变化时是否真的触发了通知。能力位变成了意愿的说明,而非行为的事实。
  • 客户端调度竞争。 harness 接收到通知时正值回合中途,此时工具调用已经在进行中,或者模型正在生成。幼稚的实现会将重新获取排队到“当前回合结束后”。当前回合通过调用一个工具结束,该工具名称是根据通知前的列表选择的,但却是根据通知后的界面解析的。

2026 发布候选版中引入的 ttlMscacheScope 字段在列表响应中有助于轮询情况,但对调度竞争无济于事。即使是一个具有 1 秒 TTL 的列表,仍然存在 1 秒的窗口期,在此期间客户端的理解滞后于服务器的真实情况。

权限问题的核心所在

值得指出的架构错误在于将工具列表视为“清单 (inventory)”而非“授权 (grant)”。清单是“这里存在什么”,而授权则是“我批准你调用什么”。这两个概念并不相同,而协议却将它们混为一谈。

当 LLM 选择工具时,决策应受授权而非清单的限制。授权是用户或操作员在连接服务器时同意的内容,加上他们在发现步骤中获知的内容。清单则是服务器当前碰巧公开的内容。在授权发布后加入清单的工具不属于该授权。从安全审查的角度来看,调用它的模型正在采取未经授权的操作——即使服务器毫无怨言地接受了调用。

该协议没有内置的方法来编码这种区别。tools/call 请求携带的是名称,而不是对特定 tools/list 响应的引用。服务器不知道客户端查看的是哪个版本的列表。客户端也不知道服务器现在提供的是哪个版本的列表。

弥合这一差距需要在两端都进行显式的工作。

弥合差距的模式

有几种模式可以解决不同部分的失效问题,大多数生产部署需要不止一种模式。

  • 工具调用中的列表版本锁定 (List-version pinning)。 扩展 tools/call 以携带调用方所依据的 tools/list 响应的版本(或哈希值)。服务器拒绝引用过时或未知版本的调用。这是最彻底的修复方式——它使授权在每次调用中都变得明确——但这需要双方都实现该扩展,而规范尚未强制要求。
  • 针对高变动服务器在每轮对话中重新获取。 在 harness 配置中将服务器标记为“高变动 (high-mutation)”,并在每个智能体回合开始时重新获取其工具列表。延迟成本是真实存在的(每个服务器每轮额外增加一次往返),但对于像项目追踪器或代码库工具这样底层表面确实会发生变化的服务器来说,这个成本是获得可靠工具列表的代价。
  • 服务端授权执行。 让服务器按会话跟踪哪些工具已披露给调用方。如果调用在针对该会话的最新 tools/list 之后添加的工具,则返回软失败响应,提示客户端在重试前重新列出。这是授权模型的服务端强制执行;它不需要客户端的配合。
  • 在 harness 中拒绝新工具 (Reject-on-novel)。 在 harness 将 tools/call 转发给服务器之前,根据缓存列表检查请求的工具名称。如果不存在,则在调用离开 harness 之前拒绝它——即使 LLM 确信该工具存在。模型认为工具存在的信念并不能证明它确实存在,而 harness 拥有作为权威的缓存列表。
  • 带有同步刷新的通知驱动缓存失效。notifications/tools/list_changed 到达时,不要将重新获取排入队列,而是阻塞下一次外发调用,直到拿到新列表。用少量的延迟损失换取表面的一致性是值得的权衡。
  • 带有披露凭据的单工具可审计性。 为每次工具调用记录向调用方披露该工具的 tools/list 响应的时间戳。如果不存在此类响应,则按定义该调用属于安全事件。审计轨迹随后可以回答安全团队的问题——“智能体是如何得知这个工具的”——即便答案是“它根本没得知”。

这些模式在延迟和一致性之间进行了权衡。每轮重新获取速度较慢但更准确。锁定列表版本更具协作性,但需要双边实现。拒绝新工具是单方面的,但会惩罚模型从部分列表中有用恢复的合法用例。

工具表面即合约

值得建立的思想模型是,服务器公开的工具表面不是静态目录,而是不断修订的合约。表面的每一次变动都是对智能体授权行为的变动。将表面视为可以无限期缓存,无异于将一份有效的合约视为博物馆展品。

这并非 MCP 所独有。任何可用操作集可能在会话中途发生变化,且调用层会缓存该集合的系统都面临同样的差距——REST API 网关、具有动态字段暴露的 GraphQL schemas、在单个方法上带有特性标志 (feature flags) 的 RPC 服务。MCP 只是让这种差距变得更糟,因为调用方是一个 LLM,其幻觉非常擅长生成恰好真实的名称。

安全交付 MCP 集成的团队将不再把 tools/list 视为发现步骤,而是将其视为授权步骤,每当底层授权可能发生变化时都需要重复该步骤。在敏感调用之前重新获取列表的 harness 并没有增加延迟——它增加的是合约检查。捕获哪个列表版本授权了每次调用的审计日志并不是额外的文书工作——它是区分“智能体在授权范围内行动”与“智能体猜中了一个真实名称”的唯一方法。

工具列表不是服务器能做什么。它是智能体被允许请求服务器做什么。以此来看待它,差距就会弥合。将其视为库存清单,你交付的智能体其幻觉就能购买真实的东西。

References:Let's stay in touch and Follow me for more thoughts and updates