From c482ddd6ad5e4cf09eb28eca5a453b24e67248a7 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:00:16 +0800 Subject: [PATCH] fix(mcp): release optional providers when transport startup fails Close the browser scenario owner alongside session, Android and servers when transport startup throws. Preserve cleanup failures in the existing diagnostics. Reviewed-PR: #3230 Co-authored-by: qorvr <330186599+qorvr@users.noreply.github.com> --- src/main/transport.ts | 24 +++++++---- .../browserScenarioProbeLifecycle.test.ts | 41 +++++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/src/main/transport.ts b/src/main/transport.ts index eb7b157d75..4846bedd9d 100644 --- a/src/main/transport.ts +++ b/src/main/transport.ts @@ -115,12 +115,17 @@ export const startMcpTransport = async ( }, ); } catch (cause: unknown) { - const [binaryCleanup, androidCleanup, serverCleanup] = - await Promise.allSettled([ - session.close(), - closeAndroid(), - closeServers(), - ]); + const [ + binaryCleanup, + androidCleanup, + serverCleanup, + optionalProviderCleanup, + ] = await Promise.allSettled([ + session.close(), + closeAndroid(), + closeServers(), + closeOptionalProviders(), + ]); serverLogger.error( { error: cause instanceof Error ? cause.message : String(cause), @@ -136,7 +141,12 @@ export const startMcpTransport = async ( ); dependencies.writeStderr(`${cleanupError.message}\n`); } - for (const cleanup of [binaryCleanup, androidCleanup, serverCleanup]) { + for (const cleanup of [ + binaryCleanup, + androidCleanup, + serverCleanup, + optionalProviderCleanup, + ]) { if (cleanup.status === "rejected") { const reason = cleanup.reason instanceof Error diff --git a/tests/boundary/process/browserScenarioProbeLifecycle.test.ts b/tests/boundary/process/browserScenarioProbeLifecycle.test.ts index 42871d13aa..66dc09d6f0 100644 --- a/tests/boundary/process/browserScenarioProbeLifecycle.test.ts +++ b/tests/boundary/process/browserScenarioProbeLifecycle.test.ts @@ -59,3 +59,44 @@ it("keeps the shared browser scenario provider open when the SDK discards its di await session.close(); } }); + +it("closes optional providers when MCP server startup throws", async () => { + const session = createTestBinarySession(() => { + throw new Error("Unexpected deep provider"); + }); + const close = vi.fn(async () => undefined); + const browserScenarioCapture: BrowserScenarioCapturePort = { + identity: () => ({ + id: "fake-browser", + name: "Fake browser", + version: "1", + }), + captureScenario: async () => { + throw new Error("Unexpected scenario capture"); + }, + close, + }; + const runtime: RuntimeDependencies = { + env: {}, + serve: () => { + throw new Error("Server startup failed"); + }, + writeStderr: () => undefined, + setExitCode: () => undefined, + registerShutdown: () => () => undefined, + }; + try { + const transport = await startMcpTransport(runtime, session, { + logger: silentLogger, + serverLogger: silentLogger, + environment: runtime.env, + delivery: new ToolResultDelivery(STDIO_DEFAULT_MAX_BUFFER_SIZE), + loadOptionalProviders: async () => ({ browserScenarioCapture }), + }); + + expect(transport).toEqual({ ok: false }); + expect(close).toHaveBeenCalledTimes(1); + } finally { + await session.close(); + } +});