Repository navigation
Description
When implementing a Stdio server that is called from the StdioClientTransport, that client transport shuts it down in a way that prevents orderly (asynchronous) shutdown of the server.
The server will not receive any message prior to SIGTERM, and the Node SIGTERM is a notoriously bad place to do async shutdown operations (any async operation will yield back to the event loop that is shutting down, with the result that your app typically exits on the first yield).
In our app, we present as a stdio MCP server and we bridge to other MCP clients, some of which (including Stdio) require orderly shutdown. Ironically, the StdioClientTransport killing our app abruptly is preventing us from killing our own stdio server at all. But any stdio server that had any important work to do at what is effectively the end of the stdio "session" will have a similar challenge.
The StdioClientTransport code in question is:
typescript-sdk/src/client/stdio.ts
Line 202 in 77a284c
| async close(): Promise<void> { |
async close(): Promise<void> {
this._abortController.abort();
this._process = undefined;
this._readBuffer.clear();
}The _abortController.abort()' is the offending code (triggers the SIGTERM in the spawned app).
The correct way to terminate such a server (particularly since we know it's a stdio server) would be to close the stdin, allowing the server to receive the signal and perform an orderly shutdown. For the sake of safety, and to support all of the servers implemented in the current code that don't do any shutdown at all, you probably want to wait a small amount of time and then trigger the abort as now.
That might look something like this:
async close() {
if (!this._process) {
return;
}
// First, close stdin to signal the server we're done
if (this._process.stdin) {
this._process.stdin.end();
}
// Give the server a reasonable amount of time to clean up
const cleanupTimeout = 5000; // 5 seconds
const startTime = Date.now();
while (this._process) {
// Check if process has exited
if (!this._process.connected) {
break;
}
// If we've waited too long, force terminate
if (Date.now() - startTime > cleanupTimeout) {
this._abortController.abort();
break;
}
// Wait a bit before checking again
await new Promise(resolve => setTimeout(resolve, 100));
}
this._process = undefined;
this._readBuffer.clear();
}
Activity
BobDickinson commentedon May 22, 2025
Upon further investigation, the orderly shutdown issues I experienced were not related to SIGTERM as described above (implementing a non-async SIGTERM handler that called an async orderly shutdown method solved that). The issue was that when StdioClientTansport close was called to terminate the process, it behaved poorly when npx was the spawned process (there was a long delay in npx terminating the node process). Spawning the node process directly from StdioClientTransport produced an orderly shutdown of the node MCP server. Please ignore (or delete) this issue.
Merge pull request modelcontextprotocol#532 from QuantGeekDev/chore/b…
Wasn't fully updated with dependency versions in modelcontextprotocol…