@mgcrea/mcp-ios-simulator
@mgcrea/mcp-ios-simulator
Model Context Protocol server for driving an iOS Simulator from a model: screenshot the screen, read its accessibility tree, tap, swipe, type, manage the app under test — and stage the device itself, which is the part no physical device can do.
No Apple Developer team. No code signing. No device trust. No tunnel. Nothing to unlock. And screenshots work with nothing installed at all.
Features
See the screen, with zero setup.
simctlcaptures it directly, scaled to exactly the device's point size, so a position read off the image is a tap coordinate with no conversion. No WebDriverAgent needed for this.Read the screen. A flattened, pruned accessibility tree — type, label, identifier and the precomputed tap point per element — instead of the tens of KB of nested JSON WebDriverAgent actually returns.
Drive the screen. Tap by coordinate or by accessibility label, swipe, type, press Home. Every action returns the resulting screen by default, so a mis-aimed tap is visible on the call that made it.
Stage the device. Dark mode, Dynamic Type size, increased contrast, a frozen 9:41 status bar, a simulated location, and app permissions granted or denied without a prompt anyone has to tap. None of this is possible on real hardware.
Push without APNs. Deliver a real remote notification with no certificate and no server.
Manage the app. Install a build, launch it with fixture arguments, terminate it, open a deep link. An app's data container is an ordinary path on this Mac, so its database or logs are read directly — there is nothing to copy off a device.
Security
Writes are on by default, and that is deliberate. The rest of the fleet is
read-only until a flag is set because a mutating tool acts on someone's real
account or someone's real phone. A simulator is neither: it holds no person's
data, and ios_simulator_erase puts it back to factory in seconds.
What that still costs you, stated honestly:
installandlaunchrun code on your Mac. A simulated process is a host process.The app's data container is a plain host path, readable by anything.
eraseis irreversible — it is the only tool behind an explicitconfirm.IOS_SIMULATOR_ALLOW_WRITES=0restores the device server's posture in one variable, and then the fourteen driving tools are absent fromtools/listrather than refused, because a refusal still lets a model try, retry and reason about a way around it.
Your credentials. There are none. The server holds no tokens and talks to no
vendor API. Everything goes through xcrun simctl and a loopback HTTP server.
Supply chain. Three runtime dependencies: @modelcontextprotocol/server,
zod, and @mgcrea/mcp-ios-core — our own, and itself
dependent on only the first two. Scaling images uses sips and reading property
lists uses plutil, both of which ship with macOS, specifically so neither an
image library nor a plist parser has to be installed.
How it reaches the simulator
Lane | Carries | Needs |
| simulators, apps, install, launch, deep links, the screenshot, appearance, status bar, location, permissions, push, boot/shutdown/erase | Xcode. Nothing else. |
WebDriverAgent over HTTP | the accessibility tree, tap, swipe, type, buttons | a runner started once with |
The asymmetry with mcp-ios-device is worth stating plainly: there, every
pixel comes through WebDriverAgent, so nothing about the screen works until a
signed runner is installed and trusted on the phone. Here simctl captures the
screen itself, so fourteen of the nineteen tools work with no runner at all
— only ios_simulator_ui_tree and the five input tools need one, and building it
takes about two minutes with no Apple account.
ios_simulator_diagnostics reports both lanes separately, so "the simulator is
fine but the runner is not up" is a distinguishable answer rather than a generic
failure.
Configure
Variable | Default | What it does |
| the only booted simulator | UDID, name, or |
| on |
|
| none | Launch arguments applied when |
|
| Must match the runner's |
|
| Explicit override; rarely needed. |
|
| Byte cap on a |
|
| Budget for one |
|
| Budget for one WebDriverAgent call. |
|
| Screenshots, launch logs, the runner log. |
| off | Log every |
The same keys in camelCase can go in ~/.config/ios-simulator-mcp/config.json
(IOS_SIMULATOR_CONFIG to move it). The environment wins per field, so a
one-off IOS_SIMULATOR_ALLOW_WRITES=0 beats a file that says true. Unknown keys
in the file are an error rather than silently ignored — a typo that looks like
"that setting had no effect" is the worst way to learn where your configuration
came from.
See .env.example for the annotated version.
Quick start
Requires macOS with Xcode and a booted simulator.
A. Everything except the tree and the taps, with no setup
npx -y @mgcrea/mcp-ios-simulatorlist, list_apps, screenshot, diagnostics, power, install, launch,
open_url, set_environment, push and erase all work immediately.
B. The input lane
ui_tree, tap, tap_element, swipe, type and press_button need a
WebDriverAgent runner. Build and start it — no team id, no signing, no prompt:
scripts/wda.sh setup # ~2 minutes
scripts/wda.sh run # leave this running
scripts/wda.sh status # is it answering?Installed from npm, the same script is the ios-simulator-wda binary:
npx -p @mgcrea/mcp-ios-simulator ios-simulator-wda setup
npx -p @mgcrea/mcp-ios-simulator ios-simulator-wda runC. Wired into a client
See .mcp.json.example.
Tools
Twenty-one. Six are read-only; the other fifteen disappear with
IOS_SIMULATOR_ALLOW_WRITES=0.
Tool | Writes? | What it does |
| Every simulator, with state and whether its runtime is installed | |
| Both lanes, the resolved target, the geometry, and who owns the WDA port | |
| Installed apps, with bundle id and host-path data container | |
| The screen, in point space — no runner needed | |
| Addressable elements with precomputed tap points | |
| Poll until something appears, or goes away | |
| ✓ | Tap a point |
| ✓ | Tap by identifier, label or predicate |
| ✓ | Drag between two points |
| ✓ | Type into the focused field, or a named one |
| ✓ | Home |
| ✓ | Boot or shut down; never implicit |
| ✓ confirm | Wipe to factory — the only irreversible tool |
| ✓ | Install a simulator |
| ✓ | Launch, with fixture arguments and captured output |
| ✓ | Kill a running app |
| ✓ | Deep links and universal links |
| ✓ | Appearance, Dynamic Type, contrast, status bar, location, permissions |
| ✓ | Seed the photo library — the way around the missing camera |
| ✓ | A remote notification, with no APNs certificate |
| ✓ | Start or restart the runner, detached |
Deliberately absent: create, clone, delete, rename, upgrade and pair
(fleet management, not driving, and delete all is a footgun with no upside);
get_app_container (list_apps already returns the path); uninstall
(install overwrites, and erase covers first-run properly); keychain,
pbcopy, spawn, diagnose and recordVideo (real capabilities that an agent
would use approximately never, and every tool costs listing bytes on every
connect).
addmedia was on that list until 0.2.0, and it was the wrong call. A simulator
has no camera, so seeding the photo library is not a nice-to-have — it is the
only way an app whose first step is "choose a photo" can be driven here at all.
Anyone hitting that had to drop out to a shell, which is exactly what these
tools exist to avoid.
Traps worth knowing
All measured on Xcode 26.6 (17F113).
simctl io … screenshot -does not write to stdout. The help text says it does. It creates a file literally named-in the current working directory and printsWrote screenshot to: …to stderr, exit 0. This server always passes a real path.simctl ioon a simulator whose runtime is missing aborts — SIGABRT, exit 134, an uncaughtNSInternalInconsistencyExceptionand a thirty-line stack trace, not an error message. Availability is checked in the resolver, before the call. On a typical machine a large fraction of simulators are in this state.erase,deleteandshutdownall accept the literalall. A target of"all"reachingsimctl erasewipes every simulator on the machine. Refused in the resolver and again in the adapter.bootedis a coin flip. simctl's own help: "If multiple devices are booted … simctl will choose one of them." Resolved to a concrete UDID here first.WebDriverAgent scans ports 8100-8199 when
USE_PORTis unset. With two booted simulators, the second runner comes up healthy on 8101 and a server pointed at 8100 drives the first one while every call reports success.scripts/wda.shalways pinsUSE_PORT, anddiagnosticsreports the port's real owner viaSIMULATOR_UDIDfromps -Eww.listappsreturns an old-style NeXTSTEP plist, not JSON.--jsonis a flag onlistand nothing else.A status bar override survives a reboot. Clear it or it quietly pollutes every later screenshot.
simctl launchenvironment variables need aSIMCTL_CHILD_prefix on the calling process, not a flag.A label belongs to the control and to every container around it. WebDriverAgent answers depth-first, so an unqualified label match lands on the navigation bar as readily as on the button — and a tap on a container does nothing while reporting success.
tap_elementnarrows a label to the interactive types first, and sayspreferredControlwhen it did.isVisibleis not always truthful. APHPickerpresented over Safari reports all nine of its asset cellsisVisible: "0"while they are on screen and tappable — a synthesised tap on one opens the preview. The defaultui_treefilter drops them, which is why the result carries afilteredtally: a short list that has been filtered and a screen that is genuinely bare are otherwise the same answer.simctl installwants a.appbundle directory built for the simulator — an.ipaor a device build fails with "No such file or directory", which reads like a path typo.simctl bootgives you no window. It runs headless, which is what an agent wants and confusing the first time;poweropens Simulator.app by default.Two runtimes can share one identifier, so the key of
list devicesis not a primary key.
Develop
pnpm install
pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm buildLicense
MIT — see LICENSE.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mgcrea/mcp-ios-simulator'
If you have feedback or need assistance with the MCP directory API, please join our Discord server