Show HN: The fastest, most token-efficient iOS Simulator MCP for AI agents An open-source project called simulator-use released an MIT-licensed MCP server that lets AI agents such as Claude Code, Codex and Cursor drive the iOS Simulator, reading a full screen in about 250 ms, sending a tap in under 1 ms and capturing a screenshot in about 60 ms, measured on an M6 Mac mini in October 2026. The tool renders each screen as one line per element — the sample sign-in screen is about 70 tokens versus roughly 40,000 for the full accessibility tree — and exposes a dozen tools covering Xcode build, install, launch, XCTest and XCUITest runs, logs, crashes, video and permission prompts. A cloud option, `npx simulator-use cloud start`, provides a remote iPhone with Xcode to Linux, Windows or Macs without Xcode. The fastest, most token-efficient way for an AI agent to use the iOS Simulator. simulator-use gives Claude Code, Codex, Cursor and any other MCP client a phone: it builds and runs your Xcode project, reads the screen as a few lines of text, taps and types by label, and runs your tests case by case. Its own native engine talks to the Simulator directly, so reading the whole screen takes about 250 ms and a tap goes out in under a millisecond, where tools that start a helper for every call take 1.5 to 3 seconds. Free and open source MIT , on any Mac with Xcode, and from Linux, Windows or a Mac without Xcode through a cloud iPhone. - Fast: a whole-screen read in about 250 ms, a tap sent in under 1 ms, a screenshot in about 60 ms measured on an M6 Mac mini, October 2026 . - Token-efficient: one line per element the agent can use, nothing else; a dozen tools with short descriptions. - Complete: build, install, launch, XCTest and XCUITest per case, logs and crashes, video, permission prompts, iPhone and iPad. - Continuously improved: weekly releases driven by thousands of daily sessions. - Anywhere: npx simulator-use cloud start gives a cloud iPhone with Xcode to any machine. It builds, runs and tests Xcode projects, reads and uses the screen of any app SwiftUI, UIKit, React Native , answers iOS's permission prompts, reads the app's logs and crashes, and records video. Demo · Sign In · iPhone 17 Pro iOS 26.5 textfield "Email" email @201,194 securefield "Password" password @201,246 switch "Remember me" remember off @341,298 button "Sign In" signIn @201,350 text "Not signed in" status @201,402 tabs: Login selected @158,822, Items @244,822 - One line per element: kind, label, id the app's accessibility identifier , state and @x,y , the center in points. - Groups, icons inside buttons, scroll bars, the status bar and the keyboard's keys are folded away. The tab bar, scrolling and the keyboard are one line each. - Only what can be tapped is listed. An alert, a sheet, a menu or a full-screen view in front hides the screen behind it, and iOS's own permission prompts read as alerts over the app. Rows under the navigation bar, the tab bar or the keyboard are counted as hidden above, below, to the left or right , not listed. - A web view in UIKit Capacitor, React Native's WebView, a sign-in page inside most apps is read point by point, and its headings, texts, fields, checkboxes, buttons and links are listed like the rest. Web views hosted by SwiftUI, Safari's own pages, in-app browsers, maps, games and iOS's out-of-process pickers have nothing for accessibility to read. They come back as one unreadable line with their place, so the agent knows to take a screenshot and tap by point. - A screenshot, when asked for, is sized to points, so a point in the picture is a point to tap. - The sign-in screen above is about 70 tokens. The full accessibility tree it comes from is about 40,000. ui act takes steps in the same words ui look prints, runs them in order, and answers with what changed: ui act steps: "fill email gary@example.com", "fill password secret1", "tap signIn", "wait \"Welcome\"" 4 steps done in 4.5 s changed: email "gary@example.com" · password "•••••••" · status "Welcome, gary@example.com" Steps: | Step | Does | |---|---| | tap id , tap "label" , tap button "label" , tap @x,y | Taps; a switch gets a real touch | | fill id text | Focuses the field, clears it, types, and reads the text back | | type text | Types into the field that has focus | | scroll down , scroll right @201,402 | Scrolls the screen, or one carousel | | swipe left , swipe left "Row 3" | Swipes the screen, or one row to reveal Delete | | hold "Row 5" seconds | Long press, for context menus | | set volume 75 | Sets a slider to a percent | | back , home , key return | The back button or edge swipe , the home button, a key | | wait "Welcome" seconds , gone "Loading" | Waits for something to appear or go | Every step is checked, so nothing succeeds silently: - Typed text is read back from the field. When iOS's autocorrect or smart punctuation changes it -- to — , ' to ’ , the text is pasted instead, which iOS takes as is. A field that still shows something else stops the call and says what it shows. - Text in any language Café, 日本語, emoji can be typed: what a US keyboard lacks goes through the Simulator's clipboard. - fill refuses anything that isn't a text field, so keys never land in whichever field still has focus. - A tap that changes nothing is reported, after waiting 2.5 s for late answers such as a permission prompt. A tap inside an area that can't be read says it can't be checked. - A target that isn't on screen yet gets two seconds to appear, then fails with the next step: "2 elements are below: scroll down", or, when an alert came up in front, "Answer it first: tap "Cancel" or "Open"". - The first failing step stops the rest and returns the screen as it is now. - A new screen comes back whole; otherwise only what changed. | Tool | CLI | Does | |---|---|---| | app run | simulator-use run | Builds the Xcode project here, installs and launches it, answers with the first screen; errors as file:line ; --xcode 26.6 builds with another installed Xcode | | devices | simulator-use devices | Lists the Simulators on this Mac with their runtime, UDID and state, and its Xcodes | | device use | simulator-use use "iPhone 17 Pro" | Chooses the Simulator the other tools use, booting it one boot at a time ; dark mode, a clean status bar | | app control | simulator-use app launch dev.example.app | install , launch answers with the first screen , terminate , uninstall , open url ; grant , revoke and reset permissions ; location ; push ; the phone's appearance , locale , timezone , text size and a clean status bar | | ui look | simulator-use look | Reads the screen; find keeps matching lines, screenshot adds a picture, full saves the whole tree | | ui act | simulator-use act "tap signIn" ... | Does steps in order and answers with what changed | | app logs | simulator-use logs | The app's print output and its own system log lines; grep , wait for , crashes | | tests run | simulator-use test | Runs the unit and UI tests; each case as it finishes, counts, each failure as file:line and the slowest; "0 tests ran" is an error | | screen record | simulator-use record start | Starts or stops a video of the screen; stop answers with the MP4 | | job status | simulator-use job | Waits up to 40 s for a long build and answers with its result, or its progress | | job cancel | simulator-use cancel | Stops a job | | cloud | simulator-use cloud start | start , status , stop a NoMac cloud phone that every tool then runs on; run builds, tests and stops in one call see Cloud | The MCP server and the CLI are built from one list, so they always match. The tool list and instructions stay under 4,000 tokens; a test enforces it. app run does the whole loop in one call: it finds the project a workspace first, then a project, an XcodeGen spec, or ios/ , picks the scheme, builds for the Simulator, installs, launches, and answers with the first screen. The app's permissions are granted when it is installed and its first launch's prompts notifications too are answered, so no prompt stops the agent; permissions: "ask" keeps iOS's prompts, to test them. open url answers iOS's "Open in …?" for a link that opens another app. Built Demo in 4.2 s, installed dev.simtap.demo. Launched Demo in 3.9 s. Demo · Sign In · iPhone 17 Pro iOS 26.5 textfield "Email" email @201,194 ... - A failed build answers with the errors only, as Sources/App.swift:49:37: error: ... , and the path to the full log. - Each folder each git worktree builds into its own DerivedData, so rebuilds are incremental and worktrees never share products. SIMULATOR USE CACHE DIR puts it on a disk you keep, so the next machine's build is incremental too. - An XcodeGen project is regenerated from project.yml before each build skipped when nothing changed , so new files are always in it. - The next call in the same folder needs no arguments. - env: "API URL=http://127.0.0.1:8080 MODE=test" gives the app those variables it reads them from ProcessInfo , as an Xcode scheme does; they stay for its later launches, and env: "" clears them. - An MCP call answers within 40 s; a longer build answers with a job, and job status waits for it. From the CLI, simulator-use run simply waits. - Two builds of one project never run at once, also from two processes: the second says which process is building it. - Expo, React Native, Flutter and Capacitor builds are not done yet; build them with their own tools, then app control install . tests run runs the scheme's tests and reads the answer from Xcode's result bundle, not from the scrolling output: Tests failed: 1 passed, 2 failed 3 in 35.3 s . Result bundle: .../tests-1792370825323.xcresult DemoTests/SignInTests/testRejectsAShortPassword : Tests/SignInTests.swift:12: XCTAssertEqual failed: "Welcome, gary@example.com" is not equal to "Invalid email or password" DemoTests/rejectsAnEmailWithoutAt : Tests/SignInTests.swift:18: Expectation failed: ... - XCTest and Swift Testing both. - Each failing test is named the way only takes it, to rerun just that one. - "0 tests ran" no test target, or a filter that matched nothing is an error, never a pass. app logs shows what the app itself said, not the thousands of lines Apple's frameworks log from inside it: - Its print output, kept from the moment app control launches it. - Its own system log lines its subsystem, or logged by its own code , streamed from launch. macOS keeps info lines only in memory and drops them within seconds when the Simulator is busy, so reading the log store afterwards misses them. - grep also searches Apple's lines, for things like a missing image or a layout warning. - wait for waits up to 25 seconds for a line, for example after a tap that starts a network call. - crashes lists crash reports with the crashing thread down to your code ItemsView.body.getter App.swift:64 , next to the app's last output, which holds Swift's own message App.swift:64: Fatal error: ... . macOS can take up to a minute to write a report. - Each agent runs its own MCP server and holds its own Simulator through device use , and its own cloud phone: one agent's cloud start never moves another's tools, its cloud stop never ends another's phone, and the phone stops when the agent's server ends. A cloud start typed in a terminal is the command line's own. - Boots never overlap, across every simulator-use process on the Mac: two Simulators booting at once on an 8 GB Mac pushed it into swap for minutes. - SIMULATOR USE MAX BOOTED caps how many run at once; asking for another shuts down the others first. - Steps on one Simulator run one at a time, across every simulator-use process on the Mac, so two agents' or two commands' taps never interleave. Agents that run simulator-use through a shell, a command per call, get the same tools: - simulator-use use is remembered: the next command uses that Simulator, also with several booted. SIMULATOR USE STATE DIR moves simulator-use's files default: simulator-use in the temporary folder . - Each command waits for its own work, so there is no job to follow. - simulator-use call