On Android, when I want Claude Code to drive a physical phone, the
answer is adb. Install the build, launch it, tap
coordinates, dump the view hierarchy, pull a screenshot. One daemon, one
cable, no signing. The mobile-mcp server sits on top of that and the
whole thing works on a real device out of the box.
On iOS, the same job is a two-server, four-terminal affair with a
signed XCUITest runner in the middle, and it falls over in ways that
have nothing to do with your app.
This post is the working setup, why it’s shaped this way, and where
it breaks. I’ve been leaning on it while rewriting a legacy Objective-C
app in SwiftUI, where the most useful verification I have is watching
the old build and the new build side by side on real hardware.
Why there’s no adb
for iPhone
The gap is architectural, not a tooling-maturity problem someone will
fix next quarter.
Android ships a debug bridge that exposes shell, input injection, and
UI dumps to anything on the host. Apple never shipped an equivalent. On
a physical iPhone, the only sanctioned way to inject UI input is through
XCTest: a signed test runner that executes on the device and takes
commands over HTTP. Every “control my iPhone” tool you’ve ever used,
Appium included, is a client for that runner. The runner is
WebDriverAgent.
So any agent that wants to tap a button on a real iPhone has to get
WebDriverAgent built, signed with a developer identity, installed,
running, and reachable from the Mac. Everything below is that sentence,
expanded.
The two layers
The setup splits cleanly into build-and-launch and see-and-touch.
Different servers own each.
Layer 1: build, install, launch, test.
XcodeBuildMCP. This is the one most Claude Code users already
have. For simulators it wraps simctl; for physical devices
it uses xcrun devicectl, which can install and launch an
app and run tests on a paired device. If you already use XcodeBuildMCP,
the physical-device tools are a matter of enabling the device workflow
in its config. This layer gets your build onto the phone and running. It
does not tap anything.
Layer 2: tap, swipe, screenshot, read the accessibility tree.
mobile-mcp. The mobile-next/mobile-mcp server handles UI
control across Android, simulators, and real iOS devices. For real
iPhones it needs three things it can’t provide itself: WebDriverAgent
installed on the device, go-ios on the host, and an open tunnel to the
device. The project’s own wiki is candid that running the tunnel and
port-forward yourself is annoying and on their roadmap to automate.
The working recipe
From the mobile-mcp wiki, adapted to how I actually run it:
- Install go-ios on the Mac. Confirm the phone is visible with
ios list. If it isn’t, that’s a pairing or Developer Mode
problem, not an MCP problem. - Clone Appium’s WebDriverAgent, open the project in Xcode, change the
bundle identifier to something unique, and sign it with your developer
account. Build and run theWebDriverAgentRunnerscheme as a
test on the device
(xcodebuild ... -destination 'platform=iOS,id=<UDID>' test).
This is the step people give up on. - In one terminal, start the tunnel:
ios tunnel start --userspace. Leave it running. On iOS 17
and later this tunnel is required; the device doesn’t expose the
services the runner needs without it. - In a second terminal, forward the runner’s port:
ios forward 8100 8100. Leave that running too. - Add mobile-mcp to Claude Code
(npx -y @mobilenext/mobile-mcp@latest) and ask it to list
the elements on your iOS device. If that works, everything works.
At that point Claude Code can install a build through XcodeBuildMCP,
then screenshot, read the accessibility tree, tap by label, swipe, and
type through mobile-mcp. The agent sees the real screen on the real
phone.
Where it breaks
Every one of these has bitten me, and none is the app’s fault.
The runner dies when idle. WebDriverAgent doesn’t survive a locked
phone or a long pause reliably. Sessions that worked ten minutes ago
fail with connection errors because the runner quietly exited. Restart
the test in Xcode.
Provisioning expires. On a free developer account the signed runner
stops launching after a week. On a paid account it’s a year, but it’s
still a clock you didn’t set. When “nothing changed and it stopped
working,” check the signing first.
The tunnel drops on reconnect. Unplug the phone, plug it back in, and
the tunnel from step 3 may still look alive while the device is on a
different address. Kill both go-ios terminals and restart them.
Managed devices may not cooperate. Developer Mode is a per-device
switch, and MDM profiles on client-issued hardware can disable it. If
you’re on a locked-down device, this path may be closed no matter how
the tooling matures. Plan to use your own hardware for agent-driven
testing.
And Maestro’s MCP, which is pleasant on simulators, wasn’t
production-ready for physical iOS in my testing. Simulators are still
the low-friction path for most agent-driven UI work; physical devices
are for the cases where the simulator lies (camera, sensors, real
network, performance, anything touching HealthKit).
Why bother
Because the ceiling on what you can delegate to an agent is your
verification loop, not the model.
A failing unit test is cheap and fast, and agents are good at fixing
those. A layout that collapses on a small screen, a gesture that doesn’t
feel right, a screen that renders differently on device than in the
simulator: those are slow, visual, and human-gated. Every one of them is
a place where the agent runs further before anyone notices it’s
wrong.
Giving the agent eyes and hands on a real phone moves some of that
verification back inside the loop. For the Objective-C-to-SwiftUI
rewrite, the pattern I use most is two simulators side by side, legacy
build on the left and new build on the right, with the agent navigating
both and comparing screens. On device, it’s the same idea with the cases
the simulator can’t reproduce.
It’s not elegant on iOS yet. It works. And the day the tunnel and
runner become one command is the day physical-device testing becomes a
normal part of an agentic mobile workflow instead of a setup story.
Setup references: the mobile-next/mobile-mcp wiki pages for iOS
real devices, and XcodeBuildMCP’s device workflow. Commands above match
the wiki at time of writing; both projects change frequently.