mcpField
I
ios-sim-mcp

ios-sim-mcp

ローカル
github.com/rmathew1973開発ツール013 週間前に更新

ios-sim-mcp について

ios-sim-mcpは、iOS Simulatorアプリを対話的に操作するMCPサーバーで、iOSの開発やテストに使えます。アクセシビリティツリーと安定した識別子で要素を特定し、任意でアプリ内の調査やネットワーク操作も行えます。

主な機能

  1. 01アクセシビリティツリーのスナップショット
  2. 02UI要素の検索と操作
  3. 03UIView階層の調査
  4. 04アプリ通信の記録とスタブ化
  5. 05アプリ内でのJavaScript実行
  6. 06システムログのストリーミング

活用シーン

  1. 01ラベルや識別子で画面を操作
  2. 02ビュー階層を使ったタップのデバッグ
  3. 03アプリのHTTP通信の調査や模擬

MCP 対応

UI操作、アプリ内調査、通信記録、ログ取得などのiOS Simulator制御機能をMCPクライアントに公開します。

設定

ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。

mcp.json · 共通設定
{
"mcpServers": {
"rmathew1973-ios-sim": {
"command": "npx",
"args": [
"-y",
"ios-sim-mcp"
]
}
}
}
起動コマンドnpx -y ios-sim-mcp

ローカルサーバーにはマシン上の実行環境(Node.js 18 以上など)が必要です。設定ファイルの場所はクライアントごとに異なります。

パッケージ
npmios-sim-mcp0.4.2

ツール

ツール一覧はまだ取得されていません

ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。

詳細

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 streamos_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_simulatorsUDID、状態、OS バージョンを含むシミュレーターの一覧を表示します
use_simulator特定の UDID を選択します(起動中のシミュレーターが 1 台だけの場合は自動選択されます)
snapshotAX ツリーをキャプチャし、参照名 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_textUIPasteboard と 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_statusURLProtocol インターセプターをインストール/削除し、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_allURL の部分文字列(および省略可能な 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_resetJSContext を破棄し、次回の eval 時にブリッジを再構築します。定義した変数や関数は消去されます
key名前(RETURN、ESC、DELETE、TAB、SPACE、F1–F12、矢印)または生の HID コードでキーを押します
buttonハードウェアボタン:HOME、LOCK、SIDE_BUTTON、SIRI、APPLE_PAY
swipe2 つの参照名/id/座標間をスワイプします。省略可能な duration と delta を指定できます
scroll読者の視点に基づいてスクロールします。down は下のコンテンツを表示します。省略可能なアンカー要素を指定できます
launch_appバンドル ID でアプリを起動します。省略可能な foreground_if_running を指定すると、先に終了します
terminate_appバンドル ID でアプリを終了します
screenshotPNG をディスクに保存するか、インラインで返します。必要な場合にのみ使用してください。AX ツリーのほうが高速で、より豊富な情報を得られます
await_quiescentAX ツリーのハッシュが stable_ms(デフォルトは 250)にわたって同一になるまで、または timeout_ms(デフォルトは 5000)に達するまで待機します。起動や画面遷移の後に、sleep の代わりに使用してください
log_start / log_tail / log_stop / log_clearos_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 ms
  • tap:100 ms
  • find: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 時にすべて自動でインストールされます):

グローバル変数型用途
appUIApplicationapp.windows、app.applicationState
key_window()→ UIWindow最前面のウィンドウ
all_windows()→ [UIWindow]すべてのシーンにある全ウィンドウ
defaultsNSUserDefaultsstringForKey、setObjectForKey など
pasteboardUIPasteboardstring の取得/設定
bundleNSBundlebundleIdentifier、infoDictionary
processNSProcessInfoprocessName、environment、processIdentifier
notif_centerNSNotificationCenter
first_responder()→ UIResponder現在フォーカスされているレスポンダー
find_view_by_ax_id(id)→ UIView | nullビューをすばやく検索
find_view_by_class(name)→ UIView | nullクラス名で最初に一致するものを検索
find_vc_by_class(name)→ UIViewController | nullrootVC から presented と children をたどって検索
post_notification(name, userInfo?)→ voidNSNotification を発行
cls(name)→ ClassNSClassFromString のショートカット
log(msg)→ voidos_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 つの安全性に関する不変条件により、エンジニアリング用シミュレーターでのみ使用できます。

  1. App Store でのリスクなし — DYLD_INSERT_LIBRARIES は SIMCTL_CHILD_* 環境変数を介した 実行時の インジェクションです。dylib がアプリのバイナリにリンク、埋め込み、同梱されることはありません。App Store ビルドでは物理的に読み込めません。
  2. 起動ごとの明示的なオプトイン — 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。

コメント

MCP は初めてですか?

プロトコルの概要から最初のサーバーの導入まで、よくある疑問にまとめて答えます。

AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。