最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
MCP Server 如何在用户权限或功能开关变化时主动发送 tools/list_changed?
时间:2026-09-16 10:20:01 编辑:袖梨 来源:一聚教程网
MCP Server 在用户权限、许可证或功能开关变化后,应该先更新该用户请求下的权威工具集合,再向对应客户端发送 `notifications/tools/list_changed`,让客户端重新调用 `tools/list`。但在 Go SDK issue #666 所描述的当前实现中,通知主要由 `AddTools` 和 `RemoveTools` 自动触发,缺少在任意业务条件变化时主动发送通知的公开入口。该 issue 仍是开放 enhancement,因此不能假设已有稳定的手动发送 API。
为什么权限变化也需要列表失效
MCP 工具集合可以依据每次请求携带的授权信息变化。例如专业许可证开放高级搜索,员工身份开放内部诊断,功能开关启用实验工具,特定 OAuth 客户端获得额外写入能力。客户端在连接初始化时缓存了旧列表,条件变化后若不通知,它仍会向模型暴露过期能力。
权限提升时,用户看不到已经可用的新工具;权限降低时,客户端仍可能展示已失效工具。后者更危险,但服务端不能依赖通知作为安全边界。每次 `tools/call` 都必须按当前权限重新鉴权,即使客户端旧缓存里仍有工具,也要拒绝未授权调用。
规范语义与 SDK API 是两层问题
协议层定义了 `notifications/tools/list_changed`:服务端告诉客户端工具列表缓存已经过期,客户端随后重新请求 `tools/list`。通知不携带完整工具差异,也不授予权限。只要列表的可观察结果改变,就适合发送这一失效信号。
SDK 层决定开发者如何发出信号。Go SDK 当前可以在调用 `AddTools` 或 `RemoveTools` 时自动发送通知,因为库能观察到注册表变化。但若工具可见性由 SDK 外部的许可证服务、会话版本、令牌或 feature flag 决定,内部注册表可能根本没有被修改,自动触发器就不会运行。
issue #666 描述的特殊架构
问题报告中的 Server 为每个请求创建新的 `mcp.Server` 实例。不同用户在构造时就获得不同工具,差异取决于许可证、令牌所属客户端、员工身份和功能开关。工具在 Server 启动前已经全部注册,运行期不会调用 AddTools 或 RemoveTools。
当平台发布新工具,或外部系统改变某个用户的可见能力时,会话层能够检测“当前列表版本”和“客户端会话已知版本”不一致,却没有合适的公开方法主动发送 list-changed。人为添加再删除一个虚假工具虽然能触发自动通知,但会污染列表并制造竞态,不应作为生产方案。
正确的目标流程
理想流程不是直接把新工具定义塞入通知,而是维护一个能力目录版本。每次请求根据用户授权计算工具快照,同时记录该会话最后确认的版本。权限或功能开关变化后,事件系统定位受影响会话,向其发送 list-changed;客户端重拉时,服务端按最新授权返回集合。
授权或 feature flag 改变
→ 更新能力目录版本
→ 找出受影响的活跃会话
→ 发送 tools/list_changed
→ 客户端重新 tools/list
→ 服务端按当前请求授权生成新列表
通知只需说明列表失效。服务端必须保证通知之后的新请求可以读取到新状态,否则客户端会刷新到旧列表。多实例系统要先使共享配置可见,再广播事件。
为什么不能强制让客户端重新初始化
另一种设想是在检测版本不匹配时返回错误、使会话失效,迫使客户端重连并重新发现工具。这种方式破坏性较大:客户端对连接错误的恢复策略不一致,有的会自动重试,有的可能持续处于损坏状态直到运行时重启。
重新初始化还会丢失在途调用、订阅和会话状态,不能替代精确的缓存失效通知。只有协议协商本身不兼容、认证上下文彻底失效或服务端无法继续维持会话时,才应考虑断开连接。
当前没有手动 API 时怎么做
第一种方案是保持工具名称与 schema 稳定,把许可判断放在调用阶段。所有客户端都能看到工具,但描述清楚适用条件,调用时按 Claims 拒绝未授权用户。这降低动态列表需求,却可能让模型看到无法使用的工具,也会扩大提示上下文。
第二种方案是由客户端或宿主按 TTL 定期重新 list。适用于权限变化不频繁、可接受短暂不一致的场景。TTL 到期只是刷新触发器,服务端调用鉴权仍需立即生效。第三种方案是由外部编排器在能力版本变化时重建连接,作为手动通知 API 落地前的临时措施。
第四种方案是维护受控的 SDK 分支,增加标准通知发送入口。但这会带来升级和兼容成本,必须有集成测试确保能力声明、会话路由与传输投递均正确。不能直接调用未导出的内部函数或伪造底层消息绕开 SDK 状态机。
不要用虚假工具触发通知
通过 AddTools 加入 dummy 工具再 RemoveTools,可能产生两次通知。客户端在第一次通知后重拉时,恰好看到虚假工具;模型上下文可能把它缓存并尝试调用。高并发下,不同会话还可能观察到不同中间状态。
即使把操作放在极短事务中,也无法保证网络和客户端刷新时机。正确 API 应直接发布列表失效事件,而不修改业务注册表。开放 issue 的价值正是补齐这一原子操作,而不是鼓励利用自动触发器副作用。
手动通知 API 应满足哪些约束
若 SDK 增加公开入口,它需要绑定具体 Server 会话或订阅流,验证 Server 已声明 `tools.listChanged`,并遵循连接生命周期。无活跃订阅者时应安全返回,而不是阻塞业务线程;连接关闭后发送应给出可判断的错误。
API 还应支持多会话选择。权限变化通常只影响某个用户,不能向所有客户端广播并泄露“存在某项内部能力”。平台级新工具发布则可能影响全部会话,需要事件总线广播到每个承载连接的实例。
SDK 不必在通知中加入版本或工具清单,版本可以保留在应用内部用于去重。客户端最终仍以 tools/list 响应为权威。手动 API 和 AddTools/RemoveTools 自动通知应走同一底层投递路径,避免行为不一致。
每请求 Server 与长期会话的矛盾
每个 HTTP 请求新建 Server 实例便于按请求注入用户状态,但主动通知需要一个长期可寻址的客户端通道。应用必须区分“处理当前请求的临时对象”和“持有活跃订阅流的会话路由器”。外部权限事件不能依赖某个已经销毁的请求实例发送。
一种架构是用共享 session registry 保存连接与通知发布能力,请求工厂只负责生成按当前授权计算列表的处理器。权限事件到达后,registry 查找用户活跃会话并发布;下一次 list 请求再由工厂读取最新状态。多进程时 registry 与事件总线需要分布式实现。
客户端也必须正确消费通知
即使 Server 能主动发送,客户端仍可能只记录日志而不重新 list。上线前必须端到端验证:能力协商中存在 listChanged;通知在线路上到达;客户端紧接着发出 tools/list;新列表进入 UI 和模型实际使用的工具集合。
客户端应合并突发通知,完整处理分页并原子替换快照。刷新失败时保留旧列表并标记过期;权限降低场景可暂停相关工具调用。服务端调用鉴权提供最终保障,客户端刷新只是改善可用性与模型选择。
缓存与授权的设计
工具列表缓存键至少包含 Server、协议版本、用户主体、OAuth 客户端和 scope 摘要。若 feature flag 也影响列表,应包含能力目录版本。不能让一个员工会话的工具快照被普通用户复用。
授权变化时先让调用鉴权立即生效,再使列表缓存失效和发送通知。这样即便通知延迟或客户端不支持动态刷新,也不会越权。权限提升则可以稍后可见,但权限降低必须优先保证拒绝调用。
测试清单
测试应覆盖许可证升级与降级、员工身份变化、token scope 变化、feature flag 开关和平台新增工具。对每种情况验证受影响会话收到通知,未受影响会话不被打扰,重新 list 得到正确集合,并且调用阶段按最新授权执行。
还要测试多个进程、会话断开、无订阅者、重复事件、事件总线至少一次交付和通知期间权限再次变化。禁止出现虚假工具中间态。若使用 TTL 或重连绕过,明确测量最长陈旧窗口。
结论
权限或功能开关变化确实属于工具列表可能改变的原因,服务端应发送标准 list-changed,让客户端重新发现。但 Go SDK issue #666 表明,当前公开能力主要围绕 AddTools 与 RemoveTools 自动通知,外部条件变化时缺少任意时机手动发送的正式入口,提案仍待调查。
在稳定 API 出现前,不要用 dummy 工具触发,也不要依赖强制断线。优先保持调用鉴权正确,再根据业务选择稳定工具接口、TTL 刷新、受控重连或维护 SDK 分支。最终方案应把能力目录、会话路由、通知投递和请求级授权分开设计,并用端到端证据确认客户端真正更新了工具快照。