(
name: string,
info: MCPServerInfo,
runtime: Runtime,
projectPath: string,
workspacePath: string,
projectSecrets: Record<string, string> | undefined,
onActivity: () => void
)
| 1523 | } |
| 1524 | |
| 1525 | private async startSingleServer( |
| 1526 | name: string, |
| 1527 | info: MCPServerInfo, |
| 1528 | runtime: Runtime, |
| 1529 | projectPath: string, |
| 1530 | workspacePath: string, |
| 1531 | projectSecrets: Record<string, string> | undefined, |
| 1532 | onActivity: () => void |
| 1533 | ): Promise<MCPServerInstance | null> { |
| 1534 | let timeoutHandle: ReturnType<typeof setTimeout> | undefined; |
| 1535 | const abortController = new AbortController(); |
| 1536 | let abortCleanupPromise: Promise<void> | null = null; |
| 1537 | |
| 1538 | const registerAbortCleanup = (cleanupPromise: Promise<void>) => { |
| 1539 | abortCleanupPromise ??= cleanupPromise; |
| 1540 | }; |
| 1541 | |
| 1542 | let didTimeout = false; |
| 1543 | const keepPendingAfterTimeout = () => new Promise<MCPServerInstance | null>(() => undefined); |
| 1544 | |
| 1545 | const startup = this.startSingleServerImpl( |
| 1546 | name, |
| 1547 | info, |
| 1548 | runtime, |
| 1549 | projectPath, |
| 1550 | workspacePath, |
| 1551 | projectSecrets, |
| 1552 | onActivity, |
| 1553 | abortController.signal, |
| 1554 | registerAbortCleanup |
| 1555 | ).then( |
| 1556 | (instance) => (didTimeout ? keepPendingAfterTimeout() : instance), |
| 1557 | (error) => { |
| 1558 | if (didTimeout) { |
| 1559 | return keepPendingAfterTimeout(); |
| 1560 | } |
| 1561 | throw error; |
| 1562 | } |
| 1563 | ); |
| 1564 | |
| 1565 | const timeout = new Promise<never>((_resolve, reject) => { |
| 1566 | timeoutHandle = setTimeout(() => { |
| 1567 | didTimeout = true; |
| 1568 | |
| 1569 | // Promise.race does not cancel the losing startup branch automatically. |
| 1570 | // Abort in-flight startup so stdio processes and partial MCP clients are cleaned up. |
| 1571 | abortController.abort(); |
| 1572 | |
| 1573 | const timeoutError = new MCPStartupTimeoutError(name, MCP_STARTUP_TIMEOUT_MS); |
| 1574 | if (!abortCleanupPromise) { |
| 1575 | reject(timeoutError); |
| 1576 | return; |
| 1577 | } |
| 1578 | |
| 1579 | const cleanupWait = abortCleanupPromise.catch((error: unknown) => { |
| 1580 | log.debug("[MCP] Error waiting for startup cleanup", { |
| 1581 | name, |
| 1582 | error: getErrorMessage(error), |
no test coverage detected