Skip to main content
Glama

ios-device-mcp

ios-device-mcp: an MCP server for testing on a real iPhone

An MCP server that drives a real iPhone or iPad (not the simulator) over USB for app development testing. It can build and install your app, launch it with console log capture, read the accessibility tree, tap, swipe, type and take screenshots. It can also talk to Siri and press the Action button, so an agent can run your App Shortcuts and App Intents on the phone.

More about it: individuation.dev/projects/ios-device-mcp.

How it works

  • devicectl (Xcode's CoreDevice CLI) lists devices and installs, uninstalls and launches apps.

  • runner/ is a small XCUITest bundle whose single test never ends. It runs an HTTP server on the device (port 22087) and maps requests to XCUITest calls (XCUICoordinate.tap, typeText, snapshot(), XCUIScreen.screenshot()…).

  • src/ is the Node MCP server. It builds the runner with xcodebuild build-for-testing, starts it with xcodebuild test-without-building, and reaches it through the device's CoreDevice tunnel address (fd…::1). The tunnel works over USB and Wi-Fi with no port forwarding.

The driver starts automatically the first time a UI tool runs, takes about 5–8 s to start, and stops when the MCP server exits.

Related MCP server: open_phone

Requirements

  • macOS with Xcode 16+ (tested with Xcode 27.1) and xcodegen (brew install xcodegen); ffmpeg for audio recording (brew install ffmpeg)

  • Node 20+

  • An iPhone/iPad that is paired, has Developer Mode on, is unlocked, and is registered in your developer team

Setup

git clone <repo-url> ios-device-mcp && cd ios-device-mcp
npm install && npm run build

Your Team ID is in Xcode > Settings > Accounts (select the team), or on developer.apple.com under Membership. Then register the server with Claude Code for all your projects:

claude mcp add --scope user ios-device \
  -e IOS_DEVICE_MCP_TEAM_ID=<TEAMID> \
  -- node "$PWD/dist/index.js"

Other MCP clients: run node dist/index.js over stdio with the same env vars. See .mcp.example.json for a project-scoped config.

Env var

Purpose

IOS_DEVICE_MCP_TEAM_ID

Required. Team used to sign the runner

IOS_DEVICE_MCP_BUNDLE_PREFIX

Runner bundle ID prefix (default dev.iosdevicemcp.<team>)

IOS_DEVICE_MCP_DEVICE

UDID or name of the device to use

IOS_DEVICE_MCP_ALLOW_DEVICE_REGISTRATION

1 lets the build register a new device in your team

IOS_DEVICE_MCP_PORT

Driver port (default 22087)

IOS_DEVICE_MCP_AUDIO_DEVICE

Mac mic name substring for recordings (default: first non-iPhone/iPad input)

IOS_DEVICE_MCP_RECORDINGS_DIR

Where WAVs go (default .build/recordings)

The pre-rename IPHONE_MCP_* names are still read as a fallback.

Device selection: every tool takes a device argument (UDID or name). Without one, a tool uses the device chosen with select_device, then IOS_DEVICE_MCP_DEVICE, then the one device physically connected over USB. It never picks a Wi-Fi-only device implicitly, and with several on USB it asks for a choice.

Several devices at once: each device gets its own driver, so you can drive two phones in one session by passing device per call, or select_device one and pass device only for the other. Runner builds for different devices share one build directory and run one after another.

First run on a device: iOS asks for the passcode to allow UI automation. Keep the device unlocked while the driver starts.

Agent skill (optional)

The repo also ships a skill that teaches your agent how to use these tools well: the build → launch → describe → act → verify loop, element IDs over coordinates, waiting instead of sleeping, and the device-safety rules. Install it with the skills CLI:

npx skills add ChrisThoma/ios-device-mcp

Or copy skills/ios-device-mcp into ~/.claude/skills/. The skill only guides the agent; the MCP server above is what does the work.

Tools

Tool

What it does

list_devices, list_apps

Show devices (the current default is marked) and installed apps

select_device

Set the device tools use when they don't pass device; call with no device to clear

build_app

xcodebuild a project/workspace scheme for the device and install it

install_app, uninstall_app

Install a .app/.ipa, remove an app

launch_app

Launch/relaunch; captureLogs: true buffers the app's console: print/NSLog plus os_log/Logger (logLevel: debug default, info, stdout)

get_app_logs

Read captured output; grep filters by case-insensitive regex (e.g. a [category])

terminate_app

Force-quit an app

list_app_files, pull_file

List / copy files from an app's data container (dev-signed apps). pull_file can wait for a file (waitSeconds) or for one written after the last launch (modifiedAfter: "lastLaunch"); small text files come back inline

screenshot

JPEG scaled to points, so image pixels equal tap coordinates

describe_ui

Compact accessibility tree with @(x,y) tap centers, plus system alerts

tap, double_tap, long_press, swipe, scroll

Coordinate gestures (points). tap and tap_element take recordAudio: {seconds, leadIn?, audioDevice?} to record the Mac mic around the tap and report the tap's offset in the WAV

record_audio

Record the Mac mic for N seconds

set_volume

Set media volume to a level (0–1, 16 steps), verified on the system volume indicator

tap_element, wait_for_element

Find by text / accessibility ID / type, with waiting and scrollDirection scroll-to-find

type_text

Type into the focused field (submit presses return, deleteCount deletes first)

press_button

home, volumeUp, volumeDown, action, camera. action and camera run whatever the phone has them set to, and fail on a device without that button

siri

Send text to Siri as if spoken, or tap something it shows, and read back what it shows. Its first-run "Turn on shortcuts" prompt only works from Type to Siri (double-tap the bottom edge)

open_url

Open a URL or deep link

set_orientation

Rotate the interface

start_driver, stop_driver

Manage the on-device driver explicitly (rebuild: true forces a rebuild)

Text matching in tap_element tries exact label/ID/value matches first, then whole-word matches in short labels. Every result says which element was tapped. Give your app accessibility identifiers and match with id for the most reliable tests.

Testing Siri, App Shortcuts and the Action button

Tested on an iPhone 15 Pro Max with iOS 27.0.1 and Xcode 27.1, where the Action button was pressed as well. The Camera button hasn't been tried on a device yet.

The siri tool gives Siri a phrase as if it had been spoken and returns the text Siri shows. It really runs, so this tests the path from the phrase through your App Intent to what Siri says back:

siri text: "Check MyApp"
  → the text Siri shows, one line per element
siri tap: "Turn On"
  → taps a button or row Siri is showing
press_button name: "action"
  → runs what the Action button is set to
  • Siri's own UI: use tap on siri. A coordinate tap goes to the app behind Siri and dismisses it.

  • Timing: Siri has no finished signal, so the tool reads the screen after a fixed wait. Raise wait (default 6 s) for a slow intent.

  • First run of an app's shortcuts: the "Turn on shortcuts with Siri?" prompt doesn't work after text sent this way. It fails with "Something went wrong", with the logs putting the failure inside Siri, before the app. Use Type to Siri once instead: double_tap the bottom edge of the screen, tap the "Ask Siri" field so the keyboard shows, type_text the phrase, then tap the send arrow at the field's right end by its coordinates in a screenshot. Return adds a new line, and while the keyboard is up Siri reports its buttons below the screen, so siri tap: "Submit" hits a key. When the prompt shows, use siri tap: "Turn On". After that, text sent with siri works.

  • Action button: the tool presses it but doesn't change what it's set to. Choose the action in Settings first.

Known limitations

  • Foreground app: detected from the last launched app, else by scanning installed apps' states, else SpringBoard.

  • Taps go to the foreground app: coordinates are anchored to the app in front. Anchored to SpringBoard, a tap on an in-app alert button also reached the view behind the alert. A tap inside a system alert (permission prompt) is anchored to SpringBoard instead: anchored to the app, XCTest's interruption handling answered the alert itself, so "Don’t Allow" granted the permission.

  • Home button: XCUIDevice.press(.home) is ignored on some iOS 26 builds, so the driver falls back to activating SpringBoard. The press takes about 2–3 s.

  • Landscape: coordinates are screen points in the current orientation, and landscape has had little testing.

  • Logs: os_log/Logger capture works like Xcode's console: the app is launched with OS_ACTIVITY_DT_MODE (and OS_ACTIVITY_MODE=debug), which mirrors its log messages to stderr. Only messages from the app's own process are captured, not from system daemons or extensions. Private values show in plain text. Lines show [category] but not the subsystem.

  • Recording sync: recordings use ffmpeg (brew install ffmpeg), and the terminal needs microphone permission. Recording start is measured to about ±70 ms. XCUITest takes about 0.4–0.7 s to deliver a tap, so the result gives a window ("sent at … completed at …"), and in testing the sound always landed inside it.

  • Volume: iOS has no API to set the volume, and the level an app can read goes stale. set_volume presses the buttons and reads SpringBoard's volume indicator to verify the level, so the indicator briefly shows on screen.

  • Siri: phrases are sent as text, so this doesn't test how Siri hears them. Anything Siri can do happens for real, including sending messages.

  • Camera button: press_button with camera hasn't been tried on a device yet. The Action button has, on an iPhone 15 Pro Max.

  • No lock/unlock or Face ID simulation.

License

MIT. See LICENSE.

Available Tools

29 tools
build_appA

Build an Xcode project/workspace scheme for the device and (by default) install it. Returns the bundle ID. Signing uses the project's own settings.

Inputs: scheme (required), device, install, project, +2 more.

describe_uiA

Accessibility tree of the foreground app (plus any system alert), one element per line: Type "label" #identifier value=… @(centerX,centerY) WxH. Coordinates are in points and can be passed to tap.

Inputs: device, format, bundleId, includeOffscreen.

double_tapB

Double-tap at coordinates (points).

Inputs: x (required), y (required), device.

get_app_logsA

Read console output captured from an app started with launch_app captureLogs=true. os_log/Logger lines look like <timestamp> App[pid:tid] [category] message.

Inputs: bundleId (required), grep, clear, lines, +1 more.

install_appB

Install a built .app (or .ipa) onto the device.

Inputs: path (required), device.

launch_appB

Launch (or relaunch) an app in the foreground. With captureLogs, the app's console output — print/NSLog and os_log/Logger messages — is buffered for get_app_logs.

Inputs: bundleId (required), device, logLevel, arguments, +2 more.

list_app_filesA

List files in an app's data container (Documents, Library, tmp…). Works for development-signed apps only.

Inputs: bundleId (required), device, subdirectory.

list_appsC

List apps installed on the device.

Inputs: device, includeSystem.

list_devicesA

List physical iPhones/iPads known to this Mac, with connection state. The device tools use by default is marked.

Inputs: no named parameters.

long_pressB

Press and hold at coordinates (points).

Inputs: x (required), y (required), device, duration.

open_urlC

Open a URL or deep link (custom scheme or universal link) on the device.

Inputs: url (required), device.

press_buttonA

Press a hardware button. "home" goes to the home screen. "action" and "camera" run whatever the phone has them set to, and fail on a device without that button.

Inputs: name (required), device.

pull_fileB

Copy a file out of an app's data container to the Mac. Can wait for the file to appear, or to be written after the last launch_app. Small text files are returned inline.

Inputs: path (required), bundleId (required), device, destination, +2 more.

record_audioA

Record the Mac microphone for a number of seconds (e.g. to capture what the device plays). Returns the WAV path.

Inputs: seconds (required), audioDevice.

screenshotA

Take a screenshot. By default it is scaled to screen points, so image pixel coordinates equal tap coordinates.

Inputs: device, fullRes.

scrollA

Scroll the content in a direction ("down" reveals content further down) by swiping from the middle of the screen.

Inputs: direction (required), amount, device.

select_deviceA

Choose which device later calls use when they don't pass "device". Lasts for this server session and is shared by every agent using it, so parallel agents on different devices should pass "device" instead. Omit device to clear the choice.

Inputs: device.

set_orientationB

Rotate the device's interface orientation.

Inputs: orientation (required), device.

set_volumeA

Set the device's media volume by pressing the volume buttons toward the level, verifying each step on the system volume indicator. iOS volume has 16 steps, so levels round to the nearest 1/16. Returns the final level.

Inputs: level (required), device.

siriA

Send text to Siri as if it had been spoken, or tap something Siri is showing, then return the text Siri shows. Use tap for Siri's buttons, because a coordinate tap goes to the app behind Siri and dismisses it. Anything Siri can do runs for real, including App Intents. An app's first-run "Turn on shortcuts with Siri?" prompt fails with "Something went wrong" after text sent this way. Use Type to Siri for that once: double_tap the bottom edge of the screen, tap the "Ask Siri" field so the keyboard shows, type_text the phrase, then tap the send arrow at the field's right end by coordinates from a screenshot (return adds a line, and while the keyboard is up Siri reports its buttons below the screen, so tap: "Submit" hits a key). When the prompt shows, use tap: "Turn On". Later phrases work with text.

Inputs: tap, text, wait, device.

start_driverA

Start the on-device XCUITest driver (UI tools start it automatically). The first run on a device builds the runner and iOS asks for the passcode to allow UI automation.

Inputs: device, rebuild.

stop_driverC

Stop the on-device driver.

Inputs: device.

swipeC

Drag from one point to another (points).

Inputs: toX (required), toY (required), fromX (required), fromY (required), +2 more.

tapB

Tap at screen coordinates (points).

Inputs: x (required), y (required), device, recordAudio.

tap_elementB

Find an element by text/identifier/type, waiting for it to appear, and tap its center.

Inputs: id, text, type, exact, +6 more.

terminate_appC

Force-quit an app.

Inputs: bundleId (required), device.

type_textA

Type into the focused text field (tap the field first). Use \n for return.

Inputs: text (required), device, submit, deleteCount.

uninstall_appB

Uninstall an app (and its data) from the device.

Inputs: bundleId (required), device.

wait_for_elementB

Wait until an element appears (or disappears with gone=true).

Inputs: id, gone, text, type, +6 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    B3.3/5.0

    Scored across 29 tools

    Disambiguation4/5

    Most tools target a clearly distinct action: tap, double_tap, long_press, swipe, and type_text don't collide, and app lifecycle tools (build/install/uninstall/launch/terminate) are cleanly separated. The only soft overlap is scroll versus swipe (scroll is a convenience wrapper around swiping from the middle) and tap versus tap_element, but the descriptions make the intended usage clear enough.

    Naming Consistency4/5

    Overwhelmingly consistent snake_case verb_noun (type_text, press_button, list_apps, launch_app, get_app_logs, set_orientation) with a few bare single-verb names like tap, swipe, scroll, screenshot, and siri. The deviations are short, well-understood primitives rather than a different convention, so the pattern remains predictable.

    Tool Count3/5

    29 tools is on the heavy side for a device-automation surface, and a few are infrastructure-ish (start_driver/stop_driver, which the UI tools start automatically, and select_device) that an agent rarely needs to think about. Each tool does map to a real capability, so it's justified but at the upper edge of comfortable scoping.

    Completeness4/5

    The surface covers UI interaction, accessibility inspection, screenshots, audio capture, volume, Siri, orientation, app build/install/launch/lifecycle, logs, and app-container file listing/pulling, which is strong coverage for iOS device control. Minor gaps remain: no push/write file into the container, no video capture, and no device status/info (battery, OS version, network) tool.

    Maintenance

    ActivityMaintained
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      Enables AI agents to control real iPhones and simulators on macOS, allowing for UI interaction, testing, and automation.
      65 npm
      95
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables agents to control a physical iPhone from macOS through local phone tools, including USB capture, Bluetooth HID, and AssistiveTouch interactions. It also exposes relay tools for remote access over Streamable HTTP with optional OAuth.
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Drives a physical iPhone or iPad over Appium and WebDriverAgent (XCUITest) from a Mac, letting an agent inspect the screen, tap, type, swipe, launch apps, and run Shortcuts. Safe-by-default access modes, app allowlists, and elicitation-gated destructive operations keep admin actions behind explicit approval.
      6
      45 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables AI agents to operate a real iPhone from a Mac by reading the screen over USB and sending taps, swipes, typing, and app launches over Bluetooth, with every action returning a fresh screenshot so the agent can verify its work. Runs as a local MCP server, so agents can test iOS apps on physical devices and automate everyday phone tasks.
      38 npm
      10
      MIT