Documentation
Mobdev
Mobdev puts your iPhones, iOS simulators and Android devices in one Mac app, for you and your AI agent. It reads the iPhone screen over USB. With Developer Mode, a small UI test it keeps on the iPhone taps, types and reads the UI tree over the same cable; without it, Mobdev taps and types as a Bluetooth keyboard and pointer. Simulators and Android need only Xcode or adb. Agents use it through MCP or HTTP.
Install
You need a Mac with macOS 26 or later and Xcode, an iPhone with Developer Mode and a USB data cable. Without Developer Mode the Mac needs Bluetooth LE. Download the app, open the disk image and drag Mobdev into Applications. It is signed and notarized by Apple and updates itself.
Allow Camera when macOS asks, and Bluetooth on the way without Developer Mode. macOS treats the iPhone screen like a camera, which is why it asks for camera access. To build it yourself with Xcode 26 or later:
git clone https://github.com/niklas-schmidt-dev/mobdev
cd mobdev/macos
scripts/build-app.sh
open build/Mobdev.appSet up the iPhone
The setup assistant checks off each step as soon as the Mac detects it.
- Plug it in, unlock it and tap Trust. If the Mac asks to allow the accessory, click Allow.
- Turn on Settings › Privacy & Security › Developer Mode and restart the iPhone. The switch appears once the iPhone was connected to Xcode.
- Sign in to Xcode with your Apple Account under Xcode › Settings › Apple Accounts. A free account works.
- Mobdev builds Mobdev Runner, a small UI test, and starts it on the iPhone by itself; the first build takes a minute or two. If iOS asks, turn on Settings › Developer › Enable UI Automation. With a free account, trust the developer once under Settings › General › VPN & Device Management.
- Set Auto-Lock to Never while agents work. The phone must stay unlocked.
Without Developer Mode
Choose Without Developer Mode in the setup assistant. Mobdev then taps and types as a Bluetooth keyboard and pointer and installs nothing on the iPhone.
- On the iPhone open Settings › Bluetooth and tap your Mac under Other Devices. iOS lists it by the Mac’s name. If it is missing, wait a moment or click Show on iPhone Again in Mobdev: an iPhone on the same Apple Account can miss the Mac until Mobdev offers it again.
- Turn on Settings › Accessibility › Touch › AssistiveTouch. It turns the pointer into taps. On the same page turn off Snap to Item and keep Perform Touch Gestures on, otherwise every swipe becomes a tap.
- In Mobdev, pick the keyboard layout that matches Settings › General › Keyboard › Hardware Keyboard on the iPhone.
Connect an agent
The app shows these with the right paths under Connect. They contain no secret.
Claude Code
claude mcp add --scope user mobdev -- /Applications/Mobdev.app/Contents/MacOS/Mobdev mcpClaude Code plugin, with the skills
/plugin marketplace add niklas-schmidt-dev/mobdev
/plugin install mobdev@mobdevCodex (~/.codex/config.toml)
[mcp_servers.mobdev]
command = "/Applications/Mobdev.app/Contents/MacOS/Mobdev"
args = ["mcp"]Claude Desktop, Cursor and others
{
"mcpServers": {
"mobdev": {
"command": "/Applications/Mobdev.app/Contents/MacOS/Mobdev",
"args": ["mcp"]
}
}
}HTTP clients can use http://127.0.0.1:4686/mcp with the token from Settings › API. The server speaks MCP 2026-07-28 and the earlier versions back to 2024-11-05.
From the terminal
Every tool also runs from a shell, for scripts and for agents that prefer a terminal to MCP. Mobdev tools lists them; values are JSON where they parse as JSON. It goes through the running app, so it reaches iPhones too, and works on its own with booted simulators and Android devices.
M=/Applications/Mobdev.app/Contents/MacOS/Mobdev
$M observe
$M tap_mark mark=4
$M type_text text="hello world" submit=true
$M screenshot --image screen.pngTools
Coordinates are pixels of the image screenshot returns (long edge 1280 px). Actions return a fresh screenshot unless you pass "screenshot": false. With more than one device, pass device (an id or name from list_devices) to pick one. Without it, Mobdev uses the only device, or the connected iPhone when simulators or Android devices run next to it.
| Tool | Arguments | What it does |
|---|---|---|
| list_devices | iPhones, simulators and Android devices: id, name, model, system version, ready | |
| status | Screen and input readiness, screenshot size | |
| screenshot | JPEG of the screen | |
| tap | x, y | Tap a point of the screenshot |
| long_press | x, y, seconds | Touch and hold |
| swipe | from_x, from_y, to_x, to_y, duration | Drag, e.g. to scroll |
| pinch | scale, x, y, duration | Two fingers apart or together, e.g. to zoom a map or photo |
| scroll | direction, amount, x, y | Mouse wheel |
| type_text | text, submit | Type into the focused field (up to 1000 characters per call) |
| type_secret | name, submit | Type a stored password or code without the agent seeing it |
| list_secrets | The names of the stored secrets, never their values | |
| press_key | key, modifiers | e.g. space + cmd for Spotlight |
| home | Go to the home screen | |
| open_app | name | Open an app by name or bundle ID (through Spotlight on an iPhone without Developer Mode) |
| read_screen | All visible text with positions (on-device OCR) | |
| find_text | text | Where a label is |
| tap_text | text, index | Tap a visible label |
| wait_for_text | text, timeout, gone | Wait for text to appear or disappear |
| observe | image, contains | The screen as numbered marks, cheaper than a screenshot; image draws them on one |
| tap_mark | mark | Tap a mark from the last observe |
| scroll_until_visible | text, id, direction, max_scrolls | Scroll until something shows; stops at the end of the list |
| wait_for_idle | timeout, stable | Wait until the screen stops changing |
| press_button | button | Volume, mute, play/pause; lock on simulators and Android |
| start_recording | path | Record the screen to an .mp4, by default into the project |
| save_screenshot | path | Save the screen at full resolution into the project's screenshots, e.g. for the App Store |
| stop_recording | End the recording and say where it is | |
| recent_steps | count, clear | The newest actions that worked, as steps for a test |
| run_shortcut | name | Run a shortcut from Apple’s Shortcuts app |
| ui_tree | contains, all | Elements from the accessibility tree (simulators, Android, iPhones in Developer Mode) |
| tap_element | id, text, regex, role, value, enabled, region, near, index, timeout | Tap an element by identifier, label, role, state or the label next to it |
| wait_for_element | id, text, regex, role, value, enabled, region, near, timeout, gone | Wait for an element to appear or disappear |
| run_flow | path, steps, variables, video | Replay a flow (JSON or Maestro YAML); stops at the first failing step and can save a video |
| assert_screenshot | name, threshold, mask, update | Compare the screen with a baseline picture; fails with a diff image |
| accessibility_audit | image, fail_on, ignore | Missing labels, small tap targets, low contrast and unclear labels |
| assert_with_ai | question, expect | Ask Apple Intelligence on the Mac a yes/no question about the screen |
| crawl_app | bundle_id, max_actions, seconds, avoid | Explore an app by itself and keep every crash with its steps |
| navigate_to | bundle_id, screen | Go to a screen that crawl_app mapped |
| list_projects | The projects Mobdev knows, which is active, and which one your calls use | |
| create_project | path, name, bundle_id, builds | Make a project in the app's repository and make it active |
| open_project | project | Make a project active: its folder, a repository with mobdev/, or its name |
| run_tests | project, tests, variables, video, language | Run a project's tests: every result, the first failure's screen, results.json and junit.xml |
| list_tests | project | A project's tests and its newest results |
| save_test | project, name, steps, description, platforms, file | Write a test into a project, creating it when needed |
| save_flow | project, name, steps, file | Write a flow into the project's flows/ |
| test_result | project, run | The newest run's results with each failure's step, screenshot and video |
| run_native_tests | project, platform, only, scheme, test_plan, task, wait, … | Run the app's own XCTest, Swift Testing or Gradle suite on the device, in the background |
| native_test_result | project, run | A native run's progress, else every failure with its file, line and attachments |
observe is the cheapest way for an agent to look: a numbered list of what can be read or tapped, such as [4] Button "Sign in" id=login, from the UI tree or, where there is none, from text recognition. tap_mark taps one by its number. wait_for_idle replaces fixed pauses after animations, and scroll_until_visible replaces guessed swipes.
tap_element and wait_for_element combine an identifier, a label or a regex with a role, a value, the enabled state, a part of the screen and the label next to it. {"role": "textfield", "near": {"text": "Email", "direction": "below"}} is the field under Email on iOS and Android alike. When nothing matches, the error lists what is on screen.
Passwords and codes of test accounts go into Settings › Secrets or Mobdev secret set. Agents type them with type_secret and only ever see the name, also in recorded flows and the activity. A test project’s secrets take the stored secret of the same name.
Inspect in a device’s toolbar shows the identifier and label of the element under the pointer, and a click copies the step that taps it, ready for a flow or test.
Device state
Tests and agents set up a situation directly instead of tapping through Settings: a location or route, permissions, a push notification, dark mode and text size, the language, a clean status bar for screenshots, Face ID, an app’s data and the orientation. Simulators and Android take almost all of it; an iPhone in Developer Mode takes the location, appearance, status bar, clipboard and orientation through Xcode 27.
| Tool | Arguments | What it does |
|---|---|---|
| set_location | latitude, longitude, route, speed, clear | A simulated position, or a route the device follows |
| set_permission | bundle_id, permission, state | Grant, revoke or reset location, photos, camera and more without the prompt |
| send_push | bundle_id, title, body, badge, data, payload | A push notification to a simulator app |
| set_appearance | dark, text_size, increase_contrast, reduce_motion | Dark mode, Dynamic Type and accessibility switches |
| set_language | language, bundle_id | e.g. de-DE, for the simulator or one Android app |
| set_status_bar | preset, time, battery_level, … | 9:41 and full bars for screenshots, or clear |
| biometrics | action | Answer a Face ID, Touch ID or fingerprint prompt |
| reset_app | bundle_id, keychain | Delete an app’s data as if it had just been installed |
| clipboard | text | Read the clipboard, or set it |
| set_orientation | orientation | Portrait or landscape |
{"set_permission": {"bundle_id": "com.example.MyApp", "permission": "location"}}
{"set_location": {"latitude": 52.52, "longitude": 13.405}}
{"send_push": {"bundle_id": "com.example.MyApp", "title": "Order shipped", "data": {"order": 7}}}
{"set_status_bar": {"preset": "screenshot"}}Simulators and Android
Booted iOS simulators and Android emulators and phones appear next to your iPhones, in the app and in list_devices, and take the same tools. There is nothing to set up on them: no cable, no Bluetooth, no Developer Mode.
- iOS Simulator needs Xcode. Mobdev reads the screen from the simulator itself and sends touches and keys the way Simulator.app does, so no window has to stay open. Install builds made for the simulator (
Debug-iphonesimulator); crash reports come from the Mac. - Android needs adb from the Android SDK, for example through Android Studio. Emulators and phones with USB debugging appear while adb runs. Install an
.apk,logsfollows logcat, andpress_keywithescapeis Back. Mobdev shows the screen and sends input through scrcpy’s server, which it brings along: video is fast, and any text types, emoji included.open_appmatches package names: “Settings” opens com.android.settings.
xcodebuild -scheme MyApp -destination 'generic/platform=iOS Simulator' \
-derivedDataPath build build
# install_app {"device": "iPhone 17", "path": "…/Debug-iphonesimulator/MyApp.app"}
# install_app {"device": "emulator-5554", "path": "…/app-debug.apk"}On both, ui_tree lists the elements on screen with their role, label and accessibility identifier, and tap_element and wait_for_element find them by identifier or label: steadier than OCR, and they find buttons that show only an icon. On an iPhone they need Developer Mode (see Set up the iPhone); without it agents use tap_text.
Turn them off under Settings › General in the app if you only want iPhones.
Simulators and AVDs that are not running are under Not Running in the sidebar: ▶ starts one, without a window since Mobdev shows the screen itself. New Simulator… makes one of any type and iOS version Xcode has. Agents do the same with these tools, which need no device. Mobdev deletes only simulators it made, and does not shut down a device a flow or test run is using.
| Tool | Arguments | What it does |
|---|---|---|
| list_virtual_devices | Every simulator and Android Virtual Device on the Mac, running or not | |
| boot_device | id, window | Start one and wait until it has booted; it then appears in list_devices |
| shutdown_device | id | Shut a simulator down or quit an emulator |
| create_simulator | device_type, runtime, name, boot | Make an iOS simulator of any type and iOS version Xcode has |
| delete_simulator | id | Delete a simulator that Mobdev made |
Build, run and debug
For apps you build, Mobdev also installs builds, launches them and reads their output and crash reports. The agent builds with xcodebuild, installs, drives the app with the tools above and reads the logs. These tools use Xcode’s devicectl, so they need Xcode on the Mac and Developer Mode on the iPhone (Settings › Privacy & Security › Developer Mode). Everything else works without them. Simulators and Android need neither.
| Tool | Arguments | What it does |
|---|---|---|
| list_apps | all | Apps installed for development, or every app |
| install_app | path, upload | Install a build from the Mac, or one uploaded through the relay: .app/.ipa, simulator .app or .apk |
| uninstall_app | bundle_id | Remove an app installed for development |
| launch_app | bundle_id, arguments, environment, restart | Launch an app and capture what it prints |
| stop_app | bundle_id | Stop a running app |
| open_url | url | Open a deep link, universal link or web page |
| logs | bundle_id, after, lines, contains | print, NSLog and os_log output, and how the app ended |
| crash_reports | app, name, limit | List crash reports, or read one: exception, reason, crashed thread |
xcodebuild -scheme MyApp -destination 'generic/platform=iOS' \
-derivedDataPath build -allowProvisioningUpdates build
# install_app {"path": "…/build/Build/Products/Debug-iphoneos/MyApp.app"}
# launch_app {"bundle_id": "com.example.MyApp"}
# logs {"bundle_id": "com.example.MyApp"}logs returns a cursor; pass it as after to get only new lines. When the app crashes, logs says so and crash_reports shows the report. Paths for install_app are on the Mac that runs Mobdev, also through a relay. Mobdev never removes App Store or system apps.
Developer Mode Control. Mobdev Runner is built with Xcode, signed with your development team and kept running on the iPhone, as WebDriverAgent does. While it runs, every tap, swipe, pinch, key and text goes through it, type_text types emoji too, and ui_tree, tap_element and wait_for_element work as on simulators. Turn it off for one iPhone in its info in Mobdev; that iPhone then uses Bluetooth.
Performance and React Native
performance samples a running app for up to a minute, and measure_launch times cold launches the way Xcode and Android report them. On an iPhone, performance records Instruments’ Activity Monitor, which takes 10 to 20 seconds longer, and measure_launch times the screen until it stays still. Budgets such as max_ms fail the step, so a test catches an app that got slower or heavier.
record_trace records any Instruments template on iPhones and simulators, such as Time Profiler, Allocations, Leaks, Network, SwiftUI or App Launch, attached to a running app, launching it, or for every process. On Android it records a Perfetto trace. The trace goes into the project’s output/traces, and the answer summarizes it where that is cheap: the heaviest functions and hangs, hitches, CPU and memory. Open it in Instruments, or at ui.perfetto.dev. device_load shows the whole device: its busiest processes, memory and thermal state. The device inspector in the app has the same under Performance.
| Tool | Arguments | What it does |
|---|---|---|
| performance | bundle_id, seconds, max_cpu, max_memory_mb, max_janky_percent | CPU, memory and, on Android, frames of a running app; budgets fail the call |
| measure_launch | bundle_id, runs, method, max_ms, stable | Cold launch time over several runs: fastest, median, slowest |
| record_trace | template, seconds, bundle_id, launch, path | An Instruments trace (iPhones, simulators) or a Perfetto trace (Android), with a short summary |
| device_load | count | The whole device: busiest processes by CPU and memory, memory used, thermal state |
| reload_app | port | Reload a React Native or Expo app through Metro, else through its developer menu |
| dev_menu | port | Open a React Native or Expo app’s developer menu |
{"measure_launch": {"bundle_id": "com.example.MyApp", "runs": 5, "max_ms": 1500}}
{"performance": {"bundle_id": "com.example.MyApp", "seconds": 20, "max_memory_mb": 300}}Network
The network tools show which requests an app makes, for debugging, for checking analytics and API calls, and for privacy audits. Nothing is installed on the device, and HTTPS is never decrypted. On iOS, Mobdev relaunches the app with Apple’s network diagnostics on and reads its log: method, host, status, bytes and time, but iOS hides paths and queries. On Android, Mobdev points the device at a proxy on the Mac: plain HTTP in full, HTTPS as host, bytes and time. Stopping the capture removes the proxy again, and a dev server on the Mac stays reachable as 10.0.2.2.
| Tool | Arguments | What it does |
|---|---|---|
| start_network_capture | bundle_id | Start recording an app’s requests |
| network_log | bundle_id, contains, after, query, headers | The recorded requests: method, URL, status, bytes and time |
| stop_network_capture | bundle_id | Stop recording; on Android this removes the proxy again |
| mock_response | url, status, body, content_type, clear | Android: a fixed answer for plain HTTP requests, e.g. an error from your API |
Flows and CI
A flow is a list of tool calls saved as JSON. Click Record in a device’s activity and everything you or an agent do on it is collected; on simulators, Android and iPhones in Developer Mode, clicks on named elements become tap_element, so the flow survives layout changes. Replay it with Run Flow…, the run_flow tool or, without the app, Mobdev flow. Every run stops at the first failing step and says which. Run Flow… keeps a video of each run; run_flow saves one where its video argument says. While a flow, a test run or a crawl runs, it holds its device: other agents get “Busy” until it ends, so nothing taps into the middle of it.
{
"name": "Sign in",
"steps": [
{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}},
{"tap_element": {"id": "email"}},
{"type_text": {"text": "me@example.com", "submit": true}},
{"wait_for_element": {"text": "Welcome"}}
]
}Control steps look at the screen instead of guessing waits: if runs steps when something is visible or on one platform, repeat runs them a number of times or until something shows, and retry runs them again when one fails.
{"if": {"visible": {"text": "Allow"}, "then": [{"tap_element": {"text": "Allow"}}]}}
{"repeat": {"until_visible": {"id": "checkout"}, "max": 10, "steps": [{"scroll": {"direction": "down"}}]}}
{"retry": {"times": 2, "steps": [{"tap_element": {"id": "pay"}}, {"wait_for_text": {"text": "Paid"}}]}}Maestro flows run as they are: run_flow, Mobdev flow and tests take .yaml files, and Mobdev convert turns a flow into the other format.
Mobdev convert login.yaml > login.json # Maestro to Mobdev
Mobdev convert login.json > login.yaml # Mobdev to MaestroMobdev flow runs on booted simulators and Android devices, for scripts and CI. It exits 0 when every step passed and 1 when one failed; --artifacts keeps a video of the run (run.mp4), the activity, crash reports and a screenshot of the failure. In CI, prefer tap_element to the OCR tools, which do not work on GitHub’s virtualized Macs.
/Applications/Mobdev.app/Contents/MacOS/Mobdev flow sign-in.json \
--device "$UDID" --artifacts flow-artifactsOn GitHub, the Mobdev action does all of it on a hosted Mac: it boots a simulator, installs your build, runs a test project or a flow, puts the results into the job summary and, if you want, a pull request comment, and keeps the videos and screenshots as an artifact.
- uses: niklas-schmidt-dev/mobdev/actions/test@main
with:
project: mobdev
comment: trueProjects
A project is a folder in your app’s repository, usually mobdev/, with everything Mobdev keeps for the app. Commit it all but output/, which brings its own .gitignore and appears only when Mobdev first writes there.
mobdev/
mobdev.json the app, its builds and what every test starts with
tests/ tests
flows/ saved flows
baselines/ assert_screenshot's reference pictures
screenshots/ save_screenshot's pictures, such as store screenshots
maps/ crawl_app's map of each app
output/ test runs, recordings, crawls, failed checks and flow videos- New Project… in the app writes
mobdev/mobdev.jsoninto the repository you choose, with the bundle ID it finds there in an Expo config, the Xcode project or Gradle, and Open Project… adds an existing one. Projects come first in the sidebar, each with its tests, flows, screenshots, app map, recordings and runs. - The project you select is the active one: recordings, screenshots, saved flows and tests, crawls and named baselines land there without a path.
- An agent’s working folder comes first:
Mobdev mcpsends the project of the folder it runs in, so agents in two repositories, or two worktrees of one, never save into each other’s project. Agents find projects withlist_projectsand switch withopen_project. - A new project’s overview says what to do next: connect your agent, let it write the first tests, run them and run them in CI, with prompts to copy that name the skill for each job.
- A test run stays in its project, even when another one becomes active meanwhile.
- Each project shows its app’s icon, found in the repository: the icon of an Expo
app.json, the iOS app icon or Android’s launcher icon. Choose any other image file in the project’s overview.
Tests
A project’s tests are files in its tests/ folder, and mobdev.json names the app, its builds per platform and the steps every test starts with. Each file is one test, a flow with a name, a description and the platforms it runs on. A test passes when every step does, so it ends with a wait that proves the result. ${NAME} takes a variable from the project, the environment or the run. A secret’s value comes from the run, the environment or a secret stored under Settings › Secrets, and never appears in results.
{
"name": "My App",
"app": {"bundle_id": "com.example.MyApp", "builds": {"simulator": "build/MyApp.app"}},
"before_each": [{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}}],
"variables": {"EMAIL": "me@example.com"},
"secrets": ["PASSWORD"]
}A project’s Tests in the app runs them on a device and shows every result with its steps, the screenshot of a failure and the video. Agents get the same through list_tests, save_test, run_tests and test_result: they drive the app until a path works, save it as a test, run it and fix it from the failing step. Mobdev test runs a project in CI without the app and writes results.json, junit.xml and a video and a screenshot per test.
/Applications/Mobdev.app/Contents/MacOS/Mobdev test mobdev/ \
--device "$UDID" --artifacts test-artifactsRepeat --device to run on several devices at once, or add --simulator "iPhone 17" to make a simulator of that type for the run and delete it afterwards. --emulator Pixel_9_Pro starts that Android Virtual Device read-only for the run, also several copies at once, and shuts it down again. --language de-DE runs the tests in that language, repeated in each language in turn, and puts the old one back afterwards. Each device and language gets its own folder, and summary.md is ready for a CI job summary. A test step like {"save_screenshot": {"path": "${LANGUAGE}/01-home"}} saves store screenshots in every language. When an agent has found a path through the app, recent_steps hands it the actions that worked, to save with save_test.
Native tests
run_native_tests runs the app’s own XCTest, XCUITest, Swift Testing or Gradle suite on the chosen device: xcodebuild test on a simulator or a Developer Mode iPhone, connectedDebugAndroidTest on an emulator or phone, and testDebugUnitTest on the Mac. Mobdev finds the workspace, scheme and gradlew in the repository, or reads them from native_tests in mobdev.json. Every failure comes back with its message, file and line, failed UI tests with their screen recording. Long runs go on in the background, so agents and relays are not cut off; native_test_result reads them. Each run keeps junit.xml and the result bundle, and the app shows them under Tests › Native Tests.
"native_tests": {
"ios": {"workspace": "MyApp.xcworkspace", "scheme": "MyApp", "test_plan": "CI"},
"android": {"gradle_root": "android", "task": "connectedDebugAndroidTest"}
}
Mobdev test mobdev/ --native --simulator "iPhone 17" --artifacts native-artifactsChecks
Three tools judge the screen and fail their step when it does not pass, so a test checks how a screen looks, not only what is on it.
{"assert_screenshot": {"name": "home", "mask": ["home.clock"]}}
{"accessibility_audit": {"fail_on": "error"}}
{"assert_with_ai": {"question": "Is any text cut off?", "expect": "no"}}assert_screenshotcompares the screen with a baseline picture. The first run records it in the project’sbaselinesfolder; commit it with your tests. A failure keeps a diff image with the changed regions marked, in the run or the project’soutput/. The status bar is left out, andmaskleaves out more, such as a clock or a map.accessibility_auditfinds buttons without a label, tap targets smaller than iOS and Android recommend, text below WCAG’s contrast ratios and labels that read like file names or identifiers.assert_with_aiasks Apple Intelligence on the Mac a yes/no question about the screen. The screenshot stays on the Mac, which needs Apple Intelligence turned on.
Explore an app
crawl_app explores an app by itself, without a model: it launches the app, taps every element it has not tried, maps the screens with screenshots and keeps every crash with the steps that cause it as a flow to replay. It never taps text fields or anything that reads like delete, pay, buy, send or sign out. The map goes into the project’s maps/ and the screenshots into its output/. navigate_to then goes straight to a mapped screen by its title.
{"crawl_app": {"bundle_id": "com.example.MyApp", "max_actions": 60}}
{"navigate_to": {"bundle_id": "com.example.MyApp", "screen": "Settings"}}App Store Connect
With an App Store Connect API key, Mobdev lists your apps in the sidebar with their builds, TestFlight groups, App Store versions and customer reviews, and agents read the same with the appstore_ tools. They need no device. app is a bundle ID or app ID; without it Mobdev takes the active project’s.
- In App Store Connect open Users and Access › Integrations › App Store Connect API and create a team key with the App Manager role.
- Download its .p8 file, which Apple offers only once, and copy the issuer ID above the list of keys.
- In Mobdev open Settings › App Store Connect, enter the issuer ID, choose the .p8 file and click Save Key.
The key stays on your Mac, readable only by you. No tool returns it and it never goes through a relay; Mobdev signs short-lived tokens with it and talks to Apple’s API only. Adding a build to a TestFlight group, setting What to Test and replying to reviews change what testers and customers see: agents may do that only after you turn on Agents may change App Store Connect in the same settings, and in the app each change asks first.
| Tool | Arguments | What it does |
|---|---|---|
| appstore_apps | The key’s apps: name, bundle ID, app ID | |
| appstore_builds | app, version, limit | Builds, newest first, with processing and TestFlight states |
| appstore_testflight | app | TestFlight groups, their testers count, public link and builds |
| appstore_versions | app, limit | App Store versions and their review state |
| appstore_reviews | app, rating, territory, unanswered, limit | Customer reviews and your replies |
| appstore_add_build_to_group | app, build, version, group | Give a TestFlight group a build |
| appstore_set_what_to_test | app, build, version, text, locale | Set a build’s What to Test |
| appstore_reply_to_review | review, text | Reply to a customer review, or replace your reply |
Skills
Skills give your agent whole workflows on top of the tools: the build and debug loop, smoke tests, reproducing a reported bug, App Store and Google Play screenshots, React Native and Expo apps, onboarding audits and competitor research. They work with Claude Code, Codex, Cursor and other agents that read SKILL.md files.
npx skills add niklas-schmidt-dev/mobdevOr copy a folder from skills/ into your agent’s skills directory.
Remote access
To reach the phone from another computer, the Mac keeps an outgoing WebSocket to a relay. No port is opened on the Mac, and the relay stores nothing.
- Sign in to the dashboard and create an access token.
- Click “Open in Mobdev”, or paste the token under Remote Access in the app.
- Copy the remote command from the app and run it on the other computer:
claude mcp add --transport http mobdev-remote \
https://relay.mobdev.sh/h/<mac-name>/mcp \
--header "Authorization: Bearer mdc_…"The client key (mdc_…) is derived from a secret that never leaves your Mac. Anyone with it can control the phone; “New Client Key” in the app revokes it. Revoking the access token in the dashboard disconnects the Mac.
Builds from anywhere
A cloud agent or a CI job builds the app in its own container, uploads the build to the Mac through the relay and installs it there with install_app and the upload’s id. The relay keeps nothing; the Mac deletes the build a day after its last use. The script needs only a shell and curl; on a Mac, Mobdev upload does the same.
curl -fsSLO https://raw.githubusercontent.com/niklas-schmidt-dev/mobdev/main/scripts/mobdev-upload.sh
export MOBDEV_URL=https://relay.mobdev.sh/h/<mac-name> MOBDEV_KEY=mdc_…
id=$(sh mobdev-upload.sh app-debug.apk)
# install_app {"upload": "<id>", "device": "emulator-5554"}Live view
Turn on “Allow live view” under Remote Access in the app, then click “Live” next to a device in the dashboard to watch its screen in the browser: click to tap, drag to swipe and type on it. “Share…” there creates a link that expires, to view only or to control, which anyone can open without an account until you revoke it. The app shows while someone watches and can stop it. Watching counts as active time. Agents open the same stream with the client key at /v1/live; see the relay’s protocol.
Limits of the hosted relay
Free connects 1 Mac and includes 20,000 requests and 10 active hours a month. Pro ($9 USD a month, plus applicable tax) connects 3 Macs and includes 1,000,000 requests and 300 active hours a month. Active time counts while a Mac works on a request; a connected Mac that waits costs nothing. The dashboard shows what you used. Mobdev on the Mac and a relay you run yourself have no limits.
When a limit is reached, the relay answers with HTTP 429 and a Retry-After header:
- More than 50 requests in 10 seconds with one client key.
- More than 4 requests waiting for the same Mac.
- The month’s requests or active time used up. Retry-After points to when they renew.
Run your own relay
The relay in the repository speaks the same protocol. It is one Go binary or a small Docker image:
cd relay
RELAY_HOST_ACCESS_TOKEN=choose-one go run .
# or
docker build -t mobdev-relay . && docker run -p 8080:8080 mobdev-relayPut it behind HTTPS and enter its URL in the app. With RELAY_HOST_ACCESS_TOKEN set, only Macs that know the token may connect.
Security and privacy
- The local API binds to 127.0.0.1, needs a bearer token and rejects browser requests.
- Text recognition runs on the Mac. No screen content leaves it unless your agent sends it to its model.
- The hosted relay stores your email, hashed access tokens, which Macs are connected and the iPhones they report. Requests and screenshots only pass through memory. See privacy.
- Agents act on your real phone with your accounts. Keep a person in the loop for anything that sends messages, pays or deletes.
- Under Settings › Agents, list apps agents may never open, such as your bank. It prevents mistakes; it is not a sandbox.
- Secrets that agents type are kept in a file only you can read. Agents see their names, never their values, and through a relay only the ones you allow.