fix: reuse pooled MCP client connections in pi extension - #52
Conversation
- Migrate from @modelcontextprotocol/sdk v1 to @modelcontextprotocol/client v2 (plus @modelcontextprotocol/server as a devDependency for tests) - Pool one lazily connected MCP client per server so callTool no longer pays the full initialize handshake on every execution - Reset and retry once on a fresh connection when the pooled session goes stale - Close pooled clients on session_shutdown (reload/quit/new/resume/fork), terminating the server-side session before tearing down the transport - Add optional server-config injection and a transport fetch seam so the lifecycle is tested against a real in-process MCP server
Add a source-grounded You.com search and contents skill for coding-agent web lookup and cited synthesis. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Reverts 80a8c90 feat: add You web search skill. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
Droid finished @EdwardIrby's task —— View job Droid review complete. The pooled-connection design is correct in the serial case and the new lifecycle tests pass, but the reset/retry and session_shutdown teardown paths close shared connections without coordinating with concurrent in-flight callers, a real concurrency hazard. No security issues were found. |
| const withPooledMcpClient = async <T>(server: McpServerTarget, fn: (client: Client) => Promise<T>): Promise<T> => { | ||
| try { | ||
| return await fn((await getMcpClient(server)).client) | ||
| } catch { |
There was a problem hiding this comment.
[P1] resetMcpClient closes a shared pooled connection out from under concurrent in-flight callersmcpClients is module-level shared state, and getMcpClient returns the same Client object to every concurrent caller keyed by server. When two callTool invocations to the same server run concurrently and one throws, withPooledMcpClient's catch calls resetMcpClient, which calls closeMcpConnection (transport.terminateSession() + client.close()) on that shared connection while the other caller's callTool is still in flight on it. That in-flight call then fails or hangs on a torn-down transport; that caller's own catch then calls resetMcpClient again and can close the fresh connection the first retrier just cached, cascading failures. The same class of hazard exists in closeMcpClients on session_shutdown. There is no in-flight reference counting to prevent closing a connection that outstanding callers are using. Trigger: two concurrent tool calls to the same MCP URL where one fails. Fix: track in-flight callers per connection and defer close until the last caller drains, or have each caller snapshot its own connection and only close the old one once all outstanding callers have finished.
- pass successful structuredContent to the model as pretty JSON - keep tool error content unchanged when calls fail - store structuredContent in details instead of the full MCP result - fold MCP connection lifecycle tests into the main Pi spec Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
Droid encountered an error —— View job Droid is reviewing code and running a security check… |
Track active calls per pooled MCP client entry and defer closing stale entries until all concurrent users drain. This prevents one failing tool call from closing a shared connection while another call is still in flight. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
Droid review complete. LGTM — no new issues found. The previously reported concurrency hazard (resetMcpClient closing shared connections under concurrent in-flight callers) has been properly resolved by commit 01c5a9b, which adds activeCalls reference counting and closeWhenIdle deferral so connections are only torn down after the last caller drains. All six lifecycle tests pass and type checks are clean. The toToolResult details field change from full MCP result to structuredContent is a deliberate API change per the PR scope, with tests updated to match. |
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
|
Droid finished @EdwardIrby's task —— View job Pass 2 validation complete. No candidate comments were produced by Pass 1, so there are no inline review comments to post. Summary: The pooled MCP client connection design is correct: |
Summary
session_shutdownValidation
bun run --cwd packages/pi test