Skip to content

Bound stdio server process termination in closeGracefully (#937) - #1257

Open
caiocv18 wants to merge 1 commit into
modelcontextprotocol:mainfrom
caiocv18:stdio-bounded-termination-937
Open

caiocv18 wants to merge 1 commit into
modelcontextprotocol:mainfrom
caiocv18:stdio-bounded-termination-937

Conversation

@caiocv18

Copy link
Copy Markdown

Closes #937.

Problem. StdioClientTransport.closeGracefully() sends TERM (Process#destroy) and then waits on Process#onExit() with no limit. A server that ignores TERM keeps closeGracefully() from ever completing, and the process stays alive. Reproduced on main with sh -c "trap '' TERM; while :; do sleep 1; done": no completion within 15 s, child still running (details in the issue).

Change. This follows the surface proposed in the issue:

  • ServerParameters.Builder#processTerminationTimeout(Duration) and ServerParameters#getProcessTerminationTimeout(). Default 10 s. The value must be positive and fit in Long.MAX_VALUE nanoseconds; a larger Duration would overflow when Reactor schedules the timeout.
  • In closeGracefully(), after destroy(), the wait on onExit() is bounded by that timeout. On timeout it logs a warning, calls destroyForcibly() and waits for the exit. The scheduler disposal that follows is unchanged.
  • docs/client.md: one paragraph on the timeout in the STDIO section.

Existing constructors and methods are unchanged; the additions are source and binary compatible. One behavior change: a server that needs more than 10 s to exit after TERM is now killed. Callers that need longer can raise the timeout.

Relation to #966. #966 replaces process.destroy() with destroyProcessTree(process) on the same line. The two changes compose: tree destroy for the TERM step, then this bounded wait and forced fallback. Whichever merges second needs a trivial rebase on that line.

Tests. StdioClientTransportTerminationTests (in mcp-test, next to StdioClientTransportTests):

  • server that ignores TERM, timeout 1 s: closeGracefully() completes within 10 s and the process is gone (POSIX only; on Windows destroy() is TerminateProcess and cannot be ignored);
  • server that exits on TERM, timeout 30 s: completes within 10 s, so the timeout is not waited out;
  • builder: default 10 s; null, zero, negative and overflowing values rejected.

Without the change, the first scenario does not complete within 15 s (the reproduction posted on the issue).

Validation (Linux, JDK 25):

  • ./mvnw install -pl mcp-core: 505 tests, 0 failures, javaformat validate ok.
  • ./mvnw test -pl mcp-test -Dtest='Stdio*': all pass. The surefire "kill self fork JVM ... 30 seconds after System.exit(0)" message also appears on main without this change.

…xtprotocol#937)

closeGracefully() sent TERM and then waited on Process#onExit with no
limit, so a server that ignores TERM kept the transport from closing
and left the process running.

- ServerParameters gets processTerminationTimeout (default 10 s,
  must be positive and fit in Long.MAX_VALUE nanoseconds)
- After TERM, closeGracefully() waits up to that timeout, then calls
  destroyForcibly() and waits for the exit
- StdioClientTransportTerminationTests covers a server that ignores
  TERM, one that exits on TERM, and the builder validation

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StdioClientTransport should support bounded child process termination

1 participant