Skip to content

iOS Simulator: agent commands stall ~20–30s (sometimes minutes) and freeze the MCP server — devicekit/mobilecli connection handling #358

Description

@TonyDowney

Summary

On an iOS Simulator, sequential mobile-mcp commands that go through the on-device devicekit agent (screenshot, list_elements, click/tap, swipe) exhibit pathologically variable latency — most calls stall for ~20–30 s, a few return sub-second, and some hang indefinitely (observed up to multiple minutes in real use). Because the MCP server has no client-side timeout and resolves the device synchronously, a single stalled command makes the entire MCP server appear frozen to the agent/host (Claude Code, etc.) for the duration.

This is not the large-accessibility-tree timeout from #288 — it reproduces with a bare screenshot on the iOS Home screen (a trivial tree), and it persists after every environmental fix we could think of (full list below).

Environment

mobile-mcp 0.0.59 (latest)
bundled mobilecli 0.3.79 (also tested 0.3.83 — no change)
Device iPhone 16 Pro Simulator, iOS 18.6
Xcode 26.5 (17F42)
macOS 26.5.1 (Apple Silicon, arm64)
Node v25.9.0

Symptom (user-facing)

  • mobile_list_available_devices (no device arg, pure simctl) — always instant.
  • mobile_save_screenshot / mobile_take_screenshot / mobile_list_elements_on_screen / mobile_click_on_screen_at_coordinates (all take a device arg → go through getRobotFromDevicemobilecli → devicekit agent) — routinely stall 20–30 s; intermittently hang indefinitely.

The instant-vs-stall split by whether the tool touches the agent isolates the problem to the mobilecli + devicekit-agent path, not the MCP transport.

Reproduction (isolated to the bundled mobilecli, below the MCP layer)

Boot a single iPhone 16 Pro / iOS 18.6 simulator, then loop screenshots directly against the bundled binary:

BIN=node_modules/mobilecli/bin/mobilecli-darwin-arm64
UDID=<booted iPhone 16 Pro, iOS 18.6>

"$BIN" agent install --device "$UDID"
for i in $(seq 1 10); do
  /usr/bin/time "$BIN" screenshot --device "$UDID" --format png --output /tmp/s_$i.png
  sleep 3
done

Observed (fresh agent install; 45 s hard cap per call; on the Home screen):

agent install exit=0
  call  1: OK     28643 ms
  call  2: OK       943 ms
  call  3: OK      2846 ms
  call  4: OK     29095 ms
  call  5: OK      7644 ms
  call  6: OK     24263 ms
  call  7: OK     20141 ms
  call  8: OK     10060 ms
  call  9: OK       392 ms
  call 10: OK       390 ms

Latency swings from 0.39 s to 29 s on identical, trivial calls. With a tighter cap (25–30 s), the long calls present as outright hangs (and earlier runs with a 60 s cap produced genuine >60 s hangs; one real-world mobile_save_screenshot hung ~17 minutes before being interrupted). In several runs forcibly killing a stalled call appeared to "free" the next one, which is what produced an apparent OK/HUNG alternation at fixed timeouts.

Diagnosis / hypothesis

The ~25–30 s clustering looks like an internal connection/read timeout firing on a stale agent connection: each successful command seems to leave the devicekit agent holding a connection, so the next command's first attempt blocks until a ~25–30 s timeout elapses, then reconnects and succeeds. Occasionally the recovery itself stalls, yielding the multi-minute hangs.

Two things turn this from "slow" into "freezes the whole server":

  1. No client-side timeout on the mobilecli calls — a stall is unbounded.
  2. In server.js getRobotFromDevice, mobilecli.agentStatus / agentInstall use synchronous execFileSync, so a stall blocks the MCP server's event loop entirely (every queued tool call waits).

Ruled out (so this isn't mistaken for a local/setup problem)

Related

Suggested directions

  1. Reuse or cleanly tear down the devicekit-agent connection per command so the next command doesn't block on a stale-connection timeout (the apparent ~25–30 s stall).
  2. Add a client-side timeout to mobilecli invocations in mobile-mcp so a stalled command fails fast instead of wedging the whole MCP server.
  3. Make device resolution async (avoid synchronous execFileSync in getRobotFromDevice) so one stall can't block the event loop for other tools.

Happy to run diagnostics or test a patch against this setup — the repro is consistent here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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