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 deadlock — xcodebuild 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)
- Use XcodeBuildMCP from an agent client for device or simulator builds over several sessions (or leave old sessions open).
- Observe multiple
xcodebuildmcp / node … xcodebuildmcp mcp processes remaining alive from prior sessions (ps shows several start times).
- Invoke
build_device or build_sim (or equivalent CLI path that goes through the same executor).
- Build log freezes at planning, often right after target listing /
CreateBuildDescription / external tool probes (e.g. clang -v -E -dM …).
- 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:
-
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).
-
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.
-
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.
-
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.
-
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.
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 deadlock —
xcodebuildblocks inwrite()on stdout while flooding destination-resolution warnings during planning. This is amplified by stale / multiplexcodebuildmcpMCP server processes left alive by previous agent sessions.This is not a compile, SPM, or project-configuration failure. Healthy
-describeAllArchivableProductsnormally returns in ~2s; hung processes sat at 0% CPU for 15+ minutes with no progress.Environment
npm exec xcodebuildmcp@latest mcp/ Homebrew install (observed on 2.6.x; please confirm whether 2.7.0 changes this path)Reproduction (intermittent)
xcodebuildmcp/node … xcodebuildmcp mcpprocesses remaining alive from prior sessions (psshows several start times).build_deviceorbuild_sim(or equivalent CLI path that goes through the same executor).CreateBuildDescription/ external tool probes (e.g.clang -v -E -dM …).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
xcodebuildMain thread blocked writing planning / destination-resolution log output 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:
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 parentxcodebuildmain thread is blocked inwrite(). 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):
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
xcodebuildhave accumulated.4. Workaround that avoids the hang
Running the same build from a terminal with an active drain does not deadlock:
teekeeps 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:
xcodebuildmcp/ related node processes before building.xcodebuild+ orphaned wrappers.Root cause analysis (from runtime stacks + package source)
A. Pipe deadlock
During planning,
xcodebuildenumerates schemes / run destinations and can flood stdout with destination-resolution warnings (device discovery /DTDeviceKitpath). 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) andxcodebuild'swrite()blocks forever. Planning never completes.B. How XcodeBuildMCP captures output (relevant implementation details)
In the installed package’s command executor (
defaultExecutor):stdio: ["ignore", "pipe", "pipe"].datalisteners that:stdout += chunk/stderr += chunk) with no max size cap in the released code path we inspected;fs.writeSyncper chunk (sync disk I/O on the event loop).xcodebuildand 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 multiplexcodebuildmcpprocesses 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 -dMprobes look like known SwiftBuild planning deadlocks, but here parentwrite()blockage explains the freeze: children wait on a wedged parent.Impact
build_*/test_*) block indefinitely → whole coding session unusable.xcodebuild | tee.xcodebuild/SWBBuildServiceprocesses until manual kill or reboot.Proposed fixes
Ordered by leverage:
Build / exec watchdog (highest priority)
If
xcodebuildproduces 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).Always-safe drain
writeSyncon every chunk.maxOutputBytes/ Buffer[] + truncate) so huge floods cannot crash the MCP process mid-read (related PR discussion around preventingRangeError: Invalid string length).Single-instance / peer hygiene
xcodebuildmcp mcpprocesses (lifecycle already samples peers /peer-count-high).npm execwrappers don’t leave orphaned MCP servers after the agent dies.Reduce the warning flood that fills the pipe
Client-visible hang recovery
tool_timeoutand ensure server-side kill still runs so orphans don’t outlive the client timeout.xcodebuildchildren, and “possible pipe stall” guidance.Expected outcome
xcodebuildcannot wedge an agent forever.Related upstream issues / PRs (for context, not duplicates)
Happy to provide more stack samples,
sample/spindumpartifacts, or a minimal project repro if useful. I can also test patches against a multi-wrapper stress scenario.