Skip to main content

engineering

Composing an MCP surface: how SimDrive's 33 tools fit together

Maurice Carrier ·

If you’ve watched the iOS test-automation surface get rebuilt every few years — XCUITest, EarlGrey, Appium, Maestro — you know each one is shaped for a human author writing in a DSL. They work, but they push every iteration through a selector tree that drifts and a syntax that has to be learned.

When we started SimDrive, the first design question wasn’t “which DSL.” It was: what does the call site look like when the caller is an LLM agent, not a human writing YAML?

This post is the architecture answer. Thirty-three MCP tools across five groups. Vision-first observe + act. Recordings that capture exactly what happened and replay free in CI. No selectors. No XML page source.

Why agentic test tools want a different shape than YAML test tools

A human writing a Maestro flow knows the screen they’re targeting. They write tapOn: "Sign In" because they remember there’s a Sign In button. The selector is a contract with their own memory.

An LLM agent doesn’t have that memory between turns. It has whatever the last tool call returned. If you hand it a YAML DSL with selectors, it has to either (a) keep a mental model of the UI tree across turns, or (b) re-dump the hierarchy on every action. Both are expensive. Both are flaky.

The agent wants the loop the human is actually executing: observe, decide, act, observe again. It wants a screenshot it can see. It wants a list of mark IDs it can refer to. It wants its action to be validated against the state it just saw — so if the screen has moved on, the action fails fast instead of tapping into the void.

That’s the loop SimDrive is shaped around. Everything else follows.

The five groups

The 33 tools sort cleanly into five groups by what they do for the agent.

Observation (1 tool). Just observe. The agent calls it and gets back an annotated screenshot, a list of typed marks (id, role, label, frame), the foreground app’s bundle id, and the device identity. The annotated screenshot is the picture; the marks are the addresses. Subsequent actions can refer to marks by numeric id, by label text, or by stable_id.

Action (7 tools). tap, tap_and_wait_keyboard, swipe, type_text, press_key, clear_field, dismiss_sheet. When the agent passes a semantic target (mark id, label text, stable_id), the call is resolved against the last observation. If the screen has changed in a way that invalidates the target, the action fails before it does anything destructive. Raw (x, y) coordinates are accepted too as an escape hatch — useful for regions the observer didn’t detect.

Record & replay (5 tools). record_start and record_stop bracket a recording. Every action inside the bracket gets captured with its screenshot. replay runs the recording back deterministically, with SSIM-based parity gates that fail if the rendered UI drifts beyond a configured threshold. list_replays and validate_replay are the supporting cast.

Lifecycle (3 tools). session_start, session_end, session_status. A session is a bound (simulator UDID or paired device) target. The agent starts one, drives it, and ends it. Plus device targeting (list_devices) for paired iPhones and iPads.

Diagnostics + hygiene (12 tools). logs, app_state, apps, crashes, doctor, perf, perf_baseline, perf_compare, memory, dismiss_first_launch_alerts, pre_grant_permissions, set_appearance, plus journey helpers (load_journey, lint_recordings, migrate_recording) and version. These are the long tail. Most flows don’t need them. The ones that need them really need them — debugging a perf regression or a crash on first launch requires the whole subkit.

The reason to group is composability. The agent doesn’t need to think “which tool” — it thinks “which group.” Observe the screen; act on it; record what I did; replay it later. That’s the spine.

Vision-first: why observe() + act() beat coordinate-only tap(x, y)

Early prototypes had a single composite primitive: screenshot_and_tap(label) that took a screenshot internally, ran OCR, found the label, tapped it. One call. Hidden state.

It failed in two ways the agent surface couldn’t recover from.

The first: when OCR found the wrong region, the agent had no chance to second-guess it. The tool returned success. The screen didn’t change. The agent’s next observation was confused — it thought the tap had landed, but the UI hadn’t moved.

The second: the agent couldn’t reason about ambiguity. Two buttons labeled “Continue” on the same screen — which one? The composite tool picked one and hoped. The agent never saw the choice.

Splitting into observe + act fixes both. The agent sees the annotated screenshot. It sees there are two “Continue” marks, ids 7 and 12. It decides — based on context — which one to tap. The action records which id it referenced. The replay reproduces that exact decision.

The cost is one extra tool call per action. The benefit is that the model is in the loop on every decision that matters. For an agentic test surface, that’s the right trade.

Recording is a contract, not a transcript

The third design call was the recording format. Two options:

A pure transcript — “tap at (412, 318), wait 800ms, tap at (412, 488)” — replays fast but breaks the moment screen layout changes. Bumping the font size shifts every coordinate.

A pure DSL — “tap the Continue button” — survives layout changes but requires the replay runner to re-resolve selectors at replay time, which is exactly the selector-drift problem the agent surface was built to avoid.

SimDrive’s recording is the middle path: it captures the semantic target the agent referenced (mark id, label, or stable_id), plus the screenshot from the moment of capture. At replay time, the runner re-observes the screen, looks for the target that was recorded, and validates the rendered frame against the captured screenshot via SSIM. Layout drift inside the SSIM threshold is tolerated. Drift past the threshold fails the replay loudly.

This is why replays run free. No AI tokens. No selector resolution magic. Just the typed target, the new observation, and a pixel-difference check.

What’s hard, what’s solved, what’s emerging

Solved: simulator recording and replay. The simulator is deterministic enough, and the SSIM gates are calibrated tightly enough, that recordings authored once typically replay green across iOS minor versions.

Emerging: real-device flake handling. The hardest part of SimDrive is real-device WebDriverAgent bootstrap. Xcode 16 + iOS 26 added new requirements (-allowProvisioningUpdates, devicectl JSON output, signing surprises). Once WDA is up and stable on a paired device, the action tools and recordings work the same as on the simulator. Getting WDA up reliably takes the most engineering hours per shipped feature in the codebase. The recommended pattern is still: author on the simulator, prove the recording is stable there, then add device replays.

Open: cloud orchestration. SimDrive is local-first by design. If you want to fan out replays across a real-device matrix, point a cloud farm (BrowserStack, Sauce, etc.) at the recordings. We are not building a hosted dashboard or a device farm.

Try it in two commands

Terminal window
pip install simdrive
simdrive trial start --email you@example.com

That gives you 14 days of every tool. Wire it into Claude Code by adding a single block to .mcp.json:

{
"mcpServers": {
"simdrive": {
"command": "simdrive"
}
}
}

Restart Claude Code. The 33 tools appear in its picker. Ask Claude to start a session on the iPhone 15 Pro simulator and observe the screen. The annotated screenshot comes back in the next turn. From there it’s just observe-act-observe until you have a recording worth replaying.