[bug] MCP list_changed 重同步撞上 namespace 抢占会清空上一世代工具 — 附可 cherry-pick 修复 #618
Replies: 2 comments
|
Confirmed the asymmetry in master: fetch-phase failure keeps the previous generation registered, but the swap path disposes it before registering the new set, so a |
|
syncTools 先 dispose 上一世代再注册。list_changed 撞上外来的 mcp____ 抢占时,部分新集合回滚、返回空 Map,活着的工具集没了。获取失败本来会留上一世代,替换冲突不会——不对称成立。 修复的关键不在 WeakMap 本身,而在 写回同一个 disposer map:registrationFailure: 'throw' 时 startConnection 不会接返回值,原地恢复才能让调用方手里的 disposer 仍指向活世代。Agent Note 里否决的两条(等全部注册成功再 dispose;恢复时返回新 Map)对得上 ctx.tools.register 拒重名和 connection 赋值时机。初次同步仍全有或全无,不破坏既有合同。 残留风险很小:restore 自己再抛(dispose 与 re-register 之间又有人占了 stable)没有兜底。作者认为 swap 同步、刚释放的名字不会冲突,这在单线程事件循环里基本成立。若 enqueueSync 已串行化 list_changed,这条就可以合。 |
Uh oh!
There was an error while loading. Please reload this page.
摘要 / Summary
syncTools在notifications/tools/list_changed重同步时先dispose上一世代,再注册新列表。若外部工具已经占了某个mcp__<serverName>__名,替换抛错、部分新集合回滚,然后返回空 Map。上一世代已经被释放,会话中途该服务器工具集变成空的。获取阶段失败本来会保留上一世代;替换阶段冲突不会。A re-sync swap that hits a foreign namespace squat disposes the live generation and returns empty. Fetch-phase failure already keeps the previous tools; swap-phase conflict should too.
复现 / Repro
初次同步(上一世代为空)撞上抢占 → 该服务器零工具,这是预期的全有或全无。问题只出在「已经有活着的上一世代」的重同步。
根因 / Root cause
packages/mcp/mcp-client/src/tools.ts的 phase-2 swap:for (const dispose of previous.values()) dispose()ctx.tools.register(definition)return new Map()没有记住上一世代的
ToolDefinition,所以无法把它们装回去。connection.ts的enqueueSync对重同步固定registrationFailure: 'contain',于是只打一行 error 日志,调用方拿到空 Map 并当作当前世代。这和「获取失败保留上一世代、成功重同步才替换」的不对称是同一条路径上的洞。
修复 / Fix (cherry-pick ready)
63ad379917—fix(mcp-client): restore previous tools when a re-sync swap conflicts成功世代用
WeakMap<ToolDisposers, Map<string, ToolDefinition>>记住定义。替换失败时释放部分新集合,把上一世代重新注册进同一个 disposer map(startConnection在throw路径不会赋值返回值)。初次同步仍返回空。ToolDisposers类型不变。验收 / Acceptance
pnpm exec vitest run packages/mcp/mcp-client/tests/mcp-client.spec.ts通过(48)。初次同步抢占仍回滚为空;重同步抢占恢复mcp__srv__stable;registrationFailure: 'throw'先恢复再抛出。All reactions