ios-simulator-mcp
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, underContents/Developer/ApplicationsDevice Hub, under
Contents/Applications/DeviceHub.app
open -a Simulator✅ works
❌ "Unable to find application named 'Simulator'"
set_orientation/get_orientation❌ unavailable
✅ via
devicectlEverything 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 withDEVICE_UI_APPif 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.jsonworks on both machines.Xcode 27 separately made
xcrun devicectlsimulator-aware — it lists simulated devices (Reality: simulated) and adds capabilitiessimctlnever had, notably rotation.simctlandidbthemselves 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 |
|
Tap, swipe, type, key press, home button, accessibility tree | |
Orientation (rotate / query) |
|
Device UI window |
|
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 |
|
Homebrew |
|
Python 3.9–3.12 (for the |
|
Node.js 18+ (20+ recommended) |
|
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 simulatorsApple Silicon vs Intel:
idb-companioninstalls to/opt/homebrew/binon Apple Silicon and/usr/local/binon Intel. Both are searched automatically; override withIDB_COMPANION_DIRif 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.mjsto 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 nodeand 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 onPATH.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_appuse it automatically. If several apps are candidates it says so and asks for an explicitbundleIdrather than guessing.Which dev server — probes
127.0.0.1:8081(and:8082for 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:8081only works with Expo Go. Anexpo-dev-clientbuild registersexp+<slug>instead, and opening the wrong scheme fails withLSApplicationWorkspaceerror 115 ("Simulator device failed to open"). Reading it from the bundle is what makesopen_appwork 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 |
| (auto-detected) | Overrides which app |
| (auto-detected) | Overrides the |
| (auto-detected) | Overrides the |
|
| Fallback device name (Xcode-version dependent) |
| (auto) | App name or absolute |
|
| Device Hub bundle id (Xcode 27+); only used if the name lookup fails |
|
| Usually identical everywhere. Also used to reach |
|
| Where you installed |
|
| Intel Mac → |
| temp dir + | Where PNGs are saved |
Using it
Start Metro in your app:
npm start(app on 8081) ornpm run storybook:server(Storybook on 8082).Boot a simulator with
boot_device, thenopen_app(oropen_storybook).screenshotto see the screen.get_accessibility_treeto find an element's center point.tap/swipe/type_textto drive it, thenscreenshotagain to confirm.
Tools
Tool | What it does |
| Capture the screen; returns PNG + path + pixel size + logical (point) size + scale |
| Dump elements with labels, roles, frames, and computed centers |
| Simulator lifecycle |
| Start/stop an app by bundle id (auto-detected, or |
| Deep links. |
| Tap at (x, y) in points |
| Tap at a 0..1 fraction of the screen |
| Swipe between two points |
| Type into the focused field |
| Send a HID key code (40 = Return, 42 = Backspace, 44 = Space) |
| Press the Home button |
| Rotate: |
| Report the current physical orientation |
| 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 |
| Run |
| Run |
| Use the full path |
| Set |
MCP server won't start in VS Code | Use |
Tools don't appear in chat | Reload window; approve the server in Copilot Chat → MCP settings |
| Run |
| The URL scheme isn't registered by any installed app. Detection reads the scheme from the bundle — if you've overridden |
| Several apps installed/running. Pass |
| Start Metro ( |
| Expected on Xcode 27 — Simulator.app is gone. The server uses Device Hub; set |
| Leftovers from uninstalled runtimes. They're tagged |
Rotation has no effect |
|
| 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 |
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.jsonDevelopment
npm run dev # tsx watch — edits to src/ restart the server
npm run build # type-check + compile to dist/