`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: ```java 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: 1. Stop accepting new messages. 2. Complete the transport sinks. 3. Send graceful termination with `process.destroy()`. 4. Wait up to a configurable timeout. 5. If the process is still alive, call `process.destroyForcibly()`. 6. 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: ```java 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: 1. `closeGracefully()` completes within the configured timeout plus a small buffer. 2. The child process is no longer alive after `closeGracefully()` completes. 3. The transport schedulers and sinks are cleaned up.