modelcontextprotocol / modelcontextprotocol/typescript-sdk

StdioClientTransport.close() does not kill the process tree, leaving orphan processes

Open
#2,023 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug fix proposed P2 ready for work
Dominant language
TypeScript
Stars
13.4k
Forks
2.2k
Avg merge
3d 15h
Merged PRs (30d)
4

Description

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

  1. Start an MCP server via a wrapper (e.g. npx @some/mcp-server)
  2. Call transport.close()
  3. 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.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at StdioClientTransport.close() and inspect how processToClose.kill('SIGTERM') and processToClose.kill('SIGKILL') behave for wrapper commands. Reproduce with an npx, uvx, or python -m server, then verify that closing the transport terminates the wrapper and its descendants on macOS/Linux and Windows.

Written by the indexing model from the issue text.

Assessment

Tech stack
nodejs, typescript
Domain
backend, devtools
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.