From fd008cd8e0b8277e691efd8e69dc96ac8e188d9e Mon Sep 17 00:00:00 2001 From: Kaoru0822 Date: Tue, 6 Oct 2026 03:58:12 -0700 Subject: [PATCH] fix(mcp): retain shutdown failure diagnostics --- src/main/shutdown.ts | 30 +++++++++++++- tests/boundary/mcp/shutdown.test.ts | 61 +++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 tests/boundary/mcp/shutdown.test.ts diff --git a/src/main/shutdown.ts b/src/main/shutdown.ts index f4bf1e4d7..eb6cc229d 100644 --- a/src/main/shutdown.ts +++ b/src/main/shutdown.ts @@ -29,7 +29,11 @@ export const createShutdown = (input: { return shutdownPromise; }; const requestShutdown = (): void => { - shutdown().catch(() => { + shutdown().catch((cause: unknown) => { + serverLogger.debug( + { failure_cause: describeShutdownFailure(cause) }, + "MCP shutdown rejected", + ); dependencies.setExitCode(1); serverLogger.error(MCP_SHUTDOWN_FAILED); dependencies.writeStderr(`${MCP_SHUTDOWN_FAILED}\n`); @@ -38,3 +42,27 @@ export const createShutdown = (input: { unregisterShutdown = dependencies.registerShutdown(requestShutdown); return { shutdown, request: requestShutdown }; }; + +const describeShutdownFailure = ( + cause: unknown, +): Readonly> => { + if (cause instanceof Error) { + const code = "code" in cause ? cause.code : undefined; + return { + name: cause.name, + message: cause.message, + ...(typeof code === "string" || + (typeof code === "number" && Number.isFinite(code)) + ? { code } + : {}), + }; + } + if ( + cause === null || + typeof cause === "string" || + typeof cause === "boolean" || + (typeof cause === "number" && Number.isFinite(cause)) + ) + return { type: cause === null ? "null" : typeof cause, value: cause }; + return { type: typeof cause }; +}; diff --git a/tests/boundary/mcp/shutdown.test.ts b/tests/boundary/mcp/shutdown.test.ts new file mode 100644 index 000000000..4465d7120 --- /dev/null +++ b/tests/boundary/mcp/shutdown.test.ts @@ -0,0 +1,61 @@ +import type { StdioServerHandle } from "@modelcontextprotocol/server/stdio"; +import pino from "pino"; +import { expect, it } from "vitest"; + +import { createManagedBinarySession } from "../../../src/application/runtime.js"; +import { MCP_SHUTDOWN_FAILED } from "../../../src/main/messages.js"; +import { createShutdown } from "../../../src/main/shutdown.js"; +import type { RuntimeDependencies } from "../../../src/main/types.js"; + +it("keeps shutdown output stable while logging the rejected cause at debug level", async () => { + const logs: string[] = []; + const output: string[] = []; + const exitCodes: number[] = []; + const requests: Array<() => void> = []; + const failure = Object.assign( + new TypeError("fixture transport close failed"), + { + code: "ECONNRESET", + }, + ); + const handle: StdioServerHandle = { + close: () => Promise.reject(failure), + }; + const dependencies: RuntimeDependencies = { + env: {}, + serve: () => { + throw new Error("unused fixture transport"); + }, + writeStderr: (text) => output.push(text), + setExitCode: (code) => exitCodes.push(code), + registerShutdown: (request) => { + requests.push(request); + return () => undefined; + }, + }; + const logger = pino({ level: "debug" }, { write: (line) => logs.push(line) }); + createShutdown({ + handle, + session: createManagedBinarySession(), + unregisterReload: () => undefined, + dependencies, + serverLogger: logger, + }); + + requests[0]?.(); + await new Promise((resolve) => setImmediate(resolve)); + + expect(exitCodes).toEqual([1]); + expect(output).toEqual([`${MCP_SHUTDOWN_FAILED}\n`]); + expect(logs.map((line) => JSON.parse(line))).toContainEqual( + expect.objectContaining({ + level: 20, + msg: "MCP shutdown rejected", + failure_cause: { + name: "TypeError", + message: "fixture transport close failed", + code: "ECONNRESET", + }, + }), + ); +});