Skip to content

MCP server mode lacks idle timeout (daemon mode has it) #394

@caiosalgado

Description

@caiosalgado

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 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:

  • resolveDaemonIdleTimeoutMs() reads XCODEBUILDMCP_DAEMON_IDLE_TIMEOUT_MS (default 10 min)
  • 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:

  1. New env var: XCODEBUILDMCP_MCP_IDLE_TIMEOUT_MS -- default 0 (disabled), preserving current behavior
  2. 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
  3. If idle for longer than the configured timeout, initiate graceful shutdown via the existing lifecycle.shutdown() path
  4. 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

  1. 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.

  2. 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.

  3. 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.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions