apple-debug-mcp
Provides tools for interacting with Apple Simulators, including lifecycle management (boot, shutdown, slim), UI automation (tap, type, swipe, screenshot), app lifecycle (install, launch, terminate), profiling with xctrace, live streaming, debugging, crash symbolication, and network interception.
Integrates with the Xcode Simulator runtime to manage and debug iOS apps, offering tools for simulator control, UI automation, app lifecycle, profiling with xctrace, and debugging within the Xcode Simulator environment.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@apple-debug-mcplease an iPhone simulator, launch my app, and capture a screenshot"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
apple-debug-mcp
An MCP debug daemon for Apple Simulators: multiple AI agent sessions share one Mac's simulator pool while the daemon tracks who holds which simulator (leases) and auto-dispatches freed simulators to the next waiter in line. It runs locally as a single shared daemon over Streamable HTTP, built on simslim for simulator lifecycle (boot/shutdown/slim, low-RAM slim boots) and AXe for UI automation.
Not affiliated with Apple. This is an independent community project. Apple, Xcode, Simulator, and Instruments are trademarks of Apple Inc., used here only to describe what this software connects to.
Agent sessions (Orca / Claude Code / TMA / scripts)
│ Streamable HTTP (POST /mcp), one MCP connection per session
▼
apple-debug-mcp daemon ── shared state: leases + wait queue + auto-dispatch
│ child processes
├── simslim ── boot/shutdown/slim, pool state
└── axe ────── lease holders onlyFeatures
65 tools across 10 groups — pool/lease/queue, AXe UI automation, device extras, synthetic camera, app lifecycle, xctrace profiling, live streaming, llmdb debugging, crash symbolication, and an HTTPS intercept proxy
One shared daemon — leases, the wait queue, and auto-dispatch live in a single process; per-session stdio servers would split the pool
Lease ownership gates — each lease holds one simulator and only its holder may drive UI, apps, debugging, or the proxy through it
Wait queue with auto-dispatch — when no simulator matches, callers get a ticket; releases, TTL expiry, and external-shutdown detection hand the next match to the first fitting ticket (FIFO)
Optional dependencies degrade gracefully — serve-sim, llmdb, and mitmdump are probed at boot; the daemon starts without them and only the tools that need them fail, with install hints
Operator-safe CLI —
doctoris read-only,launchd restartrefuses while leases are held, hand-edited plists are never overwritten silentlySingle version source —
package.jsonfeeds/health, the MCP handshake (serverInfo), and theserverfield ofpool_statusalike
Related MCP server: Simulator MCP
Prerequisites
macOS + Xcode (Simulator runtime)
Node.js 18 or later
simslimandaxe:
brew install mobai-app/tap/simslim
brew install cameroncooke/axe/axeOptional, enabling only the tools named:
npm install -g serve-sim # sim_rotate, sim_ca_debug, sim_memory_warning, camera_*, stream_*
brew install alexmx/tools/llmdb # debug_*
brew install mitmproxy # net_proxy_*, net_assert_flow, map_local, map_remoteInstall
From a checkout of this repo, five steps:
1. npm install
2. npm run build
3. npm install -g . # recommended: the launchd plist points at the stable npm-global bin (~/.npm-global/bin)
4. apple-debug-mcp doctor # read-only check; every failure prints its exact fix
5. apple-debug-mcp launchd create # write the plist + load itThen register the daemon URL in your clients and verify it answers:
apple-debug-mcp install --client all # Claude Code / Codex / Muse (user scope)
curl http://127.0.0.1:3100/health # {"ok":true,"version":"...","gitSha":"...",...}
tail -f ~/.apple-debug-mcp/daemon.logDefaults (fixed):
Daemon path: the npm-global bin (
~/.npm-global/bin/apple-debug-mcp) first, falling back to this checkout'sdist/index.jswith a loud warning.SIM_MCP_DAEMON_PATHoverrides both.Log:
~/.apple-debug-mcp/daemon.log, stdout and stderr combined.State:
~/.apple-debug-mcp/state.json(leases/queue), lockfiledaemon.lockbeside it.installwrites user scope only. Project scope could point sessions at different daemons and split the pool — one shared daemon is the premise.The CLI never pings the npm registry. The local
package.jsonis the only version source.
Quick start
Check the daemon is up, then take a lease:
apple-debug-mcp launchd status{ "session_id": "<your session id>", "client": "claude-code", "task": "log in and screenshot" }Call lease_acquire with that body. With spare capacity you get a lease immediately; otherwise you get a wait ticket (ticket_id, position) — poll queue_status until it shows status: "assigned" with a lease_id. Drive the simulator with the ui_* tools, renew long work with lease_heartbeat, and finish with lease_release, which instantly dispatches the freed simulator to the first matching ticket.
Configuration
install writes Streamable-HTTP URL entries only. Shapes verified against the real configs on macOS:
{
"mcpServers": {
"apple-debug": { "type": "http", "url": "http://127.0.0.1:3100/mcp" }
}
}Claude Code (user scope, ~/.claude.json → mcpServers); manual equivalent:
claude mcp add -s user --transport http apple-debug http://127.0.0.1:3100/mcpCodex (~/.codex/config.toml):
[mcp_servers.apple-debug]
url = "http://127.0.0.1:3100/mcp"Muse (~/.config/muse/settings.json → mcpServers):
"apple-debug": { "type": "streamable-http", "url": "http://127.0.0.1:3100/mcp" }Preview or inspect without writing:
apple-debug-mcp install --client codex,muse --dry-run # preview the change
apple-debug-mcp install --client all --status # registration status onlyUnknown client names fail as "Unsupported" without touching any file. Every edited config is backed up to <config>.bak-sim-mcp first (created once, never overwritten).
Environment variables (all optional):
Variable | Default | Description |
|
| Listen address |
|
| Lease/queue persistence file |
|
| Binary paths |
|
| serve-sim binary path (optional dependency) |
|
| llmdb binary path (optional dependency) |
|
| mitmdump binary path (optional dependency) |
|
| First port scanned for per-lease stream helpers |
|
| First port scanned for the lease-held proxy listener |
|
| Cap on concurrently booted simulators; excess waits |
|
| Lease survival without a heartbeat |
|
| Lease/queue sweep interval |
|
| Helper child-process timeout |
|
| Simulator boot timeout |
|
| Screenshot storage |
| (unset) | Override the daemon entry the plist launches |
Upgrade
git pull
npm install
npm run build
npm install -g . # when the launchd plist points at the npm-global bin
apple-debug-mcp launchd restart # refused while leases are held; --force overrides
apple-debug-mcp launchd status # loaded + /health + versionRe-running launchd create regenerates the plist while preserving its port/host. A hand-edited plist shows a diff and is never overwritten without --force.
Uninstall
apple-debug-mcp install --client all --remove # unregister from client configs
apple-debug-mcp launchd remove # unload + delete the plist
npm uninstall -g apple-debug-mcp # when installed via npm-globalTools
65 tools across 10 groups. Tools marked * require a lease_id and accept only the holding lease.
Group | Tools |
Pool/state (5) |
|
Lease/queue (7) |
|
UI automation (8) * |
|
Device extras (5) |
|
Synthetic camera (5) |
|
App lifecycle (5) * |
|
Profiling (1) * |
|
Live streaming (2) * |
|
Debugging (20) * |
|
Network proxy (7) * |
|
Key behaviors:
lease_acquire: matches conditions (udid/devicesubstring /osVersionprefix /platform), auto-boots when needed, auto-slims stock simulators (--no-reboot, custom profiles untouched), else enqueues a ticket.lease_release, TTL expiry, and external-shutdown detection auto-dispatch the freed simulator to the first matching ticket (FIFO;auto_boottickets may boot one). Release does not shut the simulator down — it hands over booted; callsim_shutdownseparately to stop it.sim_shutdownrefuses on a held simulator unlessforce=true; rebootingsim_slim,sim_rotate,sim_ca_debug, andsim_memory_warningbehave the same, naming the holder in the error.camera_start/camera_switchare lease-gated the same way;camera_mirror/camera_status/camera_stopignore leases.ui_screenshotreturns a PNG path plus an image block.sim_rotatetakesportrait|portrait_upside_down|landscape_left|landscape_right;sim_ca_debugtakesoption=blended|copies|misaligned|offscreen|slow-animationsplus anenabledboolean.sim_log_streamcollects live logs for a window (default 5s, max 60s),sim_log_showreads the past--lastwindow (default5m); both arexcrun simctl spawn-based, so they need no serve-sim. Results are{log, truncated, timedOut}—streamalways ends on window expiry, sotimedOut: trueis normal.Log filters: a full predicate (
predicate, e.g.process == "MyApp") or theprocessshorthand; both combine with AND. Predicates are rejected before the call when empty or unbalanced. Quote string literals with double quotes (eventMessage CONTAINS "x"); parens inside literals are ignored, soprocess == "My(App)"is valid.Output past
max_chars(default 20000) keeps the tail with a…[truncated N chars]prefix. Prefer a narrow window (timeout_sec/last) plus aprocessfilter.App lifecycle is
xcrun simctl-based with no extra dependencies:app_launchreturns{pid};terminatefails on a non-running app ("found nothing to terminate");uninstallquietly succeeds on a missing bundle (idempotent). Every call is recorded inlease_actions.profile_tracerecords withxcrun xctrace record --deviceinto a.tracefile (default template "Time Profiler", default 5s, max 60s).--attachoften cannot find the simulator process ("Cannot find process"), so whole-system recording is the default. Note: on this host family simulator-target recording stalls at "Starting recording..." (host recording works), so the smoke test asserts the validation path only.Streaming is opt-in:
lease_acquirewithstream: true(a stream failure only warns; the lease stands) orstream_start/stream_stop(both idempotent;stream_startreturns the existing URL when already streaming). The URL lives on the lease'sstreamUrl, visible inpool_status/sessions_list/lease_list. One stream costs one serve-sim helper process plus MJPEG CPU — turn it on to look, off when done;ui_screenshotis cheaper for stills. Ports scan upward fromSERVE_SIM_PORT_BASEbecause serve-sim kills whatever occupies its requested port instead of scanning (and its:3100default would collide with this daemon).Debugging wraps the llmdb CLI (an lldb-dap wrapper with session JSON):
debug_attach {bundle_id|pid}(exactly one) ordebug_launch {path, args?}opens one session per lease, then break/continue/inspect. Needs a debuggable (get-task-allow) third-party build plus one-time host developer-tools authorization — stock Apple apps refuse attach. llmdb 0.2.0 takesfile:linebreakpoints only (no symbol breakpoints),expandtakes a variablesReference number (not an expression), andlaunchtakes a binary path (not a bundle ID).symbolicateisatos-based host-local crash symbolication:lease_idis optional (validated and logged when given), with no udid/force gates.binaryis effectively required (no live-pid mode).Network proxy:
net_proxy_start {lease_id, rules?}intercepts the lease simulator's HTTPS (mitmdump + generated rewrite addon + mitm CA into the simulator keychain);net_proxy_set_rulesswaps rules (restarts only mitmdump, same port);net_proxy_stopis idempotent and restores the host proxy snapshot;net_assert_flowpolls the holder's flow log (JSONL). Rules arematch{url_regex?, method?}→action{status?, headers?, body?, delay_ms?, request?, map_local?, map_remote?}, first match wins, max 50; omittedmatchmatches everything,rules: []is transparent intercept-and-log. See Security for the host-wide warning and consent boundary.
Usage examples
"Boot a simulator, hold it, and tap through login."
{ "session_id": "sess-1", "client": "claude-code", "task": "login flow", "device": "iPhone 16" }{ "lease_id": "<from lease_acquire>" }{ "lease_id": "<lease>", "label": "Continue" }That is lease_acquire, then ui_describe, then ui_tap on the named element (label matches the accessibility label; ui_button is for hardware buttons only). Finish with lease_release.
"Record a 10-second Time Profiler trace of the app under test."
{ "lease_id": "<lease>", "duration_sec": 10 }That is profile_trace; the result carries the .trace path.
"Rewrite one API response to a fixture file while the test runs."
{ "lease_id": "<lease>", "rules": [{ "match": { "url_regex": "api\\.example\\.com/v1/feed" }, "action": { "map_local": "/tmp/feed.json" } }] }{ "lease_id": "<lease>", "url_regex": "api\\.example\\.com/v1/feed", "min_count": 1 }That is net_proxy_start, then net_assert_flow to prove the app hit the endpoint. Stop with net_proxy_stop as soon as the test is done.
Instructions for AI agents
There must be exactly one shared daemon. Never start a per-session stdio server — the pool and queue would not be shared. If
/healthis unreachable, say so instead of working around it with directsimctl.Pass your tool's session ID as
session_idwith aclientlabel sosessions_listshows who runs where. The server does not enforce the format.A wait ticket is normal. Poll
queue_status; do not boot simulators out of band to skip the line.Only the holding lease may call that lease's gated tools. On a holder-mismatch error, stop and report the holder — do not retry with
force=trueunless the user asked for preemption.Renew long work with
lease_heartbeatbefore the TTL (default 30 min) expires; expired leases are reclaimed and redispatched.Release with
lease_releasewhen done. Release hands the simulator over booted — shut it down withsim_shutdownonly if the task needs it stopped.Turn the proxy and streams off when done:
net_proxy_stop,stream_stop. Both cost host-wide resources while on.
Troubleshooting
Start with doctor — every failure prints its exact fix:
apple-debug-mcp doctor
apple-debug-mcp launchd status # plist present? loaded? /health reachable (which version)?
tail -f ~/.apple-debug-mcp/daemon.logCommon failures:
/healthunreachable but the plist is loaded: the daemon crashed at boot — the log shows why (missingsimslim/axeis the usual cause).Port conflict: the daemon exits
10when the port is taken,11when another daemon holds the lockfile (generic failures exit1). Restart viaapple-debug-mcp launchd restart, never by launching a second daemon.launchd restartrefuses with active leases: wait for release or re-run with--force(drops in-flight work).Hand-edited plist:
create/removeshow a diff and refuse without--force.installreports "already installed" on the second run — that is the idempotent success path, not an error.Optional-tool failure naming
serve-sim,llmdb, ormitmdump: install that binary (see Prerequisites); the daemon runs fine without it otherwise.debug_attachrefused: the build needsget-task-allow, the host needs its one-time developer-tools approval, and stock Apple apps always refuse.Host proxy left on after a daemon crash (SIGKILL skips cleanup): kill mitmdump and switch every service off by hand:
pkill -f "mitmdump -p "
networksetup -listallnetworkservices | tail -n +2 | while IFS= read -r svc; do
svc="${svc#\*}" # strip the disabled marker
sudo networksetup -setwebproxystate "$svc" off
sudo networksetup -setsecurewebproxystate "$svc" off
doneNormal shutdown (SIGINT/SIGTERM), lease_release, TTL expiry, sweeps, and external-shutdown detection all restore the snapshot automatically.
Security
Lease-gated mutation. UI, apps, profiling, streaming, debugging, and the proxy all require the holding
lease_id; cross-lease calls fail naming the holder. Only one proxy holder exists across all leases.sudo is narrow and explicit. Host proxy control needs passwordless sudo for exactly four
networksetupsubcommands —-setwebproxy,-setsecurewebproxy,-setwebproxystate,-setsecurewebproxystate— always assudo -n(never interactive). Reads (-listallnetworkservices,-getwebproxy/-getsecurewebproxy) need no sudo. Nothing is checked or changed at daemon boot, because every-set*mutates host state;net_proxy_startfails with a sudoers-setup hint when authorization is missing.Rules validate before any host change. A bad rule is rejected without touching host networking.
Host-wide proxy warning. While the proxy is on, all host web traffic (including other apps' background traffic) flows through mitmdump, and intranet sites on a private CA may break until stop/restore. Keep it on only as long as the test needs. The mitm CA stays in the simulator keychain after stop (harmless without the proxy; full removal is
xcrun simctl keychain <udid> reset).No silent destructive actions.
doctoris read-only (no--fix); restart refuses under active leases; hand-edited plists need--force; local-only mode exists because process-scoped interception (mitmdump --mode local:…) needs a manually approved macOS System Extension and is unverifiable in unattended environments, so only host mode ships.Localhost only by default. The daemon binds
127.0.0.1:3100; the MCP endpoint has no authentication, so do not rebind it to a shared interface.
What this is not
This is a local, single-host debug daemon. It manages simulators on the Mac it runs on, keeps its state in a local JSON file, and serves MCP over unauthenticated localhost HTTP. It is not a multi-host device farm, not a CI XCTest runner, and not a general macOS automation server — the tools cover the simulator debug loop (boot, lease, drive UI, profile, debug, intercept) and project their results down to what a model can usefully read.
It is also not a simulator runtime itself. Booting, slimming, and UI driving delegate to simslim, AXe, serve-sim, llmdb, and the Xcode toolchain; when those drift, doctor and the weekly CI smoke run are the early warning.
Development
Requires Node.js 18 or later, macOS + Xcode for anything past unit tests.
npm install
npm test # unit tests (orchestrator/wrappers/store/CLI/launchd/doctor/install/…)
npm run typecheck
npm run smoke # live-simulator E2E (daemon boot → boot → queue → dispatch → shutdown)
npm run dev # tsx hot runCommand | What it does |
| Runs the suite. This is the one to run. |
| Types only, no emit. |
| Compile to |
| Live E2E against real simulators. Needs macOS + Xcode + simslim/axe. |
CI has two jobs: check (Linux, every PR/push: typecheck + unit tests + build) and smoke-macos (main pushes, weekly Mondays, manual dispatch, and PRs labeled run-smoke: installs simslim/axe via brew and runs the live smoke to catch external CLI/Xcode drift early). Dependabot opens weekly npm and GitHub Actions update PRs.
Layout: src/index.ts (HTTP daemon/session map) · src/server.ts (MCP tool definitions) · src/orchestrator.ts (acquire/release/dispatch) · src/store.ts (JSON persistence) · src/simslim.ts · src/axe.ts · src/servesim.ts · src/simlog.ts · src/simapps.ts · src/xctrace.ts · src/llmdb.ts · src/symbolicate.ts · src/netproxy.ts · src/extras.ts · src/exec.ts · src/cli.ts · src/version.ts · src/singleton.ts · src/launchd.ts · src/doctor.ts · src/install.ts · src/migrate.ts · test/
(A Korean translation of this README is welcome later — contributions accepted.)
Licence
MIT — see the license field in package.json.
This server cannot be deployed
Maintenance
Related MCP Connectors
Build, run, and inspect iOS apps in disposable hosted Simulators from cloud coding agents.
- LimrunOAuthcom.limrun
Cloud iOS simulators and Android emulators your agent can create, drive, and throw away.
Cloud macOS with Xcode for AI agents: run commands, build, test and ship iOS apps.
Drive real devices from your AI Coding tool. Embed a client SDK (Unity, Godot, Flutter, iOS/macOS, Android, React Native, Web) in your app, then capture screenshots, traverse the UI tree, inject taps and key events, and run automated test tasks on the physical device over a secure relay.
Related MCP Servers
AlicenseNot gradedqualityBmaintenanceHigh-performance MCP server for iOS development and test automation. Gives AI coding assistants direct access to iOS simulators with sub-20ms screenshots, UI interaction, building, testing, and an intelligent operator mode.8MIT- FlicenseNot gradedqualityDmaintenanceEnables AI to control iOS simulators through the MCP protocol. Supports device management, UI automation, and network interception including screenshot capture, text input, and HTTP request mocking.-
- AlicenseNot gradedqualityFmaintenanceEnables AI agents to control real iPhones and simulators on macOS, allowing for UI interaction, testing, and automation.56 npm91MIT
- AlicenseNot gradedqualityDmaintenanceGives AI agents the ability to interact with iOS simulators through 21+ tools for mobile app testing.10,447 npmMIT