-
-
Notifications
You must be signed in to change notification settings - Fork 222
Description
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:
- The
npm execwrapper becomes orphaned (reparented to launchd/PID 1) - The wrapper doesn't close its stdin pipe to the actual xcodebuildmcp process
- The MCP server never receives the stdin end/close event
- 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 thisThis 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