MCP servers running as child processes receive SIGINT when the user presses Ctrl+C and SIGTERM when the parent process shuts down. Without signal handlers, the Node.js process exits immediately (SIGINT default), potentially mid-write on an in-progress JSON-RPC response. The client receives a truncated or empty message and marks the server as crashed. CWE-404 (improper resource shutdown) covers this: open database connections, file handles, and HTTP keep-alives are abandoned rather than closed, which can leave the remote systems in inconsistent state.
Medium because abrupt termination corrupts in-progress responses and leaks resources, but the immediate impact is limited to the session in progress at shutdown time.
Register SIGINT and SIGTERM handlers that call server.close() before exiting. The SDK's close() drains in-flight messages and closes the transport cleanly.
// src/index.ts
async function shutdown() {
console.error('Shutting down...')
await server.close() // closes transport, drains in-flight messages
process.exit(0)
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
const transport = new StdioServerTransport()
await server.connect(transport)
If your server holds database connections or open file handles, close them inside shutdown() before calling process.exit(0).
ID: mcp-server.error-resilience.graceful-shutdown
Severity: medium
What to look for: Count all signal handlers (SIGINT, SIGTERM) and cleanup routines. Enumerate whether the server completes in-flight requests before shutting down. Check for signal handlers (process.on('SIGINT'), process.on('SIGTERM'), signal.signal() in Python). When the server receives a shutdown signal, it should close the transport cleanly, finish any in-progress tool calls if possible, and release resources (database connections, file handles). Check that the server doesn't just call process.exit() immediately (which could corrupt in-progress responses).
Pass criteria: The server handles SIGINT and SIGTERM, closes the transport cleanly, and exits. In-progress operations are either completed or cancelled gracefully. At least 1 signal handler must be registered for graceful shutdown.
Fail criteria: No signal handlers (abrupt termination may corrupt in-progress responses), or signal handlers that call process.exit(0) immediately without cleanup.
Skip (N/A) when: All checks skip when no MCP server is detected.
Cross-reference: For timeout handling during shutdown, see timeout-handling.
Detail on fail: "No SIGINT/SIGTERM handlers — server will be killed abruptly, potentially corrupting in-progress responses or leaving resources open" or "Signal handler calls process.exit(0) immediately without closing the transport or cleaning up database connections"
Remediation: Handle shutdown signals gracefully:
// src/index.ts — graceful shutdown
process.on('SIGINT', async () => { await server.close(); process.exit(0) })
const server = new McpServer({ name: 'my-server', version: '1.0.0' })
const transport = new StdioServerTransport()
async function shutdown() {
console.error('Shutting down...')
await server.close() // closes transport and cleans up
process.exit(0)
}
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
await server.connect(transport)
import signal
import asyncio
async def shutdown(server):
await server.close()
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, lambda: asyncio.create_task(shutdown(server)))
loop.add_signal_handler(signal.SIGTERM, lambda: asyncio.create_task(shutdown(server)))