ios-sim-mcp について
ios-sim-mcpは、iOS Simulatorアプリを対話的に操作するMCPサーバーで、iOSの開発やテストに使えます。アクセシビリティツリーと安定した識別子で要素を特定し、任意でアプリ内の調査やネットワーク操作も行えます。
主な機能
- 01アクセシビリティツリーのスナップショット
- 02UI要素の検索と操作
- 03UIView階層の調査
- 04アプリ通信の記録とスタブ化
- 05アプリ内でのJavaScript実行
- 06システムログのストリーミング
活用シーン
- 01ラベルや識別子で画面を操作
- 02ビュー階層を使ったタップのデバッグ
- 03アプリのHTTP通信の調査や模擬
MCP 対応
UI操作、アプリ内調査、通信記録、ログ取得などのiOS Simulator制御機能をMCPクライアントに公開します。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
ios-sim-mcp
Chrome の DevTools MCP がブラウザーを操作するように iOS Simulator を操作する MCP サーバーです。安定した要素参照を使ったセマンティックなアクセシビリティツリーのスナップショット、200ms 未満の操作を実現し、スクリーンショットを見て座標を推測するループは不要です。
既存の iOS シミュレーター自動化ツールは、時間がかかる(Appium、スクリーンショットベースのエージェント)か、事前にスクリプトを記述する必要があるものばかりだったため、開発しました。このツールを使えば、LLM がシミュレーターを対話的に操作できます。ラベルや ID で要素を探し、参照 ID でタップし、UI が落ち着くまで待って、繰り返します。操作速度は人間とほぼ同じです。
このツールを開発した理由
既存の iOS-sim MCP の多くは、スクリーンショットを撮り、モデルに座標を特定させてから、ピクセルをタップする仕組みです。これは遅く(画像データがコンテキストを占有します)、コストが高く(ビジョントークンを消費します)、壊れやすい(レイアウトが少し変わるだけで動作しなくなります)方法です。
iOS アプリは、VoiceOver が使用するものと同じ、構造化されたアクセシビリティツリーをすでに公開しています。このツリーを照会すると、ラベル、ロール、AX 識別子、フレーム、有効状態を持つすべてのインタラクティブな要素を取得できます。意味に基づいて目的の要素を探し、安定した参照 ID で指定して、直接操作できます。Chrome MCP が DOM に対して行うことを、iOS Simulator の UIView 階層に対して行います。
仕組み
3 つのレイヤーのうち、現在 2 つを提供しています。
| レイヤー | 状態 | 技術 | 提供機能 |
|---|---|---|---|
| 1 — AX ツリー+操作 | ✅ 提供中 | idb(Facebook の CoreSimulator ブリッジ) | スナップショット、検索、タップ、入力、スワイプ、スクロール、起動、スクリーンショット。すべて約 100ms |
| 2a — Dylib の実動作確認 | ✅ 提供中 | SIMCTL_CHILD_* 経由の DYLD_INSERT_LIBRARIES | コンストラクターが main() より前に対象アプリ内で実行され、os_log 経由でライフサイクルを記録 |
| 2b — プロセス内 RPC | ✅ 提供中 | Unix ソケット+JSON-Lines | 実行中のアプリとの往復が 呼び出しあたり約 1ms |
| 2c — View のイントロスペクション | ✅ 提供中 | メインスレッドでの UIView の走査 | view_tree、view_hit_test — クラス階層、フレーム、レスポンダーチェーン、VC アノテーション |
| 2d — ネットワークのインターセプト | ✅ 提供中 | URLProtocol+URLSessionConfiguration のスウィズル | アプリ内のすべての HTTP リクエスト/レスポンス(ヘッダー、本文、タイミング)を取得。証明書のインストールも mitmproxy も不要 |
| 2e — JS の評価 | ✅ 提供中 | JSContext+JSExport ブリッジ | eval_js({code}) で app / key_window() / defaults / pasteboard / bundle / process / View 検索ヘルパーを利用可能。Chrome の Runtime.evaluate に相当 |
| 2f — ネットワークのスタブ化 | ✅ 提供中 | 同じ URLProtocol、レスポンスを合成 | URL の部分文字列と任意のメソッドで、定型レスポンスを登録可能。カスタムのステータス/ヘッダー/本文に対応し、遅いネットワークのシミュレーション用に delay_ms も指定可能 |
| 3 — システムログ | ✅ 提供中 | xcrun simctl spawn log stream | os_log を 5000 行のリングバッファにストリーミング |
idb は CoreSimulator のプライベートフレームワークと直接通信します。WebDriverAgent も、シミュレータープロセスへの HTTP 通信も介さないため、呼び出しごとのレイテンシーはネットワーク往復ではなく、ローカルのサブプロセスに近いものになります。
必要条件
- Xcode と iOS Simulator を備えた macOS
idb(brew tap facebook/fb && brew install idb-companion && pipx install fb-idb)- Bun(
curl -fsSL https://bun.sh/install | bash) - 起動済みのシミュレーター(
xcrun simctl boot <udid>を実行するか、Xcode で開いてください)
インストール
最短の方法(npm+npx)
# Register with Claude Code, one line:
claude mcp add -s user ios-sim -- npx -y ios-sim-mcp
初回実行時に npx がパッケージを取得します。次のコマンドで確認してください。
claude mcp list | grep ios-sim # ios-sim: ... ✓ Connected
レイヤー 2 の dylib はユニバーサルバイナリ(arm64+x86_64)としてビルド済みで同梱されているため、Apple Silicon と Intel の両方の Mac でそのまま動作します。
ソースからインストール
git clone https://github.com/rmathew1973/ios-sim-mcp.git
cd ios-sim-mcp
bun install
./dylib/build.sh # builds the Layer 2 dylib (only needed if you'll use inject:true)
続いて、登録します。
# Prefer the absolute bun path — clients with a minimal PATH fail on bare `bun`:
claude mcp add -s user ios-sim -- "$HOME/.bun/bin/bun" run "$(pwd)/src/server.ts"
Grok に登録する(CLI/Desktop)
Grok MCP の spawn PATH は /usr/bin:/bin のみであることがよくあります。その場合、bun をそのまま指定すると次のエラーが発生します。
Failed to spawn MCP server 'ios-sim': No such file or directory (os error 2)
~/.grok/config.toml で bun の絶対パスを使用してください。
[mcp_servers.ios-sim]
command = "/Users/YOU/.bun/bin/bun"
args = ["run", "/Users/YOU/Projects/ios-sim-mcp/src/server.ts"]
enabled = true
startup_timeout_sec = 60
または:
grok mcp add ios-sim -- "$HOME/.bun/bin/bun" run "$HOME/Projects/ios-sim-mcp/src/server.ts"
MCP の設定を変更した後、Grok セッションを再起動してください。確認するには、grok mcp list を実行し、ios-sim が接続済み(failed ではない)と表示されることを確認します。
Claude Desktop に登録する
~/Library/Application Support/Claude/claude_desktop_config.json に追加します。
{
"mcpServers": {
"ios-sim": {
"command": "npx",
"args": ["-y", "ios-sim-mcp"]
}
}
}
ソースからインストールした場合は、ローカルパスを指定します。
{
"mcpServers": {
"ios-sim": {
"command": "/Users/YOU/.bun/bin/bun",
"args": ["run", "/absolute/path/to/ios-sim-mcp/src/server.ts"]
}
}
}
Claude Desktop を終了して、再度開きます。
その他(汎用 MCP クライアント)に登録する
標準的な stdio MCP サーバーです。bun run src/server.ts を実行し、JSON-RPC をパイプで渡します。動作するクライアントの例は、test/smoke.ts を参照してください。
ツール
| ツール | 機能 |
|---|---|
list_simulators | UDID、状態、OS バージョンを含むシミュレーターの一覧を表示します |
use_simulator | 特定の UDID を選択します(起動中のシミュレーターが 1 台だけの場合は自動選択されます) |
snapshot | AX ツリーをキャプチャし、参照名 e1..eN を割り当て、コンパクトなテキスト表示を返します。フィルター:interactive(デフォルト)、actionable、all |
find | スナップショットを id(AXUniqueId)、label、labelContains、role、actionable で検索します |
tap | {ref}、{id}、または {x,y} を指定してタップします。省略可能な duration を指定すると長押しになります |
type_text | フォーカス中のフィールドにテキストを入力します。省略可能な {ref} または {id} を指定すると、タップしてから入力します。dylib が読み込まれている場合、バイト単位で正確に入力できるよう自動的にペースト経由で入力します。iOS の自動修正や先頭文字の大文字化は行われません。via: "keystroke" でキー入力経由を、via: "paste" でペースト経由を強制できます |
paste_text | UIPasteboard と first-responder の paste: を使い、バイト単位で正確なテキストを入力します。dylib のインジェクションが必要です。メールアドレス、パスワード、OAuth トークンなど、大文字と小文字を区別するあらゆるテキストに適しています |
view_tree | 実行中のアプリの UIView 階層をメインスレッドで走査します。クラス名、ウィンドウ座標でのフレーム、alpha/hidden/interactive、テキストコンテンツを報告し、ビューコントローラーの境界に注釈を付けます。snapshot よりも詳細な情報を返し、カスタム描画ビュー、一時的なオーバーレイ、SwiftUI の内部も確認できます。class_filter/ax_id_contains/text_contains でフィルタリングできます。dylib のインジェクションが必要です |
view_hit_test | 「(x,y)でのタップを実際に受け取るビューはどれか」を確認します。最前面のビューと、UIApplication までの完全なレスポンダーチェーンを返します。tap が期待どおりに動作しない場合に適したデバッグツールです。dylib のインジェクションが必要です |
network_start / network_stop / network_status | URLProtocol インターセプターをインストール/削除し、URLSessionConfiguration をスウィズルして、アプリ内を流れる URLSession ベースの HTTP リクエストをすべて記録します。オプション:max_records、max_body_bytes、filter_url_substring。dylib のインジェクションが必要です |
network_tail | キャプチャされた最新の N 件のレコードを返します。デフォルトでは、リクエストごとに 1 行(id、method、status、timing、sizes、URL)を返します。full=true を指定すると、ヘッダーと本文のプレビューをインラインで含めます。since_id で次のページに進みます |
network_get_body | 特定のレコード id のリクエストまたはレスポンス本文全体を取得します(max_body_bytes まで。デフォルトは 256KB)。テキスト本文の場合、base64 と UTF-8 デコード結果を返します |
network_clear | リングバッファと保持されているすべての本文を削除します |
network_self_test | アプリ内から HTTP リクエストを送信し、キャプチャ経路をエンドツーエンドで検証します |
network_stub / network_stubs / network_unstub / network_unstub_all | URL の部分文字列(および省略可能な method)に基づいて、定型の HTTP レスポンスを登録します。リクエストを転送せずにステータス/ヘッダー/本文を合成します。delay_ms でネットワークの遅延をシミュレートします。スタブ化されたリクエストも network_tail に stubbed:true として表示されます |
eval_js | インジェクションされたアプリ内の永続的な JSContext で、任意の JavaScript を実行します。ブリッジされるグローバル:app、key_window()、all_windows()、defaults、pasteboard、bundle、process、notif_center、first_responder()、find_view_by_ax_id、find_view_by_class、find_vc_by_class、post_notification、cls、log。状態は呼び出しをまたいで保持されます。dylib のインジェクションが必要です |
eval_js_reset | JSContext を破棄し、次回の eval 時にブリッジを再構築します。定義した変数や関数は消去されます |
key | 名前(RETURN、ESC、DELETE、TAB、SPACE、F1–F12、矢印)または生の HID コードでキーを押します |
button | ハードウェアボタン:HOME、LOCK、SIDE_BUTTON、SIRI、APPLE_PAY |
swipe | 2 つの参照名/id/座標間をスワイプします。省略可能な duration と delta を指定できます |
scroll | 読者の視点に基づいてスクロールします。down は下のコンテンツを表示します。省略可能なアンカー要素を指定できます |
launch_app | バンドル ID でアプリを起動します。省略可能な foreground_if_running を指定すると、先に終了します |
terminate_app | バンドル ID でアプリを終了します |
screenshot | PNG をディスクに保存するか、インラインで返します。必要な場合にのみ使用してください。AX ツリーのほうが高速で、より豊富な情報を得られます |
await_quiescent | AX ツリーのハッシュが stable_ms(デフォルトは 250)にわたって同一になるまで、または timeout_ms(デフォルトは 5000)に達するまで待機します。起動や画面遷移の後に、sleep の代わりに使用してください |
log_start / log_tail / log_stop / log_clear | os_log を 5000 行のリングバッファにストリームします。bundle_id の部分文字列、level、または NSPredicate でフィルタリングできます |
セッション例
> snapshot
# Settings screen=402x874 v=5 elements=15
e3 Button "Apple Account" #com.apple.settings.primaryAppleAccount @(201,213)
e5 Button "General" #com.apple.settings.general @(201,406)
e6 Button "Accessibility" #com.apple.settings.accessibility @(201,458)
e14 TextField "Search" ="Search" @(201,822)
...
> tap {id: "com.apple.settings.general"}
tapped e5/com.apple.settings.general
> await_quiescent {stable_ms: 250}
stable hash=8326196f25c8 samples=4 elapsed=1731ms
> snapshot
# Settings v=10 elements=13
e2 Button "Settings" #BackButton @(38,84)
e6 Button "About" #About @(201,418)
...
> type_text {ref: "e14", text: "wifi"}
typed 4 chars into e14
iPhone 17 Pro/iOS 26.5/M シリーズ Mac での測定値:
snapshot:127~190 mstap:100 msfind:1 ms 未満(キャッシュ済みスナップショットをメモリ内で検索)await_quiescent:400 ms~2.5 s(実際の UI が落ち着くまでの時間)
設計上の注意点
- スナップショットはバージョン管理され、キャッシュされます。 すべてのアクション(
tap、swipe、type_text、launch_app、button、key)でキャッシュが無効になり、状態変更後に古い参照が残ることはありません。findと参照の解決では、古いデータに基づいて操作するのではなく、スナップショットを再取得するよう案内されます。 - 参照はスナップショット内では安定していますが、スナップショットをまたぐと安定しません。
v=10のe5は、v=11のe5と同じ要素ではありません。AXUniqueIds(iOS のaccessibilityIdentifier)はスナップショットをまたいでも安定しているため、UI が変化した後もコントロールを指定する際に推奨されます。 await_quiescentは退化した AX ツリーを無視します。launch_appの後、約 1 秒間はidbが子要素のないAXApplicationルートのみを返します。単純なハッシュ安定性チェックでは、空のツリーを受け取ってすぐに終了してしまいます。実装では退化したスナップショットが検出されるたびに、安定性タイマーをリセットします。scrollの方向は閲覧者の視点に基づきます。downは「下にあるものを見る」という意味です(指は上にドラッグします)。指の動く方向ではなく、マウスホイールや Page Down の慣習に合わせています。デフォルトのジェスチャー時間を 0.4 秒にすることで、指を離した位置にある背後の要素を iOS がタップとして認識するのを防ぎます。
ヒント:自分のアプリにアクセシビリティ識別子を追加する
インタラクティブなコントロールに accessibilityIdentifier が設定されていると、この MCP は最速かつ最も確実に動作します。
Button("Sign In") { ... }
.accessibilityIdentifier("login_submit_button")
その場合、tap({id: "login_submit_button"}) は O(1) で実行でき、ラベルの変更、ローカライズ、レイアウトのずれにも影響されません。
レイヤー 2 の dylib(任意、起動ごとにオプトイン)
launch_app({bundle_id, inject: true}) は、SIMCTL_CHILD_DYLD_INSERT_LIBRARIES を使って小さな dylib を対象アプリに読み込みます。dylib は /tmp/ios-sim-mcp-<bundle-id>.sock に Unix ソケットを開き、JSON-Lines RPC を提供します。これにより、実行中のアプリと通信できます。
dylib_ping({ echo: "hi" }) // pong from com.apple.Preferences (RTT 1ms)
dylib_info({}) // {pid, bundle_id, process_name, bundle_path, methods, ...}
dylib_call({ method: "...", params }) // generic; future phases register more methods
接続後の IPC は、呼び出しごとに 1 ミリ秒未満です。レイヤー 1 のサブプロセス起動オーバーヘッドに比べ、桁違いに低コストです。接続は遅延方式(最初の dylib_* 呼び出し時)で確立され、バンドル ID ごとにプールされます。再起動すると、古いクライアントは自動的に無効になります。
気軽な操作でも inject: true で起動する理由
iOS は入力の最初の文字を自動で大文字にし、" と ' をスマートクォートに置き換え、単語に見えるものには自動修正を適用します。UI の操作では、こうした変換によって以下のような問題が気づかないうちに発生します。
[email protected]が[email protected]になる → サーバーがメールアドレスを拒否する → 明らかな原因が分からないままログインに失敗するcaféが iOS で表示すらできない文字列になる → フィールドに不正なデータが渡されるa"b'cがスマートクォートへの置き換えと自動修正によりAになる
dylib が読み込まれている場合、type_text は自動的に UIPasteboard.general.string = text と、最初のレスポンダーに対する paste: を使うようになります。キーボードを完全に回避するため、渡した内容がバイト単位で正確に入力されます。
一度だけビルドしてください。
./dylib/build.sh # outputs dylib/build/libios-sim-mcp.dylib
この dylib は iOS シミュレーター向け(LC_BUILD_VERSION のプラットフォームは 7、iOS 14 以降)で、このツールを使って inject: true で起動したアプリにのみ読み込まれます。ディスク上のアプリは変更せず、プロセスの終了後に痕跡を残しません。
snapshot と view_tree を使い分ける場面
どちらも実行中の UI を調べますが、参照するツリーは異なります。
snapshot(Layer 1、AX) | view_tree(Layer 2c、UIView) | |
|---|---|---|
| ソース | iOS のアクセシビリティツリー | 実行中の UIView 階層 |
| dylib が必要か | いいえ | はい(inject: true) |
| レイテンシ | 約 150 ms(サブプロセス) | 約 5 ms(プロセス内) |
SwiftUI の accessibilityIdentifier を認識するか | はい | いいえ(SwiftUI は伝播しません) |
UIKit の accessibilityIdentifier を認識するか | はい(設定されている場合) | はい(設定されている場合) |
| カスタム描画されたビューを認識するか | いいえ(AX ノードがありません) | はい |
SwiftUI の内部構造(UIHostingController など)を認識するか | フラット化されます | 階層全体 |
| ビューコントローラーの境界を認識するか | いいえ | はい |
| alpha/isHidden/interactionEnabled を認識するか | 一部 | はい |
経験則:「このコントロールをタップする」ワークフロー(特に SwiftUI アプリ)では snapshot と find を使います。「実際に画面に表示されているもの」と「なぜ動作しないのか」を調査する場合は view_tree を使います。
ネットワークキャプチャ(Layer 2d)
launch_app({ bundle_id: "com.yourco.app", inject: true })
network_start({ max_body_bytes: 262144 })
// ... use the app normally ...
network_tail({ n: 20, full: true })
// → #34 POST 201 142ms ttfb=98ms reqB=312B resB=2.3KB application/json https://api.yourco.com/orders
// request_headers:
// Authorization: Bearer eyJhbG...
// Content-Type: application/json
// request_body:
// {"sku":"PRO_MONTHLY","quantity":1,"coupon":"WELCOME10"}
// response_body:
// {"order_id":"ord_8a3f2c","total":2700,...}
network_get_body({ id: 34, which: "response" }) // full body if it was truncated
対象範囲:
- ✅
URLSessionベースのすべての通信をキャプチャします。ネイティブのURLSession、URLRequest、Alamofire、SwiftUI のAsyncImageなど、URLSessionConfiguration.defaultまたは.ephemeralを使用するものが対象です。 - ✅ 復号済みの HTTP/HTTPS ボディを取得します。TLS の上位にある URLSession パイプライン内で処理します。
- ✅ 証明書のインストール、mitmproxy、プロキシ設定は不要です。
- ⚠️ 次の通信はキャプチャしません。生の
CFNetwork/nw_connection_t(BSD ソケット層を直接利用して書かれた低レベルネットワーク通信)、バックグラウンドのURLSession、URLSession を迂回する gRPC ライブラリ、WKWebViewのリソース読み込み(別プロセス)。 - ⚠️
HTTPBodyStreamのリクエストボディは記録されますが、キャプチャされません(ストリームを読み切って巻き戻す必要があるためです)。 - ⚠️
network_startより前に作成されたセッションには後から適用されません。アプリ起動時の通信をキャプチャする必要がある場合は、アプリを再起動するか、inject:trueを指定して再起動してください。
ネットワークスタブ(Layer 2f)
任意の URL 部分文字列に対して定型レスポンスを登録できます。dylib がリクエストを転送する代わりにレスポンスを生成します。バックエンドに触れることなく、エラーパスや低速ネットワークのテストを決定論的に実行できます。
// Force a 401 for the next login attempt
network_stub({
url_substring: "/api/auth/login",
method: "POST",
status: 401,
headers: { "Content-Type": "application/json" },
body: '{"error":"invalid_credentials"}',
})
// Simulate slow API for offline-UX testing — 3 second delay
network_stub({
url_substring: "/api/products",
delay_ms: 3000,
status: 200,
body: '[{"sku":"FAKE","name":"Stub Product"}]',
})
// Now drive the app — login fails as if password was wrong; product list spinner shows
network_stubs() // list active stubs
network_unstub({id}) // remove one
network_unstub_all() // clear them all
スタブ化されたリクエストも network_tail に stubbed:true と一致する stub_id とともに表示されるため、生成されたレスポンスを受け取る前に、アプリが想定どおりの内容を送信したことを確認できます。
実行中のアプリのスクリプト操作(Layer 2e)
launch_app({ bundle_id: "com.yourco.app", inject: true })
eval_js({ code: "bundle.bundleIdentifier" })
// → "com.yourco.app"
eval_js({ code: "app.windows[0].rootViewController.title || 'untitled'" })
// → "Home"
// Toggle a feature flag stored in NSUserDefaults
eval_js({ code: "defaults.setBoolForKey(true, 'feature.dark_mode'); 'set'" })
// Inspect or mutate a live view
eval_js({ code: "var v = find_view_by_class('UITextField'); v && (v.text = '[email protected]')" })
// Define a helper, use it later — state persists across calls
eval_js({ code: "function visibleWindows(){return all_windows().filter(w => !w.isHidden && w.alpha > 0.01);}" })
eval_js({ code: "visibleWindows().length" })
// → 1
ブリッジされたグローバル変数(初回の eval_js 時にすべて自動でインストールされます):
| グローバル変数 | 型 | 用途 |
|---|---|---|
app | UIApplication | app.windows、app.applicationState |
key_window() | → UIWindow | 最前面のウィンドウ |
all_windows() | → [UIWindow] | すべてのシーンにある全ウィンドウ |
defaults | NSUserDefaults | stringForKey、setObjectForKey など |
pasteboard | UIPasteboard | string の取得/設定 |
bundle | NSBundle | bundleIdentifier、infoDictionary |
process | NSProcessInfo | processName、environment、processIdentifier |
notif_center | NSNotificationCenter | |
first_responder() | → UIResponder | 現在フォーカスされているレスポンダー |
find_view_by_ax_id(id) | → UIView | null | ビューをすばやく検索 |
find_view_by_class(name) | → UIView | null | クラス名で最初に一致するものを検索 |
find_vc_by_class(name) | → UIViewController | null | rootVC から presented と children をたどって検索 |
post_notification(name, userInfo?) | → void | NSNotification を発行 |
cls(name) | → Class | NSClassFromString のショートカット |
log(msg) | → void | os_log に書き込み(log_tail から確認できます) |
JSC の JSExport は UIView、UIWindow、UIViewController、UILabel、UIButton、UITextField、UITextView、UIApplication、NSBundle、NSProcessInfo、NSUserDefaults、UIPasteboard に実装されているため、追加のブリッジなしで一般的なプロパティの参照やメソッド呼び出しが機能します。セレクターのコロンはキャメルケースの関数名になります。setObject:forKey: → setObjectForKey(value, key)。
アプリの拡張機能、ヘルパー、プラグインホスト
SIMCTL_CHILD_DYLD_INSERT_LIBRARIES が影響するのは、simctl が直接起動するプロセスのみです。SpringBoard、Mediaserverd、その他のシステムデーモンには影響しません。アプリ自身のサブプロセス(XPC ヘルパー、オンデマンドで起動するアプリ拡張機能:共有、アクション、今日のウィジェット)では、dyld が環境変数を尊重すれば dylib が読み込まれます。動作を安定させるため、次のようにしています。
- 読み込まれたプロセスで
[NSBundle mainBundle].bundleIdentifierが空または nil の場合(XPC ヘルパー、一部のプラグインホスト)、dylib はソケットのバインドを通知なくスキップします。MCP への公開は行われず、メインアプリとの/tmp/ios-sim-mcp-*.sockパスの競合も発生しません。 - 読み込まれたプロセスが独自のバンドル ID を持つ実際のアプリ拡張機能(例:
com.yourco.app.ShareExtension)の場合、dylib は/tmp/ios-sim-mcp-<extension-bundle-id>.sockに個別のソケットを開きます。dylib_*ツールにそのバンドル ID を渡せば、拡張機能を個別に操作できます。 - 通常のフローでは、メインアプリに対して
launch_appのみを実行します。共有やウィジェットの操作中に iOS が起動する拡張機能にも技術的にはアクセスできますが、通常は操作対象にしません。拡張機能を操作する場合は、すべてのツール呼び出しでbundle_idを明示的に設定してください。
配布モデル — SwiftPM ではなく実行時インジェクションを採用する理由
Option 1 を提供しています。事前ビルド済みのユニバーサル dylib を DYLD_INSERT_LIBRARIES で実行時に注入します。開発者が Xcode プロジェクトに追加するものは何もありません。SPM の依存関係も、ビルド設定も、#if DEBUG ガードも不要です。dylib はアプリに物理的に含まれないため、App Store への提出や本番ビルドに影響しません。
検討した代替案は次のとおりです。
| Option | 利用できる機能 | 発生するコスト |
|---|---|---|
| 1. 実行時の DYLD インジェクション(現在の方式) | 統合不要。あらゆるシミュレーターのビルドで動作します。App Store には影響しません。 | 公開 API と JSExport でブリッジされた ObjC に限定されます。internal な Swift 型にはアクセスできません。 |
| 2. SwiftPM の開発用依存関係 | @testable インポートにより、アプリの internal 型にアクセスできます。コンパイル時に #if DEBUG で制御できます。アプリ側で定義した境界にカスタムフックを設置できます。 | 開発者はパッケージを追加し、#if DEBUG で制御し、テストビルドで多少のコンパイル負荷を受け入れる必要があります。 |
| 3. 両方 | 両方を利用したい開発者に最適です。 | 保守対象の範囲が 2 倍になります。 |
Option 1 で 90% のケース(プロジェクトを変更せず、あらゆる iOS シミュレーターを操作するケース)に対応できるため、まずこれを提供しました。ユーザーチームが自分たちの Swift の内部実装を調査したい場合は、将来的に Option 2/3 を追加するのも妥当です。
安全性と本番環境での利用方針
Layer 2 dylib は、ホストアプリを決してクラッシュさせないように設計されています。
SIGPIPEはグローバルに無視され、SO_NOSIGPIPEは fd ごとに設定されるため、MCP 側のソケットが閉じてもホストプロセスが終了することはありません。- ディスパッチされるすべてのタスク(accept ループ、接続ごとの処理、URLSession のデリゲートコールバック、スタブの合成、ビューの走査、JS の評価)は
@tryでラップされ、例外が発生した場合は伝播させずにos_logでログを記録します。 - accept ループは、一時的な
accept()の失敗時に終了せず、250ms のバックオフで処理を継続します。 - メソッドハンドラーはバックグラウンドキューで実行されます。UIKit に触れるものは、呼び出し元がタイムアウトを指定できる
dispatch_syncを介してメインスレッドに渡されます。 - バッファーには上限があります。ログ行は 5,000 件、ネットワークレコードは 500 件(設定可能)、ボディは 256KB(設定可能)です。
- すべての共有状態は
NSLockで保護されています。
これは開発/テスト用のツールであり、本番ビルド向けではありません。 次の 2 つの安全性に関する不変条件により、エンジニアリング用シミュレーターでのみ使用できます。
- App Store でのリスクなし —
DYLD_INSERT_LIBRARIESはSIMCTL_CHILD_*環境変数を介した 実行時の インジェクションです。dylib がアプリのバイナリにリンク、埋め込み、同梱されることはありません。App Store ビルドでは物理的に読み込めません。 - 起動ごとの明示的なオプトイン —
launch_appのinject: falseがデフォルトです。inject: trueを明示的に指定した場合に限り、かつシミュレーター内でのみ dylib が読み込まれます(Apple の強化ランタイムとコード署名の要件により、実機や本番ビルドへの DYLD インジェクションはできません)。
inject: true を指定せずに dylib 専用ツール(view_tree、eval_js、network_* など)を誤って呼び出した場合は、修正方法を示す具体的なエラーが返されます。ハングやクラッシュは発生しません。スクリプトで例外を発生させずに機能を検出するには、dylib_health を使用してください。
ロードマップ
describe_point— ジェスチャーの対象をデバッグするための、「(x,y)の下にあるものは?」という機能です。- 動画録画 —
idb record-videoをrecord_start/record_stopとしてラップします。 - 複数シミュレーターでの並列実行 — 複数の起動済みデバイスで 1 つのサーバーを共有し、マトリックステストを行います。
- JS インタラクティブ REPL ツール — 履歴付きの複数行バッファーで、アドホックな調査に便利です。
ライセンス
MIT。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。