Skip to content

Commit 1dc7b7d

Browse files
fix: handle container shutdown signals (#54)
* fix: handle container shutdown signals Register SIGINT and SIGTERM handlers so the server closes its active transport before exiting. This lets HTTP and stdio deployments stop cleanly in containers instead of hanging on shutdown. * changeste * fix lint
1 parent 965b27d commit 1dc7b7d

File tree

5 files changed

+73
-10
lines changed

5 files changed

+73
-10
lines changed

.changeset/khaki-mangos-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@speakeasy-api/docs-mcp-server": patch
3+
---
4+
5+
Register SIGINT and SIGTERM handlers so the server closes its active transport before exiting. This lets HTTP and stdio deployments stop cleanly in containers instead of hanging on shutdown.

packages/server/src/bin.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ program
115115
);
116116

117117
if (options.transport === "http") {
118-
await startHttpServer(server, {
118+
const { shutdown } = await startHttpServer(server, {
119119
port: options.port,
120120
...(options.gitCommit || options.buildDate
121121
? {
@@ -129,8 +129,10 @@ program
129129
pretty: options.logPretty,
130130
logLevel: options.logLevel,
131131
});
132+
registerShutdown(shutdown);
132133
} else {
133-
await startStdioServer(server);
134+
const { shutdown } = await startStdioServer(server);
135+
registerShutdown(shutdown);
134136
}
135137
});
136138

@@ -151,3 +153,31 @@ function parseIntOption(value: string): number {
151153
}
152154
return parsed;
153155
}
156+
157+
function registerShutdown(cleanup: () => Promise<void>): void {
158+
let shuttingDown = false;
159+
160+
const shutdown = async (signal: string) => {
161+
if (shuttingDown) {
162+
return;
163+
}
164+
shuttingDown = true;
165+
166+
try {
167+
await cleanup();
168+
} catch (error) {
169+
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
170+
process.stderr.write(`Failed to shut down on ${signal}: ${message}\n`);
171+
process.exitCode = 1;
172+
} finally {
173+
process.exit();
174+
}
175+
};
176+
177+
process.once("SIGINT", () => {
178+
shutdown("SIGINT");
179+
});
180+
process.once("SIGTERM", () => {
181+
shutdown("SIGTERM");
182+
});
183+
}

packages/server/src/http.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ export interface StartHttpServerOptions extends Pick<
5252

5353
export interface HttpServerHandle {
5454
httpServer: http.Server;
55+
shutdown: () => Promise<void>;
5556
fetch: (request: Request) => Response | Promise<Response>;
5657
port: number;
5758
}
@@ -86,7 +87,21 @@ export async function startHttpServer(
8687
const actualPort = await listenOnAvailablePort(httpServer, port);
8788
logger.info("started mcp server", { url: `http://localhost:${actualPort}/mcp` });
8889

89-
return { httpServer, fetch: app.fetch, port: actualPort };
90+
const shutdown = async (): Promise<void> => {
91+
return new Promise((resolve, reject) => {
92+
logger.info("shutting down http server");
93+
94+
httpServer.close((error) => {
95+
if (error) {
96+
reject(error);
97+
return;
98+
}
99+
resolve();
100+
});
101+
});
102+
};
103+
104+
return { httpServer, fetch: app.fetch, port: actualPort, shutdown };
90105
}
91106

92107
interface SessionEntry {

packages/server/src/logging.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import stream from "node:stream";
21
import {
32
configure,
3+
getConsoleSink,
44
getJsonLinesFormatter,
55
getLogger,
66
getLogLevels,
7-
getStreamSink,
87
type LogLevel,
98
} from "@logtape/logtape";
109
import { getPrettyFormatter } from "@logtape/pretty";
@@ -43,10 +42,7 @@ export async function configureDefaultLogger(
4342

4443
await configure({
4544
sinks: {
46-
console: getStreamSink(
47-
stream.Writable.toWeb(process.stderr) as unknown as WritableStream<Uint8Array>,
48-
{ formatter },
49-
),
45+
console: getConsoleSink({ formatter }),
5046
},
5147
loggers: [
5248
{ category: ["logtape", "meta"], lowestLevel: "warning", sinks: ["console"] },

packages/server/src/stdio.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
11
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
33

4-
export async function startStdioServer(factory: () => McpServer): Promise<void> {
4+
export interface StdioServerHandle {
5+
server: McpServer;
6+
transport: StdioServerTransport;
7+
shutdown: () => Promise<void>;
8+
}
9+
10+
export async function startStdioServer(factory: () => McpServer): Promise<StdioServerHandle> {
511
const transport = new StdioServerTransport();
612
const server = factory();
713
await server.connect(transport);
14+
15+
const shutdown = async () => {
16+
await transport.close().catch((err) => {
17+
if (err) console.error("Failed to close transport:", err);
18+
});
19+
await server.close().catch((err) => {
20+
if (err) console.error("Failed to close mcp server:", err);
21+
});
22+
};
23+
24+
return { server, transport, shutdown };
825
}

0 commit comments

Comments
 (0)