以前、Flutterに"存在しないネイティブブリッジ"をAIに書かせて、iOS/Androidで動かすまで検証してみたという記事で、XcodeBuildMCPのAXeベースUI自動操作が、あるFlutterアプリでアクセシビリティツリーを認識できず(原因未特定)、座標タップで回避した、という話を書いた。
今回はその続き。iOS/Android両対応の汎用モバイル自動操作MCPであるmobile-mcp(@mobilenext/mobile-mcp)を新たに検証し、XcodeBuildMCP(AXeベース)・Dart MCP(widget_inspector、Flutter VM Serviceベース)と同じアプリ・同じ操作で実際に比較してみた。ついでに、前回「原因未特定」のまま残していた謎についても、1つの仮説を実験で検証している。
検証環境: Flutter 3.44.4 / Dart 3.12.2。iOSシミュレータ(iPhone 17 Pro, iOS 26.5)・Androidエミュレータ(Pixel相当端末, Android 17)。XcodeBuildMCP v2.6.2。mobile-mcp v0.0.62(npx -y @mobilenext/mobile-mcp@latest)。
- mobile-mcpリポジトリ: https://github.com/mobile-next/mobile-mcp
検証用アプリ
TextField・Switch・Slider・3件のListTile・カウンターボタンを持つ、設定画面ふうのシンプルなFlutterアプリを新規に作成した。ボタン1個だけだと比較の見せ場が少ないため、複数種類のウィジェットを含めている。
// 起動時に --dart-define=MODE=baseline|no_retain|retained で切り替える
const String kMode = String.fromEnvironment('MODE', defaultValue: 'baseline');
SemanticsHandle? _retainedHandle;
void main() {
WidgetsFlutterBinding.ensureInitialized();
if (kMode == 'no_retain') {
SemanticsBinding.instance.ensureSemantics(); // 戻り値を保持せず捨てる
} else if (kMode == 'retained') {
_retainedHandle = SemanticsBinding.instance.ensureSemantics(); // 保持する
}
runApp(const ProbeApp());
}
MODEは、前回の記事で「原因未特定」のまま残した謎(SemanticsBinding.instance.ensureSemantics()を追加してもアクセシビリティツリーの欠落が直らなかった)について、「SemanticsHandleの戻り値を保持しないと効果がないのでは」という仮説を実際に検証するための実験用フラグ。
実験1: 「SemanticsHandleを保持しないと壊れる」仮説は誤りだった
baseline(何もしない) / no_retain(ensureSemantics()を呼ぶが戻り値を保持しない) / retained(呼んで保持する)の3パターンで、XcodeBuildMCPのui-automation snapshot-uiとmobile-mcpのmobile_list_elements_on_screenを実行した。
| モード | XcodeBuildMCP(AXe) | mobile-mcp |
|---|---|---|
| baseline | 39要素中6ターゲット検出 | 6要素検出 |
| no_retain | 39要素中6ターゲット検出 | 6要素検出 |
| retained | 39要素中6ターゲット検出 | 6要素検出 |
3パターンとも全く同じ結果になった。 つまり「SemanticsHandleを保持しないから壊れる」という仮説は、少なくともこの単純なアプリでは反証された。前回のアクセシビリティツリー空問題は、ensureSemantics()の呼び方とは別の要因(Native Assets/FFI周りか、元のアプリ固有の何か)によるものだった可能性が高い。根本原因は依然として未特定のままであることを正直に記しておく。
余談だが、この過程でno_retainモードの初回起動時に実装ミスで実際にクラッシュした。
[ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: Binding has not yet been initialized.
runApp()より前にSemanticsBinding.instanceへアクセスしたのが原因(WidgetsFlutterBinding.ensureInitialized()が未呼び出しだった)。main()の先頭に追加して解決した。単純な初期化順序のミスであり、今回の仮説検証そのものとは無関係。
実験2: 3ツールでの検出内容の違い
baselineモードのiOSアプリに対する、各ツールの生の検出結果を見てみる。
XcodeBuildMCP(xcodebuildmcp ui-automation snapshot-ui --output json):
"targets": [
"e21|typeText|text-field|名前||",
"e25|tap|switch|通知を受け取る|0|",
"e31|tap|button|りんご||",
"e32|tap|button|みかん||",
"e33|tap|button|ぶどう||",
"e37|tap|button|タップ||"
],
"scroll": ["e20|swipe|scroll-view|||"],
"text": ["e39|text|text|0||"]
mobile-mcp(mobile_list_elements_on_screen、iOS):
TextField(名前) / Switch(通知を受け取る) / Button(りんご/みかん/ぶどう) / Button(タップ) / StaticText(0)
どちらもSliderが検出結果に出てこない。 XcodeBuildMCPの--output jsonを確認したところ、検出カテゴリはtargets(tap/typeText)・scroll・textの3種類のみで、スライダーのような「adjustable(値を増減する)」ロールの分類が現状無いことが分かった。mobile-mcp側も同様に、iOSでは検出対象から漏れていた。
一方、Dart MCPのwidget_inspector get_widget_treeは、Flutter VM Service経由でウィジェットツリーそのものを取得する。実際の出力(読みやすいようSizedBoxなどのレイアウト用ウィジェットは省略してインデント表示、それ以外は生データのまま):
[root]
ProbeApp
MaterialApp
ProbePage
Scaffold
ListView
TextField-[<'nameField'>]
SwitchListTile-[<'notifySwitch'>]
Text "通知を受け取る"
Slider-[<'volumeSlider'>]
ListTile-[<'item_0'>]
Text "りんご"
ListTile-[<'item_1'>]
Text "みかん"
ListTile-[<'item_2'>]
Text "ぶどう"
Center
ElevatedButton-[<'probeButton'>]
Text "タップ"
Center
Text-[<'countText'>] "0"
AppBar
Text "MCP Probe (baseline)"
Slider-[<'volumeSlider'>]も含めて、ウィジェットツリー上のすべての要素がキー付きで見えている。XcodeBuildMCP・mobile-mcpの出力(それぞれ6要素・6要素)と比べると、こちらは省略なしの完全なツリーだ。OS側のアクセシビリティAPIが対応していないロールでも、VM Service経由なら関係なく取得できる、という違いが具体的に確認できた。
実験3: Androidだとmobile-mcpがSliderも検出した
同じアプリをAndroidエミュレータで動かし、mobile-mcpで確認したところ、iOSでは検出できなかったSliderが検出できた。実際の出力(該当部分そのまま):
{
"type": "android.widget.SeekBar",
"text": "",
"label": "50%",
"coordinates": { "x": 600, "y": 759, "width": 144, "height": 144 }
}
labelに現在値がそのまま"50%"として入った状態でSeekBarが検出されている。iOSでは同じSliderが検出結果に一切現れなかったのと対照的だ。
mobile-mcpは内部的に、iOSではXCUITest/idb系、AndroidではUIAutomator系のOSネイティブなアクセシビリティAPIを使っていると考えられ、同じFlutterウィジェット(Slider)でも、どちらのOSのアクセシビリティAPIが「adjustable」ロールをどこまで表現できるかによって、検出結果がプラットフォームごとに変わることが具体的に確認できた。XcodeBuildMCPはiOS/macOS専用なのでこの差自体を体験できないが、iOS/Android両対応のmobile-mcpだからこそ見えた違いだと言える。
実験4: 実際にタップ操作をして、Dart MCPと結果が一致するか
mobile-mcpのmobile_click_on_screen_at_coordinatesで「タップ」ボタンを3回クリックし(iOS)、直後にDart MCPのwidget_inspector get_widget_treeで確認した。
mobile-mcp: mobile_list_elements_on_screen → countText相当のStaticText "3"
Dart MCP: Text-[<'countText'>] textPreview: "3"
OS操作経由(mobile-mcp)とVM Service経由(widget_inspector)、全く別の経路で観測した結果が一致した。 異なる自動操作系ツールを併用しても状態の整合性が保てることが実際に確認できた。
見た目でも確認しておく。mobile-mcpで通知スイッチのON・3回タップという同じ操作をiOS/Android両方に行い、それぞれスクリーンショットを撮った。
カウントが両OSとも3、スイッチもONで一致している。異なるOS・異なる自動操作経路でも、操作結果自体は素直に一致することが見た目でも確認できた。
実践上の注意点: mobile-mcpは明示的なlaunchが必要だった
flutter runでアプリを起動した直後にmobile-mcpのmobile_list_elements_on_screenを呼んだところ、アプリ画面ではなくホーム画面(Springboard)のアイコン一覧が返ってきた。 mobile_launch_appでバンドルIDを指定して明示的に起動し直したところ、正しくアプリの画面が取得できるようになった。flutter runによるインストール・起動と、mobile-mcp側が認識する「フォアグラウンドアプリ」の状態は必ずしも同期しない、という実践上の注意点だ。
まとめ
| XcodeBuildMCP | Dart MCP(widget_inspector) | mobile-mcp | |
|---|---|---|---|
| 対応OS | iOS/macOSのみ | Flutter/Dartアプリのみ(OS問わず) | iOS/Android両対応 |
| 検出方式 | OSアクセシビリティAPI(AXe) | Flutter VM Service | OSアクセシビリティAPI |
| Slider(adjustable)検出 | ✗(カテゴリ未対応) | ◎(常に見える) | △(iOS✗ / Android◎) |
| ネイティブアプリを複数OSで横断的に駆動 | ✗(ネイティブアプリも対象だがiOS/macOSのみ) | ✗(Flutterアプリのみが対象) | ◎(iOS/Android両方のネイティブアプリが対象) |
| 今回のSemanticsHandle仮説 | 影響なし(3モードとも同じ結果) | 対象外(そもそも常に見える) | 影響なし(3モードとも同じ結果) |
- 前回の記事で「原因未特定」としていたアクセシビリティツリー空問題について、有力そうに見えた仮説(
SemanticsHandleの保持漏れ)は実験の結果、否定された。根本原因は依然として謎のままである。 - 3ツールとも一長一短があり、OSのアクセシビリティAPIに依存する(XcodeBuildMCP・mobile-mcp) か Flutter VM Serviceに依存する(widget_inspector) かで、見える対象が変わる。「adjustable」ロール(Slider等)はOS依存側が弱く、しかもiOS/Androidで対応状況が違う、という具体例が今回初めて取れた。
- mobile-mcpはiOS/Android両対応かつネイティブアプリにも使える汎用性が強み。ただし「アプリを明示的にlaunchしないとホーム画面を見てしまう」という実践上の注意点があった。
- どのツールも「タップした結果」自体は正しく反映されており、複数の自動操作系ツールを併用しても状態の不整合は起きなかった。
参考リンク
- Flutterに"存在しないネイティブブリッジ"をAIに書かせて、iOS/Androidで動かすまで検証してみた — 今回の出発点となったアクセシビリティツリー空問題
- mobile-mcp: https://github.com/mobile-next/mobile-mcp
- XcodeBuildMCP: https://github.com/getsentry/XcodeBuildMCP

