Skip to content

MCP server processes orphaned when parent (Claude CLI) dies — 4GB memory leak #273

@dalegass

Description

@dalegass

Problem

When XcodeBuildMCP is launched as an MCP server via npm exec xcodebuildmcp@latest mcp (as Claude Code does), the process persists indefinitely after the parent process exits. Each orphaned instance consumes up to 4GB of resident memory.

This is especially painful when running multiple Claude Code sessions — orphaned xcodebuildmcp processes accumulate and grind the system to a halt.

$ ps aux | grep xcodebuild
dale  24113 352.3 23.5 450778096 3945760  ??  R  3:59PM  1:15.03 node ...xcodebuildmcp mcp
dale  24114   0.0  0.3 446631664  48928   ??  S  3:59PM  0:00.84 node ...xcodebuildmcp mcp
dale  24053   0.0  0.3 435888672  47712   ??  S  3:59PM  0:00.63 npm exec xcodebuildmcp@latest mcp
dale  24052   0.0  0.3 435888944  47712   ??  S  3:59PM  0:00.64 npm exec xcodebuildmcp@latest mcp

Root Cause

start-mcp-server.js relies on stdin end/close events to detect parent death (lines 99–104). However, when spawned through npm exec, there's an intermediate node wrapper process. When the parent (Claude CLI) dies:

  1. The npm exec wrapper becomes orphaned (reparented to launchd/PID 1)
  2. The wrapper doesn't close its stdin pipe to the actual xcodebuildmcp process
  3. The MCP server never receives the stdin end/close event
  4. The process runs forever with no work to do

Additionally, unlike daemon mode (which has idle-shutdown.js with a 10-minute timeout), the MCP server mode has no idle timeout at all.

Suggested Fix

Add a parent-PID watchdog to start-mcp-server.js. When the parent process dies, macOS/Linux reparents the child to init/launchd (PID 1). Polling process.ppid detects this:

--- a/src/server/start-mcp-server.ts
+++ b/src/server/start-mcp-server.ts
@@ in startMcpServer(), after enrichSentryContext() and before shuttingDown declaration
+    // Parent-process watchdog: detect orphaned process when parent dies.
+    // When the parent (Claude CLI / npm exec) exits, the OS reparents us to
+    // init/launchd (PID 1). Poll every 5s and shut down if that happens.
+    const originalPpid = process.ppid;
+    const ppidWatchdog = setInterval(() => {
+      if (process.ppid !== originalPpid) {
+        clearInterval(ppidWatchdog);
+        log("info", `Parent process died (was ${originalPpid}, now ${process.ppid}); shutting down`);
+        void shutdown("parent-died");
+      }
+    }, 5000);
+    ppidWatchdog.unref(); // Don't keep the event loop alive just for this

This is 8 lines, zero dependencies, works on macOS and Linux, and has no effect during normal operation.

Environment

  • macOS 15 (Darwin 25.3.0), Apple Silicon
  • XcodeBuildMCP 2.2.1
  • Claude Code 2.1.75 (VS Code extension)
  • Launched via: npm exec xcodebuildmcp@latest mcp

🤖 Generated with Claude Code

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions