An MCP (Model Context Protocol) server that gives Claude Code autonomous iOS testing capabilities — navigate screens, interact with UI elements, detect bugs, and generate structured test reports, all without writing a single line of XCUITest.
Claude reads the iOS accessibility tree (via Meta's idb tool) to understand every element on screen — labels, types, enabled state, frame coordinates — and combines that with screenshots when visual confirmation is needed. Unlike XCUITest, there are no fragile element locators to maintain: Claude finds elements by label using fuzzy matching, adapts when the UI changes, and builds a directed screen graph to track coverage and prevent infinite navigation loops.
| Dependency | Version | Notes |
|---|---|---|
| macOS | 13+ | Ventura or later |
| Xcode | 15+ | With iOS 17+ simulator runtime |
| Node.js | ≥ 20.0.0 | LTS recommended |
| Claude Code | latest | npm install -g @anthropic/claude-code |
| idb | any | Strongly recommended — install via brew install idb-companion |
idb is optional but highly recommended. Without it, accessibility tree inspection is unavailable and Claude falls back to screenshot-only mode.
1. Clone the repo and install dependencies
git clone https://github.com/your-org/ios-simulator-mcp
cd ios-simulator-mcp
npm install2. Build
npm run build3. Install idb companion (recommended)
brew tap facebook/fb
brew install idb-companion4. Add to your Claude Code config
Open ~/.claude/claude_desktop_config.json (create it if it doesn't exist) and add:
{
"mcpServers": {
"ios-simulator": {
"command": "node",
"args": ["/absolute/path/to/ios-simulator-mcp/dist/src/index.js"]
}
}
}Replace /absolute/path/to/ios-simulator-mcp with the actual path on your machine.
5. Restart Claude Code
The ios-simulator tools will appear in Claude's tool list on next startup.
# 1. Boot a simulator
xcrun simctl boot "iPhone 15 Pro"
# 2. Install your app
xcrun simctl install booted /path/to/YourApp.app
# 3. Ask Claude to test it
# In Claude Code:
# "Test com.yourcompany.YourApp and generate a bug report"Claude will navigate all reachable screens, interact with UI elements, detect crashes and errors, and produce a markdown report with severity-ranked bugs and coverage statistics.
| Tool | Description |
|---|---|
list_simulators |
Lists all available simulators with UDID, name, OS, and state |
get_booted_simulator |
Returns UDID and metadata of the currently booted simulator |
install_app(app_path) |
Installs a .app bundle to the booted simulator |
launch_app(bundle_id, reset_state?) |
Launches the app; reset_state: true simulates first-run |
terminate_app(bundle_id) |
Terminates the running app |
get_app_info(bundle_id) |
Returns version, build, minimum OS, entitlements |
| Tool | Description |
|---|---|
get_accessibility_tree() |
Returns all UI elements as a flat array with labels, types, frames |
describe_current_screen() |
Identifies the current screen type and suggests next test actions |
find_element(query, strategy?) |
Finds an element by label, type, value, or fuzzy match |
| Tool | Description |
|---|---|
take_screenshot(screen_name?) |
Captures a screenshot (resized to ≤1024px, JPEG 85%) |
compare_screenshot(baseline_path, threshold?) |
Diffs current screen against a baseline; highlights changes in red |
| Tool | Description |
|---|---|
tap_by_label(label, screenshot_on_fail?) |
Taps an element by fuzzy label match |
tap_at(x, y) |
Taps at absolute screen coordinates |
input_text(label, value, clear_first?) |
Types text into a field (clears first by default) |
swipe(direction, distance_percent?, screen_width?, screen_height?) |
Swipes in a direction |
scroll_to_element(label, direction?, max_scrolls?) |
Scrolls until element is visible |
press_hardware_button(button) |
Presses home, lock, rotate_left, or rotate_right |
long_press(label, duration_ms?) |
Long-presses an element |
drag(from_label, to_label) |
Drags from one element to another |
| Tool | Description |
|---|---|
wait_for_element(label, timeout_ms?, poll_interval_ms?) |
Polls until element appears |
wait_for_screen_stable(timeout_ms?) |
Waits until screen stops changing (animations done) |
wait_for_element_gone(label, timeout_ms?) |
Polls until element disappears |
| Tool | Description |
|---|---|
assert_element_exists(label) |
Fails if element is not in the accessibility tree |
assert_text_equals(label, expected) |
Fails if element's text doesn't match |
assert_element_enabled(label) |
Fails if element is disabled |
assert_no_crash() |
Checks DiagnosticReports for new crash logs |
assert_no_error_in_logs(patterns?) |
Scans console logs for error patterns |
| Tool | Description |
|---|---|
inject_user_defaults(bundle_id, key, value) |
Sets a UserDefaults key before launch |
read_user_defaults(bundle_id, key) |
Reads a UserDefaults key |
set_keychain_value(bundle_id, key, value) |
Injects a keychain entry (idb only) |
reset_app_state(bundle_id) |
Clears all app storage and relaunches fresh |
set_location(latitude, longitude) |
Spoofs GPS location |
set_permissions(bundle_id, permissions) |
Sets app permissions (camera, location, etc.) |
| Tool | Description |
|---|---|
get_console_logs(lines?, filter?) |
Returns recent simulator console output |
get_crash_logs(bundle_id?) |
Returns crash report summaries from DiagnosticReports |
get_network_calls(since_ms?) |
Returns HTTP calls recorded by the mock server |
start_mock_server(port?, routes_config?) |
Starts the bundled HTTP mock server |
stop_mock_server() |
Stops the mock server and returns the full call log |
| Tool | Description |
|---|---|
register_screen(name, description?) |
Registers a screen in the coverage graph |
mark_screen_tested(name, result, notes?) |
Records pass / fail / skip / blocked for a screen |
add_navigation_edge(from, to, action) |
Records a screen transition in the graph |
get_screen_graph() |
Returns the full directed graph with coverage stats |
add_bug(severity, title, description, screen, ...) |
Records a bug with severity, screen, and optional screenshot |
get_test_report(format, app_name?, app_version?) |
Generates markdown or JSON test report |
reset_test_session() |
Clears all state to start a fresh test run |
Place .yaml files in test-flows/ to define critical user journeys. Claude executes these before free exploration, so important paths are always tested first.
name: "Checkout Flow"
description: "Full purchase from product selection to confirmation"
steps:
- action: tap_by_label
label: "Add to Cart"
- action: wait_for_element
label: "Cart (1)"
timeout_ms: 3000
- action: tap_by_label
label: "Checkout"
- action: wait_for_screen_stable
- action: input_text
label: "Card Number"
value: "4111111111111111"
- action: assert_text_equals
label: "Order Total"
expected: "$9.99"
- action: tap_by_label
label: "Place Order"
- action: wait_for_element
label: "Order Confirmed"
timeout_ms: 10000
- action: assert_no_crashSupported actions mirror the tool names: tap_by_label, tap_at, input_text, swipe, scroll_to_element, press_hardware_button, wait_for_element, wait_for_element_gone, wait_for_screen_stable, assert_element_exists, assert_text_equals, assert_element_enabled, assert_no_crash, assert_no_error_in_logs, take_screenshot.
Use the built-in HTTP mock server to test network-dependent features without a real backend.
1. Start the server before launching your app
// Via Claude tool call:
start_mock_server(3210, {
"GET /api/user": { "status": 200, "body": { "id": 1, "name": "Test User" } },
"POST /api/login": { "status": 200, "body": { "token": "mock-token-123" } },
"GET /api/feed": { "status": 200, "delay_ms": 500, "body": { "items": [] } },
"POST /api/purchase": { "status": 500, "error": "Payment unavailable" }
})2. Point your app at the mock server
inject_user_defaults("com.your.app", "APIBaseURL", "http://127.0.0.1:3210")3. Inspect captured calls
get_network_calls() // returns all calls since server started
get_network_calls(since_ms) // filter by timestamp4. Stop and review
stop_mock_server() // returns full call log with methods, URLs, bodies, status codesAny route not in your config returns 200 {} — the app never hangs waiting for an unmatched endpoint.
You can also hit GET http://127.0.0.1:3210/mock/calls directly from any HTTP client to inspect captured requests.
See mock-server/config.example.json for a full example configuration.
- Custom-drawn UIs: Screens rendered with Metal, SpriteKit, or OpenGL won't appear in the accessibility tree. Claude will fall back to screenshot analysis for these, with reduced interaction capability.
- Large apps: Apps with 50+ screens need predefined flows in
test-flows/to be efficient. Free exploration alone will not provide full coverage in a reasonable time. - Simulator only: Physical device testing is not supported. The tools rely on
xcrun simctlandidb, both of which target simulators. - Single app: Multi-app flows (e.g. testing a share sheet that opens another app) are not supported. Testing is scoped to one app at a time.
- idb dependency: Accessibility tree inspection requires
idb-companion. Without it,get_accessibility_treeis unavailable and Claude relies entirely on screenshots. - Network proxy: The mock server works for apps that accept a configurable API base URL. Apps with hardcoded production URLs cannot be redirected without a proper network proxy.
- Fork the repo and create a feature branch.
- Run tests before and after your changes:
npm test - Keep TypeScript strict — no
anycasts without a comment explaining why. - New tools need both an implementation in
src/tools/and tests insrc/__tests__/. - Update the tools reference table in this README.
- Open a pull request with a description of what changed and why.
Bug reports and feature requests are welcome via GitHub Issues.
MIT — see LICENSE for details.
