Skip to content

Build hangs forever at "Planning" — xcodebuild pipe deadlock on stdout write(), amplified by stale MCP server instances #492

Description

@WolfTheDesigner

Summary

Local builds via XcodeBuildMCP (build_sim / build_device / test_sim, etc.) intermittently hang forever at the "Planning" phase and never reach compilation. The MCP tool call has no self-timeout, so the agent session wedges until the process is killed or the machine is restarted.

Root cause (stack-confirmed): pipe deadlockxcodebuild blocks in write() on stdout while flooding destination-resolution warnings during planning. This is amplified by stale / multiple xcodebuildmcp MCP server processes left alive by previous agent sessions.

This is not a compile, SPM, or project-configuration failure. Healthy -describeAllArchivableProducts normally returns in ~2s; hung processes sat at 0% CPU for 15+ minutes with no progress.

Environment

  • macOS (Apple Silicon)
  • Xcode 26.x (observed with iPhoneOS 26.5 SDK)
  • XcodeBuildMCP via npm exec xcodebuildmcp@latest mcp / Homebrew install (observed on 2.6.x; please confirm whether 2.7.0 changes this path)
  • Multiple AI coding agents / terminal sessions starting independent MCP servers against the same project over a workday

Reproduction (intermittent)

  1. Use XcodeBuildMCP from an agent client for device or simulator builds over several sessions (or leave old sessions open).
  2. Observe multiple xcodebuildmcp / node … xcodebuildmcp mcp processes remaining alive from prior sessions (ps shows several start times).
  3. Invoke build_device or build_sim (or equivalent CLI path that goes through the same executor).
  4. Build log freezes at planning, often right after target listing / CreateBuildDescription / external tool probes (e.g. clang -v -E -dM …).
  5. MCP tool call never returns.

Harder to reproduce cleanly on a fresh reboot with a single MCP instance; much easier when stale wrappers accumulate.

Evidence

1. Stack sample of deadlocked xcodebuild

Main thread blocked writing planning / destination-resolution log output to stdout:

_describeAllArchivableProductsAndExit()
 └ AllSchemesDescriptor.describe
   └ ProjectDescriptor._representativeDestinationInfos
     └ IDERunDestinationManager runDestinationsForScheme…
       └ …runDestinationsForScheme….cold.5
         └ DVTConsoleLogger logMessage
           └ fprintf → __swrite → __write_nocancel   ← BLOCKED writing to stdout

A secondary thread sits in DTDeviceKit startServiceBrowsers (physical-device Bonjour / device discovery), which is a plausible source of the destination-resolution warning flood that fills the pipe.

The hung process was effectively xcodebuild -describeAllArchivableProducts (or an equivalent planning path), sleeping for minutes with no child processes. A healthy run of that path returns in ~2s.

2. Frozen build log (same freeze point on retries)

Log freezes at planning after target listing, for example:

    Target 'App_App' in project 'App' (no dependencies)
    Target '… Notifications' in project '…' (no dependencies)
GatherProvisioningInputs
CreateBuildDescription
ExecuteExternalTool …/swiftc --version
ExecuteExternalTool …/actool --version --output-format xml1
ExecuteExternalTool …/clang -v -E -dM -arch arm64 -isysroot …/iPhoneOS….sdk -x objective-c -c /dev/null
   ← FROZEN HERE

Child clang -v -E -dM … capability probes can appear hung at 0% CPU. That is a symptom, not the root cause: probes stall because the parent xcodebuild main thread is blocked in write(). Killing only the probes does not clear the hang; the parent stays wedged. Re-launching freezes at the same line.

3. Live process snapshot pattern (multi-wrapper amplifier)

Typical bad state (times illustrative):

PID    STARTED     ELAPSED   %CPU  COMMAND
…      13:57       ~1:50     0.0   npm exec xcodebuildmcp@latest mcp     ← stale wrapper gen 1
…      13:57       ~1:50     0.0   node …/xcodebuildmcp mcp              ← stale wrapper gen 1
…      15:31       ~16 min   0.0   xcodebuild -project … -scheme …
                                     -destination generic/platform=iOS
                                     -collect-test-diagnostics never
                                     -derivedDataPath … build            ← deadlocked MCP-spawned build
…      15:32       ~15 min   0.0   npm exec xcodebuildmcp@latest mcp     ← wrapper gen 2
…      15:32       ~15 min   0.0   node …/xcodebuildmcp mcp              ← wrapper gen 2

Multiple MCP server generations coexist and compete over the same project / DerivedData / device discovery. Full Mac restart is the most reliable clear when several wrappers + a deadlocked xcodebuild have accumulated.

4. Workaround that avoids the hang

Running the same build from a terminal with an active drain does not deadlock:

xcodebuild … 2>&1 | tee /path/to/build.log

tee keeps reading the pipe. Direct CLI without a non-draining parent is also much safer than a wedged MCP capture path.

Other mitigations that reduce incidence:

  • Kill stale xcodebuildmcp / related node processes before building.
  • Prefer a single MCP client session / single server instance per machine when possible.
  • Full reboot clears deadlocked xcodebuild + orphaned wrappers.

Root cause analysis (from runtime stacks + package source)

A. Pipe deadlock

During planning, xcodebuild enumerates schemes / run destinations and can flood stdout with destination-resolution warnings (device discovery / DTDeviceKit path). If the process that owns the stdout pipe stops draining (or drains too slowly relative to the flood), the kernel pipe buffer fills (~64 KB) and xcodebuild's write() blocks forever. Planning never completes.

B. How XcodeBuildMCP captures output (relevant implementation details)

In the installed package’s command executor (defaultExecutor):

  • Spawns with stdio: ["ignore", "pipe", "pipe"].
  • Attaches data listeners that:
    • append to in-memory strings (stdout += chunk / stderr += chunk) with no max size cap in the released code path we inspected;
    • forward chunks to pipeline handlers.
  • Build pipeline log capture uses fs.writeSync per chunk (sync disk I/O on the event loop).
  • There is no build-level timeout / watchdog that kills a stalled xcodebuild and returns an error to the MCP client.

If the Node process crashes mid-stream (e.g. huge output → string length limits — see also unmerged direction around large-output handling), freezes the event loop, or is one of several competing/orphaned MCP servers, the child can be left with an undrained or unhealthy pipe and block forever.

C. Amplifier: orphaned / multiple MCP servers

Separate issue class (orphaned MCP after parent agent exit, especially under npm exec) means multiple xcodebuildmcp processes stay alive for hours. That matches the multi-generation snapshot above and makes pipe + DerivedData + device-discovery contention much more likely. Idle-timeout on the MCP server (when enabled) does not help a hung in-flight build tool call, because that request is still considered active.

D. Misleading secondary symptom

Hung clang -v -E -dM probes look like known SwiftBuild planning deadlocks, but here parent write() blockage explains the freeze: children wait on a wedged parent.

Impact

  • Agent tool calls (build_* / test_*) block indefinitely → whole coding session unusable.
  • Users learn to avoid XcodeBuildMCP and fall back to raw xcodebuild | tee.
  • System can accumulate multiple multi‑GB orphaned MCP servers + stuck xcodebuild / SWBBuildService processes until manual kill or reboot.

Proposed fixes

Ordered by leverage:

  1. Build / exec watchdog (highest priority)
    If xcodebuild produces no new stdout/stderr (or no parsed stage progress) for N minutes, kill the process group and return a structured MCP error. Never leave the tool call open forever. Optionally make N configurable (related prior ask: custom build timeout).

  2. Always-safe drain

    • Prefer async file stream (or OS-level redirect) for full logs instead of sync writeSync on every chunk.
    • Cap in-memory accumulation (maxOutputBytes / Buffer[] + truncate) so huge floods cannot crash the MCP process mid-read (related PR discussion around preventing RangeError: Invalid string length).
    • Consider piping planning-heavy discovery through a path that always drains even if the MCP client disconnects.
  3. Single-instance / peer hygiene

    • Detect other live xcodebuildmcp mcp processes (lifecycle already samples peers / peer-count-high).
    • On start: warn hard, refuse second instance per user/workspace, or offer kill-stale policy.
    • Parent/ppid watchdog so npm exec wrappers don’t leave orphaned MCP servers after the agent dies.
  4. Reduce the warning flood that fills the pipe

    • Prefer explicit destinations when known; avoid broad device destination enumeration when not needed.
    • Where safe, quiet / filter destination-resolution spam during planning.
    • Document that device builds with many paired devices increase flood risk.
  5. Client-visible hang recovery

    • Document recommended client tool_timeout and ensure server-side kill still runs so orphans don’t outlive the client timeout.
    • Doctor / diagnostics: report multi-instance peers, long-running xcodebuild children, and “possible pipe stall” guidance.

Expected outcome

  • A stalled planning/xcodebuild cannot wedge an agent forever.
  • Multi-session days don’t leave N competing MCP servers + deadlocked builds.
  • Even if Apple’s planning path floods stdout, the wrapper keeps draining (or times out cleanly).

Related upstream issues / PRs (for context, not duplicates)

Happy to provide more stack samples, sample/spindump artifacts, or a minimal project repro if useful. I can also test patches against a multi-wrapper stress scenario.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions