Skip to content

Commit 3d3234b

Browse files
committed
fix: async onclose, stdin EOF detection, SIGTERM in examples
Three related improvements to server lifecycle handling: 1. Allow async onclose callbacks on Transport and Protocol. MCP servers that hold external resources (browser sessions, database connections) need to await cleanup before the process exits. The onclose signature changes from `() => void` to `() => void | Promise<void>`, matching the existing pattern used by onsessionclosed in StreamableHTTPServerTransport. All transports and Protocol._onclose now await the callback. 2. Close StdioServerTransport when stdin ends. The transport listened for data and error but not EOF. When the MCP client disconnects, the transport stays open and onclose never fires. This is especially visible with containerized servers using docker run with automatic removal: without onclose the server never exits and the container accumulates. 3. Add SIGTERM handlers alongside SIGINT in all examples. MCP servers run as background processes spawned by clients, not interactively. SIGTERM is what container runtimes and process managers send to stop a process.
1 parent e86b183 commit 3d3234b

24 files changed

+195
-43
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
"@modelcontextprotocol/core": patch
3+
"@modelcontextprotocol/server": patch
4+
"@modelcontextprotocol/client": patch
5+
---
6+
7+
Allow async `onclose` callbacks on Transport and Protocol. The signature changes from `() => void` to `() => void | Promise<void>`, and all call sites now await the callback. This lets MCP servers perform async cleanup (e.g., releasing browser sessions or database connections) when the transport closes.
8+
9+
Close `StdioServerTransport` when stdin reaches EOF, so containerized servers exit cleanly on client disconnect.
10+
11+
Add SIGTERM handlers alongside SIGINT in all examples, since MCP servers run as background processes stopped by SIGTERM, not interactively via Ctrl+C.

examples/client/src/elicitationUrlExample.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -813,6 +813,10 @@ process.on('SIGINT', async () => {
813813
console.log('\nReceived SIGINT. Cleaning up...');
814814
await cleanup();
815815
});
816+
process.on('SIGTERM', async () => {
817+
console.log('\nReceived SIGINT. Cleaning up...');
818+
await cleanup();
819+
});
816820

817821
// Start the interactive client
818822
try {

examples/client/src/simpleOAuthClient.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,11 @@ async function main(): Promise<void> {
448448
client.close();
449449
process.exit(0);
450450
});
451+
process.on('SIGTERM', () => {
452+
console.log('\n\n👋 Goodbye!');
453+
client.close();
454+
process.exit(0);
455+
});
451456

452457
try {
453458
await client.connect();

examples/client/src/simpleStreamableHttp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,10 @@ process.on('SIGINT', async () => {
997997
console.log('\nReceived SIGINT. Cleaning up...');
998998
await cleanup();
999999
});
1000+
process.on('SIGTERM', async () => {
1001+
console.log('\nReceived SIGINT. Cleaning up...');
1002+
await cleanup();
1003+
});
10001004

10011005
// Start the interactive client
10021006
try {

examples/server/src/elicitationFormExample.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,22 @@ async function main() {
453453
process.on('SIGINT', async () => {
454454
console.log('Shutting down server...');
455455

456+
// Close all active transports to properly clean up resources
457+
for (const sessionId in transports) {
458+
try {
459+
console.log(`Closing transport for session ${sessionId}`);
460+
await transports[sessionId]!.close();
461+
delete transports[sessionId];
462+
} catch (error) {
463+
console.error(`Error closing transport for session ${sessionId}:`, error);
464+
}
465+
}
466+
console.log('Server shutdown complete');
467+
process.exit(0);
468+
});
469+
process.on('SIGTERM', async () => {
470+
console.log('Shutting down server...');
471+
456472
// Close all active transports to properly clean up resources
457473
for (const sessionId in transports) {
458474
try {

examples/server/src/elicitationUrlExample.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,20 @@ process.on('SIGINT', async () => {
731731
console.log('Server shutdown complete');
732732
process.exit(0);
733733
});
734+
process.on('SIGTERM', async () => {
735+
console.log('Shutting down server...');
736+
737+
// Close all active transports to properly clean up resources
738+
for (const sessionId in transports) {
739+
try {
740+
console.log(`Closing transport for session ${sessionId}`);
741+
await transports[sessionId]!.close();
742+
delete transports[sessionId];
743+
delete sessionsNeedingElicitation[sessionId];
744+
} catch (error) {
745+
console.error(`Error closing transport for session ${sessionId}:`, error);
746+
}
747+
}
748+
console.log('Server shutdown complete');
749+
process.exit(0);
750+
});

examples/server/src/jsonResponseStreamableHttp.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,7 @@ process.on('SIGINT', async () => {
163163
console.log('Shutting down server...');
164164
process.exit(0);
165165
});
166+
process.on('SIGTERM', async () => {
167+
console.log('Shutting down server...');
168+
process.exit(0);
169+
});

examples/server/src/simpleStatelessStreamableHttp.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,8 @@ process.on('SIGINT', async () => {
169169
// eslint-disable-next-line unicorn/no-process-exit
170170
process.exit(0);
171171
});
172+
process.on('SIGTERM', async () => {
173+
console.log('Shutting down server...');
174+
// eslint-disable-next-line unicorn/no-process-exit
175+
process.exit(0);
176+
});

examples/server/src/simpleStreamableHttp.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,3 +815,19 @@ process.on('SIGINT', async () => {
815815
console.log('Server shutdown complete');
816816
process.exit(0);
817817
});
818+
process.on('SIGTERM', async () => {
819+
console.log('Shutting down server...');
820+
821+
// Close all active transports to properly clean up resources
822+
for (const sessionId in transports) {
823+
try {
824+
console.log(`Closing transport for session ${sessionId}`);
825+
await transports[sessionId]!.close();
826+
delete transports[sessionId];
827+
} catch (error) {
828+
console.error(`Error closing transport for session ${sessionId}:`, error);
829+
}
830+
}
831+
console.log('Server shutdown complete');
832+
process.exit(0);
833+
});

examples/server/src/simpleTaskInteractive.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,3 +741,18 @@ process.on('SIGINT', async () => {
741741
console.log('Server shutdown complete');
742742
process.exit(0);
743743
});
744+
process.on('SIGTERM', async () => {
745+
console.log('\nShutting down server...');
746+
for (const sessionId of Object.keys(transports)) {
747+
try {
748+
await transports[sessionId]!.close();
749+
delete transports[sessionId];
750+
} catch (error) {
751+
console.error(`Error closing session ${sessionId}:`, error);
752+
}
753+
}
754+
taskStore.cleanup();
755+
messageQueue.cleanup();
756+
console.log('Server shutdown complete');
757+
process.exit(0);
758+
});

0 commit comments

Comments
 (0)