Claude Desktop: orphaned MCP subprocesses accumulate after 4-min tool-call timeout
Filed at request of Anthropic support (ticket reference on file) who confirmed this is a known Claude Desktop issue and asked for the diagnostic materials to be submitted publicly. This affects Claude Desktop specifically — Claude Code on the same machine does not exhibit the behavior. I realize this tracker is primarily for Claude Code; support redirected me here as the official public intake. Happy to move if there's a dedicated Desktop tracker I missed.
Summary
Claude Desktop does not reap stdio-based MCP server subprocesses when a tool call hits the hardcoded 4-minute timeout. On retry, a fresh subprocess is spawned without cleaning up the previous one. Over time, orphaned processes accumulate and eventually exhaust resources, at which point any MCP tool call hangs until the user fully quits and restarts Desktop.
Environment
- macOS (Apple Silicon), Claude Desktop latest stable
- Same machine also runs Claude Code; both clients configured to use the same MCP server binary via identical config shape
- Example MCP server:
verygoodplugins/whatsapp-mcp— Python stdio server - Reproducible with any blocking stdio MCP server (see minimal repro below, no WhatsApp dependency)
Reproduction
Minimal repro (no external dependencies)
- Save this as
hang-server.py:
#!/usr/bin/env python3
import sys, json, time
def send(msg):
sys.stdout.write(json.dumps(msg) + "\n"); sys.stdout.flush()
for line in sys.stdin:
try: msg = json.loads(line)
except: continue
mid, method = msg.get("id"), msg.get("method", "")
if method == "initialize":
send({"jsonrpc":"2.0","id":mid,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{}},"serverInfo":{"name":"hang-repro","version":"1.0"}}})
elif method == "tools/list":
send({"jsonrpc":"2.0","id":mid,"result":{"tools":[{"name":"trigger_hang","description":"Deliberately blocks","inputSchema":{"type":"object","properties":{}}}]}})
elif method == "tools/call":
time.sleep(600) # block — this is the point
elif method == "notifications/initialized":
pass
else:
send({"jsonrpc":"2.0","id":mid,"error":{"code":-32601,"message":"not found"}})
- Add to
~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"hang-repro": {
"command": "/usr/bin/python3",
"args": ["/absolute/path/to/hang-server.py"]
}
}
}
- Fully quit & restart Claude Desktop
- In a new conversation: "Use the trigger_hang tool"
- Claude invokes the tool. Spinner appears.
- Open Terminal:
ps aux | grep hang-server.py— one Python process, <5s old ✅ - Wait 4 minutes. Claude reports timeout.
ps aux | grep hang-server.py— Python process is still alive, 4+ min old ❌
Expected: Desktop should have killed it.
- Retry the tool in the same or a new conversation.
ps aux | grep hang-server.py— two Python processes now visible. The old orphan plus a fresh one.
Each retry adds another orphan. On Intel or constrained Apple Silicon systems, ~8 accumulated orphans is enough to make any further MCP tool call hang indefinitely due to resource contention.
Evidence from real-world usage
Over 2 days of WhatsApp MCP usage, a local launchd watchdog (pkill -9 on processes matching the MCP server pattern, older than 15 minutes, every 5 minutes) logged 23 separate orphan-kill events. Without the watchdog, Desktop became unusable within hours and required a full Cmd+Q + restart cycle.
Side-by-side comparison on the same machine, same binary, same config shape:
| Metric | Claude Code | Claude Desktop |
|---|---|---|
| Successful MCP tool calls over ~1 week | ~100% | degrades after hours as orphans accumulate |
| Idle Python MCP processes | 0-1 (clean shutdown) | 0-N (orphans persist across sessions) |
| 4-min timeout events | 0 | 23 over 2 days |
| User intervention | none | Cmd+Q + relaunch; or external watchdog |
The Go bridge underlying the WhatsApp MCP server responds to health checks within ~16 ms; the Python MCP server starts cleanly from a terminal; SQLite has no lock conflict. The only variable that differs between working and failing behavior is the client.
Hypotheses (unconfirmed)
Two possible root causes, either would explain the observed pattern:
- Timeout without SIGKILL escalation. Desktop sends SIGTERM on timeout but does not wait for exit and does not escalate to SIGKILL. A Python server blocked in
sys.stdin.readline()may not handle SIGTERM promptly, so the child remains alive. - Retry spawns without reaping. The retry path creates a fresh subprocess without calling
waitpid(or equivalent) on the previous PID. The old PID reference is dropped, leaving the OS with a running orphan.
Suggested fix direction: after SIGTERM on timeout, give the child N seconds to exit; if still alive, SIGKILL; always waitpid before spawning a retry.
Diagnostic materials
A 22 KB zip package is attached to this issue (next comment — GitHub web UI drag-drop). Structure:
| Folder | What |
|---|---|
| 01-watchdog-logs/ | 23 orphan-kill events with PID + age + timestamp |
| 02-claude-desktop-mcp-logs/ | Desktop's MCP log (sanitized — phone numbers and content strings redacted) + main.log MCP-related excerpt |
| 03-watchdog-config/ | launchd plist + shell script of the workaround |
| 04-minimal-repro/ | Self-contained Python hang-server + step-by-step repro instructions |
| 05-cross-client-ab/ | Extracted WhatsApp MCP config snippets from both Code and Desktop showing identical shapes |
Privacy: logs have been sanitized before packaging (phone numbers → [PHONE]@s.whatsapp.net, content strings >20 chars → [REDACTED-]). Watchdog log contains process metadata only.
Workaround currently in use
launchd agent com.local.whatsapp-mcp-watchdog running every 5 min:
pids=$(pgrep -f "whatsapp-mcp-server/.venv/bin/python3 main.py")
for pid in $pids; do
age=$(( $(date +%s) - $(ps -o lstart= -p "$pid" | xargs -I{} date -j -f "%a %b %d %T %Y" {} +%s) ))
[ "$age" -gt 900 ] && kill -9 "$pid"
done
Not a real fix — keeps the machine usable while this is resolved upstream. The same pattern works for any stdio MCP server (adjust the pgrep match).
Support ticket
Anthropic support confirmed this is a known Claude Desktop issue and asked me to file the diagnostics publicly so engineering can triage. Happy to add the ticket reference if useful; also happy to provide longer log windows, unsanitized logs under NDA, or join a debug session.
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗