Mobdev について
Mobdevは、iPhone、iOSシミュレーター、Android端末を1つのウィンドウで操作するMacアプリです。人やAIエージェント向けに、画面表示、入力、アプリ管理、エージェント用インターフェースを提供します。
主な機能
- 01画面ミラーリングとタッチ・キーボード入力
- 02アプリのインストール、起動、管理
- 03コンソール出力とクラッシュレポートの確認
- 04操作フローの記録と再生
- 05UI要素の検査とタップ手順の生成
活用シーン
- 01コーディングエージェントでモバイル端末を操作
- 02端末へのアプリのインストールとテスト
- 03アプリログやクラッシュ情報の確認
- 04テストフローの記録と再生
MCP 対応
MobdevはMCPサーバーを内蔵し、接続端末の操作、画面表示、入力操作をクライアントに公開します。stdioまたはStreamable HTTPで接続できます。
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
このサーバーのパッケージやリモートエンドポイントはまだ取得できていません。プロジェクトの README の手順に従ってください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
Mobdev for Mac
Mobdev アプリ:あなたと AI エージェントのために、iPhone、iOS シミュレーター、Android デバイスを 1 つのウィンドウにまとめます。無料、オープンソース、約 2 MB、アカウント不要です。
Mobdev は USB ケーブル経由で iPhone の画面を読み取り、Bluetooth 経由でキーボードとポインターとして動作し、タップや入力を行います。スマートフォンには何もインストールされません。開発者モードも脱獄も不要です。(iPhone でオプションの UI ツリーを使う場合のみ、小さな UI テストが実行されます。)起動済みのシミュレーターや Android デバイスにも同じツールを使用できます。エージェントには MCP サーバーと小規模な HTTP API が提供されます。 オプションのリレーを使うと、別の場所にいるエージェントからデバイスにアクセスできます。
USB cable: screen frames (like QuickTime)
┌─────────┐ ◀──────────────────────────────────────── ┌────────┐
│ Mac │ │ iPhone │
│ Mobdev │ ────────────────────────────────────────▶ │ │
└─────────┘ Bluetooth LE: keyboard + pointer (HID) └────────┘
▲ ▲ AssistiveTouch turns
│ └── MCP / HTTP on 127.0.0.1 the pointer into taps
│
└── optional outgoing connection to a relay (see ../relay)
要件
- Bluetooth LE を搭載し、macOS 26 以降を実行している Mac(インターフェイスは Liquid Glass を使用します)。macOS 27 と Swift 6.4 で開発されています。
- iPhone と USB データケーブル。充電専用ケーブルでは電源しか供給されず、画面は表示されません。
- ビルドする場合:Xcode 26 以降(Swift 6.2 以降、macOS 26 SDK)。
ビルドと実行
cd macos
MOBDEV_VARIANT=release scripts/build-app.sh # builds build/Mobdev.app (ad hoc signature)
open build/Mobdev.app # allow Bluetooth and Camera when macOS asks
macOS は iPhone の画面をカメラのように扱うため、カメラへのアクセスを求めるプロンプトが表示されます。証明書で署名するには、CODESIGN_IDENTITY を設定します。UNIVERSAL=1 を指定すると、Apple シリコンと Intel 向けにビルドされます。アドホック署名の場合、再ビルドのたびに macOS からアクセス許可を再度求められることがあります。
MOBDEV_VARIANT=release を指定しない場合、スクリプトは開発用の Mobdev Dev(build/Mobdev Dev.app)をビルドします。これはインストール済みの Mobdev に変更を加えず、隣で実行できる別アプリです。バンドル ID は dev.mobdev.mac.dev、設定とシークレットは独自の ~/Library/Application Support/dev.mobdev.mac.dev に保存され、ポートは 4687、URL スキームは mobdev-dev://(ダッシュボードのリンクがインストール済みのアプリを引き続き開くようにするため)、MCP 名は mobdev-dev です。琥珀色のアイコンを使用し、自動更新はありません。両方のアプリは Mac の Bluetooth を共有するため、タッチをテストする前にどちらか一方を終了してください。
iPhone のセットアップ(一度だけ)
初回起動時にセットアップアシスタントが手順を案内します。各機能について説明する画面でカメラと Bluetooth へのアクセスを求め、Mac が検出すると各手順を完了済みにします。**Mobdev › iPhone をセットアップ…**から再度開けます。iPhone がない場合は、ウェルカム画面で シミュレーターと Android のみを選択してください。セットアップするまでは、カメラや Bluetooth へのアクセスを求められません。
- iPhone を接続してロックを解除し、信頼をタップします。Mac からアクセサリの使用許可を求められた場合は、許可をクリックします。画面が Mobdev に表示されます。
- iPhone で 設定 > Bluetooth を開き、その他のデバイスの下にあるこの Mac をタップしてペアリングします。iOS では「Mobdev」ではなく、Mac の名前(システム設定 › 一般 › 共有)で表示されます。同じ Apple Account を使用している iPhone では、Handoff によってすでに Mac を認識し、キャッシュ済みの Bluetooth サービスを信頼しているため、Mac が表示されないことがあります。iPhone がペアリングされるまで、Mobdev は 8 秒後に Bluetooth サービスを再構築し、その後は間隔を倍にしながら最長 2 分まで繰り返します。これにより iOS が再度検索します。セットアップ画面の iPhone で再表示を使うと、すぐに再構築できます。
- 設定 > アクセシビリティ > タッチ > AssistiveTouchをオンにします。iOS にポインターが表示され、クリックがタップに変わります。同じ画面で 項目にスナップをオフにし、タッチジェスチャを実行はオンのままにします。「項目にスナップ」がオンだと、iOS はポインターを最も近い項目に移動し、スワイプするたびにその項目をタップします。Mobdev は最初のスワイプの前と、ミラー画面でドラッグした後に、クリックせずにこの設定を確認します。結果はデバイスパネルと
statusに ポインターとして表示され、ポインターがスナップする状態ではswipeを実行できません。 - Mobdev で、iPhone の「設定 > 一般 > キーボード > ハードウェアキーボード」に合ったキーボードレイアウト(U.S. または German)を選びます。
- エージェントが作業している間は、自動ロックを「しない」に設定します。iPhone のロックは解除したままにしてください。
iOS では、ケーブルの有無にかかわらず、Bluetooth キーボードが接続されている間は画面上のキーボードが非表示になります。5 分間入力がないと、Mobdev は Bluetooth 接続を解放するため、iPhone に内蔵キーボードが再び表示されます。その後、Mobdev から次にタップまたはキー入力すると、数秒以内に再接続されます(5 分間の設定は
MOBDEV_BLUETOOTH_REST_SECONDSで変更できます)。iPhone または iPad をペアリングせずに接続した場合、Mobdev はペアリングできるよう、10 分間接続を維持します。その後は Show on iPhone Again で再接続できます。ミラー上にマウスを移動するとすぐに接続され、Bluetooth のスリープを防ぐため、最初のクリックも遅延なく届きます。iPhone がすべての入力機器に再接続するまでは、ポインターの移動を伴わないクリックが送信されないよう、Mobdev は入力を拒否します。
アプリ内のミラーはライブ表示です。クリックしてタップ、ドラッグしてスワイプ、トラックパッドを2本指でスワイプ(横方向も可能)、ホイールでスクロール(横方向のホイール操作、または Shift キーを押しながらのスクロールで横方向に移動)できます。フォーカスがある間は文字を入力できます。 ⌘V で Mac のクリップボードの内容をスマートフォンに入力します。右側のインスペクターには不足している項目が表示され、Activity にはエージェントのすべての操作が記録されます。Settings(⌘,)ではキーボードレイアウトと API トークンを設定できます。
- Apps(インスペクター内)には、開発用にインストールされたアプリ、またはすべてのアプリが表示されます。
.app、.ipa、.apkをドロップしてインストールできます。アプリの起動、停止、削除、コンソールのライブ表示、クラッシュレポートの表示、ディープリンクのテストもできます。エージェントと同じツールを使用するため、iPhone では Developer Mode と Xcode が必要ですが、シミュレーターと Android では何も必要ありません。 - Diagnose…(セットアップ手順の下、およびデバイス情報内)では、ケーブル、映像、iPhone の USB 画面インターフェイス、macOS の画面キャプチャヘルパー、Bluetooth、AssistiveTouch、Developer Mode をチェックし、それぞれの修正方法(停止したキャプチャヘルパーの再起動など)を提示します。
- Projects(サイドバー上部)には、アプリのテスト、フロー、スクリーンショット、マップ、録画、実行結果がリポジトリ内のフォルダーに保存されます。Projects を参照してください。
- Record と Run Flow…(アクティビティのヘッダー)では、フローを記録して再生できます。詳しくは以下を参照してください。
- Inspect(ツールバー)では、画面上にインスペクターを重ねて表示します。ポインターの下にある要素が、ロール、ラベル、識別子とともに枠で囲まれます。クリックするとデバイスをタップする代わりに、
{"tap_element":{"id":"login"}}のような、その要素をタップするステップをコピーします。シミュレーター、Android、および UI ツリーが有効な iPhone で利用できます。 - Settings › Agents には、エージェントが開けないアプリが表示されます。セキュリティとプライバシーを参照してください。
エージェントを接続する
アプリには、正しいパスが設定された、コピーしてすぐ使える構成が表示されます。stdio 版ではトークンは不要です。Mobdev mcp はトークンをローカルで読み取り、必要に応じてアプリをバックグラウンドで起動します。TCP ポートではなく、アプリのデータディレクトリ内にある Unix ソケット経由でアプリに接続するため、Mac 上の別のユーザーがアプリになりすましてトークンを収集することはできません。
Claude Code:
claude mcp add --scope user mobdev -- /Applications/Mobdev.app/Contents/MacOS/Mobdev mcp
Codex(~/.codex/config.toml):
[mcp_servers.mobdev]
command = "/Applications/Mobdev.app/Contents/MacOS/Mobdev"
args = ["mcp"]
Claude Desktop、Cursor、その他のクライアント:
{ "mcpServers": { "mobdev": { "command": "/Applications/Mobdev.app/Contents/MacOS/Mobdev", "args": ["mcp"] } } }
Streamable HTTP に対応するクライアントでは、Authorization: Bearer <token> を指定して http://127.0.0.1:4686/mcp も使用できます(トークンはアプリ内でコピーしてください)。サーバーは MCP 2026-07-28 と、2024-11-05 から 2025-11-25 までの initialize ベースの旧バージョンに対応しています。
ツール
座標は、screenshot が返す画像のピクセル単位です(長辺は 1280 px、原点は左上)。screenshot が false でない限り、操作の実行後に MCP 経由で最新のスクリーンショットが返されます。デバイスが複数ある場合は、list_devices の ID または名前を device に渡して選択してください。指定しない場合、Mobdev はデバイスが1台ならそのデバイスを使用し、シミュレーターまたは Android デバイスと同時に接続されている場合は iPhone を使用します。
| ツール | 引数 | |
|---|---|---|
list_devices | iPhone、起動済みシミュレーター、Android デバイス:ID、名前、モデル、システムバージョン、準備状況 | |
status | 画面と入力の準備状況、スクリーンショットのサイズ | |
screenshot | 画面の JPEG | |
tap | x、y、double | 座標を指定するすべてのツールと同様に、x と y には画面に対する割合(例:"50%")も指定できます |
long_press | x、y、seconds | |
swipe | from_x、from_y、to_x、to_y、duration | |
scroll | direction(up/down/left/right)、amount、x、y | マウスホイール。right はさらに右側のコンテンツを表示します |
type_text | text、submit | フォーカス中のフィールドに入力します。1 回の呼び出しで最大 1000 文字です |
press_key | key、modifiers、count | 例:Spotlight では space と cmd を組み合わせます。volume_up と volume_down はボタンを押します(iPhone、Android) |
home | ||
open_app | name | iPhone では Spotlight 経由で開きます。シミュレーターと Android では名前またはバンドル ID で開きます |
read_screen | 画面上に表示されているすべてのテキストとその位置(デバイス上の OCR) | |
find_text | text | |
tap_text | text、index、double、hold | 画面に表示されているラベルをタップします。double は 2 回タップし、hold は指定秒数だけ長押しします |
wait_for_text | text、timeout、gone | |
observe | image、contains | 番号付きマークとして画面を表示します。UI ツリーの要素があればそれを、なければ認識されたテキストを表示します。image はスクリーンショット上にマークを描画します |
tap_mark | mark | 直前の observe によるマークをタップします |
scroll_until_visible | text、id、direction、max_scrolls | 表示されるまでスクロールします。リストの末尾に達すると停止します |
wait_for_idle | timeout、stable | 画面の変化が止まるまで待機します。ステータスバーは無視します |
ui_tree | contains、all | アクセシビリティツリーから要素を取得します。ロール、ラベル、識別子、値、位置が含まれます。シミュレーター、Android、UI ツリーが有効な iPhone で利用できます |
tap_element | id、text、index、timeout、double、hold | 識別子またはラベルで要素をタップし、最大 5 秒間待機します |
wait_for_element | id、text、timeout、gone | wait_for_text と同様に、ツリーから要素を確認します |
assert_screenshot | name または path、threshold、mask、compare_status_bar、update | 画面をベースライン画像と比較し、差分があれば失敗します。ベースラインがない場合は記録します(チェックを参照) |
accessibility_audit | image、fail_on、ignore | UI ツリーから、ラベルの欠落、小さすぎるタップ領域、低いテキストコントラスト、重複または不明瞭なラベルを検出します |
assert_with_ai | question、expect | Mac 上の Apple Intelligence に、画面について「はい」または「いいえ」で答えられる質問をします |
run_flow | path、steps、variables、video | フロー(JSON または Maestro YAML)を再生し、最初に失敗したステップで停止します。video は録画(.mp4)を保存します |
run_tests | project、tests、variables、video、language | このデバイスでプロジェクトのテストを実行します。すべての結果、最初の失敗時の画面、results.json、junit.xml を取得します |
list_projects | Mobdev が認識しているプロジェクト、アクティブなプロジェクト、呼び出しで使用するプロジェクトを表示します | |
create_project | path、name、bundle_id、builds | ~/code/my-app/mobdev のようなプロジェクトを作成し、アクティブにします。bundle_id がない場合は、リポジトリにアプリが 1 つだけあればそれを選択し、複数ある場合は一覧を表示します |
open_project | project | フォルダー、mobdev/ を含むリポジトリ、または名前でプロジェクトをアクティブにします |
list_tests | project | プロジェクトのテストと最新の結果を表示します |
save_test | project、name、steps、description、platforms、file | プロジェクトにテストを書き込みます。必要に応じてプロジェクトも作成します |
save_flow | project、name、steps、file | プロジェクトの flows/ にフローを書き込みます |
test_result | project、run | 最新の実行結果を表示します。各失敗について、ステップ、スクリーンショット、動画が含まれます |
save_screenshot | path | プロジェクトの screenshots/ に画面をフル解像度の PNG として保存します。例:de-DE/iphone-6.9/01-home |
crawl_app | bundle_id、max_actions、max_screens、max_depth、seconds、avoid、output | モデルを使わずにアプリを探索し、画面のマップを作成します。また、クラッシュのたびにそこへ至るステップとともに記録します |
navigate_to | bundle_id、screen、map | crawl_app が把握しているマップ上の画面に移動します |
list_apps | all | 開発用にインストールされたアプリ、またはすべてのアプリを表示します |
install_app | path | この Mac 上のパスにあるビルドをインストールします。iPhone では .app/.ipa、シミュレーターでは .app、Android では .apk を指定します |
uninstall_app | bundle_id | 開発用にインストールされたアプリのみを対象とします |
launch_app | bundle_id、arguments、environment、restart | アプリの出力を取得します |
stop_app | bundle_id | |
open_url | url | ディープリンク、ユニバーサルリンク、ウェブページを開きます。UI ツリーがある場合は、iOS の「“…”で開きますか?」を確認します |
logs | bundle_id、after、lines、contains | 起動したアプリの出力と終了状況 |
crash_reports | app、name、limit | レポートを一覧表示します。name を指定するとレポートを読み込みます |
start_network_capture | bundle_id | アプリが行うリクエストを記録します(ネットワークを参照) |
network_log | bundle_id、contains、after、limit、query、headers | 記録されたリクエストを表示します。メソッド、URL、ステータス、送受信バイト数、所要時間、検出場所が含まれます |
stop_network_capture | bundle_id | キャプチャを終了します。Android ではデバイスから Mobdev のプロキシを削除します |
mock_response | url、status、body、content_type、clear | Android:プロキシ経由のプレーン HTTP リクエストに対する固定応答を設定します |
set_location | latitude、longitude、route、speed、clear | シミュレートした位置、またはデバイスがたどるルートを設定します |
set_permission | bundle_id、permission、state | プロンプトを表示せずに、位置情報、写真、連絡先、カメラなどの権限を許可、取り消し、またはリセットします |
send_push | bundle_id、title、body、badge、data、payload | シミュレーター上のアプリにプッシュ通知を送信します |
set_appearance | dark、text_size、increase_contrast、reduce_motion | ダークモード、Dynamic Type、アクセシビリティの切り替えを設定します |
set_language | language、bundle_id | 例:de-DE。シミュレーター、または Android アプリ 1 つの言語を設定します |
set_status_bar | preset、time、battery_level、battery_state、wifi_bars、cellular_bars、network | screenshot は 9:41 とフルバーを設定します。clear でリセットします |
biometrics | action(match/fail/enroll/unenroll) | Face ID、Touch ID、または指紋認証のプロンプトに応答します |
reset_app | bundle_id、keychain | アプリをインストールしたばかりの状態になるよう、アプリのデータを削除します |
clipboard | text | クリップボードを読み取るか、text の内容に設定します |
set_orientation | orientation | portrait、landscape_left、landscape_right、portrait_upside_down |
start_recording | path | stop_recording まで画面を .mp4 に録画します。デフォルトではプロジェクトの output/recordings に保存します |
stop_recording | 録画を終了し、保存先を表示します | |
recent_steps | count、clear | 成功した最新のアクションを、save_test 用のフローステップとして表示します |
run_shortcut | name | Apple のショートカットアプリからショートカットを実行します |
performance | bundle_id、seconds、max_cpu、max_memory_mb、max_janky_percent | 実行中のアプリの CPU、メモリ、(Android の)フレームを測定します。予算を超えると呼び出しは失敗します。シミュレーターと Android で利用できます |
measure_launch | bundle_id、runs、method、max_ms、stable | 複数回の実行におけるコールド起動時間を測定します。最小値、中央値、最大値を表示します |
dev_menu | port | React Native または Expo アプリの開発者メニューを開きます |
reload_app | port | React Native または Expo アプリを Metro 経由で再読み込みします。Metro が利用できない場合は、開発者メニュー経由で再読み込みします |
テキスト認識には Mac 上の Apple の Vision フレームワークを使用します。エージェントがモデルに送信しない限り、画面の内容がマシンの外に出ることはありません。各認識は新しいプロセス(Mobdev __text)で実行され、Neural Engine が失敗すると CPU に切り替わります。それでも失敗した場合、テキストツールは Mobdev Runner を使ってシミュレーター、Android、iPhone の UI ツリーから応答し、そのことを知らせます。UI ツリーがない場合は、認識に失敗したことを伝え、screenshot を提案します。 |
ターミナルから
Mobdev call <tool> はシェルからツールを 1 つ実行するためのコマンドで、スクリプトや MCP よりターミナルを好むエージェントに適しています。Mobdev <tool> key=value … はその短縮形で、Mobdev tools はツールの一覧を表示します。JSON として解析できる値は JSON として、それ以外の値はテキストとして扱われます。
M=/Applications/Mobdev.app/Contents/MacOS/Mobdev
$M observe
$M tap_mark mark=4
$M type_text text="hello world" submit=true
$M screenshot --image screen.png
$M call set_location '{"latitude": 52.52, "longitude": 13.405}' --device "iPhone 17"
実行中のアプリを経由するため、iPhone にも届き、Activity に表示されます。アプリがない場合、または --local を指定した場合は、起動済みのシミュレーターや Android デバイス上で Mobdev flow と同様に単独で実行されます。--json は結果全体を出力し、--image は返されたスクリーンショットを保存します。--project はツールが保存したものを受け取るプロジェクトを指定します(既定値は現在のフォルダーのプロジェクトです。Projects を参照)。ツールが成功した場合は 0、エラーを報告した場合は 1、実行できなかった場合は 2 で終了します。start_recording、tap_mark、ネットワークキャプチャなど、先行する呼び出しを前提とする操作にはアプリが必要です。
画面の確認、マーク、待機
observe は、エージェントが画面を確認する最も手軽な方法です。UI ツリーがある場合はそこから、ない場合はテキスト認識から、読み取りまたはタップできる項目を番号付きリストで表示します。例:[4] Button "Sign in" id=login (295, 640)。Android のリストのように、ラベルのないタップ可能な行では、その行内のテキストが使われます。image: true を指定すると、番号を重ねたスクリーンショットが追加され、座標の判断が苦手なモデルに役立ちます。tap_mark は番号でマークをタップします。画面が変わったら、もう一度 observe を呼び出してください。フローの記録中にマークをタップすると、フローが番号に依存しないよう、tap_element(または tap_text)として保存されます。
scroll_until_visible は、要素(id)またはテキストが表示されるまでスクロールし、リストがそれ以上動かなくなると停止します。wait_for_idle は、アニメーションや読み込み後に一定時間待つ代わりに、画面に stable 秒間変化がない状態になるまで待機します(ステータスバーの変化は対象外です)。
デバイスの状態
テストやエージェントでは、設定画面を操作する代わりに、状況を直接設定します。
| iOS シミュレーター | Android | iPhone | |
|---|---|---|---|
set_location | simctl location。ルートも設定できます | エミュレーター(geo fix)。Mobdev ではルートに沿って移動できます | devicectl、デベロッパーモード |
set_permission | simctl privacy:カレンダー、連絡先、位置情報、写真、マイク、モーション、リマインダー、Siri(カメラと通知は対象外) | pm grant/revoke。カメラと通知にも対応しており、任意の android.permission 名も指定できます | なし |
send_push | APNs ペイロードを指定した simctl push | アプリには配信されず、シェルから投稿される通知 | なし。プッシュ通知はお使いの APNs キーを通じて送信されます |
set_appearance | simctl ui。視差効果を減らすには Xcode 27 が必要です | ダークモード、フォントサイズ、コントラスト、アニメーション | devicectl、デベロッパーモード |
set_language | シミュレーター全体。アプリの再起動が必要です | 1 つのアプリ(Android 13 以降) | なし |
set_status_bar | simctl status_bar | System UI デモモード | devicectl、デベロッパーモード |
biometrics | Face ID と Touch ID。登録の有無は問いません | エミュレーターの指紋センサー(設定で一度登録してください) | なし |
reset_app | アプリのデータを削除します。オプションでキーチェーンも削除できます | pm clear | なし。代わりに再インストールしてください |
clipboard | simctl pbcopy/pbpaste | なし | devicectl、デベロッパーモード |
set_orientation | Xcode 27 | 任意 | devicectl、デベロッパーモード |
iPhone の列では Xcode 27 の devicectl を使用します。シミュレーターでは動作を確認済みですが、iPhone ではまだ確認できていません。iOS アプリを別の言語で一度だけ起動するには、launch_app の arguments に
["-AppleLanguages", "(de)", "-AppleLocale", "de_DE"] を指定すると、シミュレーターと iPhone の両方で動作します。set_language で設定した言語は、アプリを再度起動すると反映されます。システムのアラートは、シミュレーターを再起動すると反映されます。シミュレーターのスクリーンショットは、デバイスが回転しても向きが変わりません。アプリの表示は横向きになりますが、タップには同じ画像が使われるため、エージェントが見ている要素の位置にタップされます。
録画と最近のステップ
start_recording は、stop_recording が呼び出されるまで画面を .mp4 に録画します。保存先はプロジェクトの output/recordings です。Mobdev のフォルダーにプロジェクトがない場合は recordings/ に保存されます。ただし、path で別の場所を指定した場合はその場所に保存されます。シミュレーターと iPhone では毎秒約 10 フレームで録画し、30 分後に自動停止します。recent_steps は、デバイス上でエージェントまたはアプリが実行し、成功した最新の操作をフローステップとして返します。表示操作と失敗した呼び出しは除外され、入力操作はまとめられます。エージェントがアプリ内の経路を見つけると、そのステップを save_test で保存します。
run_shortcut は、Apple のショートカットアプリにあるショートカットを名前で実行します。デベロッパーモードのシミュレーターと iPhone では shortcuts:// リンクを使い、それ以外では Spotlight を使います。ショートカットを使うと、Focus のオンや明るさの変更など、ツールから外部経由では実行できない操作も行えます。
開発者向けツール
list_appsからcrash_reportsまでのツールを使うと、開発したアプリの一連の作業を完了できます。エージェントはxcodebuildでビルドし、ビルドをインストールしてアプリを起動し、上記のツールで操作して、出力やクラッシュレポートを読み取ります。これらのツールはXcodeのdevicectlを使用するため、MacにXcodeが、iPhoneにデベロッパーモード(「設定」>「プライバシーとセキュリティ」>「デベロッパーモード」)が必要です。それ以外の機能は、どちらもなくても動作します。
xcodebuild -scheme MyApp -destination 'generic/platform=iOS' -derivedDataPath build build
# install_app {"path": "/path/to/build/Build/Products/Debug-iphoneos/MyApp.app"}
# launch_app {"bundle_id": "com.example.MyApp"}
# logs {"bundle_id": "com.example.MyApp"} → print, NSLog and os_log lines
launch_appはOS_ACTIVITY_DT_MODEを設定し、Xcodeと同様に、Loggerとos_logのメッセージがコンソールに届くようにします。devicectlが起動直後にコンソールへ接続するため、最初の数ミリ秒に出力された行は欠落することがあります。logsはcursorを返します。これをafterとして渡すと、それ以降の新しい行のみを取得できます。アプリが終了またはクラッシュすると、logsがそのことを示し、crash_reportsにレポートが一覧表示されます。crash_reportsにnameを指定すると、レポートが~/Library/Application Support/dev.mobdev.mac/crash-reportsにコピーされ、例外、原因、クラッシュしたスレッドが返されます。レポートにシンボルがない場合、自分のコードのフレームにはatosで使用できるアドレスが含まれます。install_appは、Mobdevを実行しているMac上のpathを読み取ります。エージェントがリレー経由で接続している場合も同様です。クラウドエージェントのコンテナーなど、別の場所で作成したビルドは、先にアップロードしてからuploadでインストールします(どこからでもビルドをインストールするを参照)。uninstall_appは、App Storeアプリとシステムアプリのアンインストールを拒否します。
パフォーマンス
performanceは、実行中のアプリを指定したseconds(デフォルトは5、最大60)にわたってサンプリングし、CPUの平均使用率とピーク使用率(1コアに対する割合)、メモリの開始時・終了時・ピーク時の値、Androidではフレーム数を報告します。measure_launchはアプリを停止して再起動し、runs回(デフォルトは3回)計測します。予算を設定すると、どちらもテストやフローのチェックとして実行されます。アプリがmax_cpu(平均)、max_memory_mb(ピーク)、max_janky_percent、またはmax_ms(中央値)を超えると、数値とともに呼び出しが失敗します。
- iOS Simulator:アプリはMac上のプロセスです。Mobdevは、そのシミュレーター内で
launchctl listを実行してpidを特定し、proc_pid_rusageを使ってCPU時間と物理フットプリント(Xcodeのゲージに表示されるメモリ量)を読み取ります。起動時間は、simctl spawn <udid> log streamでストリーミングされるiOS独自のApplicationFirstFramePresentationサインポストです。これは、SpringBoardが起動要求を受け取ってからアプリの最初のフレームが表示されるまでの時間であり、XcodeのOrganizerとMetricKitが報告する数値です。シミュレーターにはフレーム時間がないため、フレーム時間の計測にはAndroidまたはInstrumentsを使用してください。シミュレーターはMacのCPUを共有するため、計測値で比較できるのはアプリ自身の実行間であり、iPhoneでの速度ではありません。 - Android:1つのadbシェルコマンドで、約1秒ごとに
/proc/<pid>/statとVmRSSを読み取り、計測前にdumpsys gfxinfoをリセットしてから計測後に読み取ります(フレーム数、ジャンクの割合、フレーム時間の50~99パーセンタイル)。また、dumpsys meminfoからPSSを取得します。起動時間は、am force-stopの後に実行するam start -WのTotalTimeです。 - iPhone:CPUとメモリの計測にはInstrumentsが必要です。そのため、
performanceはxcrun xctrace record --template 'Activity Monitor'で記録する方法を案内します。measure_launchでは、代わりに画面表示を計測します。 method: "screen"(すべてのデバイス):起動アニメーションなどで最初に変化したフレームから、画面がstable秒間(デフォルトは2秒)静止する前に最後に変化した時点までを計測します。最初のフレームの後にアプリが読み込む内容も含まれますが、起動画面やスプラッシュ画面がstableより長く表示されると、その時点で計測が終了します。- リレー経由の呼び出しは90秒でタイムアウトするため、
secondsと、起動のruns回分の計測時間をその範囲内に収めてください。
React Native、Expo、Flutter
reload_appはこの Mac 上の Metro のメッセージソケット(ws://localhost:<port>/message、デフォルトのポートは 8081。Expo CLI も対象)に接続し、接続中のすべてのアプリにreloadを送信します。これは Metro のターミナルでrを入力するのと同じです。Expo CLI がこれを受け付けるのは同じ Mac からの場合のみです。アプリが接続されていない場合、または Metro から応答がない場合は、Mobdev が開発メニューを開き、Reload をタップします。dev_menuはシミュレーターを振ります(Darwin 通知com.apple.UIKit.SimulatorShakeを受け取ったシミュレーター内の UIKit が、最前面のアプリを振ります)。Android では Menu キー(input keyevent 82)を押し、Mac から振ることのできない iPhone では、Metro 経由でdevMenuを送信します。- Expo Go プロジェクトを開くには
open_urlとexp://127.0.0.1:8081を使用し、開発ビルドには<scheme>://expo-development-client/?url=…を使用します。tap_elementでのidにはtestIDを指定します。mobdev-react-nativeスキルでは、一連の操作、ログ、Flutter について説明しています。
ネットワーク
start_network_capture、network_log、stop_network_capture を使うと、アプリがどのリクエストを送信しているかを確認できます。デバッグ、分析データや API 呼び出しの確認、プライバシー監査に役立ちます。デバイスには何もインストールせず、HTTPS の復号も行わないため、各プラットフォームで確認できる内容は異なります。
| iOS シミュレーター、iPhone(Developer Mode) | Android | |
|---|---|---|
| 方法 | CFNETWORK_DIAGNOSTICS=1 と ACTIVITY_LOG_STDERR=1 を指定してアプリを再起動し、CFNetwork がコンソールに出力するログを読み取ります | デバイスの接続先を Mac 上の Mobdev のプロキシに設定します(settings put global http_proxy)。エミュレーターからは 10.0.2.2、スマートフォンからは adb reverse 経由でアクセスできます |
| 対象となるリクエスト | アプリ独自の URLSession リクエスト | Android のプロキシ設定に従うすべてのアプリ。アプリの識別はできません |
| 表示される情報 | メソッド、スキームとホスト、ステータス、送受信バイト数、所要時間、HTTP バージョン、host not found などのエラー | 平文 HTTP:メソッド、完全な URL、ステータス、バイト数、所要時間、ヘッダー。HTTPS:ホスト、ポート、送受信バイト数、トンネルの所要時間 |
| 表示されない情報 | パスとクエリ(iOS のログでは <redacted> と表示されます)、Web ビュー(WebKit は別プロセスで読み込みます)、URLSession 以外のソケット | HTTPS 内を流れるデータ、Flutter アプリなど、プロキシ設定を無視するアプリ |
- 各エントリーには、どこで検出されたか(
source:cfnetworkまたはproxy)が表示されます。リクエストは完了すると一覧に表示されます。実行中のリクエストと開いたままの HTTPS トンネルは、実行中として別に表示されます。queryが true でない限り、クエリは?…と表示されます。headers: trueを指定すると、平文 HTTP のヘッダーが追加されます。ただし、Authorization、Proxy-Authorization、Cookie、Set-Cookieは<redacted>に置き換えられます。 - iOS:
ACTIVITY_LOG_STDERRを指定すると、アプリのシステムログ全体がコンソールにコピーされます。ネットワークスタックの行はlogsではなくnetwork_logに出力され、この方法でアプリを実行している間は、その他のシステムログがlogsに表示されます。stop_network_captureは記録を停止しますが、アプリの診断設定はlaunch_appで再度起動するまで有効です。iOS 27 シミュレーターで動作を確認済みです。iPhone のコンソールも同じ方法でdevicectl経由で取得できますが、まだ試していません。 - Android:プロキシは 127.0.0.1 のみで待ち受け、平文 HTTP リクエストをそれぞれ独立した接続で転送します。Mobdev は
stop_network_captureの実行時、デバイスが切断されたとき、または Mobdev がデバイスの監視を停止したとき、さらに Mobdev やコマンドラインでの実行が終了したときに設定を削除します。クラッシュ後は、デバイスを検出した次の Mobdev が設定を削除します。他のツールが設定したプロキシを置き換えることはありません。アプリはキャプチャ開始前に開いた接続を維持するため、監視対象のアプリを再起動するにはbundle_idを指定してください。キャプチャ中も、エミュレーター上のアプリから Mac 上の開発サーバーへ10.0.2.2でアクセスできます。プロキシはそのアプリに代わって Mac の127.0.0.1に接続し、アプリが使用したアドレスを記録します。 mock_response(Android)は、URL にurlを含む平文 HTTP リクエストに対して、固定のステータスと本文を返します。たとえば、http://10.0.2.2:3000の開発サーバーに対してエラー状態を試す場合に使えます。- キャプチャは実行中のアプリに適用されます。フロー(
run_flow、Mobdev flow)から開始・停止することもできます。
シミュレーターと Android
起動中の iOS シミュレーターと Android エミュレーターおよびスマートフォンは、アプリ内と
list_devices で iPhone の隣に表示され、同じツールを利用できます。これらのデバイスでは
セットアップは不要です。ケーブルも Bluetooth も Developer Mode も必要ありません。iPhone
だけを表示したい場合は、「設定」›「一般」でオフにしてください。
- iOS Simulator(Xcode が必要):Mobdev はシミュレーターのフレームバッファーから画面を読み取り、
Simulator.app や idb と同じように、Xcode の SimulatorKit を通じてタッチ、キー、ホームボタンを
送信します。そのため、ヘルパーアプリも、開いたままにしておくウィンドウも必要ありません。キーは
シミュレーター独自のキーボードレイアウトに従います。アプリの起動には
devicectl、停止にはstop_appを通じてsimctlを使用します。クラッシュレポートは~/Library/Logs/DiagnosticReportsにあります。install_appにはシミュレーター向けにビルドした.appを指定します:xcodebuild -scheme MyApp -destination 'generic/platform=iOS Simulator' build。 - Android(Android Studio などで入手できる Android SDK の adb が必要):adb サーバーが
実行されている間、エミュレーターと USB デバッグが有効なスマートフォンが表示されます。Mobdev が
adb サーバーを起動することはありません。画面表示と入力には scrcpy
のサーバーを使用します。初回使用時(画像の取得またはタップ時。約 0.5 秒)、Mobdev は adb 経由で
サーバーを
/data/local/tmpに転送して起動します。長辺が 1280 ピクセルの H.264 ストリームを VideoToolbox でデコードするため、ウィンドウには毎秒 20 枚の画像が表示され、スクリーンショットの 取得にコストはかかりません。実際のタイミングに沿ったタッチ操作(長押しは押し続け、スワイプは 移動)や、ピンチ用の 2 本指操作に対応します。type_textは印字可能な ASCII 文字をキーイベントとして、 その他のテキスト(ウムラウト、絵文字、CJK 文字)はクリップボードと貼り付けを通じて入力するため、 入力後もクリップボードにテキストが残ります。clipboardはデバイスのクリップボードを読み取り、 設定します。サーバーはシェルユーザーとして実行され、Mobdev の終了時またはデバイスが切断された ときに終了して、自身のコピーを削除します。異常終了後やデバイスの再起動後には再び起動します。 サーバーを実行できない場合、Mobdev は adb にフォールバックし、その理由をログに記録します。 この場合は、生のscreencapフレーム(毎秒 1~3 枚)、タッチとキー操作用のinput、ASCII テキストのみが使われ、クリップボードには対応しません。MOBDEV_ANDROID_SCRCPY=0を設定すると、 Mobdev は adb のみを使用します。escapeを指定したpress_keyは「戻る」操作です。open_appは 起動可能なパッケージ名に一致するアプリを開きます(「Settings」はcom.android.settingsを開きます)。launch_appはアプリの logcat を追跡し、crash_reportsはクラッシュバッファーを読み取ります。install_appには.apkを指定します。argumentsとenvironmentは無視されます。
UI ツリー
シミュレーターと Android では、ui_tree はアクセシビリティツリーから画面上の要素を一覧表示します。
各要素には、ロール、ラベル、識別子、値、スクリーンショット上の位置が含まれます。tap_element と
wait_for_element は、識別子(accessibilityIdentifier、または Android のリソース ID。全体または
末尾の部分)またはラベルを使って要素を検索します。OCR より安定しており、アイコンのみを表示する
ボタンも見つけられます。シミュレーターは idb と同じように macOS のアクセシビリティ変換を通じて
読み取ります。Android では uiautomator dump を使用し、約 2 秒かかります。iPhone では UI ツリーを
有効にする必要があります(下記参照)。有効にするまでは、ツールが有効化方法を案内します。その間は
read_screen と tap_text を使用できます。
iPhone の UI ツリー
iOS では、スマートフォン上で実行中の UI テストからのみ、アプリのアクセシビリティツリーを参照できます。iPhone で Developer Mode を有効にし、Mac で Xcode を使って、Mobdev で iPhone の情報を開き、UI Tree からチームを選択して Turn On をクリックします。Mobdev は小さな XCUITest(Runner/)である Mobdev Runner をビルドし、チームの署名を付けて iPhone 上で実行し続けます。これは WebDriverAgent と同じ仕組みです。これにより、シミュレーターと同じように ui_tree、tap_element、wait_for_element が使えるようになり、Record はクリックした要素の名前を記録します。
- チーム:Mobdev はキーチェーン内の Apple Development 証明書のチーム(名前の括弧内にある ID ではなく、証明書の組織単位)を一覧表示し、最も新しいものを選択します。プロビジョニングプロファイルを作成して iPhone を登録するには、Xcode がそのチームの Apple Account にサインインしている必要があります(Xcode の Settings)。ランナーのバンドル ID は
dev.mobdev.runner.<team>です。 - 初回起動:ビルドには 1~2 分かかります。次回以降の起動は数秒です。無料の Apple Account を使っている場合、iOS から開発者を信頼するよう一度求められます。Settings › General › VPN & Device Management で設定してください。起動に失敗すると、xcodebuild の理由と、通常の解決方法(Settings › Developer › Enable UI Automation を有効にするなど)が UI Tree の下に表示されます。Try Again をクリックすると再ビルドされます。
- 実行中:Mobdev は
xcodebuild test-without-buildingを実行し続け、終了すると再度起動します。iPhone のケーブルを抜くか Mobdev を終了すると停止し、iPhone を再接続すると起動します。ランナーは iPhone 上で MobdevRunner-Runner として表示されます。ビルド、ログ、プロジェクトのコピーは~/Library/Application Support/dev.mobdev.mac/runner/<udid>にあります。 - ランナーは iPhone のループバックインターフェースでのみ、ポート 47270 で HTTP リクエストに応答します。Mobdev は USB ケーブル経由で usbmuxd を使って接続します。また、すべてのリクエストにはその起動用に生成されたトークンが含まれるため、iPhone 上のほかのアプリからは利用できません。タップは引き続き Bluetooth 経由で行われます。絵文字など、キーボード配列にキーがない文字はランナーが入力します。
プロジェクト
プロジェクトはアプリのリポジトリ内にあるフォルダーで、通常は mobdev/ です。Mobdev がそのアプリ用に保存するものがすべて含まれます。
mobdev/
mobdev.json the app, its builds and what every test starts with (see Tests)
tests/ tests
flows/ saved flows
baselines/ assert_screenshot's reference pictures
screenshots/ save_screenshot's pictures, such as App Store screenshots
maps/ crawl_app's map of each app
output/ test runs, recordings, crawls, failed checks and flow videos
output/ 以外はすべてコミットしてください。専用の .gitignore が付属するため、リポジトリの .gitignore はそのまま保たれます。また、Mobdev が初めてその場所に書き込んだときにのみ表示されます。フォルダーがプロジェクトになるのは mobdev.json がある場合だけです。それ以外のフォルダーは、何かが保存されたときに作成されます。
- 新規プロジェクト…(⇧⌘N、またはサイドバーの下にある +)では、リポジトリを選択すると、その中に
mobdev/mobdev.jsonが作成されます。リポジトリで宣言されているバンドル ID が入力されます。対象は Expo のapp.jsonまたはapp.config、Xcode プロジェクト(または XcodeGen のproject.ymlまたはInfo.plist)、Gradle のapplicationId、または Capacitor の設定です。最初に iOS と Android で共通するものが選ばれます。フィールドの横にあるメニューには、テストターゲットと依存関係を除き、見つかったものがすべて表示されます。プロジェクトを開く…(⌘O)では、リポジトリを選択した場合も含めて既存のプロジェクトを追加でき、プロジェクトがないフォルダーでは作成も選べます。サイドバーではプロジェクトが最初に表示され、選択中のプロジェクトにはテスト、フロー、スクリーンショット、アプリマップ、記録、実行結果が表示されます。 - 新しいプロジェクトの概要には、次に何をすればよいかが表示されます。開始するでは、4 つの手順に沿って進めます。各手順は完了すると自動的にチェックされます。エージェントを接続する(Claude Code プラグイン、または Codex、Cursor などの MCP 設定に加え、
npx skills add niklas-schmidt-dev/mobdev)、最初のテストを書いてもらう、テストを実行する、CI で実行する、という手順です。その下のエージェントに依頼するには、テストの作成、アプリのマッピング、変更ごとの確認、バグの再現、ストア用スクリーンショット、オンボーディングの監査、CI 用にコピーできるプロンプトがあり、プロジェクトのフォルダー、アプリ、デバイスが入力され、作業を行うスキルの名前も記載されています。 - 名前を変更…(プロジェクトの概要にある ⋯ メニュー、またはコンテキストメニューから選択)では、
mobdev.json内の名前を変更できます。この名前はテスト結果や CI にも表示されます。ファイルのその他の内容は、書き込まれたとおりに保たれます。 - 選択したプロジェクトがアクティブになります。パスを指定せずに保存したものは、そのプロジェクトに保存されます。対象は記録、
save_screenshot、save_flow、save_test、アプリのマップを使ったクロール、名前付きのassert_screenshotベースラインです。テストツールではprojectを省略した場合にこのプロジェクトが使われ、Record の**保存…**ではそのflows/が開き、フローを実行ではそのプロジェクトのフローが表示されます。 - エージェントの作業フォルダーが優先されます。
Mobdev mcpとMobdev callは、実行場所のフォルダーに対応するプロジェクト(そこにあるmobdev/、またはリポジトリのルートまでの上位フォルダーにあるmobdev/。別のものを指定する場合はMOBDEV_PROJECT)を送信し、アプリはそのエージェントからの呼び出しにそのプロジェクトを使用します。そのため、異なるリポジトリ、または同じリポジトリの異なるワークツリーで作業するエージェントが、互いのプロジェクトに保存することはありません。リレー経由のエージェントはアクティブなプロジェクトを使います。 - テスト実行と、プロジェクト内のフローファイルは、別のプロジェクトがその間にアクティブになっても、元のプロジェクトに残ります。
- 各プロジェクトには、T3 Code がプロジェクトのファビコンを見つける方法と同じ方法でリポジトリ内から見つけたアプリアイコンが表示されます。Expo の
app.jsonのアイコン、iOS のAppIcon(最大の画像)、最高密度の Android のic_launcher、それらがない場合は Web アプリで使われるファビコンまたはロゴが対象です。モノレポのapps/…内も検索されます。プロジェクトの概要にあるアイコン、またはコンテキストメニューのアイコンをクリックすると、そこから見つかった別の画像、任意の画像ファイル(PNG、JPEG、SVG、ICO、WebP、HEIC…)、またはアイコンなしを選べます。選択はこの Mac のアプリ設定に保存されます。 - エージェントは
list_projectsでプロジェクトを確認し、open_projectで切り替え、create_projectで作成できます。 - プロジェクトを指定しない場合は、これまでどおりすべて Mobdev のフォルダーに保存されます。
すべての言語のストア用スクリーンショットは、それらを保存するテストを --language を指定して実行すると作成されます。
{"name": "Store screenshots", "platforms": ["ios"], "steps": [
{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}},
{"set_status_bar": {"preset": "screenshot"}},
{"wait_for_element": {"id": "home.title"}},
{"save_screenshot": {"path": "${LANGUAGE}/iphone-6.9/01-home"}}
]}
Mobdev test mobdev/ --simulator "iPhone 17 Pro Max" --language en-US --language de-DE --test "Store screenshots"
フローと CI
フローはツール呼び出しを JSON で保存したリストで、1 行に 1 ステップずつ記述します。引数のないツールは名前だけを記述します。Maestro の YAML フローも実行できます(下記)。
{
"name": "Sign in",
"steps": [
{"install_app": {"path": "build/Build/Products/Debug-iphonesimulator/MyApp.app"}},
{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}},
{"tap_element": {"id": "email"}},
{"type_text": {"text": "[email protected]", "submit": true}},
{"wait_for_element": {"text": "Welcome"}},
"home"
]
}
制御ステップ
制御ステップは画面を確認して実行する内容を決めるため、フローで推測による待機は必要ありません。キーはツール名ではありません。
{
"name": "Check out",
"steps": [
{"launch_app": {"bundle_id": "com.example.MyApp"}},
{"if": {"visible": {"text": "Allow"}, "then": [{"tap_element": {"text": "Allow"}}]}},
{"repeat": {"until_visible": {"id": "checkout"}, "max": 10, "steps": [{"scroll": {"direction": "down"}}]}},
{"extract": {"id": "total", "into": "TOTAL"}},
{"retry": {"times": 2, "steps": [
{"tap_element": {"id": "checkout"}},
{"wait_for_element": {"text": "Pay ${TOTAL}", "timeout": 5}}
]}},
{"run": "pay.json"},
{"tap_element": {"text": "Rate this app", "timeout": 2}, "optional": true}
]
}
| ステップ | |
|---|---|
{"if": {"visible": {"text": "…"}, "then": […], "else": […]}} | 要素またはテキストが現在画面に表示されている場合は then を実行し、それ以外の場合は else を実行します(省略可)。not_visible はその逆です。{"platform": "ios"} または "android" を指定すると、デバイスによって判定します。表示されているとは、この時点で画面上にあることを意味します。UI ツリーがある場合はそこから判定し、ない場合はテキスト認識で判定します(テキストのみ)。待機は行いません。 |
{"repeat": {"times": 3, "steps": […]}} | ステップを3回実行します。最大100回です。 |
{"repeat": {"while_visible": {…}, "max": 20, "steps": […]}} | 要素が画面上に表示されている間、各ラウンドの前に確認しながらステップを実行します。until_visible は表示されるまで実行します。max(デフォルトは20、最大100)に達すると、失敗にはせずループを終了します。Maestro と同様の動作です。終了が重要な場合は、その後に待機を追加してください。 |
{"retry": {"times": 2, "steps": […]}} | 失敗した場合、最初のステップから再実行します。最大で追加2回実行します(デフォルトは1回、最大10回)。失敗した試行は結果に – と表示され、フローは失敗しません。 |
{"run": "sign-in.json"} | 別のフローファイル(JSON または Maestro YAML)を実行します。パスは、このファイルを指定したファイルからの相対パスです。テストプロジェクトでは、プロジェクトフォルダーからの相対パスも使用できます。サブフローは最大5階層までネストでき、フローが自分自身を実行するとエラーになります。run_flow のインライン steps には絶対パスが必要です。 |
{"set": {"EMAIL": "[email protected]"}} | 後続のステップで使用する変数を設定します。 |
{"extract": {"id": "total", "into": "TOTAL"}} | UI ツリーの要素を変数に読み込みます。値があればその値を、なければラベルを読み込みます。"from": "label" または "value" で選択します。tap_element と同様に要素が見つかるまで待機します(timeout のデフォルトは5秒)。index で複数の要素から選択できます。 |
${NAME}は、条件を含むステップ内の文字列では変数です。setとextract、Mobdev flowの--var NAME=value、run_flowのvariablesで設定でき、テストではmobdev.jsonからも設定できます。名前が一度も設定されていない場合、フローは最初のステップの前に、変数を使用するステップで失敗します。- 任意のステップのキーの隣に
"optional": trueを指定すると、そのステップの失敗を許容できます。結果には – と表示され、フローは続行します。 - 結果ではネストされたステップに番号が付けられます。3.2 はステップ3の内部にある2番目のステップです(
elseの番号はthenの続きになります)。repeat または retry によって再実行されたステップには、ラウンドまたは試行回数が表示されます。フローが失敗すると、最も内側の失敗したステップが「failed at step 3.2 of 7」のように表示されます。run_flowのデータにはfailed_step(3)とfailed_at("3.2")が含まれます。Mobdev flowは各ステップの完了時に表示し、制御ステップ内のステップはインデントされ、制御ステップより前に表示されます。 - フローは、制御ステップとサブフロー内のステップを含めて最大500ステップです。1回の実行は、各ラウンドと再試行をすべて数えて最大2000ステップです。キャンセルすると、ループの途中であっても直ちに停止します。
- 記録では通常のツール呼び出しが保存されます。制御ステップは手動で追加するか、エージェントに作成させてください。
記録、再生、CI
-
デバイスのアクティビティで記録すると、エージェントまたはユーザーがデバイス上で行ったすべての呼び出しと、アプリのウィンドウでのクリック、ドラッグ、キー入力が収集されます。シミュレーターと Android では、一意の識別子またはラベルを持つ要素をクリックすると、レイアウトが変わってもフローを実行できるよう、
tap_elementとして保存されます。それ以外は、同じ位置でのtapとして保存されます。**保存…**を選ぶとファイルが保存されます。別のフォルダーを選ばない限り、アクティブなプロジェクトのflows/に保存されます。エージェントはsave_flowを使ってここにフローを保存します。 -
フローを実行…、
run_flowツール(Mac 上の .json または Maestro .yaml ファイルを指定するpath、またはインラインのsteps)、およびMobdev flowはフローを再生し、最初に失敗したステップで停止して、そのステップを通知します。tap_elementは対象の要素を最大 5 秒待つため、フローで明示的な待機が必要になることはほとんどありません。 -
すべての実行で動画を保存できます。フローを実行…では動画が記録され、結果画面に動画を表示が表示されます。アプリはプロジェクトの
output/flow-videosに最新の 20 件を保存します。プロジェクトがない場合は~/Library/Application Support/dev.mobdev.mac/flow-videosに保存されます。run_flowは、videoに Mac 上の .mp4 ファイルを指定すると動画を保存します。動画はシミュレーター、iPhone、Android では毎秒約 10 フレームです(scrcpy を使わない Android ではscreencapが許す範囲で、毎秒 1~3 フレームです)。動画はフローの終了時の画面でしばらく止まるため、失敗を簡単に確認できます。 -
スクリプトや CI では、アプリを使わず、起動済みのシミュレーターと Android デバイスで
Mobdev flowを実行できます。/Applications/Mobdev.app/Contents/MacOS/Mobdev flow sign-in.json --device <udid> --artifacts out各ステップの行が出力され、すべてのステップが成功すると終了コード 0、いずれかが失敗すると 1、フローを開始できない場合は 2 で終了します。
--artifactsを指定すると、実行動画(run.mp4)、アクティビティログ、コピーしたクラッシュレポート、summary.md、および失敗時のfailure.pngが保存されます。--no-videoを指定すると動画を省略し、--var NAME=valueで変数を設定できます(繰り返し指定できます)。CI ランナーの起動が遅いとシミュレーターの起動に時間がかかるため、デバイスが認識されるまで最大 2 分待ちます。--wait <seconds>で待機時間を変更できます。Ctrl-C を押すとフローは停止しますが、動画の処理は完了します。iPhone ではアプリが起動している必要があります。MCP または HTTP API 経由でrun_flowを呼び出してください。
examples/flows には、試用できるフローと小さなアプリがあり、.github/workflows/flows.yml では GitHub の無料の macos-26 ランナー上でそれらを実行します。アプリ用の GitHub Actions ジョブは次のとおりです。
- name: Boot a simulator
run: |
UDID=$(xcrun simctl create ci "iPhone 17")
xcrun simctl bootstatus "$UDID" -b
echo "UDID=$UDID" >> "$GITHUB_ENV"
- name: Build
run: xcodebuild -scheme MyApp -destination "id=$UDID" -derivedDataPath build build
- name: Run flows
run: |
curl -fsSL -o Mobdev.dmg https://github.com/niklas-schmidt-dev/mobdev/releases/latest/download/Mobdev.dmg
hdiutil attach -nobrowse -mountpoint /tmp/mobdev Mobdev.dmg
/tmp/mobdev/Mobdev.app/Contents/MacOS/Mobdev flow flows/sign-in.json --device "$UDID" --artifacts flow-artifacts
- uses: actions/upload-artifact@v4
if: failure()
with: { name: flow-artifacts, path: flow-artifacts }
フローが失敗すると、アップロードされたアーティファクトには、失敗した画面で終わる実行動画 run.mp4 と、failure.png、アクティビティログが含まれます。成功した実行の動画も保存するには、if: always() を使用してください。
CI では OCR ツールよりも tap_element と wait_for_element を優先してください。GitHub の仮想化された Mac では Vision によるテキスト認識が応答せず、Mobdev は 90 秒後に処理を諦めるためです。
Maestro フロー
Mobdev flow、run_flow(path)、フローを実行…、およびテストプロジェクト(tests/*.yaml)では、Maestro フローも利用できます。Mobdev はファイルの読み込み時に YAML を上記のステップに変換します。実行できないコマンドがある場合は、その名前と行番号を示して読み込みに失敗するため、フローが一部だけ動作することはありません。スクリーンショットが除外された場合や、パターンがテキストとして読み込まれた場合など、変換時に起きた変更は注記として出力されます。Mobdev convert は、もう一方の形式でフローを出力します。
Mobdev convert login.yaml > login.json # Maestro to Mobdev
Mobdev convert login.json > login.yaml # Mobdev to Maestro
| Maestro | Mobdev |
|---|---|
appId、name、env、onFlowStart を指定する設定 | 名前のないコマンドを含むアプリ、フロー名、set ステップ、開始時のステップ |
launchApp(appId、clearState、clearKeychain、stopApp、arguments、permissions) | reset_app(keychain)、set_permission、launch_app(restart。arguments は -key value 形式) |
stopApp、killApp、clearState | stop_app、stop_app、reset_app |
テキスト、id、index、または point を指定する tapOn、doubleTapOn、longPressOn | tap_element(double、hold: 1)。座標を指定する場合は tap または long_press。tapOn の repeat は repeat ステップ |
inputText、eraseText、pasteText | type_text。count を指定した(デフォルトは 50、最大 100)backspace の press_key。${COPIED_TEXT} の type_text |
pressKey | press_key(Enter、Backspace、Tab、Volume Up と Down、Remote Dpad キー)。Home は home、Back は以下の back |
back | {"if": {"platform": "android", "then": [{"press_key": {"key": "escape"}}], "else": [{"tap_element": {"id": "BackButton"}}]}}:Android の Back キーです。iOS にはシステムの Back がなく、escape を押しても戻らないため、iOS では識別子が BackButton のナビゲーションバーの戻るボタンをタップします |
assertVisible、assertNotVisible | timeout を 7 に設定した wait_for_element(および gone) |
extendedWaitUntil(visible、notVisible、timeout) | 秒単位でタイムアウトを指定する wait_for_element(最大 60) |
scrollUntilVisible(element、direction、timeout) | scroll_until_visible。タイムアウト 2 秒ごとに 1 回スクロールします |
scroll、swipe(direction、または start と end、duration) | 下方向への scroll、画面の割合を指定した swipe |
openLink、waitForAnimationToEnd、setLocation、travel、setOrientation、setPermissions | open_url、wait_for_idle、set_location(travel はルートとして指定し、km/h を m/s に変換)、set_orientation、set_permission |
runFlow(ファイル、または commands。visible、notVisible、platform を指定する when。env) | run、またはコマンドをその場に記述。when は if に、env は set になります |
repeat(times、while)、retry(maxRetries、commands または file) | repeat(times、または max としての times を指定する while_visible/until_visible)、retry |
copyTextFrom、${maestro.copiedText} | COPIED_TEXT に extract し、${COPIED_TEXT} を使用 |
optional、label | "optional": true。ラベルは省略されます |
hideKeyboard、takeScreenshot | 注記付きで省略:Mobdev は iOS でハードウェアキーボードを使って入力し、実行の動画を保存します |
制限事項:
- Mobdev は JavaScript を実行しません。
evalScript、runScript、assertTrue、when: true、および変数名以外の${…}式は失敗します。また、AI コマンド、inputRandom…、機内モード、addMedia、startRecording、単独のclearKeychain、失敗後に実行されるonFlowCompleteも失敗します。 - セレクターにはテキスト、
id、index、pointを指定できます。相対指定(below、childOfなど)や状態(enabled、checkedなど)は失敗します。Maestro はラベル全体を正規表現としてテキストに照合しますが、Mobdev はテキストそのものを検索し、完全一致を優先します。そのため、アンカー、先頭または末尾の.*、エスケープは除去され、残ったパターンは注記されます。 - パーセント指定の座標はそのまま引き継がれます。ピクセル指定の座標は Mobdev のスクリーンショット(長辺 1280)のピクセルとして読み取られます。Maestro のスクリーンショットとは異なるため、注記されます。
- YAML ではブロック形式とフロー形式のマッピングおよびリスト、引用テキストとプレーンテキスト、コメント、
---、|または>ブロックを使用できます。アンカー、エイリアス、タグ、複数行にわたるプレーンテキストは、行番号とともにエラーになります。 Mobdev フローを Maestro に変換すると、Maestro コマンドがあるすべてのステップが書き込まれます。それ以外のステップ(observe、フローの先頭またはifの先頭以外にあるset、elseを伴う可視性条件のif、COPIED_TEXT 以外の名前へのextract、修飾キー付きのキーなど)は、ステップ番号とともにまとめて失敗として報告されます。7 秒の待機はassertVisibleに変換され、launch_appの前にあるreset_appはclearStateを指定したlaunchAppに変換されます。また、.json フローのrunでは、変換先の .yaml ファイル名が指定されます。
テスト
プロジェクトとは、1 つのアプリのテストを含むリポジトリ内のフォルダーです。mobdev.json でアプリを指定し、tests/ 内の各ファイルが 1 つのテストです。テストは、名前、説明、実行するプラットフォームを指定したフローか、Maestro フロー(.yaml。設定の name で名前を指定)です。run ステップは、テストの隣、またはプロジェクトフォルダーからの相対パスでサブフローを探すため、共有フローは、たとえば flows/ に置けます。
{
"name": "My App",
"app": {
"bundle_id": "com.example.MyApp",
"builds": {"simulator": "build/Build/Products/Debug-iphonesimulator/MyApp.app", "android": "app/build/outputs/apk/debug/app-debug.apk"}
},
"before_each": [{"launch_app": {"bundle_id": "com.example.MyApp", "restart": true}}],
"variables": {"EMAIL": "[email protected]"},
"secrets": ["PASSWORD"]
}
{
"name": "Sign in",
"description": "A member signs in and sees the welcome screen.",
"steps": [
{"tap_element": {"id": "email"}},
{"type_text": {"text": "${EMAIL}", "submit": true}},
{"tap_element": {"id": "password"}},
{"type_text": {"text": "${PASSWORD}", "submit": true}},
{"wait_for_element": {"text": "Welcome"}}
]
}
mobdev.json のキーはすべて省略可能です。実行時には、デバイスのプラットフォーム(iphone、simulator、または android)用のビルドを 1 回インストールしてから、before_each とテストのステップを実行します。すべてのステップが成功するとテストは成功します。そのため、結果を確認できる wait_for_element または wait_for_text でテストを終えてください。テスト中に起動したアプリがクラッシュした場合、テストはそのクラッシュによって失敗します。${NAME} は、ファイル、環境、または実行から変数を取得します。シークレットの値は環境または実行からのみ取得され、結果には表示されません。"platforms": ["android"] を指定したテストは iOS ではスキップされ、逆の場合も同様です。
-
サイドバーのプロジェクトの テスト では、デバイス上でテストを実行し、各結果とそのステップ、失敗時のスクリーンショット、実行動画を表示します。最新の 20 件の実行結果は、プロジェクトの
output/runsに保持されます。 -
エージェントも同じ機能を利用できます。
list_testsはプロジェクトを表示し、save_testはテストを書き込みます(projectで指定したフォルダーにプロジェクトがない場合は作成します)。run_testsはテストを実行し、各結果と最初に失敗したテストの画面を返します。test_resultは最新の結果を再度返します。projectを指定しない場合は、エージェントのプロジェクトまたはアクティブなプロジェクトが使われます。つまり、エージェントは上記のツールでアプリを操作して動作する手順を見つけ、重要な呼び出しをテストとして保存して実行し、失敗したステップをもとに修正できます。 -
Mobdev testは、アプリを使わずに、起動済みのシミュレーターと Android デバイス上で、スクリプトや CI 向けにプロジェクトを実行します。/Applications/Mobdev.app/Contents/MacOS/Mobdev test mobdev/ --device <udid> --artifacts test-artifacts
テストごとに 1 行を出力し、すべてのテストが成功した場合は 0、1 つでも失敗した場合は 1、テストを開始できなかった場合は 2 で終了します。--artifacts を指定すると、各テスト専用のフォルダーに results.json、junit.xml、動画、失敗時のスクリーンショット(スクリーンショットの差分など、失敗したチェックのファイルも含む)、アクティビティログ、コピーされたクラッシュレポートが保存されます。失敗したテストには、実行中にアプリが出力した内容と、アプリの終了状態も含まれます。--test <name> は 1 つのテストを実行します(繰り返し指定可能)。--var NAME=value は変数を設定します。--no-video と --wait は Mobdev flow と同様に動作します。iPhone ではアプリの実行が必要です。MCP または HTTP API から run_tests を呼び出してください。
-
複数のデバイスと言語。
--deviceを繰り返し指定すると、起動済みの複数のデバイスで同時に実行できます。また、--simulator "iPhone 17"(または"iPhone 17,<runtime id>")を追加すると、その種類のシミュレーターを作成して起動し、実行後に削除します。--emulator Pixel_9_Proは、その Android Virtual Device を読み取り専用で実行中に起動し、実行後にシャットダウンします。これを繰り返すと、同じ AVD を含む複数のコピーを同時に起動できます。読み取り専用とは、実行中の変更が AVD に保存されないことを意味します。AVD の実行中コピーはすべて読み取り専用でなければならないため、読み取り専用で起動していないコピーがあれば、先に終了してください。--language de-DEを指定すると、その言語でテストを実行します。iOS ではシミュレーターの言語、Android ではアプリの言語が使われます(Android 13 以降で、app.bundle_idが必要です)。複数の言語を指定すると、各デバイスで各言語を順番に実行します。実行時には、iOS アプリが起動中の場合は停止して新しい言語で起動し、${LANGUAGE}としてその言語をステップに渡し、終了後にデバイスの元の言語に戻します。デバイスごと、言語ごとに個別のフォルダーが作成され、最上位のsummary.mdに全体の結果がまとめられます。run_testsでもlanguageを指定できます。Mobdev test mobdev/ --simulator "iPhone 17" --simulator "iPhone SE (3rd generation)" \ --emulator Pixel_9_Pro --language en-US --language de-DE --artifacts test-artifacts
examples/tests はサンプルアプリ用のプロジェクトです。また、.github/workflows/flows.yml は、フローと並行して GitHub の macos-26 ランナー上でテストを実行します。アプリ用のジョブは、上記の例で Mobdev flow を Mobdev test に置き換え、アーティファクトに junit.xml を追加したものになります。
チェック
3つのツールは画面を判定し、条件を満たさない場合はステップを失敗させるため、テストで画面上の内容だけでなく、画面の見た目も確認できます。
{
"name": "Home looks right",
"steps": [
{"wait_for_element": {"id": "home.title"}},
{"assert_screenshot": {"name": "home", "mask": ["home.clock"]}},
{"accessibility_audit": {"fail_on": "error"}},
{"assert_with_ai": {"question": "Is any text cut off or drawn over other text?", "expect": "no"}}
]
}
assert_screenshot は、画面をベースライン画像と比較します(ビジュアルリグレッション)。
nameはbaselines/<ios|android>/<width>x<height>/<name>.pngに解決されます。テスト実行中はプロジェクト内(run_tests、Testsウインドウ、Mobdev test)、フローファイルの場合は(pathを指定したrun_flow、Mobdev flow)プロジェクト内、またはプロジェクト外ではそのファイルと同じ場所です。それ以外の場合は、エージェントのプロジェクト内またはアクティブなプロジェクト内です。現在のフォルダーのプロジェクト内またはフォルダー自体でアプリを指定せずにMobdev assert_screenshotを実行し、プロジェクトがない場合はMobdevのフォルダー内(~/Library/Application Support/dev.mobdev.mac/baselines)です。 サイズはデバイス画面のピクセル数であるため、モデルと向きごとにそれぞれベースラインが必要です。pathを指定すると代わりにPNGを指定できます。同じ形状のフル解像度スクリーンショットも使用できます。- ベースラインがない場合、画面が静止するまで待って記録し、成功としてその旨を表示します。テストと一緒に
baselinesフォルダーをコミットしてください。 CI実行時にベースラインがないと記録されて成功するため、信頼できる実行で記録してチェックインしてください。また、意図した変更後にupdate: trueを指定すると置き換えられます。 - 比較はスクリーンショットのサイズ(長辺1280 px)で行われます。色の差が約10%を超えるピクセル(YIQでの判定。pixelmatchの測定方法に準じます)は、ベースラインが最大1ピクセル移動しただけのもの(アンチエイリアス、スケーリング)ではなく、さらに隣接するピクセルが少なくとも3つ変更されている場合(圧縮ノイズ、1ピクセル幅の線)、変更としてカウントされます。比較対象ピクセルのうち
thresholdを超える数が変更されると、ステップは失敗します。デフォルトは0.0005(0.05%、約20×20 pxの正方形)で、0なら変更を許容しません。テキストが変わったり、ボタンが数ポイント移動したりすると失敗します。同じ画面が再び描画されても失敗しません。失敗する前に画面が安定するまで待ち、もう一度比較します。 compare_status_barがtrueでない限り、ステータスバー(上部の7%)は比較対象から除外されます。maskを指定すると、さらに[x, y, width, height]でスクリーンショットのピクセル範囲を指定するか、要素のIDまたはラベル(UIツリーがない場合は認識されたテキスト)を指定して除外できます。例:時計、アバター、地図。- 失敗すると
<name>-diff.png(変更されたピクセルは赤、許容された差異は黄色、マスクされた領域は青、各変更領域を枠で囲みます)と<name>-actual.pngが出力されます。実行中はテストのフォルダーに保存され、results.jsonにはテストのfilesとして一覧表示されます。実行外では、コミット対象にならないようプロジェクトのoutput/checksに保存されます。プロジェクトがない場合はベースラインと同じ場所に保存されます。ツールは差分と変更領域を返します。その後、テストが成功するとファイルは再び削除されます。
accessibility_audit は、UIツリーから画面をチェックします(シミュレーター、Android、Mobdev Runnerを使用するiPhone)。
| ルール | エラー | 警告 |
|---|---|---|
missing_label | ラベルがなく、内部にテキストもないタップ可能な要素(アイコンのみのボタンなど) | ラベルまたはプレースホルダーのないフィールド |
small_target | 24×24 ptまたはdp未満(WCAG 2.2の最小サイズ) | 44×44 pt(iOS)または48×48 dp(Android)未満 |
low_contrast | コントラスト比が3:1未満のテキスト | コントラスト比が4.5:1未満のテキスト(通常のテキストに対するWCAG AA) |
duplicate_label | 同じラベルを持つ異なるターゲット | |
unclear_label | ic_close.png などのファイル名 | close_button、btnBack、chevron.right などの識別子、または「button」のみ |
- コントラストは、スクリーンショット内の各テキストやボタンで測定されます。最も一般的な色が背景色で、面積のわずかな部分を占める色のうち最も異なる色がテキスト(またはボタンのアイコン)です。無効な要素や写真のような複雑な背景は対象外です。
- サイズは画面のスケールを使用します。Android では
wm density、iOS では、幅が 1000 px 以上の iPhone は 1 ポイントあたり 3 ピクセル、iPad と旧型 iPhone は 2 ピクセルです。 - 問題は上から順に、要素ごとにまとめて表示されます。
image: trueを指定すると、各要素を番号付きのボックスで囲んだスクリーンショットが返され、エラーのある要素は赤で表示されます。fail_on: "error"(または"warning")を指定すると、その種の問題がある場合に呼び出しが失敗します。これはテストでの利用を想定しています。ignoreはルールを無視します。 - Apple と Google の標準コントロールでも、コントラスト比 4.5:1 や 44 pt を満たさない場合があります(iOS の白地に青は約 3.5:1 です)。そのため、警告は参考情報として扱い、エラーで失敗させてください。
assert_with_ai は、画面について「はい」か「いいえ」で答える質問を Mac 上の Apple Intelligence に行い、回答が expect(デフォルトは yes)と異なる場合、またはモデルが判断できない場合に失敗します。結果には理由も含まれます。macOS 27 ではモデルはスクリーンショットを確認します。画像を取得しない macOS 26 では、画面上の要素を読み取ってその旨を伝えます。データが Mac の外に送信されることはありません。Apple Intelligence を有効にし、モデルをダウンロードした Mac が必要です。いずれかが不足している場合は、その内容が通知されます。呼び出しには数秒かかります。デバイス上のモデルは小さいため、一度に質問するのは画面に見えている項目 1 つだけにし、ツリーから判別できる内容には wait_for_element を使用してください。
記録されたフローにはチェックが保持されます。accessibility_audit は fail_on を指定した場合にのみチェックを行い、指定しない場合は確認のみを行います。
HTTP API
すべて 127.0.0.1:4686(MOBDEV_PORT で変更可能)でリッスンし、Authorization: Bearer <token> が必要です。トークンは ~/Library/Application Support/dev.mobdev.mac/token にあります。
TOKEN=$(cat ~/Library/Application\ Support/dev.mobdev.mac/token)
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4686/v1/status
curl -H "Authorization: Bearer $TOKEN" http://127.0.0.1:4686/v1/screenshot -o screen.jpg # ?format=png, ?full=1
curl -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:4686/v1/tools/tap_text -d '{"text":"Settings"}'
curl -H "Authorization: Bearer $TOKEN" -X POST http://127.0.0.1:4686/v1/tools/type_text -d '{"text":"hello","submit":true}'
POST /v1/tools/<name> はツールの引数を受け取り、{"ok", "text", "data"} を返します。スクリーンショットを含めるには "screenshot": true を指定します。GET /v1/tools はスキーマ付きでツールの一覧を返します。/v1/uploads は install_app 用のビルドを受け付けます(どこからでもビルドをインストールを参照)。
リモートアクセス(任意)
Remote Access をオンにすると、別のコンピューターにあるエージェントからスマートフォンにアクセスできます。Mac はリレーへの送信専用 WebSocket 接続を 1 つ維持し、Mac 上でポートが開かれることはありません。
- ホスト型リレー: mobdev.sh/dashboard でアクセストークンを作成し、Open in Mobdev をクリックします。確認後、
mobdev://connectリンクによってリレー URL とトークンが入力されます。トークンを Remote Access の下に貼り付けることもできます。 - 独自のリレー:
../relayを実行し、その URL を入力します。
アプリにリモート MCP URL とクライアントキーが表示されます。
claude mcp add --transport http mobdev-remote https://relay.mobdev.sh/h/<mac-name>/mcp \
--header "Authorization: Bearer mdc_…"
リレーはリクエストを転送するだけで、何も保存しません。クライアントキーを持つ人は誰でもスマートフォンを操作できます。New Client Key をクリックすると古いキーは無効になり、ダッシュボードでアクセストークンを無効にすると Mac の接続が切断されます。
ライブビュー
Remote Access で Allow live view をオンにすると、リレーはデバイスの画面をブラウザーにストリーミングできます。ダッシュボードでデバイスの横にある Live を選ぶと画面が表示され、クリックでタップ、ドラッグでスワイプ、入力で文字入力ができます。アプリのミラーリングと同様です。そこで Share… を選ぶと、1 台のデバイスまたは Mac 全体へのリンクを作成できます。リンクはアカウントなしで誰でも開け、1 時間、1 日、または 1 週間、閲覧のみ、または操作も可能にできます。共有の取り消しもそこで行えます。クライアントキーを持つエージェントは、/v1/live で同じストリームを開けます(プロトコル)。
ライブビューは各 Mac で有効にするまでオフです。画面にはメッセージ、コード、その他のプライベートなデータが表示されることがあるため、誰が閲覧できるかは Mac 側で決定します。誰かが閲覧中は、デバイスのページに Watched live now と表示され、Remote Access には閲覧者(ダッシュボードのあなた、名前の横に共有リンク、クライアントキーを持つエージェント)が表示されます。それぞれに Stop が用意されています。閲覧者の参加と退出はすべてデバイスのアクティビティに記録され、ブラウザーからのタップや入力も「Browser」として表示されます。この設定をオフにすると、すべてのライブビューがただちに終了します。フレームは長辺が 800 px の JPEG で、最大毎秒 10 枚送信されます(画面に変化があったときのみ送信され、変化がない場合は 2 秒ごとに送信されます)。リレーはフレームをメモリー上でのみ中継します。
どこからでもビルドをインストール
クラウドエージェント(Claude Code on the web、Codex cloud、Cursor background agents)または CI が独自のコンテナーでアプリをビルドし、リレー経由で Mac にビルドをアップロードして、install_app {"upload": "<id>"} でその Mac に接続されたデバイスへインストールします。リレーはほかのリクエストと同様にチャンクを転送するだけで、何も保持しません。ビルドは Mobdev のフォルダー内の uploads/ に保存され、あなたのみが読み取れます。最後に使用してから 24 時間後に削除されます。
scripts/mobdev-upload.sh の実行に必要なのはシェル、curl、sha256sum または shasum だけなので、Mobdev のない Linux コンテナーでも実行できます。
curl -fsSLO https://raw.githubusercontent.com/niklas-schmidt-dev/mobdev/main/scripts/mobdev-upload.sh
export MOBDEV_URL=https://relay.mobdev.sh/h/<mac-name> MOBDEV_KEY=mdc_…
id=$(sh mobdev-upload.sh app/build/outputs/apk/debug/app-debug.apk)
curl -sS -H "Authorization: Bearer $MOBDEV_KEY" -H 'Content-Type: application/json' \
-d "{\"upload\": \"$id\", \"device\": \"emulator-5554\"}" "$MOBDEV_URL/v1/tools/install_app"
MCP 経由で接続されたエージェントは、curl の代わりに {"upload": "<id>"} を指定して install_app を呼び出します。Mac では、Mobdev upload <file> --url https://relay.mobdev.sh/h/<mac-name>(キーは MOBDEV_KEY または --key)で同じことができます。--url を指定しない場合、ビルドはこの Mac 上の Mobdev アプリに送信されます。どちらの方法でも、まず .app フォルダーを zip に圧縮し、最大 8 MiB のチャンクで送信します。チャンクの送信に失敗した場合は、Mac 側のコピーが完了した位置から再送信し、アップロード ID を出力します(--json の場合は、完了したアップロードを出力します)。
iPhone には .ipa またはデバイス向けにビルドした .app、シミュレーターにはシミュレーター向けにビルドした .app、Android には .apk が必要です。.app は zip として転送されます。ditto -c -k --keepParent MyApp.app MyApp.zip を実行すると、アプリが zip のトップレベルに配置されます。install_app は path と同様にアップロードを検証します。リレー経由でも、API トークンを使ったローカル API 経由でも、プロトコルは次のとおりです。
| リクエスト | |
|---|---|
POST /v1/uploads {"name", "size", "sha256"} | 201 {"id", "chunk_size", "received", …}。name は .ipa、.apk、または .zip で終わる必要があります。sha256 は省略可能です |
PUT /v1/uploads/<id>?offset=<n> とバイト列 | 最大 chunk_size(8 MiB)です。offset は Mac が保持している値と一致する必要があり、一致しない場合は received を含む 409 が返されます |
GET /v1/uploads/<id> | 再開に使う received と state(receiving、finishing、finished)を返します |
POST /v1/uploads/<id>/finish | サイズと SHA-256 を確認し、zip を展開します。{"path", "kind", "sha256"} を返します。繰り返し実行しても安全です |
DELETE /v1/uploads/<id> | アップロードを削除します |
zip に ../ または絶対パス、zip 内部のシンボリックリンクを経由するパス、アプリの外を指すシンボリックリンク、トップレベルに .app が 1 つ以外含まれる場合、または展開後のサイズが 4 GB を超える場合、展開前または展開直後に zip は拒否されます。setuid ビットは取り除かれます。Mobdev が保持するアップロードは最大 20 件、合計 4 GB です。空き容量を確保するために、最も長く使われていないものから削除されます。ただし、直近 10 分以内に使われたものは削除されません。各アップロードは Mobdev のログ(Console、subsystem dev.mobdev.mac)に記録され、インストールはデバイスのアクティビティに記録されます。
セキュリティとプライバシー
- API は 127.0.0.1 のみでバインドされ、ベアラートークンを必須とし、ブラウザーからのリクエスト(
Origin)と外部のHostヘッダーを拒否します。 - トークンとリレーシークレットは、あなたのみが読み取れるファイル(モード 0600)として
~/Library/Application Support/dev.mobdev.macに保存されます。MOBDEV_HOMEを設定すると、このディレクトリを変更できます。 - テレメトリーもアカウントもありません。エージェントのすべての操作は、アプリの アクティビティ に表示されます。
- ブラウザーでのライブビューは、Remote Access で Allow live view をオンにするまで無効です。誰かが画面を見ている間はアプリに表示され、停止することもできます(ライブビューを参照)。
- エージェントは、あなたのアカウントがある実際のスマートフォンを操作します。メッセージの送信、支払い、削除を行う操作では、必ず人間が確認してください。
- ブロックするアプリ(Settings › Agents、または
Mobdev flow、test、call用の名前とバンドル ID をカンマ区切りで指定するMOBDEV_BLOCKED_APPS):フロー内やテスト内でも、open_appとlaunch_appはブロックしたアプリを開きません。名前は、その名前を一部に含むバンドル ID にも一致します。これは誤操作を防ぐものであり、サンドボックスではありません。エージェントは引き続きアプリのアイコンをタップできます。
制限事項
- 1 台の Mac に複数の iPhone を接続する場合、各 iPhone はそれぞれ Bluetooth 接続を使用します。接続先は、まず名前、次にモデルで判定します。それでも判別できない場合(同じモデルで、どちらも「iPhone」と名乗る場合)は、各接続のポインターを動かして、どの画面に表示されるかを確認します。この方法には AssistiveTouch が必要です。iPhone 2 台を同時に接続するテストはまだ行っていません。
- これまでに実機で検証したのは、USB 接続した iPhone の画面キャプチャ、Bluetooth ペアリング、API、OCR、MCP、リレー、タップ、スワイプ、AssistiveTouch を使ったスクロールホイールの操作、そして iOS 27 でのドイツ語配列による入力(文字、ウムラウト、ß、記号)です。
- テキスト入力は、一般的なアクセント文字を含む米国配列とドイツ語配列のハードウェアキーボードに対応しています。絵文字やキー入力できないその他の文字を入力するには、UI ツリーをオンにする必要があります。Mobdev Runner が入力します。
- テスト済みなのは縦向きです。座標は現在のスクリーンショットのサイズに従います。
- スマートフォンはロック解除したままにする必要があります。Mobdev はパスコードを入力できません。
- シミュレーターの入力には Xcode の非公開の SimulatorKit を使用しています。Xcode 27 と 26.6 でテスト済みです。idb や AXe で起きたように、新しい Xcode で仕様が変わる可能性があります。シミュレーターの画面に表示されるのはメインディスプレイのみです。
- Xcode 27 より前では、
devicectlはシミュレーターを認識しないため、シミュレーターのアプリにはsimctlを使用します。その場合、アプリ独自の URL スキームをopen_urlで開くときに iOS の確認画面(「…で開きますか?」)が表示されます。UI ツリーがある場合、open_urlが自動で「開く」ボタンをタップするため、フローやテストでその操作を指定する必要はありません。 - Android では、印字可能な ASCII 以外のテキストは貼り付けて入力するため、クリップボードの内容が置き換わり、「貼り付け」に対応したフィールドが必要です(Android 7 以降)。scrcpy がない場合、入力できるのは ASCII 文字のみです。adb はアプリ名を認識しないため、
open_appはパッケージ名で照合します。 - scrcpy のサーバーは 3.3.4 に固定されており、そのプロトコルを Mobdev が使用します。Android 16 エミュレーターでテスト済みです。メーカーが USB デバッグ経由の入力をブロックするスマートフォン(Xiaomi の「USB デバッグ(セキュリティ設定)」など)では、
inputと同様に scrcpy でもその設定をオンにする必要があります。サーバーが操作できないタッチ(画面回転中など)は、エラーなしで無視されます。 ui_treeは、macOS の非公開のアクセシビリティ変換機能を通じてシミュレーターを読み取ります。macOS 27 上の Xcode 27 と、GitHub の macOS 26 ランナー上の Xcode 26.6 でテスト済みです。後者ではサンプルフローが変更のたびに実行されます。そこでのシミュレーターは、入力したテキストより数秒遅れることがあり、その間はアプリを読み取れません。tap_elementとwait_for_elementは、タイムアウト後も最大 15 秒間待機します。- 録画ではスクロールホイールの操作は記録されません。録画中はドラッグしてスクロールしてください。
- Android でのダブルタップ(
double)は、adb を介してinput tapを 2 回呼び出すため、アプリのダブルタップ判定時間を超えて遅くなることがあります。 - Mobdev Runner は、WebDriverAgent と同様に XCTest の非公開 API を使って、前面にあるアプリを特定します。Xcode 27 のシミュレーターでテスト済みです。フレームは縦向きを前提としています。お使いの Xcode でビルドするため、新しい Xcode では Mobdev が対応するまで動作しなくなる可能性があります。
開発
swift test # unit, HTTP, MCP, OCR and relay end-to-end tests (needs Go for the last)
scripts/build-app.sh # build/Mobdev Dev.app, the separate development app
GitHub の仮想化 macOS ランナーでは画像認識テキストが返ってこないため、CI が設定されている場合は OCR テストをスキップし、実機の Mac でのみ実行します。
開発者ツールは、スクリプト化された devicectl の応答を使ってテストします。オプトインテストでは、実機またはシミュレーターに対してテストを実行します(Xcode 27 の devicectl は両方を操作します)。
# Installs the app (it should print after launch), launches it and uninstalls it.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_APP=/path/to/App.app swift test --filter DeveloperIntegration
# Launches an installed developer app, reads its logs, stops it, reads crash reports. Installs nothing.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_BUNDLE_ID=<bundle id> swift test --filter DeveloperIntegration
# Captures the requests of an installed app that makes some within seconds of its launch.
MOBDEV_TEST_DEVICE=<udid> MOBDEV_TEST_NETWORK_APP=<bundle id> swift test --filter DeveloperIntegration
ネットワークキャプチャは、プロキシ、テストプロセス内の 127.0.0.1 上のクライアントとサーバー、http_proxy を保持する偽の adb、および iOS 27 シミュレーターで記録した CFNetwork のコンソール出力を使ってテストします。MOBDEV_TEST_INTERNET=1 swift test --filter HTTPProxyTests は、HTTP と HTTPS で curl をプロキシ経由で example.com に送信するテストも実行します。
シミュレーターと Android は、実際の adb 出力を与えた偽のデバイスとパーサーを使ってテストします。オプトインの EmulatorIntegration テストでは、起動済みのシミュレーターと実行中のエミュレーターを使って、すべてのツールを操作します。シミュレーターのテストでは、../examples/flows/fixture のフィクスチャアプリを使用します。このアプリは、タップするたびに fixture: tap <x> <y> fraction <fx> <fy> と出力し、入力されたテキストをエコーし、引数 crash を指定して起動するとクラッシュします。build.sh でビルドします。
../examples/flows/fixture/build.sh
MOBDEV_TEST_SIMULATOR=<udid> MOBDEV_TEST_SIMULATOR_APP=../examples/flows/fixture/MobdevFixture.app swift test --filter EmulatorIntegration
MOBDEV_TEST_ANDROID=emulator-5554 MOBDEV_TEST_ANDROID_APK=/path/to/any.apk swift test --filter EmulatorIntegration
Android のテストでは、am crash で Settings アプリをクラッシュさせ、.apk をインストール、削除、再インストールします。これらの変更を破棄するには、エミュレーターを -read-only を付けて実行してください。androidThroughScrcpy は、スワイプで Settings を移動させながらストリームを測定し(1 秒あたり 10 枚を超える新しい画像が生成されることを想定)、Settings の検索に「Grüße 👋」と入力して UI ツリーから読み取り、クリップボードの設定と読み取りを行い、サーバーを終了して次のサーバーを待ち、デバイスに何も残っていないことを確認します。scrcpy のサーバーは git に含まれていません。scripts/build-app.sh は、固定されたリリースをアプリの Resources にダウンロードします(SHA-256 と照合し、scrcpy のライセンスも同じ場所に配置します)。また、swift build から実行したビルドでは、初回使用時に .build/scrcpy-server-v<version> にダウンロードします。プロトコルは scrcpy 自身のテストとバイト単位で照合し、デコーダーはこの Mac でエンコードした画像を使ってテストします。セッションは偽のサーバーと偽の adb を使ってテストします。
Mobdev Runner は、偽の usbmuxd と Runner の応答を使ってテストします。オプトインの RunnerIntegration テストでは、シミュレーター向けに(署名なしで、Runner は Mac の 127.0.0.1 で待ち受けるよう)ビルドし、/tree、/tap、/type を通じて Settings を操作した後、ui_tree、tap_element、wait_for_element を実行し、停止すると Runner が終了することを確認します。iPhone で役立つ読み取り専用の確認が 2 つあります。1 つは usbmuxd にデバイス一覧を要求し、もう 1 つは TEST_RUNNER_MOBDEV_RUNNER_TOKEN=<token> xcodebuild test-without-building … で手動起動した Runner のツリーを usbmuxd 経由で読み取ります。
UDID=$(xcrun simctl create "Mobdev Runner Test" "iPhone 17")
MOBDEV_TEST_RUNNER_SIMULATOR=$UDID swift test --filter RunnerIntegration
xcrun simctl delete "$UDID"
MOBDEV_TEST_USBMUXD=<iPhone udid> swift test --filter usbmuxdListsTheConnectedIPhone
MOBDEV_TEST_RUNNER_DEVICE=<iPhone udid> MOBDEV_TEST_RUNNER_TOKEN=<token> swift test --filter runnerOnAnIPhoneAnswersOverUSB
ライブビューは、単独のテスト(LiveStreamsTests)と Go リレー経由のテスト(RelayEndToEndTests)の両方で、偽のスマートフォンを使ってテストします。ブラウザーで確認するには、オプトインの LiveHarness テストを実行します。このテストでは、アプリで Allow live view をオンにした場合と同様に、しばらくの間、この Mac 側をリレーに接続します。シミュレーターがある場合はシミュレーターの映像をストリーミングし、フィクスチャアプリのインストールと起動もできます。ない場合は偽のスマートフォンを使います。ビューアーの操作内容と、最後に行ったフィクスチャのタップを MOBDEV_LIVE_LOG に書き込みます。
MOBDEV_LIVE_RELAY=http://127.0.0.1:8787 MOBDEV_LIVE_SECRET=mdh_… MOBDEV_LIVE_TOKEN=mda_… \
MOBDEV_LIVE_SIMULATOR=<udid> MOBDEV_LIVE_APP=../examples/flows/fixture/MobdevFixture.app \
MOBDEV_LIVE_SECONDS=300 MOBDEV_LIVE_LOG=/tmp/live.log swift test --filter LiveHarness
MobdevCore には、テスト可能なものがすべて含まれています。HID レポートとジェスチャー(HID/)、画面キャプチャとテキスト認識(Capture/)、ツール(Phone/)、開発者向けツールの devicectl(Developer/)、シミュレーターと Android(Emulators/)、Mobdev Runner のビルドと usbmuxd 経由での接続(Runner/。Xcode プロジェクトは Sources/ と並んでいる Runner/)、HTTP、MCP、stdio ブリッジ(Server/)、リレークライアント(Relay/)です。Mobdev ターゲットは SwiftUI アプリで、Liquid Glass コントロールを備えた NavigationSplitView、セットアップ用のインスペクター、アクティビティ用の Table、Settings シーンで構成されています。テストでは実際のテキストを描画する偽のスマートフォンを使用するため、ハードウェアなしで OCR、tap_text、座標をテストできます。scripts/make-icon.swift は、macOS 26 のアイコングリッド上に ../assets/branding/ の Imagegen アートワークを配置します。--dev を指定すると、アンバー色の開発用バリエーションが選択されます。ブランド素材をすべて再生成する方法については、../assets/branding/README.md をご覧ください。
クレジット
このアイデアは TapKit に由来します。Bluetooth LE HID の方式(長形式の 1812 UUID、暗号化されたレポート属性、Report Reference 記述子、古いキャッシュに対応する Service Changed、AssistiveTouch 用の絶対ポインター)は、iphone-use(MIT)および sryo/clak に記載されています。独立した実装であり、TapKit および MobAI とは提携していません。Android の画面と入力には、Genymobile と Romain Vimont による scrcpy(Apache-2.0)のサーバーを使用しており、アプリには変更を加えず、ライセンスとともに同梱しています。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。