From 418b259a11843576fa97f6f7342fcfc489116eac Mon Sep 17 00:00:00 2001 From: lintianle Date: Wed, 8 Jul 2026 13:23:46 +0800 Subject: [PATCH] fix: prevent partial tool leaks and non-blocking dispose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - syncTools: on paginated listTools failure, unregister any tools already registered in the current sync before rethrowing (prevents orphans) - Effect disposer: call client.close() directly without awaiting startup completion — aborts a hanging connect promptly on HMR/dispose --- packages/mcp/mcp-client/tests/mcp-client.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/mcp/mcp-client/tests/mcp-client.spec.ts b/packages/mcp/mcp-client/tests/mcp-client.spec.ts index 5f5a9bf2f4..253cf49d87 100644 --- a/packages/mcp/mcp-client/tests/mcp-client.spec.ts +++ b/packages/mcp/mcp-client/tests/mcp-client.spec.ts @@ -119,6 +119,18 @@ describe('syncTools', () => { expect(secondDisposers.size).toBe(1) }) + it('cleans up already-registered tools when a later page fails', async () => { + const client = createMockClient([]) + client.listTools + .mockResolvedValueOnce({ tools: [{ name: 'survives_not', inputSchema: { type: 'object' } }], nextCursor: 'cursor1' }) + .mockRejectedValueOnce(new Error('page 2 network error')) + + await expect(syncTools(client as never, ctx, defaultOpts, new Map())).rejects.toThrow('page 2 network error') + + // The tool from page 1 was registered then cleaned up on failure. + expect(ctx.tools.get('survives_not')).toBeUndefined() + }) + it('drains paginated listTools responses', async () => { const client = createMockClient([]) client.listTools