
Ios Sim Mcp
io.github.rmathew1973由 GitHub 帳號 rmathew1973 發布v0.4.2更新於 Oct 27, 2026
Drive the iOS Simulator via semantic accessibility-tree snapshots. Sub-200ms, no WebDriverAgent.
概覽
透過無障礙樹快照與穩定元素參照驅動 iOS 模擬器,讓助理點擊、輸入、檢視檢視階層、擷取網路流量,並在執行中的 App 內執行 JS。
- 可以做什麼
- 列出並選擇模擬器,依 bundle id 啟動或終止 App
- 擷取無障礙樹快照,依 id、標籤或角色尋找元素,並等待介面穩定
- 點擊、長按、輸入或貼上文字、滑動、捲動、按鍵與硬體按鈕、截圖
- 檢視執行中的 UIView 階層,並命中測試某座標實際由哪個檢視接收(需注入 dylib)
- 錄製、檢視與樁接 App 內 HTTP 流量,並在 App 內執行具持久狀態的 JavaScript
- 將 os_log 輸出串流至環形緩衝區,並依 bundle id、等級或述詞過濾
- 適用情境
- 適合讓助理在模擬器中互動式驅動或測試 iOS App,不必撰寫腳本或 Appium 流程;也適合排查點擊或版面異常,以及在開發期間檢視或樁接 App 的網路請求。
- 執行需求
- macOS 並安裝 Xcode 與 iOS 模擬器、已啟動的模擬器、idb 工具(idb-companion 與 fb-idb)以及 Bun;可透過 npx 或原始碼安裝。選用的 dylib 層需自行建置,且僅在啟動時傳入 inject 為 true 才會載入。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 Ios Sim Mcp,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
ios-sim-mcp
An MCP server that drives the iOS Simulator the way Chrome's DevTools MCP drives a browser: semantic accessibility-tree snapshots with stable element refs, sub-200ms operations, and no screenshot-and-eyeball-coordinates loops.
Built because every existing iOS-simulator automation tool either crawls (Appium, screenshot-based agents) or requires pre-written scripts. This one lets an LLM drive a simulator interactively — find an element by label or ID, tap it by ref, wait for the UI to settle, repeat — at roughly the speed a human would.
Why this exists
Most existing iOS-sim MCPs work by taking a screenshot, asking the model to identify coordinates, then tapping pixels. That's slow (image bytes in context), expensive (vision tokens), and fragile (any layout shift breaks it).
iOS apps already expose a structured accessibility tree — the same one VoiceOver uses. If you query that tree, you get every interactive element with its label, role, AX identifier, frame, and enabled state. You can find what you want semantically, refer to it by a stable ref, and act on it directly. That's what Chrome MCP does for the DOM, and it's what this does for UIView hierarchies in the iOS Simulator.
How it works
Three layers, two shipping today:
idb talks directly to CoreSimulator's private framework — no WebDriverAgent, no HTTP hop into the simulator process — which is why the per-call latency is closer to a local subprocess than to a network round trip.
Requirements
- macOS with Xcode + iOS Simulator
idb(brew tap facebook/fb && brew install idb-companion && pipx install fb-idb)- Bun (
curl -fsSL https://bun.sh/install | bash) - A booted simulator (
xcrun simctl boot <udid>or open it in Xcode)
Install
Quickest path (npm + npx)
npx will fetch the package on first run. Verify:
Layer 2 dylib ships prebuilt as a universal binary (arm64 + x86_64) — works on both Apple Silicon and Intel Macs out of the box.
From source
Then register:
Register with Grok (CLI / Desktop)
Grok MCP spawn PATH is often just /usr/bin:/bin. Bare bun fails with:
Failed to spawn MCP server 'ios-sim': No such file or directory (os error 2)
Use the absolute bun binary in ~/.grok/config.toml:
Or:
Restart the Grok session after changing MCP config. Verify: grok mcp list should show ios-sim connected (not failed).
Register with Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
Or, if installed from source, point at the local path:
Quit and reopen Claude Desktop.
Register with anything else (generic MCP client)
It's a standard stdio MCP server. Run bun run src/server.ts and pipe JSON-RPC. See test/smoke.ts for a working client example.
Tools
Example session
Measured on iPhone 17 Pro / iOS 26.5 / M-series Mac:
snapshot: 127–190 mstap: 100 msfind: <1 ms (in-memory against cached snapshot)await_quiescent: 400 ms – 2.5 s (real UI settle time)
Design notes
- Snapshots are versioned and cached. Any action (
tap,swipe,type_text,launch_app,button,key) invalidates the cache so stale refs can't survive a state change.findand ref resolution will tell you to re-snapshot rather than acting on old data. - Refs are stable within a snapshot, not across snapshots.
e5inv=10is not the same element ase5inv=11. AXUniqueIds (the iOSaccessibilityIdentifier) are stable across snapshots and are the preferred way to target a control across UI changes. await_quiescentignores degenerate AX trees. For ~1s afterlaunch_app,idbreturns just theAXApplicationroot with no children. A naive hash-stability check would return instantly with an empty tree. The implementation resets the stability timer whenever it sees a degenerate snapshot.scrollis reader-perspective.downmeans "see what's below" (finger drags up), matching mouse-wheel/page-down conventions, not finger-direction. Default 0.4s gesture duration prevents iOS from interpreting the release point as a tap on the underlying element.
Tip: add accessibility identifiers to your own apps
This MCP is fastest and most reliable when interactive controls have accessibilityIdentifier set:
Then tap({id: "login_submit_button"}) is O(1) and survives label changes, localization, and layout shifts.
Layer 2 dylib (optional, opt-in per launch)
launch_app({bundle_id, inject: true}) loads a small dylib into the target app via SIMCTL_CHILD_DYLD_INSERT_LIBRARIES. The dylib opens a Unix socket at /tmp/ios-sim-mcp-<bundle-id>.sock and serves JSON-Lines RPC. You can then talk into the running app:
Once connected, IPC is sub-millisecond per call — orders of magnitude cheaper than Layer 1's subprocess-spawn overhead. Connection is lazy (first dylib_* call) and pooled per bundle id; relaunch invalidates the old client automatically.
Why you want to launch with inject: true even for casual driving
iOS auto-capitalizes the first letter of typed input, swaps " ' for smart-quotes, and runs autocorrect on anything that looks like a word. For driving UI those transformations silently mangle:
[email protected]becomes[email protected]→ server rejects the email → login fails with no obvious causecafébecomes nothing iOS can even render → field receives garbagea"b'cbecomesAafter smart-quote substitution and autocorrect
With the dylib loaded, type_text auto-routes through UIPasteboard.general.string = text + the first responder's paste:. Bypasses the keyboard entirely. Whatever you pass arrives byte-perfect.
Build it once:
The dylib is iOS-Simulator-flavored (LC_BUILD_VERSION platform 7, iOS 14+), only loads in apps you launch through this tool with inject: true, does not modify the app on disk, and leaves no trace after the process exits.
When to use snapshot vs view_tree
Both inspect the running UI but they read different trees:
Rule of thumb: use snapshot + find for "tap this control" workflows (especially in SwiftUI apps); use view_tree for "what's actually on screen and why isn't this working" investigation.
Network capture (Layer 2d)
Scope:
- ✅ Catches all
URLSession-based traffic: nativeURLSession,URLRequest, Alamofire, SwiftUIAsyncImage, anything that consumesURLSessionConfiguration.defaultor.ephemeral - ✅ Decrypted HTTP/HTTPS bodies — we sit inside the URLSession pipeline above TLS
- ✅ No certificate install, no mitmproxy, no proxy config
- ⚠️ Does NOT catch: raw
CFNetwork/nw_connection_t(low-level networking written against the BSD socket layer), backgroundURLSessions, gRPC libraries that bypass URLSession,WKWebViewresource loads (separate process) - ⚠️
HTTPBodyStreamrequest bodies are noted but not captured (would require draining + rewinding the stream) - ⚠️ Sessions constructed before
network_startare not retro-fitted; restart the app or relaunch withinject:trueif you need to catch app-startup traffic
Network stubbing (Layer 2f)
Register canned responses for any URL substring; the dylib synthesizes the response instead of forwarding. Unlocks deterministic error-path and slow-network testing without touching the backend.
Stubbed requests still appear in network_tail with stubbed:true and the matching stub_id, so you can verify the app sent what you expected before the synthesized response.
Scripting the running app (Layer 2e)
Bridged globals (all installed automatically on first eval_js):
JSC's JSExport is implemented on UIView, UIWindow, UIViewController, UILabel, UIButton, UITextField, UITextView, UIApplication, NSBundle, NSProcessInfo, NSUserDefaults, UIPasteboard so common property access and method calls work without further bridging. Selector colons become camelCase function names: setObject:forKey: → setObjectForKey(value, key).
App extensions, helpers, plugin hosts
SIMCTL_CHILD_DYLD_INSERT_LIBRARIES only affects the process simctl directly launches — not SpringBoard, not Mediaserverd, not other system daemons. Within your app's own sub-processes (XPC helpers, app extensions launched on demand: share, action, today widget), the dylib will load if dyld honors the env var. To keep things sane:
- If a loaded process has
[NSBundle mainBundle].bundleIdentifierempty or nil (XPC helpers, certain plugin hosts), the dylib silently skips socket binding — no MCP exposure, no/tmp/ios-sim-mcp-*.sockpath collision with the main app. - If a loaded process is a real app extension with its own bundle id (e.g.
com.yourco.app.ShareExtension), the dylib opens a distinct socket at/tmp/ios-sim-mcp-<extension-bundle-id>.sock. You can drive the extension independently by passing that bundle id todylib_*tools. - In normal flows you only
launch_appyour main app. Extensions launched by iOS during a share/widget interaction are technically reachable but not normally targeted — setbundle_idexplicitly on every tool call when you want one.
Distribution model — why runtime injection, not SwiftPM
We ship Option 1: prebuilt universal dylib, injected at runtime via DYLD_INSERT_LIBRARIES. Devs add nothing to their Xcode project — no SPM dep, no build setting, no #if DEBUG guard. The dylib physically isn't part of their app, so App Store submission and production builds are unaffected.
The alternatives we considered:
Option 1 covers the 90% case (driving any iOS sim with no project changes), so we shipped it first. Option 2/3 are reasonable future additions if a user team specifically wants to introspect their own Swift internals.
Safety & production posture
The Layer 2 dylib is engineered to never crash the host app:
SIGPIPEis ignored globally andSO_NOSIGPIPEset per-fd, so a closed MCP-side socket cannot kill the host process.- Every dispatched task (accept loop, per-connection serve, URLSession delegate callbacks, stub synthesis, view walks, JS eval) is wrapped in
@tryand logs viaos_logon catch rather than propagating. - The accept loop survives transient
accept()failures with a 250ms backoff instead of exiting. - Method handlers run on a background queue; UIKit-touching ones bounce to main via
dispatch_syncwith caller-controllable timeouts. - Buffers are bounded: 5,000 log lines, 500 network records (configurable), 256KB body cap (configurable).
- All shared state is
NSLock-protected.
This is a dev/test tool, not for production builds. Two safety invariants make it appropriate only for engineering simulators:
- No App Store risk —
DYLD_INSERT_LIBRARIESis a runtime injection viaSIMCTL_CHILD_*env vars. The dylib is never linked, embedded, or shipped with your app binary. App Store builds physically cannot load it. - Opt-in per launch —
launch_appdefaults toinject: false. The dylib only loads when you explicitly passinject: true, and only in the simulator (Apple's hardened-runtime + code-signing requirements prevent DYLD injection into real-device or production builds).
If you accidentally call a dylib-only tool (view_tree, eval_js, network_*, etc.) without inject: true, you get an actionable error pointing at the fix — never a hang or a crash. Use dylib_health for non-throwing feature detection in scripts.
Roadmap
describe_point— "what's under (x,y)?" for debugging gesture targets.- Video recording — wrap
idb record-videoasrecord_start/record_stop. - Multi-sim parallelism — share one server across multiple booted devices for matrix testing.
- JS interactive REPL tool — multi-line buffer with history, useful for ad-hoc exploration.
License
MIT.
來源:README.md,提交 540b8e6
工具
0版本歷史
1- v0.4.2最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分