Summary
On a cold start, the stdio transport comes up fast (initialize responds in well under a second), but the first tools/list response is delayed ~10–17s. During that window the server is up and healthy, but any MCP client that uses a short-timeout health probe for tools/list (e.g. Claude Code's claude mcp list) reports the server as Connected · tools fetch failed, even though in-session tool calls work fine once the client's longer startup timeout has elapsed.
The delay appears to come from the startup lifecycle snapshot being awaited on the critical path after the transport is running but before the server settles into steady state.
Environment
- XcodeBuildMCP: 2.6.2 (latest published; behaviour re-checked against
main HEAD — the relevant code is unchanged there)
- Transport: stdio (
xcodebuildmcp mcp)
- Host: macOS (Apple Silicon), Xcode 27
- Client: Claude Code (short
tools/list health probe); also reproducible by timing a raw tools/list over stdio
Observed behaviour / timings
Server running on stdio logged at ~385ms
initialize round-trips in ~0.8s
- First
tools/list returns after ~10–17s
- In-session tool calls (
list_sims, builds, etc.) all work — this is purely a first-response latency issue, so it presents as cosmetic but it makes the server look broken to any health-probe client.
Where it comes from (from source, main HEAD)
src/server/start-mcp-server.ts starts the stdio transport, marks the lifecycle running, and then awaits the startup snapshot on the critical path:
// src/server/start-mcp-server.ts (main)
lifecycle.markPhase('starting-stdio-transport');
await startServer(server, { /* ... */ }); // transport is now up
// ...
lifecycle.markPhase('running');
const startupSnapshot = await lifecycle.getSnapshot(); // <-- awaited before the server settles
log('info', `[mcp-lifecycle] start ${JSON.stringify(startupSnapshot)}`);
// ...
lifecycle.markPhase('deferred-initialization');
void bootstrap.runDeferredInitialization({ /* ... */ }); // <-- already fire-and-forget
lifecycle.getSnapshot() (in src/server/mcp-lifecycle.ts) gathers, among other things, the active simulator os_log sessions (listActiveSimulatorLaunchOsLogSessions()) and a peer-process sample (matchingMcpProcessCount). That snapshot is awaited synchronously in the startup sequence, so the server does not become responsive to the first tools/list until it resolves.
Notably, a runDeferredInitialization(...) fire-and-forget hook already exists immediately below the getSnapshot() call — the startup snapshot looks like a natural fit for the same deferred path.
What I ruled out
- Peer-count (
matchingMcpProcessCount) sampling is not the cause. The [mcp-lifecycle] startup anomalies: peer-count-high warning is a red herring: reaping the leaked peer processes down to a normal count did not change the ~10s. So the cost is in the snapshot's other work (the simulator os_log session enumeration path), not the peer sample.
- Not a client-side timeout knob. The delay is server-side; raising the client MCP timeout does not make
tools/list return faster — it only lets the (successful) slow response through.
Suggested fix
Move the startup getSnapshot() (and specifically the simulator os_log session enumeration it performs) off the startup critical path so the first tools/list is answered immediately — e.g. make the start-time snapshot fire-and-forget (void lifecycle.getSnapshot().then(...)) or fold it into the existing runDeferredInitialization(...) block just below it. The telemetry/anomaly logging it feeds is not something the first tools/list needs to block on.
Repro sketch
xcodebuildmcp mcp over stdio.
- Send
initialize, then immediately tools/list; time the tools/list response.
- Observe
initialize fast, tools/list ~10s+ on a cold start.
Happy to test a patch or provide more startup profiling if useful. Thanks for the great tool.
Summary
On a cold start, the stdio transport comes up fast (
initializeresponds in well under a second), but the firsttools/listresponse is delayed ~10–17s. During that window the server is up and healthy, but any MCP client that uses a short-timeout health probe fortools/list(e.g. Claude Code'sclaude mcp list) reports the server asConnected · tools fetch failed, even though in-session tool calls work fine once the client's longer startup timeout has elapsed.The delay appears to come from the startup lifecycle snapshot being
awaited on the critical path after the transport is running but before the server settles into steady state.Environment
mainHEAD — the relevant code is unchanged there)xcodebuildmcp mcp)tools/listhealth probe); also reproducible by timing a rawtools/listover stdioObserved behaviour / timings
Server running on stdiologged at ~385msinitializeround-trips in ~0.8stools/listreturns after ~10–17slist_sims, builds, etc.) all work — this is purely a first-response latency issue, so it presents as cosmetic but it makes the server look broken to any health-probe client.Where it comes from (from source,
mainHEAD)src/server/start-mcp-server.tsstarts the stdio transport, marks the lifecyclerunning, and then awaits the startup snapshot on the critical path:lifecycle.getSnapshot()(insrc/server/mcp-lifecycle.ts) gathers, among other things, the active simulator os_log sessions (listActiveSimulatorLaunchOsLogSessions()) and a peer-process sample (matchingMcpProcessCount). That snapshot isawaited synchronously in the startup sequence, so the server does not become responsive to the firsttools/listuntil it resolves.Notably, a
runDeferredInitialization(...)fire-and-forget hook already exists immediately below thegetSnapshot()call — the startup snapshot looks like a natural fit for the same deferred path.What I ruled out
matchingMcpProcessCount) sampling is not the cause. The[mcp-lifecycle] startup anomalies: peer-count-highwarning is a red herring: reaping the leaked peer processes down to a normal count did not change the ~10s. So the cost is in the snapshot's other work (the simulator os_log session enumeration path), not the peer sample.tools/listreturn faster — it only lets the (successful) slow response through.Suggested fix
Move the startup
getSnapshot()(and specifically the simulator os_log session enumeration it performs) off the startup critical path so the firsttools/listis answered immediately — e.g. make the start-time snapshot fire-and-forget (void lifecycle.getSnapshot().then(...)) or fold it into the existingrunDeferredInitialization(...)block just below it. The telemetry/anomaly logging it feeds is not something the firsttools/listneeds to block on.Repro sketch
xcodebuildmcp mcpover stdio.initialize, then immediatelytools/list; time thetools/listresponse.initializefast,tools/list~10s+ on a cold start.Happy to test a patch or provide more startup profiling if useful. Thanks for the great tool.