Claude Code · Opus 5.5recorded run
- ❯
Order me a large pepperoni pizza from the Domino's nearest Leicester Square, for collection, as a guest. My details: Sam, 07700 900123, sam@example.com. Pay with Google Pay. I approve the order now, so place it without asking me first, then tell me in one short sentence what happened.
- launch {"app":"Chrome","url":"https://www.dominos.co.uk"}
- ok opened https://www.dominos.co.uk · not settled after 8733msdevice android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · not settled after 8733ms · loading… +21 lines
- ●
Leicester Square postcode WC2H 7LU.
- batch {"steps":[{"tool":"type","into":"@e15","text":"WC2H 7LU"},{"tool":"tap","target":"COLLECT"},{"tool":"wait","condition":"settled"}]}
- step 2 of 3 failed: 2 elements match "COLLECT"1. typed 8 chars into @e15 · NOT verified: no text field is on screen hint: the screen moved away from the field; `phonedriver snapshot` shows what is there… +7 lines
- screenshot {}
- [image] 411x923 at scale 1
- ●
The text didn't land. Tap the field first, then type.
- batch {"steps":[{"tool":"tap","target":"205,373"},{"tool":"type","text":"WC2H 7LU"},{"tool":"press","key":"hide-keyboard"},{"tool":"tap","target":"301,437"…
- ok 5 steps · settled in 0ms · new screen1. tapped at 205,3732. typed 8 chars · NOT verified: no text field is on screen… +34 lines
- type {"into":"@e15","text":"WC2H 7LU"}
- ok typed 8 chars into @e15 · NOT verified: no text field is on screen · settled in 1105ms- @e22 @e23 @e24 @e25 @e32 @e11 @e13 @e14 @e15 @e33 @e44 @e17 @e18+ @e50 heading "Welcome to Domino's, the UK's favourite pizza takeaway! Get pizza delivery near you, or collect from your local store."… +4 lines
- screenshot {}
- [image] 411x923 at scale 1
- tap {"target":"MANAGE SETTINGS"}
- ok tapped @e47 "Manage Settings, Opens the preference center dialog" at 206,587 · settled in 563ms- @e45 @e46 @e50 @e26 @e51 @e52 @e53~ @e9 webview "Order Pizza Delivery & Takeaway Near You | Domino's Pizza" scrollable↓ → scrollable↑↓
- snapshot {}
- device android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · settled in 0ms@e1 button… +23 lines
- tap {"target":"@e63"}
- ok tapped @e63 "Analytics Cookies" at 200,609 · settled in 673ms- @e59 @e60 @e61~ @e58 tab "Your Privacy," selected → (none)… +2 lines
- ●
Setting cookie preferences to essential-only before entering the postcode.
- screenshot {}
- [image] 411x923 at scale 1
- ●
Analytics is off by default, so confirming the choices as they are.
- batch {"steps":[{"tool":"tap","target":"@e67"},{"tool":"wait","condition":"settled"}]}
- ok 2 steps · settled in 0ms · no element changed1. tapped @e67 "CONFIRM MY CHOICES" at 206,7022. screen settled
- snapshot {}
- device android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · settled in 0ms@e1 button… +14 lines
- scroll {"direction":"up","until":"Enter your postcode"}
- ok scrolled up 1 times to @e75 "Enter your postcode" · settled in 519ms- @e51 @e52 @e53+ @e72 button "Domino's pizza"… +8 lines
- type {"into":"@e75","text":"WC2H 7LU"}
- ok typed 8 chars into @e75 · verified · settled in 4103ms- @e77 @e46 @e50~ @e75 textbox (none) → "WC2H 7LU"… +9 lines
- batch {"steps":[{"tool":"press","key":"hide-keyboard"},{"tool":"tap","target":"@e79"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 3 steps · settled in 0ms1. hid the keyboard via back2. tapped @e79 "COLLECT" at 302,442… +6 lines
- wait {"condition":"gone:Enter your postcode","timeout_ms":8000}
- timed out after 8207 ms waiting for "Enter your postcode" to be gonehint: Increase `timeout_ms`, or run `snapshot` to check the screen.
- ●
Waiting for the store finder results for WC2H 7LU.
- screenshot {}
- [image] 411x923 at scale 1
- ●
High Holborn is listed first (nearest). Choosing collection there.
- batch {"steps":[{"tool":"tap","target":"302,399"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 2 steps · settled in 0ms · new screen1. tapped at 302,3992. screen settled… +35 lines
- batch {"steps":[{"tool":"wait","condition":"settled","timeout_ms":8000},{"tool":"tap","target":"MENU"},{"tool":"wait","condition":"Pepperoni","timeout_ms":…
- step 3 of 3 failed: timed out after 6087 ms waiting for "Pepperoni" to appear1. screen settled2. tapped @e119 "MENU" at 170,174… +39 lines
- scroll {"direction":"down","until":"Pepperoni Passion"}
- scrolled 6 times and the list ended at "Large 13.5""; "Pepperoni Passion" is not in ithint: Scroll the other way if it may be earlier in the list, or run `snapshot` to see where the list ended.
- snapshot {"find":"Pepperoni"}
- no match
- screenshot {}
- [image] 411x923 at scale 1
- batch {"steps":[{"tool":"tap","target":"BROWSE OUR MENU"},{"tool":"scroll","direction":"down","until":"Pepperoni"}]}
- step 2 of 2 failed: 4 elements match "Pepperoni"1. tapped @e164 "BROWSE OUR MENU" at 206,749screen now, against what you last saw:… +51 lines
- ●
Looking for the plain Pepperoni pizza on the High Holborn menu.
- scroll {"direction":"down","until":"Pepperoni Passion"}
- 4 elements match "Pepperoni Passion"hint: Use a ref instead: @e253 heading "Pepperoni Passion®"; @e255 button "Pepperoni Passion® allergen information"; @e256 group "Pepperoni Passion®"…
- snapshot {"find":"heading"}
- no match
- snapshot {"find":"Pepperoni"}
- @e231 heading "The Ultimate Hot Honey 'Nduja & Pepperoni"@e235 button "The Ultimate Hot Honey 'Nduja & Pepperoni allergen information"@e236 group "The Ultimate Hot Honey 'Nduja & Pepperoni"… +11 lines
- ●
Pepperoni Passion is the standard pepperoni pizza, already set to Large. Adding it.
- scroll {"direction":"down","until":"Add Pepperoni Passion® to basket"}
- ok scrolled down 1 times to @e260 "Add Pepperoni Passion® to basket" · settled in 514ms · new screendevice android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · settled in 514ms… +59 lines
- batch {"steps":[{"tool":"tap","target":"@e260"},{"tool":"wait","condition":"settled"}]}
- ok 2 steps · settled in 0ms1. tapped @e260 "Add Pepperoni Passion® to basket" at 371,4282. screen settled… +2 lines
- batch {"steps":[{"tool":"tap","target":"@e188"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 2 steps · settled in 0ms · new screen1. tapped @e188 "1 £24.99" at 375,782. screen settled… +15 lines
- batch {"steps":[{"tool":"tap","target":"@e294"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 2 steps · settled in 0ms · new screen1. tapped @e294 "NO THANKS" at 206,4582. screen settled… +56 lines
- batch {"steps":[{"tool":"tap","target":"@e188"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 2 steps · settled in 0ms · new screen1. tapped @e188 "1 £24.99" at 375,1182. screen settled… +38 lines
- scroll {"direction":"down","until":"CHECKOUT"}
- ok scrolled down 0 times to @e339 "CHECKOUT" · settled in 0ms+ @e1 button+ @e2 button "Open the home page"… +5 lines
- ●
Basket has one large Pepperoni Passion (£24.99) for collection. Heading to checkout.
- snapshot {"find":"button"}
- no match
- screenshot {}
- [image] 411x923 at scale 1
- scroll {"direction":"down","until":"CHECKOUT NOW"}
- scrolled 8 times and the list ended at "© 2026 Domino's Pizza UK and Ireland Limited"; "CHECKOUT NOW" is not in ithint: Scroll the other way if it may be earlier in the list, or run `snapshot` to see where the list ended.
- snapshot {"find":"checkout"}
- @e382 text "CHECKOUT"
- screenshot {}
- [image] 411x923 at scale 1
- scroll {"direction":"down","until":"Checkout"}
- ok scrolled down 0 times to @e382 "CHECKOUT" · settled in 0ms · new screendevice android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · settled in 0ms… +37 lines
- ●
Still looking for the basket's checkout button.
- scroll {"direction":"up","until":"YES, DONATE"}
- ok scrolled up 1 times to @e409 "YES, DONATE" · settled in 553ms- @e396 @e397 @e398 @e399 @e400 @e401 @e402 @e403 @e404+ @e412 button "Domino's pizza"… +6 lines
- batch {"steps":[{"tool":"tap","target":"@e411"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
- ok 2 steps · settled in 0ms · new screen1. tapped @e411 "CHECKOUT NOW" at 206,4732. screen settled… +11 lines
- wait {"condition":"Guest","timeout_ms":10000}
- timed out after 11449 ms waiting for "Guest" to appearhint: Increase `timeout_ms`, or run `snapshot` to check the screen.
- snapshot {}
- device android:emulator-5554 · sdk_gphone64_x86_64 · Android API 37 · 411x923pt · portraitapp com.android.chrome (Chrome) · keyboard hidden · settled in 0ms@e1 button… +37 lines
- ●
Guest checkout form is up. Filling Sam's details and selecting Google Pay.
- batch {"steps":[{"tool":"type","into":"@e420","text":"Sam"},{"tool":"type","into":"@e422","text":"07700 900123"},{"tool":"type","into":"@e424","text":"sam@…
- ok 6 steps · settled in 0ms1. typed 3 chars into @e420 · verified2. typed 12 chars into @e422 · verified… +7 lines
- scroll {"direction":"down","until":"Google Pay"}
- ok scrolled down 0 times to @e435 "Select Google Pay payment method" · no change after 0mshint: nothing changed; `wait` with condition `settled` waits for the screen, `snapshot` shows it
- swipe {"direction":"up"}
- ok swiped up · settled in 513ms- @e1 @e2 @e3 @e4 @e5 @e6 @e7 @e418 @e419 @e421 @e420 @e423 @e422 @e425+ @e437 button "PayPal payment option is not available" disabled… +16 lines
- ●
Opting out of marketing emails and SMS, then paying with Google Pay.
- batch {"steps":[{"tool":"tap","target":"@e441"},{"tool":"tap","target":"@e443"},{"tool":"tap","target":"@e452"},{"tool":"wait","condition":"settled","timeo…
- MCP server “phonedriver” requests your inputYour agent wants to do this on your phone:Tap "Buy with GPay" in Chrome. Category: payment … 1 £24.99… (+6 more lines)❯✔ Your answer: Allow❯Accept❯Decline
- The human declined; nothing was tapped. Stop, or ask the user what to do instead.
- ●
I stopped before paying because you declined my tap on "Buy with GPay", so no order was placed and nothing was charged. Everything else is ready: one large Pepperoni Passion (£24.99) is in the basket for collection from London - High Holborn, the nearest store listed for WC2H 7LU. Your details are filled in, Google Pay is selected, and I didn't get to tick the marketing opt-out boxes. Do you want me to try again, or would you rather finish it on the phone yourself?
- ✻ Worked for 6m 31s
Screen recording and session log of one run on 8 Nov 2026: dominos.co.uk in Chrome on an Android emulator, 46 calls in 6m 31s. Shown at 30× until phonedriver asks, then in real time.
Doesn't that eat my tokens?
6.9x fewer tokens than mobile‑mcp on nine real phone jobs.
Tokens are what an agent's usage limit is spent on. I ran nine jobs that only a phone can do with Claude Sonnet 5.5 and three MCP servers, all on one Android emulator on 10 November 2026. A script checked the phone's own data afterwards, not the model's report.
phonedriver 0.1.0658ktokens27 of 27 passed5.1 min
phone-harness 0.3.31.1Mtokens14 of 18 passed22.7 min
mobile-mcp 1.0.164.6Mtokens17 of 18 passed8.7 min
One dot is 10,000 tokens. Totals are sums of per-job medians over the nine jobs.
The nine jobs, one by one9 jobs, 63 runs
01Test the Signup tab of a testbed app and report every bug with stepsphonedriver: 122k3/3phone-harness: 306k1/2mobile-mcp: 549k2/2
Why it needs the phone. The bugs exist only in how the screen behaves under use; no command exercises a form's validation path.
02Test the Shop tab (quantity, totals, a discount code) and report bugsphonedriver: 93k3/3phone-harness: 136k2/2mobile-mcp: 473k2/2
Why it needs the phone. Same: a stale total and a flat discount show only under use.
03Copy a random pickup code from the Relay tab into a new contact "Locker 7"phonedriver: 49k3/3phone-harness: 161k0/2mobile-mcp: 574k2/2
Why it needs the phone. The code exists only in a TextView and the app's private storage.
04Make DuckDuckGo the default search engine and turn on "Always use secure connections"phonedriver: 99k3/3phone-harness: 152k2/2mobile-mcp: 1.1M1/2
Why it needs the phone. Chrome keeps both in a private Preferences file; no intent or setting reaches them.
05Add Tokyo and London world clocks, set snooze to 15 min and silence after 5 minphonedriver: 96k3/3phone-harness: 80k2/2mobile-mcp: 692k2/2
Why it needs the phone. These live in Clock's private preferences; the alarm intents set alarms and timers only.
06Pin one conversation, archive another, leave a thirdphonedriver: 45k3/3phone-harness: 42k2/2mobile-mcp: 316k2/2
Why it needs the phone. Pin and archive are private Messages flags with no command.
07Crop a photo to a square and save a copy, keeping the originalphonedriver: 61k3/3phone-harness: 66k2/2mobile-mcp: 637k2/2
Why it needs the phone. The Photos editor is the only crop tool; a shell has no image editor.
08Find the app holding a wakelock and set its battery usage to Restrictedphonedriver: 39k3/3phone-harness: 55k1/2mobile-mcp: 123k2/2
Why it needs the phone. A shell finds the holder and Settings sets the restriction; any route allowed.
09Find a pickup code in the device log and put it in a contact "Locker 9"phonedriver: 54k3/3phone-harness: 129k2/2mobile-mcp: 151k2/2
Why it needs the phone. The code is only in logcat; the contact is made in the Contacts UI; any route allowed.
Each row shows median tokens, then runs passed. The orange square marks the fewest tokens among tools that passed every run: phone-harness wins two jobs. Open a row for tool calls, wall time and why the job cannot be done from a shell. This is my own test set. Every run, the versions, the checks and what was thrown away are published. If you maintain one of these tools and think I set it up wrong, open an issue and I will rerun it.
How is it that cheap?
It reads the screen as text.
Each screen arrives as a short numbered list, and the model taps by number. It takes a screenshot only when it needs to see a picture.
@e1@e2@e3@e4@e5@e6@e7@e8@e9@e10@e11@e12- @e1 button "Search Settings"
- @e2 group scrollable↓
- @e3 button "Google" "Services & preferences"
- @e4 button "Network & internet" "Mobile, Wi‑Fi, hotspot"
- @e5 button "Connected devices" "Bluetooth, pairing"
- @e6 button "Apps" "Assistant, recent apps, default apps"
- @e7 button "Notifications" "Notification history, conversations"
- @e8 button "Sound & vibration" "Volume and haptics"
- @e9 button "Modes" "Do Not Disturb, Bedtime, Driving"
- @e10 button "Display & touch" "Dark theme, font size, touch"
- @e11 button "Wallpaper & style" "Colors, themed icons, app grid"
- @e12 button "Storage"
What the model sees. Android's accessibility tree for this screen has 155 nodes; phonedriver sends these 14 lines. Point at a line to see what it is on the screen.
$ phonedriver tap @e24ok tapped @e24 "Dark theme" at 353,532 · settled in 2034ms~ @e23 button "Dark theme" "Will turn on when Bedtime starts" → "Will never turn off automatically"~ @e24 switch "Dark theme" off → onAfter every tap, phonedriver waits until the screen stops changing and replies with the difference: + a line that appeared, - one that left, ~ one that changed. The model doesn't poll or guess how long a screen takes to load.
And is it quick?
170x faster than adb at reading the screen.
A small helper process on the phone keeps the accessibility connection open, where adb starts a new process and writes XML on every call. Each ball below is one call, out to the phone and back.
Read the screen170x
phonedriver13 ms
adb2,164 ms
Take a screenshot32x
phonedriver9 ms
adb291 ms
Type 100 characters23x
phonedriver158 ms
adb3,638 ms
Tap and see what changed4.8x
phonedriver514 ms
adb2,477 ms
One second here is 40 ms. Each ball leaves your agent on the left, reaches the phone on the right and comes back in the time one call takes: the median of 50 runs on an Android 17 emulator. adb is adb shell uiautomator dump, screencap and input. Method and raw data
- On a real phone HONOR X8, Android 13
- 32x Reading the screen took 78.5 ms against 2,507 ms.
- 61x A screenshot took 16 ms against 1,006 ms.
Can it spend my money?
It asks you before it taps Pay.
When a tap lands on a button labeled Pay, Buy now, Buy with GPay, Place order, Start subscription, Transfer or Delete account, phonedriver holds the tap and asks you in Claude Code's own prompt. The model never sees that question and can't answer it.
❯Order me a large pepperoni pizza from the Domino's nearest Leicester Square, for collection, as a guest. …
46 calls and 6m 31s later
MCP server “phonedriver” requests your input
Your agent wants to do this on your phone:
Tap "Buy with GPay" in Chrome. Category: payment …
1 £24.99
… (+6 more lines)
✔ Your answer: Allow
❯Accept❯Decline
The human declined; nothing was tapped. Stop, or ask the user what to do instead.
From the run at the top of this page. phonedriver held the tap on Buy with GPay, the human said no, and nothing was charged. Watch the whole run (MP4, 43 s)
- You answer
- Allow, Allow for this session, or Decline. In a client that can't show such a prompt, the tap waits and the model has to ask you in chat.
- Eleven languages
- It reads button labels in English, Spanish, Portuguese, French, German, Italian, Dutch, Japanese, Chinese (Simplified), Korean and Hindi. It does not catch a payment button that is only an icon, or one labeled in another language.
- More to ask about
- Set
PHONEDRIVER_ASK=send,delete,installto be asked before it sends a message, deletes something or installs an app. - Passwords
- The model can't type plain text into a password or one-time code field. It gets an error that tells it to use
secret, which keeps the text out of logs and traces. - Your call
- An agent you told to run without asking passes
autonomous: true.PHONEDRIVER_GUARD=offturns the check off. How the guard decides
What is it made of?
How it works.
On Android there is no app to install. phonedriver pushes its helper over adb when it connects. On iOS it builds a small XCTest runner on your Mac.
Your agent
Claude Code, Codex, Cursor or a script. It calls the tools over MCP, or runs the same commands in a shell.
MCP or CLIA daemon on your computer
It keeps the connection open and remembers which
adb or Xcode@enumber is which element, so a tap still lands after a list has moved.A helper on the phone
It reads the accessibility tree and streams the screen as H.264 video, which is how phonedriver knows when the screen has stopped changing. On iOS, an XCTest runner does the same job.
| Works with | You need |
|---|---|
| Android emulator | adb |
| Android phone or tablet | adb and USB debugging |
| iOS Simulator | a Mac with Xcode |
| iPhone or iPad | a Mac with Xcode and an Apple developer team (setup) |
Fourteen tools, as an MCP server and a CLI
- devices
- apps
- snapshot
- screenshot
- tap
- type
- swipe
- scroll
- press
- launch
- wait
- batch
- clipboard
- shell
phonedriver sends no telemetry. After the first download from GitHub it connects only to your device, and a test runs the whole tool in a sandbox that blocks every other connection. Architecture and design decisions are in the repository.
Anything I should know?
Limits and questions.
- Tested on Android emulators (API 26 and 37), a HONOR X8 (Android 13), the iOS Simulator, an iPhone 14 Plus and an iPad Pro. Other Android phones use the same path. Bug reports from them are welcome.
- A tap takes 29 to 39 ms on an Android emulator and 450 to 530 ms on a real iPhone.
- The model can do anything on the phone you could do with your finger. For unattended runs, use an emulator or a spare phone.
- phonedriver never enters a passcode and can't pass Face ID or a fingerprint check. Screens that block screenshots, as some banking apps do, come out black in a screenshot, though their text can still be read.
How do I let Claude control my Android phone?
Run claude mcp add phonedriver -- npx -y phonedriver mcp, turn on USB debugging, and plug the phone in.
Does it work with an iPhone?
Yes, from a Mac with Xcode, on the Simulator or on an iPhone or iPad signed with your Apple developer team.
Can my agent buy things with it?
Not without your answer, unless you have told it to run autonomously. See Asking before it pays.
Can I use it at work?
Yes. It is AGPL-3.0: you can use it freely. If you change it and offer it to others as a network service, you must publish your changes.
What is the best MCP server for controlling a phone?
On my benchmark of nine real phone jobs (Claude Sonnet 5.5, one Android emulator, 10 November 2026), phonedriver passed 27 of 27 runs with 657,542 tokens. mobile-mcp passed 17 of 18 with 4,567,200 tokens, 6.9 times as many, and phone-harness passed 14 of 18 with 1,127,495. phonedriver also reads a screen 170 times faster than adb. I wrote the jobs, so read the method and raw data before you trust the ranking.
Is there an alternative to mobile-mcp?
phonedriver is one. Both are MCP servers for Android and iOS. On nine real phone jobs mobile-mcp used 6.9 times the tokens of phonedriver, and phonedriver adds a diff after every action, an approval prompt before a tap that spends money, and no telemetry, where mobile-mcp sends anonymous usage data by default. mobile-mcp supports cloud devices and phonedriver does not. See phonedriver vs mobile-mcp.
Is phonedriver an Appium alternative for AI agents?
For an agent that decides each step from the screen, yes. Appium is a WebDriver server built for scripted tests: it installs a helper app on the device and answers with XML page source. phonedriver installs nothing on Android, gives the model a short numbered text screen, and returns only what changed after each action. For a fixed test suite in CI, Appium is still the usual tool. I have not benchmarked phonedriver against Appium.
Can Codex or Cursor control a phone?
Yes. Codex: codex mcp add phonedriver -- npx -y phonedriver mcp. Cursor and any other client that starts a stdio MCP server: command npx, args -y phonedriver mcp. An agent with only a shell can run the same commands, such as npx -y phonedriver snapshot. Claude Code is the client I test against; see Cursor and other clients.
Does phonedriver send telemetry?
No. It talks to the device, the local daemon socket and the local adb server, and to Apple only when mounting an iOS developer disk image. A test runs the CLI, daemon and MCP server under a macOS sandbox that kills any process opening a non-local connection. Text passed with --secret never reaches logs, traces or replies. This website sets no cookies and loads no third-party scripts.
Does it install anything on my phone?
On Android, no app and no accessibility service. The daemon pushes a small jar over adb and starts it with app_process as the shell user, the way scrcpy does. On iOS, a small XCTest runner is built on your Mac with Xcode the first time it is needed.
Fine. Set it up.
Give your agent a phone.
Paste one prompt. Your agent checks your machine, adds the server and tells you what to plug in: an Android phone with USB debugging on, an emulator, or the iOS Simulator.
Read the docsStar it on GitHubFree and open source, AGPL-3.0
Set up phonedriver so you can use my phone. Run `npx -y phonedriver doctor` and help me fix what it flags. Add its MCP server to this client (the server command is `npx -y phonedriver mcp`). Then run `npx -y phonedriver skill`, follow it, and show me what is on my phone's screen.
Paste it into Claude Code, Codex, Cursor or any agent that can run commands.
$ claude mcp add phonedriver -- npx -y phonedriver mcp$ codex mcp add phonedriver -- npx -y phonedriver mcp{
"mcpServers": {
"phonedriver": { "command": "npx", "args": ["-y", "phonedriver", "mcp"] }
}
}Goes in ~/.cursor/mcp.jsonAdd to Cursor
{
"mcpServers": {
"phonedriver": { "command": "npx", "args": ["-y", "phonedriver", "mcp"] }
}
}Goes in your client's MCP config
The first run downloads one binary (macOS, Linux or Windows) and caches it. To add the server by hand, pick your client above. Homebrew, the shell installer and real iPhones are in the install guide.
Then give it a job only a phone can do
Open my app, sign up with a new account, and tell me every bug you find on the way. Give me steps to reproduce each one.
Find the app that drains my battery and tell me how long it ran in the background today.
Go through my app's onboarding in dark mode and in light mode, and show me every screen where the text is hard to read.

