fix(core): handle unhandled promise rejection in mcp-client-manager (#14701)

This commit is contained in:
HyeongHo Jun
2026-01-03 01:06:56 +09:00
committed by GitHub
parent c29a8c12b3
commit 8a0190ca3b
2 changed files with 59 additions and 15 deletions
@@ -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();
});
});
}); });
+23 -15
View File
@@ -163,8 +163,7 @@ export class McpClientManager {
return; return;
} }
const currentDiscoveryPromise = new Promise<void>((resolve, _reject) => { const currentDiscoveryPromise = new Promise<void>((resolve, reject) => {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
(async () => { (async () => {
try { try {
if (existing) { 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 { } finally {
// This is required to update the content generator configuration with the // This is required to update the content generator configuration with the
// new tool configuration. // new tool configuration.
@@ -221,28 +227,30 @@ export class McpClientManager {
} }
resolve(); resolve();
} }
})(); })().catch(reject);
}); });
if (this.discoveryPromise) { if (this.discoveryPromise) {
this.discoveryPromise = this.discoveryPromise.then( // Ensure the next discovery starts regardless of the previous one's success/failure
() => currentDiscoveryPromise, this.discoveryPromise = this.discoveryPromise
); .catch(() => {})
.then(() => currentDiscoveryPromise);
} else { } else {
this.discoveryState = MCPDiscoveryState.IN_PROGRESS; this.discoveryState = MCPDiscoveryState.IN_PROGRESS;
this.discoveryPromise = currentDiscoveryPromise; this.discoveryPromise = currentDiscoveryPromise;
} }
this.eventEmitter?.emit('mcp-client-update', this.clients); this.eventEmitter?.emit('mcp-client-update', this.clients);
const currentPromise = this.discoveryPromise; const currentPromise = this.discoveryPromise;
// eslint-disable-next-line @typescript-eslint/no-floating-promises void currentPromise
currentPromise.then((_) => { .finally(() => {
// If we are the last recorded discoveryPromise, then we are done, reset // If we are the last recorded discoveryPromise, then we are done, reset
// the world. // the world.
if (currentPromise === this.discoveryPromise) { if (currentPromise === this.discoveryPromise) {
this.discoveryPromise = undefined; this.discoveryPromise = undefined;
this.discoveryState = MCPDiscoveryState.COMPLETED; this.discoveryState = MCPDiscoveryState.COMPLETED;
} }
}); })
.catch(() => {}); // Prevents unhandled rejection from the .finally branch
return currentPromise; return currentPromise;
} }