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

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions