Skip to main content
Glama
shaun-hutch

ios-simulator-mcp

by shaun-hutch

ios-simulator-mcp

⚠️ Disclaimer

This project is entirely vibe-coded. Every line of it was written by AI — specifically DeepSeek V4 Flash running inside GitHub Copilot — through back-and-forth conversation. It was not designed, hand-written, reviewed, or audited by a human engineer. Assume there are bugs, questionable decisions, and design flaws that a person would have caught.

It exists for one simple reason: I wanted a way to control the iOS Simulator and Device Hub from an AI assistant, and this was the fastest path there. It is a personal tool that I'm sharing in case it's useful to someone else — not production software, and not something anyone should depend on.

Use it at your own risk. Read the code before trusting it with anything that matters. No warranty, no support, and no affiliation with Apple.

If you want to understand what AI-written code looks like, or you just need to poke at a simulator from a chat window, you're in the right place. 🙂

An MCP server that gives an AI assistant (e.g. VS Code Copilot) "eyes and hands" on the iOS Simulator — the same idea as browser automation, but for your simulator:

  • See — screenshots, returned as images the assistant can view directly.

  • Act — taps, swipes, typing, key presses, home button, app launch.

  • Target — an accessibility tree with element frames and computed centers.

Built and verified on macOS with Xcode 27 / iOS 27, Node 24, and Homebrew 6. Xcode ≤ 26 is supported too — see Xcode version support below; only the device-UI app and rotation differ. Developed against an Expo / React Native dev-client app, but it works with any app installed in a simulator.

Xcode version support (26 and 27)

The server runs on both. Only the device-UI app and rotation differ:

Xcode ≤ 26

Xcode 27

Device UI app

Simulator.app, under Contents/Developer/Applications

Device Hub, under Contents/Applications/DeviceHub.app

open -a Simulator

✅ works

"Unable to find application named 'Simulator'"

set_orientation / get_orientation

❌ unavailable

✅ via devicectl

Everything else — screenshot, boot, tap/swipe/type, accessibility tree

The UI app is found by probing the active developer dir (xcode-select -p) and testing both locations on disk, so neither version needs configuration — including the fact that Device Hub lives in a non-standard path that LaunchServices does not reliably index. Override with DEVICE_UI_APP if your Xcode lives somewhere unusual.

On Xcode ≤ 26 the two orientation tools stay registered but return a clear "not available" error rather than disappearing, so the same .vscode/mcp.json works on both machines.

Xcode 27 separately made xcrun devicectl simulator-aware — it lists simulated devices (Reality: simulated) and adds capabilities simctl never had, notably rotation. simctl and idb themselves are unchanged.


How it works

A thin stdio MCP server (Node + TypeScript, @modelcontextprotocol/sdk) that shells out to existing macOS CLIs — no iOS code and no app changes required:

Capability

CLI

Screenshot, list/boot/shutdown simulators, launch/terminate apps, open URLs, light/dark mode

xcrun simctl

Tap, swipe, type, key press, home button, accessibility tree

idb ui

Orientation (rotate / query)

xcrun devicectl

Device UI window

openDevice Hub (Xcode 27+) or Simulator.app

The assistant is multimodal, so a screenshot returned from the screenshot tool is directly visible — no OCR needed.


Prerequisites

Tool

Check with

macOS + Xcode (provides xcrun simctl)

xcrun simctl list devices

Homebrew

brew --version

Python 3.9–3.12 (for the idb venv)

python3 --version

Node.js 18+ (20+ recommended)

node --version

VS Code with Copilot Chat (or any MCP client)


Setup on a fresh Mac

1. Install idb (the touch-input layer)

xcrun simctl cannot send taps/swipes/typing, so we add Facebook's idb (open source, from Meta — https://github.com/facebook/idb):

brew tap facebook/fb
brew trust facebook/fb        # Homebrew 6+ requires tap trust; skip on older Homebrew
brew install idb-companion

python3 -m venv ~/.local/idb-venv
~/.local/idb-venv/bin/pip install --upgrade pip fb-idb

~/.local/idb-venv/bin/idb list-targets   # sanity check — should list simulators

Apple Silicon vs Intel: idb-companion installs to /opt/homebrew/bin on Apple Silicon and /usr/local/bin on Intel. Both are searched automatically; override with IDB_COMPANION_DIR if needed (see Configuration).

2. Install and build the server

cd ios-simulator-mcp
npm install
npm run build                 # compiles src/ → dist/

To move to another machine you only need src/, dist/, package.json, package-lock.json, tsconfig.json, and this README (node_modules/ is reinstalled).

3. Register with VS Code Copilot

Shortcut: this repo ships a setup skill that automates all of the below — run node .github/skills/setup-ios-simulator-mcp/scripts/doctor.mjs to check the whole install (prerequisites, build freshness, MCP registration, a live server handshake) and print the fix for anything that's wrong. See .github/skills/setup-ios-simulator-mcp/SKILL.md.

Create a .vscode/mcp.json in your workspace (or add the server via Copilot Chat → MCP settings) using absolute paths:

{
  "servers": {
    "ios-simulator": {
      "type": "stdio",
      "command": "/usr/local/bin/node",
      "args": ["/absolute/path/to/ios-simulator-mcp/dist/index.js"]
    }
  }
}

No env block is needed — see Point it at your app below.

  • Find your Node path with which node and use that exact value.

  • VS Code launches GUI processes with a minimal PATH, so absolute paths are required — don't rely on nvm/Homebrew being on PATH.

  • Reload the window, then approve the new ios-simulator server in Copilot Chat → MCP settings.

4. Point it at your app (optional)

You shouldn't normally need to configure anything. The server is app-agnostic — no defaults for any particular app, and no paths to your codebase — but it works out what to target by inspecting the simulator:

  • Which app — the only non-system app running in the simulator, else the only one installed. launch_app / terminate_app use it automatically. If several apps are candidates it says so and asks for an explicit bundleId rather than guessing.

  • Which dev server — probes 127.0.0.1:8081 (and :8082 for Storybook) for a Metro server that's actually listening.

  • Which deep link — reads the URL schemes the installed app registers, so an Expo dev client gets the exp+<slug>://expo-development-client/?url=... link it actually accepts.

Why the deep link is read from the app: a bare exp://127.0.0.1:8081 only works with Expo Go. An expo-dev-client build registers exp+<slug> instead, and opening the wrong scheme fails with LSApplicationWorkspace error 115 ("Simulator device failed to open"). Reading it from the bundle is what makes open_app work without configuration.

The environment variables below exist only to override that detection, for when it can't work it out: an app that isn't installed or running yet, several candidate apps, or a dev server on a non-standard port.

5. Running on more than one Mac

mcp.json uses absolute paths, so a config copied between machines may point at a node that doesn't exist there. On the second machine, run which node and update command (and the args path to this repo). Nothing else needs changing: the Xcode 26/27 differences are detected at runtime, and idb/Homebrew paths are already overridable via env vars.


Configuration (environment variables)

Set these in the env block of .vscode/mcp.json (recommended) or your shell. Values in bold are the ones most likely to differ on another Mac.

Variable

Default

Notes

SIM_APP_BUNDLE_ID

(auto-detected)

Overrides which app launch_app / terminate_app default to

SIM_APP_URL

(auto-detected)

Overrides the open_app deep link (skips port probing)

SIM_STORYBOOK_URL

(auto-detected)

Overrides the open_storybook deep link

DEFAULT_SIMULATOR

iPhone 17

Fallback device name (Xcode-version dependent)

DEVICE_UI_APP

(auto)

App name or absolute .app path opened after booting. Auto = Device Hub, then Simulator.app

DEVICE_HUB_BUNDLE_ID

com.apple.dt.Devices

Device Hub bundle id (Xcode 27+); only used if the name lookup fails

XCRUN_PATH

/usr/bin/xcrun

Usually identical everywhere. Also used to reach devicectl

IDB_PATH

~/.local/idb-venv/bin/idb

Where you installed fb-idb

IDB_COMPANION_DIR

/opt/homebrew/bin

Intel Mac → /usr/local/bin

SCREENSHOT_DIR

temp dir + ios-simulator-mcp

Where PNGs are saved


Using it

  1. Start Metro in your app: npm start (app on 8081) or npm run storybook:server (Storybook on 8082).

  2. Boot a simulator with boot_device, then open_app (or open_storybook).

  3. screenshot to see the screen.

  4. get_accessibility_tree to find an element's center point.

  5. tap / swipe / type_text to drive it, then screenshot again to confirm.

Tools

Tool

What it does

screenshot

Capture the screen; returns PNG + path + pixel size + logical (point) size + scale

get_accessibility_tree

Dump elements with labels, roles, frames, and computed centers

list_devices / boot_device / shutdown_device

Simulator lifecycle

launch_app / terminate_app

Start/stop an app by bundle id (auto-detected, or SIM_APP_BUNDLE_ID)

open_url / open_app / open_storybook

Deep links. open_app / open_storybook build the dev-client URL for a listening Metro server

tap

Tap at (x, y) in points

tap_normalized

Tap at a 0..1 fraction of the screen

swipe

Swipe between two points

type_text

Type into the focused field

press_key

Send a HID key code (40 = Return, 42 = Backspace, 44 = Space)

press_home

Press the Home button

set_orientation

Rotate: portrait / portraitUpsideDown / landscapeLeft / landscapeRight (via devicectl)

get_orientation

Report the current physical orientation

toggle_appearance

Light/dark mode

Coordinate conventions: tap/swipe use points (same space as the accessibility tree). Screenshots are 3× that (pixel size ÷ 3 = points on modern iPhones). tap_normalized sidesteps scale entirely.

Rotating via set_orientation swaps the screenshot's width/height (e.g. 1206×2622 portrait ↔ 2622×1206 landscape) and the point space with it, so re-read the accessibility tree after rotating rather than reusing old coordinates.


Troubleshooting

Symptom

Fix

No available formula with the name "idb-companion"

Run brew tap facebook/fb first (tap for facebook/idb)

Refusing to load formula from untrusted tap

Run brew trust facebook/fb

idb: command not found

Use the full path ~/.local/idb-venv/bin/idb, or set IDB_PATH

idb_companion not found when the server runs

Set IDB_COMPANION_DIR to your Homebrew bin

MCP server won't start in VS Code

Use which node for command; absolute path in args

Tools don't appear in chat

Reload window; approve the server in Copilot Chat → MCP settings

No booted simulator

Run boot_device first, or xcrun simctl boot "iPhone 17"

open_app fails with LSApplicationWorkspaceErrorDomain error 115

The URL scheme isn't registered by any installed app. Detection reads the scheme from the bundle — if you've overridden SIM_APP_URL with a bare exp:// URL, swap it for the app's exp+<slug>://expo-development-client/?url=... link

Could not detect a single app

Several apps installed/running. Pass bundleId, or set SIM_APP_BUNDLE_ID

No dev server responding on :8081

Start Metro (npm start), or set SIM_APP_URL

Unable to find application named 'Simulator'

Expected on Xcode 27 — Simulator.app is gone. The server uses Device Hub; set DEVICE_UI_APP to override

list_devices shows duplicates (e.g. three iPhone 17 Pro)

Leftovers from uninstalled runtimes. They're tagged UNAVAILABLE and never auto-selected; pass an explicit udid to disambiguate

Rotation has no effect

devicectl needs the device booted; check with get_orientation

Could not set orientation… on another Mac

Xcode ≤ 26 has no simulator rotation — expected. Everything else still works

Screenshot shows the old screen right after a tap

Wait ~300ms for the navigation animation

Element has no label in the tree

Add testID/accessibilityIdentifier in your app for stable ids


Project layout

ios-simulator-mcp/
├── src/
│   ├── index.ts     # MCP server + tool definitions
│   ├── config.ts    # paths, app identity, ports (env-overridable)
│   ├── exec.ts      # child-process runner (adds Homebrew to PATH)
│   ├── simctl.ts    # xcrun simctl helpers (screenshot, boot, launch…)
│   ├── devicectl.ts # xcrun devicectl helpers (orientation; Xcode 27+)
│   └── idb.ts       # idb ui helpers (tap/swipe/type/tree)
├── dist/            # compiled output (npm run build)
├── package.json
└── tsconfig.json

Development

npm run dev      # tsx watch — edits to src/ restart the server
npm run build    # type-check + compile to dist/