Repository navigation
Description
Summary
When two real iOS devices are used with the same daemon and their agents are started at different times, the second device silently sends all of its commands to the first device's agent. Device info, screenshots, taps and the MJPEG stream for device B actually come from / act on device A until the daemon exits.
Verified on two real USB iPhones (iPhone 11 Pro and iPhone 12 Pro Max) on 1.0.13, and present in 1.0.19 (latest) and on main at 7e9699e.
Steps to reproduce
- Connect two real iOS devices with different screen sizes (375x812 and 428x926 in the test).
- Wait for the daemon to discover both. Call
StartAgenton device A and wait until it succeeds. - Then call
StartAgenton device B. - Ask device B for its device info or a screenshot.
Expected
Device B reports its own size (375x812, 1124x2436 px screenshots).
Actual
Device B reports device A's size (428x926, 1284x2778 px screenshots). Its commands are answered by device A's agent, so nothing ever fails visibly - the answers are just for the wrong device. The wrong binding persists until the daemon exits, because device objects are cached.
Root cause
- Every real iOS device is created with a hardcoded client for
localhost:8100:Line 160 in 7e9699e
device.deviceKitClient = devicekit.NewDeviceKitClient("localhost:8100") StartAgentfirst checks that default client and returns early when anything answers:Lines 503 to 505 in 7e9699e
if _, err := d.deviceKitClient.GetStatus(); err == nil { return nil } - A device's own agent forward takes the first free port in 8100-8299, which is 8100 for the first device:
Line 577 in 7e9699e
port, err := findAvailablePortInRange(portRangeStart, portRangeEnd)
So when device B starts later, the GetStatus() check succeeds against device A's already-running agent on 8100, StartAgent returns early, and B never gets its own forwarder or client.
This only shows when the agents start at different times, which is why it can appear intermittent.
Affected versions
- 1.0.13
- 1.0.19 (latest)
main(7e9699e)
Related
mobile-next/mobile-mcp#253 (closed 2025-09-05 with "multiple iOS devices work now") reported a similar symptom earlier. This appears to be a separate remaining path - the default localhost:8100 client means the second device never sets up its own forward when the agents start at different times - so opening this as a new ticket as requested there.
A fix is being prepared: stop creating the default localhost:8100 client, only trust GetStatus through the device's own running forwarder, fail safe when no agent client exists for the device yet, plus Go regression tests. Happy to send it as a PR.
Activity
kwancheng commentedon Nov 10, 2026
Still reproduces on 1.0.22 (latest), and on 1.0.21.
Two real USB iPhones on iOS 27.2 (an iPhone 13 Pro at 390x844 and an iPhone 11 at 414x896), driven through mobile-mcp 1.0.16 with a single daemon. Once the first phone's agent was forwarded on 8100, every command naming the second phone, including
device info, screenshots and taps, landed on the first one. Restarting the daemon only swapped which phone won.Workaround that confirms the root cause: occupy TCP 8100 on all interfaces before the daemon starts, with anything that doesn't answer as an agent (e.g.
python3 -m http.server 8100 --bind 0.0.0.0 --directory /var/empty, which returns 404). TheGetStatus()check onlocalhost:8100then fails, and each device sets up its own forward. Here the phones got 8101 and 8102, and both report their own screen sizes.Note that the placeholder has to bind
0.0.0.0, not127.0.0.1.IsPortAvailable("localhost", …)passesnet.ParseIP("localhost"), which isnil, so it binds the wildcard address, and a loopback-only listener doesn't make 8100 look busy.