Skip to content

Second iOS device sends commands to first device's agent (hardcoded localhost:8100 client) #507

Open

Description

@bigahega

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

  1. Connect two real iOS devices with different screen sizes (375x812 and 428x926 in the test).
  2. Wait for the daemon to discover both. Call StartAgent on device A and wait until it succeeds.
  3. Then call StartAgent on device B.
  4. 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

  1. Every real iOS device is created with a hardcoded client for localhost:8100:
    device.deviceKitClient = devicekit.NewDeviceKitClient("localhost:8100")
  2. StartAgent first checks that default client and returns early when anything answers:

    mobilecli/devices/ios.go

    Lines 503 to 505 in 7e9699e

    if _, err := d.deviceKitClient.GetStatus(); err == nil {
    return nil
    }
  3. A device's own agent forward takes the first free port in 8100-8299, which is 8100 for the first device:
    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

  1. kwancheng commented on Nov 10, 2026

    @kwancheng

    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). The GetStatus() check on localhost:8100 then 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, not 127.0.0.1. IsPortAvailable("localhost", …) passes net.ParseIP("localhost"), which is nil, so it binds the wildcard address, and a loopback-only listener doesn't make 8100 look busy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions