skip to content

How should a client shut down an MCP stdio server subprocess cleanly?

level: middleimportance: should knowfreq 50%

answer

  1. it is a process operation
  2. the polite signal is end-of-file
  3. then wait, then escalate
  4. in-flight work is not rescued
  5. no session to close since 2026-07-28

basics

~10 s

Close the server's stdin so it sees end-of-file and exits on its own, wait a reasonable period for the process to end, and only then force-terminate it with an operating-system signal.

solid answer

~50 s

Shutdown on the stdio binding is a **process** operation, not a protocol message — there is no goodbye request to send. The client, which owns the subprocess, closes the server's **stdin**; the server sees end-of-file, finishes what it can and exits. The client then waits a reasonable time for the process to terminate, and only if it has not exited does it escalate to an operating-system terminate signal (`SIGTERM`, then `SIGKILL` if the process still refuses to die). Two things follow. First, any request still in flight when stdin closes simply dies unanswered; because MCP is stateless in revision 2026-07-28, the client re-issues it as a **new request with a new JSON-RPC id** against a freshly launched server rather than resuming anything. Second, there is nothing to "log out" of: the 2026-07-28 revision removed the `initialize` handshake and protocol-level sessions, so an open subprocess is not a conversation whose state must be torn down.

code

python · 13 lines
python
import subprocess

proc = subprocess.Popen(["my-mcp-server"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)

proc.stdin.close()
try:
    proc.wait(timeout=5)
except subprocess.TimeoutExpired:
    proc.terminate()
    try:
        proc.wait(timeout=5)
    except subprocess.TimeoutExpired:
        proc.kill()

go deeper

for a junior

Remember the order: close the server's stdin first, wait for it to exit, and only force-terminate if it does not. There is no shutdown message to send.

for a middle

Explain why end-of-file is the signal rather than a protocol message, and what happens to in-flight requests — they die unanswered and must be re-issued as new requests with new ids.

for a senior

Show the operational side: a grace period before SIGTERM and SIGKILL, reaping children so no server is orphaned holding locks, timing out pending requests, and backing off before relaunching a crash-looping server.

for a principal

Own the replacement policy. Statelessness in 2026-07-28 makes servers disposable, so decide when a host restarts a server versus surfacing failure, and how re-issued non-idempotent tool calls are made safe.

## Shutdown belongs to the process layer MCP defines no shutdown request. Under revision 2026-07-28 there is not even a lifecycle handshake to unwind: `initialize` and `notifications/initialized` were removed, protocol-level sessions are gone, and the spec is explicit that "an open connection, such as a STDIO process, is not a conversation or session". Termination is therefore whatever the underlying transport offers, and on stdio that means ordinary subprocess management by the party that spawned the process — the client. ## The prescribed sequence 1. **Close the server's stdin.** This is the polite signal. The server's read on stdin returns end-of-file, which is the unambiguous "no more requests are coming". A well-behaved server stops accepting work, releases resources, flushes and exits. 2. **Wait for the process to exit.** Give it a reasonable grace period — a small number of seconds is typical — and reap the exit status. 3. **Force-terminate if it has not exited.** Send `SIGTERM`; if the process still has not exited after another short wait, send `SIGKILL`. On platforms without POSIX signals the equivalent terminate call applies. The escalation matters: skipping straight to a kill denies the server any chance to flush a partly written file, close a database handle or remove a lock file. ## Why stdin-close rather than a message End-of-file is the one signal both peers get for free and cannot ignore. A shutdown *message* would need the server to be healthy enough to read and dispatch it — exactly the condition you cannot count on when you are trying to stop it. It would also need a rule for what happens to in-flight work, which the protocol deliberately does not specify. EOF sidesteps all of that. The server side has the mirror obligation: it may terminate on its own by closing its stdout and exiting, and the client detects the shutdown by reading EOF on stdout or by observing the process end. ## What happens to in-flight requests Nothing rescues them. When stdin closes or the process exits, any request the client has sent but not yet had answered simply never gets a response. In the 2026-07-28 model that is a deliberately cheap outcome: there is no resumability mechanism (SSE event ids and `Last-Event-ID` replay were removed), and every request is self-contained, so recovery is "launch a server and send the request again as a brand-new request with a new JSON-RPC id". The client must be prepared for the request to be re-executed, which is why non-idempotent tools matter here — re-issuing a payment call after a shutdown is a real business decision, not a transport detail. A client should also fail those pending promises deliberately rather than leaving them hanging forever; a timeout on every request is the usual safety net. ## Common mistakes **Killing the process to cancel one request.** Terminating the subprocess to abandon a single long-running call throws away every other in-flight request as well. Cancellation of a single request is a separate mechanism (`notifications/cancelled`). **Closing stdout instead of stdin.** The client owns the write end of stdin; closing the stream it reads from does not tell the server anything and may leave it writing into a broken pipe. **Killing immediately.** A `SIGKILL` with no grace period gives no chance to release resources and produces flaky cleanup in tests as well as in production. **Never reaping the child.** A client that terminates without waiting leaves zombie processes, or worse, orphaned servers holding file locks. Long-lived hosts that spawn many servers must reap them. **Assuming a session must be closed.** There is nothing session-shaped to close in 2026-07-28. The HTTP `DELETE`-to-terminate flow and the expired-session 404 belonged to the session model that this revision removed; they were never part of stdio in any case. ## Restart and supervision Because statelessness makes servers cheap to replace, a host that detects a crashed or wedged stdio server can simply relaunch it and reissue work. Keep the supervision loop honest, though: an unconditional restart around a server that crashes on startup becomes a spin loop, so back off, and surface the failure to the user rather than hiding it behind endless retries.

  • A request was in flight when you closed stdin. What is the client's recovery?
    It never gets a response, and nothing resumes it. Under 2026-07-28 every request is self-contained and there is no resumability mechanism, so the client fails the pending call and, if it still wants the work done, sends it again as a brand-new request with a new JSON-RPC id against a freshly launched server. Non-idempotent tools make that a judgement call, not an automatic retry.
  • Can the server initiate shutdown, and how does the client notice?
    Yes. A server may exit on its own, closing its stdout as it goes. The client detects it by reading end-of-file on stdout or by observing the child process terminate, and it should then fail every pending request rather than waiting forever. Relaunching is cheap because there is no session state to rebuild.
  • Why not just SIGKILL the subprocess and be done with it?
    Because the server gets no chance to flush buffers, close database handles, or remove lock files, which produces corrupted output and flaky cleanup. The sequence exists to give it an ordered exit path: EOF on stdin first, a grace period, then SIGTERM, and SIGKILL only as the last resort for a process that refuses to exit.

saying these in an interview costs you the question

  • Sends a shutdown or exit request over the protocol
  • Kills the subprocess to cancel a single request
  • Calls SIGKILL immediately with no grace period
  • Thinks a session must be closed before exiting
  • Never reaps the child and leaks orphaned servers

context