Problem
When StdioClientTransport.close() is called, only the direct child process is signaled. If the MCP server was started via a wrapper command (e.g. npx, uvx, python -m), the wrapper's child processes (the actual server) become orphans and continue running indefinitely.
Root cause
The close() method uses processToClose.kill('SIGTERM') / processToClose.kill('SIGKILL'), which calls Node's ChildProcess.kill(). This only signals the direct child PID — not its descendants.
This affects all platforms:
- macOS/Linux: No process group kill (
process.kill(-pid, signal)) is used, so children of the direct process are not signaled.
- Windows:
ChildProcess.kill() does not kill the process tree at all. Only taskkill /T /F /PID <pid> can do that.
Reproduction
- Start an MCP server via a wrapper (e.g.
npx @some/mcp-server)
- Call
transport.close()
- Observe that
npx is killed but the underlying node process remains running
Possible solutions
Option A: Kill the process tree in close() — use pgrep -P (Unix) or taskkill /T /F (Windows) to enumerate and signal all descendants before/instead of ChildProcess.kill().
Option B: Spawn with detached: true and use process.kill(-pid, signal) to kill the process group. However, this has trade-offs: if the parent crashes, detached children become permanent orphans with no automatic cleanup.
Option C: Expose a hook or option so consumers can provide their own process cleanup logic.
Problem
When
StdioClientTransport.close()is called, only the direct child process is signaled. If the MCP server was started via a wrapper command (e.g.npx,uvx,python -m), the wrapper's child processes (the actual server) become orphans and continue running indefinitely.Root cause
The
close()method usesprocessToClose.kill('SIGTERM')/processToClose.kill('SIGKILL'), which calls Node'sChildProcess.kill(). This only signals the direct child PID — not its descendants.This affects all platforms:
process.kill(-pid, signal)) is used, so children of the direct process are not signaled.ChildProcess.kill()does not kill the process tree at all. Onlytaskkill /T /F /PID <pid>can do that.Reproduction
npx @some/mcp-server)transport.close()npxis killed but the underlyingnodeprocess remains runningPossible solutions
Option A: Kill the process tree in
close()— usepgrep -P(Unix) ortaskkill /T /F(Windows) to enumerate and signal all descendants before/instead ofChildProcess.kill().Option B: Spawn with
detached: trueand useprocess.kill(-pid, signal)to kill the process group. However, this has trade-offs: if the parent crashes, detached children become permanent orphans with no automatic cleanup.Option C: Expose a hook or option so consumers can provide their own process cleanup logic.