From 58b18fc85e9c165e75d8fdbeaeeb03be6117f56c Mon Sep 17 00:00:00 2001 From: JJ Eaton <58495321+jayleaton@users.noreply.github.com> Date: Sun, 25 Oct 2026 10:07:51 +0700 Subject: [PATCH] fix(mcp-gateway): stop killing slow-starting owners during session handover The launcher gave a starting shared owner 15 s to report ready and then killed it. Owner startup waits on no peer, so it always ends in ready, an error, or an exit; the deadline only killed healthy owners on a loaded machine and ended the MCP session being moved to them. CI hit this in the stdio handover test, which spawns the TypeScript source (5-8 s to start). The handover test now fails with the launcher's stderr when the session exits, instead of waiting for the 60 s test timeout. Co-Authored-By: Claude Opus 5.5 --- packages/mcp-gateway/src/sharedGateway.test.ts | 7 +++++++ packages/mcp-gateway/src/sharedLauncher.ts | 6 ++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/mcp-gateway/src/sharedGateway.test.ts b/packages/mcp-gateway/src/sharedGateway.test.ts index a55eddab4c5bc..79f2d7ffd8ebc 100644 --- a/packages/mcp-gateway/src/sharedGateway.test.ts +++ b/packages/mcp-gateway/src/sharedGateway.test.ts @@ -672,10 +672,17 @@ describe("shared MCP gateway", () => { T3_MCP_GRANTS: "{}", }, }); + let stderr = ""; + transport.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString(); + }); cleanup.push(() => client.close()); await client.connect(transport); const changed = Promise.withResolvers(); client.setNotificationHandler(ToolListChangedNotificationSchema, () => changed.resolve()); + // A launcher that cannot move the session exits; fail with its reason instead of waiting. + // oxlint-disable-next-line unicorn/prefer-add-event-listener -- MCP Client exposes an onclose callback rather than a DOM event API. + client.onclose = () => changed.reject(new Error(`Launcher exited: ${stderr.trim()}`)); expect((await client.listTools()).tools.length).toBeGreaterThan(0); await first!.close(); diff --git a/packages/mcp-gateway/src/sharedLauncher.ts b/packages/mcp-gateway/src/sharedLauncher.ts index 8e2fe558b498a..532ff1d209a72 100644 --- a/packages/mcp-gateway/src/sharedLauncher.ts +++ b/packages/mcp-gateway/src/sharedLauncher.ts @@ -39,9 +39,9 @@ export function launchSharedOwner( }, }, ); - const timeout = AbortSignal.timeout(15_000); + // Startup waits on no peer, so it always ends in ready, an error, or an exit. A deadline would + // only kill a healthy owner a loaded machine starts slowly; a stuck launcher is its host's to end. const cleanup = () => { - timeout.removeEventListener("abort", onTimeout); child.off("error", fail); child.off("exit", onExit); child.off("message", onMessage); @@ -52,7 +52,6 @@ export function launchSharedOwner( if (child.connected) child.disconnect(); reject(error); }; - const onTimeout = () => fail(new Error("Timed out starting the shared MCP gateway owner.")); const onExit = (code: number | null) => fail(new Error(`Shared MCP gateway owner exited before startup (code ${code}).`)); const onMessage = (value: unknown) => { @@ -66,7 +65,6 @@ export function launchSharedOwner( resolve(child); } }; - timeout.addEventListener("abort", onTimeout, { once: true }); child.once("error", fail); child.once("exit", onExit); child.on("message", onMessage);