はじめに
AIエージェントでアプリ操作を自動化する動きが広がっています。Web ブラウザなら Playwright MCP が有名ですが、モバイルアプリでは Appium が定番です。
GitHub を検索すると、Appium を MCP(Model Context Protocol)サーバーとしてラップした実装はいくつか見つかります。しかし、Playwright MCP のように CLI として実装したものは見当たりませんでした。そこで、appium-cli を作成してみました。
既存の MCP サーバー方式にはトークン消費の課題があります。MCP では、ツール呼び出しの結果がすべて LLM のコンテキストウィンドウに直接返されるため、UI ツリー全体(数千〜数万トークン)がそのまま会話コンテキストを消費します。
モバイルアプリの UI ツリーは Web ページよりも深くなりがちです。スナップショットを取るたびに巨大なツリーがコンテキストに積み上がり、トークン上限に達しやすくなります。
MCP 方式
CLI 方式(appium-cli)
appium-cli は UI ツリーをファイルに書き出し、LLM にはメタデータとパスだけ返すので、トークン消費を大幅に抑えられます。これは playwright-cli が採用している方針と同様の試みです。
技術要素
トークン数削減のための実装方法
appium-cli のキモは artifact bundle です。appium-cli snapshot を実行すると、以下の 5 つのファイルが .appium-cli/snapshots/ ディレクトリに生成されます。
| ファイル | 内容 | 用途 |
|---|---|---|
compact.yml |
境界ボックスなしのコンパクトなYAMLツリー | 人間/LLM が読みやすい形式 |
full.yml |
境界ボックス付きの完全なYAMLツリー | ref 解決・座標ベースの操作用 |
refs.json |
ref → ロケータ戦略のマッピング | 要素の特定に使用 |
index.json |
ロール別集計・ref一覧・コンテナ情報 | 構造の俯瞰・検索用 |
meta.json |
snapshot_id、source、screen_id 等 | アーティファクト管理 |
LLM の stdout に返るのはメタデータだけです:
snapshot_id: native-abc123
source: native
screen_id: d4f8e2...
artifacts:
compact: .appium-cli/snapshots/native-abc123/compact.yml
full: .appium-cli/snapshots/native-abc123/full.yml
refs: .appium-cli/snapshots/native-abc123/refs.json
index: .appium-cli/snapshots/native-abc123/index.json
meta: .appium-cli/snapshots/native-abc123/meta.json
LLM が UI の詳細を知りたいときは、snapshot_search、snapshot_refs、snapshot_show --ref=xxx といった抽出コマンドで必要な部分だけ取り出せます。
Native の場合
Native スナップショットは、Appium が返す UIAutomator XML(Android)を元にしています。
appium-cli snapshot
内部では以下の 7 段パイプラインで XML を処理します:
-
Parse — Appium の
getPageSourceで XML を取得・パース -
Infer semantics — Android のクラス名(
android.widget.Button等)からロール(button、textbox等)を推定 - Prune / Collapse — 不可視要素の除去、意味のないラッパーの折り畳み
-
Detect containers —
RecyclerView、ScrollView等のスクロール可能コンテナを検出 -
Assign refs —
resource-idやcontent-descから安定した ref 名を生成 - Action targets — タップ可能な要素にアクション対象を設定
- Locator strategies — ref ごとに複数のロケータ戦略(ID、accessibility_id、xpath、座標)を生成
生成されるコンパクト YAML はこのような形式です:
screen: Settings
screen_id: a3b2c1...
- heading "Settings" [ref:settings_title]
- list [ref:settings_list] [scrollable:vertical]
- row "Wi-Fi" [ref:wifi]
- text "Connected"
- row "Bluetooth" [ref:bluetooth]
- text "Off"
- row "Display" [ref:display]
- button "Search" [ref:search_button]
alerts: none
nav: back, home, recent
ref 名は要素の属性から導出されます:
-
resource-id="com.android.settings:id/wifi"→wifi -
content-desc="Search"→search_button - 重複する resource-id には
_2、_3のサフィックスが付きます
Web の場合
Web スナップショット(Chrome / WebView)は、DOM を元にしています。
appium-cli web_snapshot
DOM の取得は 2 段階です:
- JavaScript 抽出(優先) — ページ内で JS を実行し、アクセシビリティツリーを構築
- HTML パーサーフォールバック — JS が使えない場合は HTML を直接パース
Web の ref 名は HTML/ARIA 属性から導出されます:
-
id="login-form"→web_login_form -
data-testid="submit-btn"→web_submit_btn -
aria-label="Search"→web_search -
<a>タグのテキスト →web_link_xxx
ロケータ戦略も Web に特化しています:
- CSS selector(最優先)
- link text / partial link text
- xpath
- tag name
- 座標(フォールバック)
2 つの違いはなぜ発生するのか?
Native と Web でスナップショットの仕組みが異なるのは、元データの構造が根本的に違うからです。
| 観点 | Native | Web |
|---|---|---|
| 元データ | UIAutomator XML | DOM / HTML |
| 要素の識別 |
resource-id、content-desc
|
id、data-testid、aria-label、CSS |
| ロール推定 | Android クラス名から推定 | HTML タグ + ARIA ロールから取得 |
| コンテナ |
RecyclerView、ScrollView 等を自動検出 |
<ul>、<table>、<form> 等 |
| スクロール | ネイティブスクロール方向を検出 | ブラウザ標準スクロール |
| ref プレフィックス | なし(login、search) |
web_ 付き(web_login、web_search) |
しかし、LLM から見た使い方は同じです。snapshot か web_snapshot を呼べばアーティファクトが生成され、返った ref を使って tap、type_text、scroll_down で操作できます。内部のスナップショット方式の違いは appium-cli が吸収します。
典型的な使い方
LLM エージェントが appium-cli を使う典型的なワークフローは、「観察 → 判断 → 操作 → 確認」 のループです。
Snapshot で取得できる内容
appium-cli snapshot は、画面上のすべてのインタラクティブ要素をアクセシビリティツリーとして返します。各要素には以下の情報が含まれます:
- ロール(button、textbox、link、heading 等)
- 名前(ボタンのラベル、テキストフィールドのヒント等)
- ref(操作に使う安定した識別子)
- 状態(checked、disabled、selected 等)
- 値(テキストフィールドの入力値等)
LLM の解釈と操作の流れ
具体的な例で説明します。LLM が「Settings アプリで Wi-Fi をオンにする」タスクを実行する場合:
ステップ 1: appium-cli snapshot → メタデータが返る
ステップ 2: appium-cli snapshot_refs latest --role=switch → スイッチ要素の一覧を取得
wifi_switch: switch "Wi-Fi" [off]
bluetooth_switch: switch "Bluetooth" [on]
ステップ 3: LLM が判断 — 「wifi_switch が off なので tap する」
ステップ 4: appium-cli tap wifi_switch → タップ実行
ステップ 5: 操作後に自動的にスナップショットが取られ、結果を確認
このように、LLM はテキストベースの UI 情報だけで判断と操作を行えるため、スクリーンショット画像の解析(Vision API)が不要です。トークン効率がさらに良くなります。
機能一覧
appium-cli は以下のコマンドグループを提供します。
環境・ライフサイクル
| コマンド | 説明 |
|---|---|
doctor |
環境診断(Node.js、Appium、SDK 等の状態確認) |
devices |
接続デバイスの列挙 |
server start/stop/status |
Appium サーバーの起動・停止・状態確認 |
session start/stop/status |
WebDriver セッションの開始・終了・状態確認 |
観察(Observation)
| コマンド | 説明 |
|---|---|
snapshot |
ネイティブ UI のアクセシビリティスナップショット |
web_snapshot |
WebView / Chrome の DOM スナップショット |
snapshot_search "text" |
スナップショット内のテキスト検索 |
snapshot_refs |
アクション可能な ref の一覧 |
snapshot_show --ref=xxx |
特定 ref の詳細表示 |
describe ref |
要素の詳細情報 |
find_by_text "text" |
テキストで要素を検索 |
screenshot |
スクリーンショット画像の取得 |
web_query "selector" |
CSS セレクタで Web 要素を検索 |
generate_locator ref |
ref のロケータ戦略を表示 |
基本アクション
| コマンド | 説明 |
|---|---|
tap ref |
要素をタップ |
type_text ref "text" |
テキスト入力 |
click ref |
Web 要素のクリック |
fill ref "text" |
Web フォーム要素への入力 |
scroll_down [ref] |
下にスクロール(ref 省略で全画面) |
scroll_up [ref] |
上にスクロール |
swipe_left [ref] |
左にスワイプ |
press_key back |
キー入力(back、home、enter 等) |
wait 2 |
指定秒数待機 |
wait_for --text "Welcome" |
テキスト出現まで待機 |
ジェスチャー
| コマンド | 説明 |
|---|---|
long_press ref |
長押し |
double_tap ref |
ダブルタップ |
drag ref x y |
ドラッグ |
fling_up [ref] |
高速フリック |
pinch_open ref |
ピンチオープン(拡大) |
pinch_close ref |
ピンチクローズ(縮小) |
アプリ管理・デバイス情報
| コマンド | 説明 |
|---|---|
get_device_info |
デバイス情報の取得 |
get_current_app |
現在のアプリを取得 |
activate_app pkg |
アプリの起動 |
terminate_app pkg |
アプリの終了 |
list_apps |
インストール済みアプリ一覧 |
is_locked |
画面ロック状態の確認 |
get_orientation |
画面の向きを取得 |
WebView / Chrome 操作
| コマンド | 説明 |
|---|---|
list_contexts |
利用可能なコンテキスト一覧 |
webview_switch |
WebView コンテキストに切替 |
native_switch |
ネイティブコンテキストに戻る |
goto "url" |
URL に遷移 |
tabs list/switch/new/close |
タブ操作 |
web_eval "js" ref |
JavaScript 実行 |
console_messages |
ブラウザコンソールログ |
コンテナ・検証
| コマンド | 説明 |
|---|---|
list_containers |
コンテナ要素の一覧 |
find_container "name" |
コンテナの検索 |
within_container ref |
コンテナ内の要素一覧 |
assert_visible --text "text" |
テキストの表示確認 |
動作例
ここでは、appium-cli を使って Android 端末上の Chrome ブラウザで Web サイトを操作する 2 つの実例を紹介します。いずれも AI エージェント(Claude Sonnet 4.6)が自律的に実行した結果です。
Test 1: GitHub VSCode Issues の自動操作
タスク: GitHub の microsoft/vscode リポジトリの Issues ページを開き、以下を自動実行する
- Issues 一覧ページを開く
- スナップショットを取り、最初の 5 件の Issue タイトルを抽出
-
label:bugでフィルタする - フィルタ後のスナップショットから Bug Issue 5 件のタイトルを抽出
- 最初の Bug Issue をクリックして詳細を開く
- 詳細ページのスナップショットからタイトル・作成者・ラベルを取得
- 結果をまとめて報告
テスト結果
LLM エージェントは以下の手順を自律的に実行し、結果を構造化して報告しました
ブラウザの画面:
LLM エージェントの出力結果:
📊 実行結果レポート
1. Issues一覧ページ
URL: https://github.com/microsoft/vscode/issues
2. 最初に表示されるIssue 5件(フィルタなし)
┌───┬────────────────────────────────────────────────────────────────────┬───────────┐
│ # │ タイトル │ Issue番号 │
├───┼────────────────────────────────────────────────────────────────────┼───────────┤
│ 1 │ Copilot is unaware of deferred tools schema, makes them uncallable │ #315526 │
├───┼────────────────────────────────────────────────────────────────────┼───────────┤
│ 2 │ bag │ #315524 │
├───┼────────────────────────────────────────────────────────────────────┼───────────┤
│ 3 │ Feature Request: Custom Background Colors for Chat Sessions │ #315522 │
├───┼────────────────────────────────────────────────────────────────────┼───────────┤
│ 4 │ Ability to restore selection in explorer │ #315520 │
├───┼────────────────────────────────────────────────────────────────────┼───────────┤
│ 5 │ Auto Updates Problems │ #315519 │
└───┴────────────────────────────────────────────────────────────────────┴───────────┘
3. label:bug でフィルタ
URL: https://github.com/microsoft/vscode/issues?q=is%3Aissue+is%3Aopen+label%3Abug
4. Bug Issue 5件(フィルタ後)
┌───┬────────────────────────────────────────────────────────────────────────────────────────┬───────────┐
│ # │ タイトル │ Issue番号 │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼───────────┤
│ 1 │ Better indication when a user is in overage │ #315379 │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼───────────┤
│ 2 │ Suggest widget doesn't work at all when inline suggestion from Copilot are enabled │ #315373 │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼───────────┤
│ 3 │ agent sessions: spinning if commit button causes error │ #315363 │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼───────────┤
│ 4 │ In chat warning shows in every new window │ #315361 │
├───┼────────────────────────────────────────────────────────────────────────────────────────┼───────────┤
│ 5 │ Spurious 'this workspace requires agent plugin' notification with Claude model (local) │ #315347 │
└───┴────────────────────────────────────────────────────────────────────────────────────────┴───────────┘
5 & 6. 最初のBug Issue 詳細
🔗 URL: https://github.com/microsoft/vscode/issues/315379
┌──────────────┬──────────────────────────────────────────────────────────────────────────────┐
│ 項目 │ 内容 │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ タイトル │ Better indication when a user is in overage #315379 │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ 作成者 │ cwebster-99 │
├──────────────┼──────────────────────────────────────────────────────────────────────────────┤
│ ラベル │ bug(Issue identified by VS Code Team member as probable bug)、 │
│ │ UBB(Issues related to UBB) │
└──────────────┴──────────────────────────────────────────────────────────────────────────────┘
実行の流れ:
LLM は appium-cli web_snapshot で Web ページのアクセシビリティツリーを取得し、テキスト情報からIssueタイトルやラベルを読み取りました。label:bug フィルタは URL パラメータを構築して appium-cli goto でナビゲーションしています。Issue 詳細のクリックは appium-cli tap で ref を指定して実行しました。
すべての操作がスナップショットの ref ベースで行われており、座標指定やスクリーンショット解析は一切使っていません。
Test 2: DemoQA フォーム自動入力と検証
タスク: https://demoqa.com/automation-practice-form を開き、以下を自動実行する
- ページを開きスナップショットを取る
- 全フィールドを入力する(名前、メール、性別、電話番号、生年月日、科目、趣味、住所、州、市)
- 各入力後にスナップショットで入力値を確認する
- Submit ボタンを押す
- 送信後のモーダルのスナップショットを取り、表示内容を読み取る
- 入力値と確認内容が一致するか検証して報告する
テスト結果
LLM エージェントは全 10 項目のフォーム入力を正確に行い、Submit 後のモーダル内容と入力値の完全一致を確認しました。
ブラウザの画面:
LLM エージェントの出力結果:
実行結果報告
ステップ1〜3: フォーム入力と確認
すべてのフィールドを正常に入力し、入力値をスナップショットで確認しました。
ステップ4: Submit完了
ステップ5〜6: モーダル確認内容と入力値の検証
┌────────────────┬──────────────────┬──────────────────┬──────┐
│ Label │ 入力値 │ モーダル確認値 │ 一致 │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Student Name │ Taro Yamada │ Taro Yamada │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Student Email │ taro@example.com │ taro@example.com │ ✅ │
│ │ ( │ ( │ │
│ │ mailto:taro@exam │ mailto:taro@exam │ │
│ │ ple.com) │ ple.com) │ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Gender │ Male │ Male │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Mobile │ 0901234567 │ 0901234567 │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Date of Birth │ 15 May 1990 │ 15 May,1990 │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Subjects │ Computer Science │ Computer Science │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Hobbies │ Sports │ Sports │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Picture │ (未入力) │ (空欄) │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ Address │ Tokyo, Japan │ Tokyo, Japan │ ✅ │
├────────────────┼──────────────────┼──────────────────┼──────┤
│ State and City │ NCR / Delhi │ NCR Delhi │ ✅ │
└────────────────┴──────────────────┴──────────────────┴──────┘
全10項目が一致。入力値とモーダルの確認内容は完全に一致しています。
※ Date of Birthはモーダルに 15 May,1990(カンマあり)と表示されますが、
これはdemoqaサイト側の表示フォーマットであり、日付値 15 May 1990 と同一です。
実行の流れ:
このテストは appium-cli の Web 操作能力を総合的に検証するものです。LLM は以下のように各種コマンドを使い分けました:
-
web_snapshot: フォームの構造を把握し、入力対象の ref を特定 -
fill ref "text": テキストフィールドへの入力 -
click ref: ラジオボタン(Gender: Male)やチェックボックス(Hobbies: Sports)の選択 -
fill ref "text" --slowly: React Select コンポーネント(Subjects)は 1 文字ずつ入力してサジェスト表示を待ち、候補をクリック -
select_option ref "value": ドロップダウン(State, City)の選択 -
tap ref: Submit ボタンの送信 -
web_snapshot(送信後) : モーダルの内容を読み取り、入力値と照合
特に注目すべきは、日付ピッカーや React Select のような複雑な UI コンポーネントも、スナップショットで状態を確認しながら操作できている点です。
まとめ
CLI アプローチの利点
appium-cli は「MCP ではなく CLI」という設計判断により、以下の利点を実現しています:
-
トークン効率 — UI ツリーをファイルに書き出し、LLM のコンテキストにはメタデータのみ返す。必要な情報だけをピンポイントで取得できる。
-
シンプルなインターフェース —
appium-cli snapshot→appium-cli tap refという直感的な CLI コマンド。MCP のプロトコル層が不要。 -
エージェント非依存 — 特定の LLM フレームワークに依存しない。シェルコマンドが実行できるエージェントなら何でも使える(Claude Code、Copilot CLI、GPT Agent 等)。
-
デバッグ容易性 — すべてのスナップショットがファイルとして残るため、操作の再現や問題の特定が容易。
Playwright MCP との比較
Playwright MCP(Web ブラウザ自動化)と appium-cli(モバイル自動化)は、同じ「LLM エージェントによる UI 自動化」の異なるアプローチです。
| 観点 | Playwright MCP | appium-cli |
|---|---|---|
| プロトコル | MCP(Model Context Protocol) | CLI(シェルコマンド) |
| 対象 | Web ブラウザ | Android(+ WebView / Chrome) |
| UI ツリー | コンテキストに直接返す | ファイルに保存、メタデータのみ返す |
| トークン効率 | ツリーサイズに比例して消費 | ほぼ一定(メタデータ数行) |
| 要素指定 | スナップショットの ref | スナップショットの ref |
ソースコード

