From 8a0190ca3bc4253529be0075c0ceab2702b8d804 Mon Sep 17 00:00:00 2001 From: HyeongHo Jun <88872409+kamja44@users.noreply.github.com> Date: Sat, 3 Jan 2026 01:06:56 +0900 Subject: [PATCH] fix(core): handle unhandled promise rejection in mcp-client-manager (#14701) --- .../core/src/tools/mcp-client-manager.test.ts | 36 ++++++++++++++++++ packages/core/src/tools/mcp-client-manager.ts | 38 +++++++++++-------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/core/src/tools/mcp-client-manager.test.ts b/packages/core/src/tools/mcp-client-manager.test.ts index 0ff201e8e6..d2fdc5d119 100644 --- a/packages/core/src/tools/mcp-client-manager.test.ts +++ b/packages/core/src/tools/mcp-client-manager.test.ts @@ -238,4 +238,40 @@ describe('McpClientManager', () => { ); }); }); + + describe('Promise rejection handling', () => { + it('should handle errors thrown during client initialization', async () => { + vi.mocked(McpClient).mockImplementation(() => { + throw new Error('Client initialization failed'); + }); + + mockConfig.getMcpServers.mockReturnValue({ + 'test-server': {}, + }); + + const manager = new McpClientManager({} as ToolRegistry, mockConfig); + + await expect(manager.startConfiguredMcpServers()).resolves.not.toThrow(); + }); + + it('should handle errors thrown in the async IIFE before try block', async () => { + let disconnectCallCount = 0; + mockedMcpClient.disconnect.mockImplementation(async () => { + disconnectCallCount++; + if (disconnectCallCount === 1) { + throw new Error('Disconnect failed unexpectedly'); + } + }); + mockedMcpClient.getServerConfig.mockReturnValue({}); + + mockConfig.getMcpServers.mockReturnValue({ + 'test-server': {}, + }); + + const manager = new McpClientManager({} as ToolRegistry, mockConfig); + await manager.startConfiguredMcpServers(); + + await expect(manager.restartServer('test-server')).resolves.not.toThrow(); + }); + }); }); diff --git a/packages/core/src/tools/mcp-client-manager.ts b/packages/core/src/tools/mcp-client-manager.ts index fbc3b3e423..7a1443e096 100644 --- a/packages/core/src/tools/mcp-client-manager.ts +++ b/packages/core/src/tools/mcp-client-manager.ts @@ -163,8 +163,7 @@ export class McpClientManager { return; } - const currentDiscoveryPromise = new Promise((resolve, _reject) => { - // eslint-disable-next-line @typescript-eslint/no-floating-promises + const currentDiscoveryPromise = new Promise((resolve, reject) => { (async () => { try { if (existing) { @@ -212,6 +211,13 @@ export class McpClientManager { ); } } + } catch (error) { + const errorMessage = getErrorMessage(error); + coreEvents.emitFeedback( + 'error', + `Error initializing MCP server '${name}': ${errorMessage}`, + error, + ); } finally { // This is required to update the content generator configuration with the // new tool configuration. @@ -221,28 +227,30 @@ export class McpClientManager { } resolve(); } - })(); + })().catch(reject); }); if (this.discoveryPromise) { - this.discoveryPromise = this.discoveryPromise.then( - () => currentDiscoveryPromise, - ); + // Ensure the next discovery starts regardless of the previous one's success/failure + this.discoveryPromise = this.discoveryPromise + .catch(() => {}) + .then(() => currentDiscoveryPromise); } else { this.discoveryState = MCPDiscoveryState.IN_PROGRESS; this.discoveryPromise = currentDiscoveryPromise; } this.eventEmitter?.emit('mcp-client-update', this.clients); const currentPromise = this.discoveryPromise; - // eslint-disable-next-line @typescript-eslint/no-floating-promises - currentPromise.then((_) => { - // If we are the last recorded discoveryPromise, then we are done, reset - // the world. - if (currentPromise === this.discoveryPromise) { - this.discoveryPromise = undefined; - this.discoveryState = MCPDiscoveryState.COMPLETED; - } - }); + void currentPromise + .finally(() => { + // If we are the last recorded discoveryPromise, then we are done, reset + // the world. + if (currentPromise === this.discoveryPromise) { + this.discoveryPromise = undefined; + this.discoveryState = MCPDiscoveryState.COMPLETED; + } + }) + .catch(() => {}); // Prevents unhandled rejection from the .finally branch return currentPromise; }