Your AI can use your phone.

Claude Code, Codex, Cursor. Android and iOS.

Set up phonedriver

Claude Code · Opus 5.5recorded run

  1. ❯

    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.

  2. launch {"app":"Chrome","url":"https://www.dominos.co.uk"}
  3. 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
  4. ●

    Leicester Square postcode WC2H 7LU.

  5. batch {"steps":[{"tool":"type","into":"@e15","text":"WC2H 7LU"},{"tool":"tap","target":"COLLECT"},{"tool":"wait","condition":"settled"}]}
  6. 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
  7. screenshot {}
  8. [image] 411x923 at scale 1
  9. ●

    The text didn't land. Tap the field first, then type.

  10. batch {"steps":[{"tool":"tap","target":"205,373"},{"tool":"type","text":"WC2H 7LU"},{"tool":"press","key":"hide-keyboard"},{"tool":"tap","target":"301,437"…
  11. 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
  12. type {"into":"@e15","text":"WC2H 7LU"}
  13. 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
  14. screenshot {}
  15. [image] 411x923 at scale 1
  16. tap {"target":"MANAGE SETTINGS"}
  17. 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↑↓
  18. snapshot {}
  19. 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
  20. tap {"target":"@e63"}
  21. ok tapped @e63 "Analytics Cookies" at 200,609 · settled in 673ms- @e59 @e60 @e61~ @e58 tab "Your Privacy," selected → (none)… +2 lines
  22. ●

    Setting cookie preferences to essential-only before entering the postcode.

  23. screenshot {}
  24. [image] 411x923 at scale 1
  25. ●

    Analytics is off by default, so confirming the choices as they are.

  26. batch {"steps":[{"tool":"tap","target":"@e67"},{"tool":"wait","condition":"settled"}]}
  27. ok 2 steps · settled in 0ms · no element changed1. tapped @e67 "CONFIRM MY CHOICES" at 206,7022. screen settled
  28. snapshot {}
  29. 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
  30. scroll {"direction":"up","until":"Enter your postcode"}
  31. ok scrolled up 1 times to @e75 "Enter your postcode" · settled in 519ms- @e51 @e52 @e53+ @e72 button "Domino's pizza"… +8 lines
  32. type {"into":"@e75","text":"WC2H 7LU"}
  33. ok typed 8 chars into @e75 · verified · settled in 4103ms- @e77 @e46 @e50~ @e75 textbox (none) → "WC2H 7LU"… +9 lines
  34. batch {"steps":[{"tool":"press","key":"hide-keyboard"},{"tool":"tap","target":"@e79"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  35. ok 3 steps · settled in 0ms1. hid the keyboard via back2. tapped @e79 "COLLECT" at 302,442… +6 lines
  36. wait {"condition":"gone:Enter your postcode","timeout_ms":8000}
  37. timed out after 8207 ms waiting for "Enter your postcode" to be gonehint: Increase `timeout_ms`, or run `snapshot` to check the screen.
  38. ●

    Waiting for the store finder results for WC2H 7LU.

  39. screenshot {}
  40. [image] 411x923 at scale 1
  41. ●

    High Holborn is listed first (nearest). Choosing collection there.

  42. batch {"steps":[{"tool":"tap","target":"302,399"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  43. ok 2 steps · settled in 0ms · new screen1. tapped at 302,3992. screen settled… +35 lines
  44. batch {"steps":[{"tool":"wait","condition":"settled","timeout_ms":8000},{"tool":"tap","target":"MENU"},{"tool":"wait","condition":"Pepperoni","timeout_ms":…
  45. 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
  46. scroll {"direction":"down","until":"Pepperoni Passion"}
  47. 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.
  48. snapshot {"find":"Pepperoni"}
  49. no match
  50. screenshot {}
  51. [image] 411x923 at scale 1
  52. batch {"steps":[{"tool":"tap","target":"BROWSE OUR MENU"},{"tool":"scroll","direction":"down","until":"Pepperoni"}]}
  53. 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
  54. ●

    Looking for the plain Pepperoni pizza on the High Holborn menu.

  55. scroll {"direction":"down","until":"Pepperoni Passion"}
  56. 4 elements match "Pepperoni Passion"hint: Use a ref instead: @e253 heading "Pepperoni Passion®"; @e255 button "Pepperoni Passion® allergen information"; @e256 group "Pepperoni Passion®"…
  57. snapshot {"find":"heading"}
  58. no match
  59. snapshot {"find":"Pepperoni"}
  60. @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
  61. ●

    Pepperoni Passion is the standard pepperoni pizza, already set to Large. Adding it.

  62. scroll {"direction":"down","until":"Add Pepperoni Passion® to basket"}
  63. 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
  64. batch {"steps":[{"tool":"tap","target":"@e260"},{"tool":"wait","condition":"settled"}]}
  65. ok 2 steps · settled in 0ms1. tapped @e260 "Add Pepperoni Passion® to basket" at 371,4282. screen settled… +2 lines
  66. batch {"steps":[{"tool":"tap","target":"@e188"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  67. ok 2 steps · settled in 0ms · new screen1. tapped @e188 "1 £24.99" at 375,782. screen settled… +15 lines
  68. batch {"steps":[{"tool":"tap","target":"@e294"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  69. ok 2 steps · settled in 0ms · new screen1. tapped @e294 "NO THANKS" at 206,4582. screen settled… +56 lines
  70. batch {"steps":[{"tool":"tap","target":"@e188"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  71. ok 2 steps · settled in 0ms · new screen1. tapped @e188 "1 £24.99" at 375,1182. screen settled… +38 lines
  72. scroll {"direction":"down","until":"CHECKOUT"}
  73. ok scrolled down 0 times to @e339 "CHECKOUT" · settled in 0ms+ @e1 button+ @e2 button "Open the home page"… +5 lines
  74. ●

    Basket has one large Pepperoni Passion (£24.99) for collection. Heading to checkout.

  75. snapshot {"find":"button"}
  76. no match
  77. screenshot {}
  78. [image] 411x923 at scale 1
  79. scroll {"direction":"down","until":"CHECKOUT NOW"}
  80. 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.
  81. snapshot {"find":"checkout"}
  82. @e382 text "CHECKOUT"
  83. screenshot {}
  84. [image] 411x923 at scale 1
  85. scroll {"direction":"down","until":"Checkout"}
  86. 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
  87. ●

    Still looking for the basket's checkout button.

  88. scroll {"direction":"up","until":"YES, DONATE"}
  89. 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
  90. batch {"steps":[{"tool":"tap","target":"@e411"},{"tool":"wait","condition":"settled","timeout_ms":8000}]}
  91. ok 2 steps · settled in 0ms · new screen1. tapped @e411 "CHECKOUT NOW" at 206,4732. screen settled… +11 lines
  92. wait {"condition":"Guest","timeout_ms":10000}
  93. timed out after 11449 ms waiting for "Guest" to appearhint: Increase `timeout_ms`, or run `snapshot` to check the screen.
  94. snapshot {}
  95. 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
  96. ●

    Guest checkout form is up. Filling Sam's details and selecting Google Pay.

  97. batch {"steps":[{"tool":"type","into":"@e420","text":"Sam"},{"tool":"type","into":"@e422","text":"07700 900123"},{"tool":"type","into":"@e424","text":"sam@…
  98. ok 6 steps · settled in 0ms1. typed 3 chars into @e420 · verified2. typed 12 chars into @e422 · verified… +7 lines
  99. scroll {"direction":"down","until":"Google Pay"}
  100. 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
  101. swipe {"direction":"up"}
  102. 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
  103. ●

    Opting out of marketing emails and SMS, then paying with Google Pay.

  104. batch {"steps":[{"tool":"tap","target":"@e441"},{"tool":"tap","target":"@e443"},{"tool":"tap","target":"@e452"},{"tool":"wait","condition":"settled","timeo…
  105. 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
  106. The human declined; nothing was tapped. Stop, or ask the user what to do instead.
  107. ●

    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?

  108. ✻ Worked for 6m 31s

The human says no. Nothing is charged.0:0030×

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

phonedriver122k3/323 calls50 s

mobile-mcp549k2/245 calls95 s

phone-harness306k1/220.5 calls477 s

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

phonedriver93k3/315 calls41 s

mobile-mcp473k2/233.5 calls63.5 s

phone-harness136k2/213.5 calls242.5 s

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

phonedriver49k3/38 calls31 s

mobile-mcp574k2/235.5 calls68 s

phone-harness161k0/214.5 calls190 s

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

phonedriver99k3/313 calls45 s

mobile-mcp1.1M1/252.5 calls99 s

phone-harness152k2/216.5 calls140.5 s

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

phonedriver96k3/313 calls39 s

mobile-mcp692k2/244 calls61.5 s

phone-harness80k2/29 calls84 s

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

phonedriver45k3/36 calls21 s

mobile-mcp316k2/217.5 calls33.5 s

phone-harness42k2/25.5 calls37 s

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

phonedriver61k3/38 calls31 s

mobile-mcp637k2/233 calls60.5 s

phone-harness66k2/28 calls77 s

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

phonedriver39k3/35 calls17 s

mobile-mcp123k2/27.5 calls20 s

phone-harness55k1/27.5 calls43.5 s

adb72k2/210 calls40 s

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

phonedriver54k3/38 calls31 s

mobile-mcp151k2/28 calls22 s

phone-harness129k2/29.5 calls69 s

adb131k2/210.5 calls43 s

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.

The Settings app on a Pixel 9 emulator: a search field, then rows for Google, Network & internet, Connected devices, Apps, Notifications, Sound & vibration, Modes, Display & touch, Wallpaper & style and Storage.@e1@e2@e3@e4@e5@e6@e7@e8@e9@e10@e11@e12
What you see 155 nodes in Android's accessibility tree
phonedriver snapshot14 lines
  1. device android:emulator-5554 · Pixel 9 · Android 17 · 360x800pt · portrait
  2. app com.android.settings (Settings) · keyboard hidden · settled in 212ms
  3. @e1 button "Search Settings"
  4. @e2 group scrollable↓
  5. @e3 button "Google" "Services & preferences"
  6. @e4 button "Network & internet" "Mobile, Wi‑Fi, hotspot"
  7. @e5 button "Connected devices" "Bluetooth, pairing"
  8. @e6 button "Apps" "Assistant, recent apps, default apps"
  9. @e7 button "Notifications" "Notification history, conversations"
  10. @e8 button "Sound & vibration" "Volume and haptics"
  11. @e9 button "Modes" "Do Not Disturb, Bedtime, Driving"
  12. @e10 button "Display & touch" "Dark theme, font size, touch"
  13. @e11 button "Wallpaper & style" "Colors, themed icons, app grid"
  14. @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.

then, one screen lateronly what changed
$ 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 → on

After 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.

four calls, slowed down 25 times
  1. Read the screen170x

    phonedriver13 ms

    adb2,164 ms

  2. Take a screenshot32x

    phonedriver9 ms

    adb291 ms

  3. Type 100 characters23x

    phonedriver158 ms

    adb3,638 ms

  4. 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.

Claude Code · Opus 5.5recorded 8 Nov 2026

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

AcceptDecline

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,install to 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=off turns the check off. How the guard decides

Do I have to put my phone down?

Your screen stays yours.

On Android 13 and later, the agent can work on a hidden second screen while you keep using the phone. Set PHONEDRIVER_DISPLAY=hidden in the MCP config, or pass --display hidden to the CLI.

An Android tablet's home screen with a clock, a calendar widget and the app dock, unchanged while the agent works.
Your screen
A test app with the text “typed on the hidden screen” in its first field, on a screen that is not shown on the tablet.
The hidden screen, same minute (its top left corner)

Tested on an emulator and on a HONOR X8 (Android 13). Typing into an app's own fields leaves no trace on your screen. Typing into a web page shows the system's clipboard note, and your clipboard is put back after. The two captures are from one run on a tablet emulator on 8 November 2026.

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.

  1. 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 CLI
  2. A daemon on your computer

    It keeps the connection open and remembers which @e number is which element, so a tap still lands after a list has moved.

    adb or Xcode
  3. 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 withYou need
Android emulatoradb
Android phone or tabletadb and USB debugging
iOS Simulatora Mac with Xcode
iPhone or iPada 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.

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.