Repository navigation
StdioClientTransport should support bounded child process termination #937
Description
StdioClientTransport starts and manages a child MCP server process. During graceful shutdown, the SDK transport currently calls process.destroy() and waits for process.onExit().
That works when the child process responds to SIGTERM, but it can hang indefinitely if the child process ignores termination or becomes stuck. Downstream, we had to implement a custom McpClientTransport to ensure proxied MCP server processes are always cleaned up when the parent shuts down.
Current behavior
The current shutdown flow is effectively:
if (this.process != null) {
this.process.destroy();
return Mono.fromFuture(process.onExit());
}There is no configurable timeout and no fallback to destroyForcibly().
Expected behavior
StdioClientTransport.closeGracefully() should support bounded process termination:
- Stop accepting new messages.
- Complete the transport sinks.
- Send graceful termination with
process.destroy(). - Wait up to a configurable timeout.
- If the process is still alive, call
process.destroyForcibly(). - Dispose the transport schedulers.
This guarantees that child MCP server processes do not survive parent shutdown indefinitely.
Proposed fix
Add a configurable process termination timeout to StdioClientTransport, with a sensible default.
Conceptually:
process.destroy();
return Mono.fromFuture(process.onExit())
.timeout(processTerminationTimeout, Mono.defer(() -> {
if (process.isAlive()) {
process.destroyForcibly();
}
return Mono.fromFuture(process.onExit());
}));This could be exposed through the existing ServerParameters builder or a dedicated StdioClientTransport constructor/builder option.
Suggested regression test
Add a test with a child process that ignores SIGTERM or does not exit promptly.
The test should verify that:
closeGracefully()completes within the configured timeout plus a small buffer.- The child process is no longer alive after
closeGracefully()completes. - The transport schedulers and sinks are cleaned up.
Activity
ZYZ666-RGB commentedon Aug 17, 2026
Hi maintainers, I'd like to work on this issue.
I verified that the current
StdioClientTransport.closeGracefully()path can wait indefinitely onprocess.onExit()afterprocess.destroy(), with no forced-termination fallback.I plan to:
destroyForcibly()only if the child is still alive;I will follow the repository's existing builder and configuration conventions and keep the public API change as small as possible. Please let me know if you prefer a particular API location or default timeout.
ZYZ666-RGB commentedon Aug 17, 2026
Thanks for the context. I need to step back from this issue for now so it remains available to other contributors.
Jellyzzzz commentedon Aug 23, 2026
Hi maintainers, I’d like to work on this issue. I’ll first reproduce the current StdioClientTransport.closeGracefully() behavior on main, then keep the change focused on bounded process termination and regression coverage for both cooperative and non-cooperative child processes. I’ll share a PR once I’ve validated the approach.
Kamenalpha commentedon Sep 17, 2026
Hi maintainers 👋 — I'd like to pick this up (the previous offer in August doesn't seem to have resulted in a PR; happy to coordinate if that's changed).
I have a focused implementation ready on top of current
main:processTerminationTimeouttoServerParameters(default 10s), exposed via the builder;StdioClientTransport.closeGracefully(), afterdestroy(), wait bounded by the timeout and fall back todestroyForcibly()when the process is still alive;StdioClientTransportTests: cooperative exit, and forced termination when SIGTERM is ignored.One scope question before I open the PR: would you prefer the knob on
ServerParametersas proposed, or a constructor/builder option directly onStdioClientTransport? I'll open the PR once you confirm the preferred surface.caiocv18 commentedon Oct 19, 2026
Reproduced on current
main(7b9b000, Linux, JDK 25): with a server started assh -c "trap '' TERM; while :; do sleep 1; done",closeGracefully()does not complete within 15 s and the child process stays alive. JUnit reproduction (Windows excluded, sincedestroy()isTerminateProcessthere):@Kamenalpha are you still planning to open the PR you mentioned on 17/09? If not, I can take it:
destroy(), wait up to a configurable timeout, thendestroyForcibly(), with this test turned into a regression test. It would also need to stay compatible with #966, which changes the same method to destroy descendants first.Maintainers: any preference on where the timeout lives,
ServerParameters.Builderor aStdioClientTransportconstructor/builder option?caiocv18 commentedon Oct 28, 2026
Opened #1257 for this, following the surface @Kamenalpha proposed above (
processTerminationTimeoutonServerParameters.Builder, default 10 s, thendestroyForcibly()). It includes the SIGTERM-ignoring reproduction from my earlier comment as a test. @Kamenalpha, if you had a different take on this, happy to compare or fold your changes in.