Closes stdin, waits out the grace period, then kills the whole tree. The escalation order is spec text; timeouts and tree-wide scope are SDK policy: https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#shutdown
(process: ServerProcess)
| 246 | |
| 247 | |
| 248 | async def _stop_server_process(process: ServerProcess) -> None: |
| 249 | """Closes stdin, waits out the grace period, then kills the whole tree. |
| 250 | |
| 251 | The escalation order is spec text; timeouts and tree-wide scope are SDK policy: |
| 252 | https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#shutdown |
| 253 | """ |
| 254 | assert process.stdin and process.stdout, "server process is spawned with pipes" |
| 255 | |
| 256 | await _close_pipe(process.stdin) |
| 257 | if not await _wait_for_process_exit(process, PROCESS_TERMINATION_TIMEOUT): |
| 258 | await _terminate_process_tree(process) |
| 259 | # Until the event loop observes the death, the transport cannot close. |
| 260 | if not await _wait_for_process_exit(process, _KILL_REAP_TIMEOUT): |
| 261 | logger.warning("MCP server process %d is still alive after the kill escalation; abandoning it", process.pid) |
| 262 | |
| 263 | # Reaps surviving Windows job members now, not at GC; no-op on POSIX. |
| 264 | close_process_job(process) |
| 265 | # A kill survivor can hold the stdout pipe open; poison the reader anyway. |
| 266 | await _close_pipe(process.stdout) |
| 267 | _close_subprocess_transport(process) |
| 268 | |
| 269 | |
| 270 | async def _close_pipe(stream: AsyncResource) -> None: |
no test coverage detected