Stdio MCP server that exits mid-session: lazy reconnect serves one call, then tools are wrongly deregistered while respawned process leaks
Summary
When a stdio MCP server process exits mid-session (deliberately, via os._exit(0) in a tool), Claude Code lazily respawns and reconnects it on the next tool call — which works, and the call succeeds against the fresh process. But immediately afterwards the harness deregisters all of that server's tools anyway ("No such tool available"), even though the freshly-respawned server process is alive and connected per the debug logs. Nothing in-session recovers it (waiting, new user messages, /model) — only /mcp reconnect or a session resume. Each occurrence also leaks the respawned server process, which stays running, reparented to init.
This also contradicts the docs, which say stdio servers are never reconnected automatically ("Stdio servers are local processes and are not reconnected automatically", https://code.claude.com/docs/en/mcp) — in practice there is a lazy reconnect, but its bookkeeping then wrongly tears the tools down.
Environment
- Claude Code 2.1.201, macOS 26.2 (arm64)
- stdio MCP server written with the Python MCP SDK (
mcp.server.fastmcp.FastMCP)
Background
The server exposes a persistent Python interpreter as an execute tool. It also has an exit tool that intentionally terminates the server process with os._exit(0), so the next call gets a genuinely fresh interpreter. The tool's own call is expected to fail with "Connection closed" — the interesting part is what the client does next.
Steps to reproduce
Minimal server (crashy.py):
from mcp.server.fastmcp import FastMCP
import os
mcp = FastMCP("crashy")
@mcp.tool()
def ping() -> str:
return f"pid {os.getpid()}"
@mcp.tool()
def exit() -> str:
os._exit(0)
mcp.run()
Register as a stdio server, then in a session ask Claude to:
- Call
ping→ works, note pid. - Call
exit→ returnsMCP error -32000: Connection closed(expected). - Call
pingagain → works: the client spawns a new server process (~320 ms) and the call returns a new pid. So far, ideal behavior. - Call
pingonce more (or check the tool registry) →Error: No such tool available: mcp__crashy__ping. The server's tools are gone from the registry, and (with tool search / deferred tools enabled) ToolSearch finds nocrashytools either. Meanwhilepsshows the respawned server process from step 3 still alive and idle, and its connection log shows no disconnect.
Debug log
From ~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/…jsonl (paths/session id redacted; the server here is the real one, named clikernel, whose execute/exit tools correspond to ping/exit above):
{"debug":"Calling MCP tool: execute","timestamp":"2026-07-05T03:40:26.572Z"}
{"debug":"Tool 'execute' completed successfully in 3ms","timestamp":"2026-07-05T03:40:26.575Z"}
{"debug":"Calling MCP tool: exit","timestamp":"2026-07-05T03:40:38.180Z"}
{"debug":"Tool 'exit' failed after 0s: MCP error -32000: Connection closed","timestamp":"2026-07-05T03:40:38.204Z"}
{"debug":"Starting connection with timeout of 30000ms","timestamp":"2026-07-05T03:40:43.025Z"}
{"debug":"Successfully connected (transport: stdio) in 319ms","timestamp":"2026-07-05T03:40:43.344Z"}
{"debug":"Connection established with capabilities: {\"hasTools\":true,\"hasPrompts\":true,\"hasResources\":true,\"hasResourceSubscribe\":false,\"serverVersion\":{\"name\":\"clikernel\",\"version\":\"1.28.1\"}}","timestamp":"2026-07-05T03:40:43.344Z"}
{"debug":"Calling MCP tool: execute","timestamp":"2026-07-05T03:40:43.344Z"}
{"debug":"Tool 'execute' completed successfully in 3ms","timestamp":"2026-07-05T03:40:43.347Z"}
Nothing further is logged: no disconnect, no error — yet after that last successful call the tools were deregistered for the rest of the session (verified over ~5 minutes, across new user messages and a /model invocation). The respawned process was still alive throughout.
Process leak
After an earlier occurrence of the same sequence the previous day, two clikernel-mcp server processes were still running a day later, reparented to pid 1. During this reproduction a transient extra spawn was also observed alongside the one that served the reconnected call, suggesting the deregistration may come from a race between two competing reconnect paths, with the loser's failure clobbering the winner's registration.
Expected behavior
Any of these would be consistent; the current mix is the worst of all worlds:
- If lazy reconnect is intended: keep the tools registered after a successful reconnect, and don't leak the old/new processes.
- If stdio servers are meant to never reconnect (as documented): fail the step-3 call cleanly and mark the server failed, rather than spawning a process that answers one call and is then orphaned.
Actual behavior
Reconnect succeeds and serves one call; tools are then deregistered while the healthy respawned process is left running, unreachable, until manual /mcp reconnect or session resume; server processes leak.
---
Filed by Claude (Opus 4.8) via Claude Code at the request of, and on behalf of, Jeremy Howard (@jph00), who observed and helped debug the issue.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗