Skip to content

Use a physical iPhone

Simulators need only Xcode. A real device needs signing, Developer Mode, and, on iOS 17+, a tunnel. Run uv run ios-mcp doctor at any point; it checks each of these and returns a remedy.

Terminal window
npm install -g go-ios
ios version

go-ios handles device communication and launches WebDriverAgent through testmanagerd without needing Xcode, which is also what allows a Linux host.

  1. Connect over USB and tap Trust.
  2. Enable Settings > Privacy & Security > Developer Mode and reboot.
  3. Confirm it is visible: ios list.

iOS 17 moved device communication from TCP to QUIC + RemoteXPC, so a tunnel must exist before anything can reach a cabled device. Nothing to do: when a cabled device has none, the server starts go-ios’s userspace tunnel, which needs no sudo, and stops it when the session ends.

To run one yourself instead, for other go-ios commands or for several sessions to share:

Terminal window
ios tunnel start --userspace # no sudo

The kernel tunnel, sudo ios tunnel start (or ./scripts/start_tunnel.sh, which carries a launchd plist for running it at boot), still works; set IOS_MCP_GOIOS__TUNNEL_MODE=kernel to have the server start that one. A phone on Wi-Fi needs no tunnel at all, since it is driven through xcodebuild.

This step is three passes, and the order matters. WebDriverAgent is not checked in, so on a fresh clone the project you need to open does not exist yet. Fetch it first:

Terminal window
./scripts/prepare_wda.sh device # clones WebDriverAgent, then tries to sign

Expect that first run to stop at signing. That is what the rest of this step fixes; run it again at the end.

Sign in to Xcode first (Settings > Accounts). Then open the WebDriverAgent project once and pick your team under Signing & Capabilities on both the WebDriverAgentLib and WebDriverAgentRunner targets:

Terminal window
open vendor/wda/WebDriverAgent/WebDriverAgent.xcodeproj

That GUI step is not optional. Signing in to Accounts alone creates no certificate; Apple issues one only when a project first asks for a team, and xcodebuild cannot do it because it does not share Xcode’s authenticated session (it fails with No Account for Team).

Free Apple IDs need their own bundle id. Apple refuses com.facebook.WebDriverAgentRunner because someone else already registered it, reporting it as “cannot be registered to your development team”. Change the Runner target’s Bundle Identifier to something unique such as com.yourname.WebDriverAgentRunner.

Authorize codesign once, or every build stops on a keychain prompt per framework, eight times over:

Terminal window
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
~/Library/Keychains/login.keychain-db

Then build and install, this time for real:

Terminal window
./scripts/prepare_wda.sh device # finds your team and device
ios install --path vendor/wda/WebDriverAgentRunner-Runner.app

Finally, trust the developer on the phone: Settings > General > VPN & Device Management > your Apple ID > Trust. iOS refuses to launch a free-account app until you do, and the host only sees an opaque deviceprocesscontrolservice error.

Tell the server which runner to launch:

Terminal window
export IOS_MCP_WDA__BUNDLE_ID=com.yourname.WebDriverAgentRunner.xctrunner
  • The build must target the device by id, not generic/platform=iOS. With a generic destination Apple never registers the phone, issues no profile, and the build fails claiming “your team has no devices”.
  • Do not strip the embedded XC*.framework copies. Removing anything from a signed bundle invalidates its signature, and installation then fails with a bare ApplicationVerificationFailed.
  • Signing expires after 7 days on a free Apple ID. Re-run prepare_wda.sh device and reinstall. ios_doctor reports the expiry date, and once it has lapsed it says so and drops the device from “Ready to automate”, since a runner whose profile has expired will not install. The simulator is unaffected and keeps working, because nothing about a simulator involves signing. A paid Developer Program membership gets a year.
Terminal window
uv run ios-mcp doctor # should report "Ready to automate: real device"
uv run ios-mcp devices

The device is never the default target. ios_open_session prefers a simulator even when a phone is connected, so acting on real hardware is always something the caller asked for by name or UDID.

Once a device has been paired over USB, it can be driven with no cable at all. The server picks the route itself:

USB Wi-Fi
Discovery go-ios CoreDevice (devicectl)
Runner launch ios runwda xcodebuild test-without-building
Reaching WebDriverAgent port forward straight to the phone’s own address
Needs a RemoteXPC tunnel yes no
Needs Xcode no yes

Nothing to configure: if go-ios can see the device, USB is used; otherwise the network route is taken automatically. ios-mcp devices says which, and ios-mcp doctor reports e.g. 1 device(s): 1 over the network.

Two things make this work. go-ios talks to usbmuxd and cannot see an unplugged device at all, so discovery has to go through CoreDevice, which also browses Bonjour. And WebDriverAgent listens on the device itself, so once it is running any host on the same network can reach it — the port forward exists only to carry traffic over USB, and there is no USB here.

The runner announces the address it bound (ServerURLHere->http://10.0.0.195:8100), which the adapter reads from the xcodebuild log. That is more reliable than guessing the device’s IP, since it is the interface WDA actually chose.

Wi-Fi is not slower in practice: a snapshot measured 2.6s over the network against 3.7s over USB, because the cost is the accessibility traversal on the device, not the transport.

Terminal window
export IOS_MCP_WDA__BASE_URL=http://10.0.0.195:8100

The server then connects to that WebDriverAgent instead of launching one, and never tears it down. Useful for a device farm, a phone on another network, or a runner you started by hand for debugging.

Capability Why
ios_set_permission simctl privacy is Simulator-only. Drive Settings, or answer the permission alert with ios_handle_alert.
appearance, location, status-bar overrides Simulator-only.

ios://capabilities reports this per session, so an agent can check rather than discover it by failing.

Symptom Cause
tunnel_down No tunnel, and none could be started. Unlock the phone, check the cable, or run ios tunnel start --userspace.
device_locked The phone slept. The session wakes it automatically, but cannot type a passcode. Set Auto-Lock to Never for long runs.
signing_invalid, or WDA stops working after a week Free profile expired. Re-run prepare_wda.sh device and reinstall.
device_not_ready Phone untrusted, Developer Mode off, or the runner not trusted under VPN & Device Management. A profile reissued after it expired asks for Trust again.
Launch fails with deviceprocesscontrolservice code 2 The developer certificate is not trusted on the phone.
ApplicationVerificationFailed on install The bundle was modified after signing.
The first run after a crash times out waiting for WebDriverAgent A runner from the previous run is still holding the device. Runners stop with the process that started them, even when it is killed, so this now means one started some other way. uv run ios-mcp reset lists it and -y stops it.

A snapshot costs roughly 3.7s on a physical iPhone against well under a second on a simulator, so each action lands around 8-12s. That is the accessibility round trip, not traversal depth: raw tree size stops growing past snapshot.max_depth = 30 while wall time stays flat to depth 60.

One case is worth knowing about because it looks like a hang. The first snapshot after an app is backgrounded blocks for 61 seconds: XCTest keeps waiting on the app that went away. No WebDriverAgent setting avoids it, so home() activates SpringBoard immediately afterwards, which drops the same snapshot to about 5s. If you background an app by some other route and the next call stalls, that is why.