Skip to content

fix: destroy descendant processes on StdioClientTransport.closeGracefully - #966

Open
k-krawczyk wants to merge 1 commit into
modelcontextprotocol:mainfrom
k-krawczyk:fix/496-destroy-process-tree
Open

k-krawczyk wants to merge 1 commit into
modelcontextprotocol:mainfrom
k-krawczyk:fix/496-destroy-process-tree

Conversation

@k-krawczyk

Copy link
Copy Markdown

Fixes #496

Summary

StdioClientTransport.closeGracefully() previously called process.destroy() on the root process only. When the configured command is a wrapper such as npx (POSIX) or cmd.exe /c npx.cmd ... (Windows), the wrapper has already forked child processes (e.g. node.exe, the MCP server itself); destroying only the wrapper leaves those descendants orphaned. On Windows this prevents the JVM from shutting down cleanly — the symptom reported in the issue.

Fix

Walk ProcessHandle.descendants() (JDK 9+, available since the repo's Java 17 baseline) and destroy each descendant before destroying the root. This avoids the OS-specific taskkill /pid X /F /T approach suggested on the issue: same effect, one cross-platform code path, no shelling out.

The descendant snapshot is best-effort — processes forked concurrently with the destroy call may be missed, but the wider tree generally dies with its parent.

Extracted as a package-private static helper destroyProcessTree(Process) for testability.

Tests

New mcp-core/.../StdioClientTransportDestroyTreeTests with three cases:

  • destroyProcessTreeTerminatesRootAndDescendants — spawns sh -c 'sleep 60 & sleep 60 & wait' (two child sleeps), captures descendant ProcessHandles, calls the helper, then awaits !isAlive() on every captured handle. POSIX-only via @DisabledOnOs(WINDOWS) since CI runs on Ubuntu.
  • destroyProcessTreeTerminatesRootWithNoDescendants — single sleep 60, ensures the root path still works when there is nothing beneath it.
  • destroyProcessTreeIsSafeOnAlreadyTerminatedProcess — runs a command that exits immediately, then calls the helper to assert it doesn't throw on a dead Process / ProcessHandle.

Verification

./mvnw verify green locally on macOS across all 11 modules (BUILD SUCCESS, 0 failures, 0 errors). This includes StdioMcpSyncClientTests and StdioMcpAsyncClientTests, which exercise the close path through a real npx subprocess.

Caveat

I'm on macOS, so the original Windows symptom is verified only by reasoning about the JDK API, not exercised end-to-end. ProcessHandle.descendants() is a JDK-level abstraction that works across platforms, but I'd appreciate a reviewer with Windows confirming the report from #496 is resolved. Happy to switch to explicit taskkill instead if maintainers prefer the explicit route.

…ully

When `closeGracefully()` is called, the previous implementation invoked
only `process.destroy()` on the root process. With wrapper commands such
as `npx` (POSIX) or `cmd.exe /c npx.cmd ...` (Windows), the wrapper has
already forked child processes (`node.exe`, the MCP server itself);
destroying only the wrapper leaves those descendants orphaned. On Windows
this prevents the JVM from shutting down cleanly.

Walk `ProcessHandle.descendants()` (JDK 9+, available since the repo's
Java 17 baseline) and destroy each descendant before destroying the root.
This avoids the OS-specific `taskkill /pid X /F /T` approach suggested on
the issue: same effect, one cross-platform code path, no shelling out.

The descendant snapshot is best-effort — processes forked concurrently
with the destroy call may be missed, but the wider tree generally dies
with its parent.

Fixes modelcontextprotocol#496

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant