Merge pull request #729 from kaoru0822-kitauji/fix/mcp-shutdown-diagnostics

fix(mcp): retain shutdown failure diagnostics
This commit is contained in:
Kaoru0822 2026-10-06 05:08:40 -07:00 committed by GitHub
commit 09c249deb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 90 additions and 1 deletions

View File

@ -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<Record<string, string | number | boolean | null>> => {
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 };
};

View File

@ -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",
},
}),
);
});