You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
MCP server mode has no idle timeout mechanism. When MCP host processes (Claude Code, Codex CLI, VS Code) remain running but stop making requests, their associated xcodebuildmcp MCP server processes stay alive indefinitely, consuming 60-145 MB RSS each. In multi-session setups, this can accumulate to 20+ idle processes over days.
Evidence (v2.3.2)
On macOS with Claude Code, Codex CLI, and VS Code Codex extension all configured with xcodebuildmcp:
$ ps aux | grep xcodebuildmcp | wc -l
21
$ ps -axo pid=,ppid=,etime=,rss=,command= | grep xcodebuildmcp
PID PPID ELAPSED RSS COMMAND
...all with living parents (ppid != 1), ages 30min to 2 days, RSS 60-145 MB each...
None are orphaned -- all have living parent processes. They are simply idle, with no requests coming from the host sessions.
Code comparison
Daemon mode (src/daemon.ts:288-314) has a full idle-shutdown loop:
setInterval checks lastActivityAt, inFlightRequests, and hasActiveRuntimeSessions()
Configurable via env var, disable by setting to 0
MCP mode (src/server/start-mcp-server.ts) has no equivalent mechanism:
Shutdown triggers are only: stdin-end, stdin-close, stdout-error, stderr-error, SIGINT, SIGTERM, uncaught-exception, unhandled-rejection (see src/server/mcp-lifecycle.ts:115-128)
No timer-based idle detection
No activity tracking for shutdown decisions
The activeOperationCount tracking infrastructure already exists and is imported in the MCP path (src/server/mcp-lifecycle.ts:8 imports getDaemonActivitySnapshot from src/daemon/activity-registry.ts), but it is only used for telemetry/anomaly detection -- not for lifecycle decisions.
Proposal
Add an opt-in idle timeout for MCP server mode:
New env var: XCODEBUILDMCP_MCP_IDLE_TIMEOUT_MS -- default 0 (disabled), preserving current behavior
When set to a positive value, a setInterval (.unref() to avoid keeping the event loop alive) checks:
Time since last MCP request completed
activeOperationCount === 0 from the existing activity registry
If idle for longer than the configured timeout, initiate graceful shutdown via the existing lifecycle.shutdown() path
Mirror the daemon implementation pattern from src/daemon.ts:288-314
The activity tracking already exists in the MCP server. The daemon mode idle-shutdown pattern is well-tested. This is primarily wiring the two together behind an opt-in flag.
Risks
Host re-spawn behavior: If the MCP server kills itself while the host is still running, the host must handle the transport disconnect gracefully. Claude Code and Codex CLI both re-spawn MCP servers on disconnect (this is how they already handle the stdin-end case from the MCP server processes orphaned when parent (Claude CLI) dies — 4GB memory leak #273 fix). However, other MCP clients may not. The opt-in default (disabled) avoids this risk entirely.
False-positive idle detection: A host could pause between requests longer than the timeout (e.g., waiting for user input during a long coding session). Users would need to choose an appropriate timeout value. This is the same trade-off the daemon mode already accepts.
Interaction with long-running operations: The activeOperationCount check ensures the server never shuts down mid-operation, but edge cases with streaming responses or tool callbacks could theoretically show zero active operations during a logical session. The existing daemon mode has the same constraint.
Summary
MCP server mode has no idle timeout mechanism. When MCP host processes (Claude Code, Codex CLI, VS Code) remain running but stop making requests, their associated
xcodebuildmcpMCP server processes stay alive indefinitely, consuming 60-145 MB RSS each. In multi-session setups, this can accumulate to 20+ idle processes over days.Evidence (v2.3.2)
On macOS with Claude Code, Codex CLI, and VS Code Codex extension all configured with xcodebuildmcp:
None are orphaned -- all have living parent processes. They are simply idle, with no requests coming from the host sessions.
Code comparison
Daemon mode (
src/daemon.ts:288-314) has a full idle-shutdown loop:resolveDaemonIdleTimeoutMs()readsXCODEBUILDMCP_DAEMON_IDLE_TIMEOUT_MS(default 10 min)setIntervalcheckslastActivityAt,inFlightRequests, andhasActiveRuntimeSessions()0MCP mode (
src/server/start-mcp-server.ts) has no equivalent mechanism:stdin-end,stdin-close,stdout-error,stderr-error,SIGINT,SIGTERM,uncaught-exception,unhandled-rejection(seesrc/server/mcp-lifecycle.ts:115-128)The
activeOperationCounttracking infrastructure already exists and is imported in the MCP path (src/server/mcp-lifecycle.ts:8importsgetDaemonActivitySnapshotfromsrc/daemon/activity-registry.ts), but it is only used for telemetry/anomaly detection -- not for lifecycle decisions.Proposal
Add an opt-in idle timeout for MCP server mode:
XCODEBUILDMCP_MCP_IDLE_TIMEOUT_MS-- default0(disabled), preserving current behaviorsetInterval(.unref()to avoid keeping the event loop alive) checks:activeOperationCount === 0from the existing activity registrylifecycle.shutdown()pathsrc/daemon.ts:288-314The activity tracking already exists in the MCP server. The daemon mode idle-shutdown pattern is well-tested. This is primarily wiring the two together behind an opt-in flag.
Risks
Host re-spawn behavior: If the MCP server kills itself while the host is still running, the host must handle the transport disconnect gracefully. Claude Code and Codex CLI both re-spawn MCP servers on disconnect (this is how they already handle the
stdin-endcase from the MCP server processes orphaned when parent (Claude CLI) dies — 4GB memory leak #273 fix). However, other MCP clients may not. The opt-in default (disabled) avoids this risk entirely.False-positive idle detection: A host could pause between requests longer than the timeout (e.g., waiting for user input during a long coding session). Users would need to choose an appropriate timeout value. This is the same trade-off the daemon mode already accepts.
Interaction with long-running operations: The
activeOperationCountcheck ensures the server never shuts down mid-operation, but edge cases with streaming responses or tool callbacks could theoretically show zero active operations during a logical session. The existing daemon mode has the same constraint.Related
src/daemon/idle-shutdown.tsprovides the pattern to follow