You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(mcp): a connection completing after close() is never reaped — abort mid-connectAll orphans a stdio child #341
McpClientManager.connectAll() and close() are not serialized, so an abort landing mid-connect can leave a live stdio child that nothing will ever reap.
consttoClose=this.servers;this.servers=[];// ... close each, then this.processes.terminate()
Interleaved on abort:
abort fires → the adapter's reaper drains its Disposer → close() runs. this.servers is still empty (nothing has been appended yet), so it closes nothing; terminate() kills whatever pids connectOne had already registered.
connectAll() then resolves and pushes the fulfilled connections into this.servers.
The disposer is already drained, so the finally finds no MCP disposer left. Those clients are never closed, and any pid registered after step 1's terminate() survives the run.
Where it bites
Both flows, identically — this is not specific to either adapter:
runLoopAdapter registers disposer.add(() => mcp.close())beforeconnectAll(), deliberately, with a comment noting that "a run aborted mid-connect can already have live children". That reasoning covers pids registered before the abort; it does not cover a connection that completes after it.
Reported by CodeRabbit on #340 and verified against the source. Filed separately rather than folded into #339 because it is pre-existing, lives in mcp/mcp-client.ts rather than in either adapter, and changes shutdown behaviour for aitm start as much as for merge-pr — that deserves its own review and its own regression test.
What
Make the manager refuse to acquire after shutdown starts. Roughly: a closing flag set at the top of close(); connectAll's append loop closes-and-terminates any late arrival instead of pushing it. Both need to hold when close() is still in flight, not only after it resolves.
Worth checking while in there: connectOne registers its child pid at a point that may also be after terminate(), so the flag has to gate registration too, or the pid is tracked by a registry nobody will drain again.
Constraints (per CLAUDE.md)
Portable ESM, strict TS, paired *.test.ts.
Acceptance criteria
Aborting during a delayed connectAll leaves no open client and no tracked pid: a regression test with a createClient that resolves on a timer, aborted mid-flight, asserts the late client's close() ran.
close() remains idempotent and a normal connect → close cycle is unchanged.
bun test + node --test + biome + tsc green in both packages.
Activity
fix(mcp): reap each late connection as it settles, not when the batch…