It appears that the most basic version of starting/stopping an MCP server leaves a handle or reference open which prevents node from exiting.
I've observed that if I process.stdin.destroy() then I can unblock MCP and get the whatever handles it is using to close.
Versions:
node: v23.2.0
@modelcontextprotocol/sdk: 1.0.4
import{Server}from'@modelcontextprotocol/sdk/server/index.js';import{StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';constserver=newServer({name: 'foo/bar',version: '0.0.1',},{capabilities: {resources: {},tools: {},},},);asyncfunctionmain(){consttransport=newStdioServerTransport();server.connect(transport);awaitserver.close();awaittransport.close();console.warn('Will I hang forever here? Unfortunately, yes.');// Destroying stdin prevents the hang, but it's ugly:// process.stdin.destroy();}main();The uncleaned handles appear to be added in server.connect(transport); since it will exit cleanly if I comment that line.
It appears that the most basic version of starting/stopping an MCP server leaves a handle or reference open which prevents node from exiting.
I've observed that if I
process.stdin.destroy()then I can unblock MCP and get the whatever handles it is using to close.Versions:
node:
v23.2.0@modelcontextprotocol/sdk:
1.0.4The uncleaned handles appear to be added in
server.connect(transport);since it will exit cleanly if I comment that line.