mcpField
F
flutter-mcp-toolkit-control

flutter-mcp-toolkit-control

github.com/arenukvern開発39461 回インストール2 週間前に更新
リポジトリ

flutter-mcp-toolkit-control について

実行中の Flutter アプリをユーザーのように操作するスキルで、アプリ開発やテストに向いています。セマンティックスナップショットでウィジェットを特定し、操作、ナビゲーション、待機、ホットリロードを実行します。

主な機能

  1. 01セマンティックスナップショットとウィジェット参照
  2. 02タップ、テキスト入力、フォーム入力
  3. 03スクロール、スワイプ、ドラッグ
  4. 04ルート遷移とキーボード入力
  5. 05条件待機とホットリロード時の取得

活用シーン

  1. 01ログインフォームの入力と送信
  2. 02画面外ウィジェットの検索と操作
  3. 03ルート遷移やダイアログの確認
  4. 04Dart の変更をホットリロードして UI を確認

MCP 対応

Flutter アプリを操作するツールを提供し、MCP クライアントから呼び出します。

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/arenukvern/mcp_flutter --skill flutter-mcp-toolkit-control

インストール後、Claude Code で/flutter-mcp-toolkit-controlを入力して読み込まれたか確認します。

詳細

使用する場面

実行中の Flutter アプリをユーザーと同じように操作する必要がある場合に、このスキルを使用します。

  • ボタン、アイコン、リスト項目、その他のインタラクティブなウィジェットをタップします。
  • フィールドにテキストを入力したり、フォームを送信したり、入力内容をクリアしたりします。
  • スクロールまたはスワイプして、画面外のコンテンツを表示します。
  • ルート間をプログラムで移動します(push、pop、popUntil)。
  • ダイアログやボトムシートを閉じます。
  • ウィジェットにキーボードフォーカスを設定してから、キー(Enter、Escape、Tab、矢印、ASCII 文字)を押します。
  • Dart ソースファイルを編集した後に、ホットリロードまたはホットリスタートします。
  • リロード、スクリーンショット、セマンティクスを 1 回のラウンドトリップで組み合わせて、すばやく反復します。

セレクター

ほとんどの操作ツールは、semantic_snapshot が返す "s_0" のような短い文字列の ref でウィジェットを指定します。画面に表示されているウィジェットの場合は、semantic_snapshot を呼び出し、返されたノードを確認して適切な ref を見つけ、それを渡します。identifierPrefix または subtreeOf(ref または identifier)でノードのセットを絞り込み、fields を使って各ノードから読み取るキーだけに限定します。いずれの場合も、ref はツリー全体のものです。画面外のターゲットで、安定したセマンティクスのテキストまたは identifier がある場合は、reveal_search を使用します。これはスナップショットの取得 → 一致の確認 → スクロールを上限付きで繰り返し、最新の ref/snapshotId を返します。

スキャンするスナップショットノードのキー:

検索対象確認するフィールド値の例
セマンティクスの identifieridentifier"nav.tasks"
表示ラベル/テキストlabel"Login"
値またはヒントvalue / hint"[email protected]"
セマンティックロールtype"button"
受け付ける操作actions["tap"]

例 — 「Login」ボタンの ref を見つけます:

semantic_snapshot()
→ nodes: [{ref:"s_0", label:"Login", actions:["tap"]}, ...]
tap_widget(ref: "s_0")

スナップショットの応答から取得した snapshotId を、すべての操作呼び出しに渡します。ツリーが変更されている場合、呼び出しは両方の ID とともに stale_snapshot を返すため、スナップショットを再取得できます。ref は最新のスナップショットに対してのみ有効です。

レシピ

テキストでウィジェットをタップする
semantic_snapshot()
→ find node where label == "Submit" → ref "s_3"
tap_widget(ref: "s_3", snapshotId: <id>)
ログインフォームに入力する
semantic_snapshot() → email ref "s_1", password ref "s_2"
fill_form(fields: [{ref:"s_1", text:"[email protected]"}, {ref:"s_2", text:"secret"}], snapshotId: <id>)
→ one round-trip; stops on first failure
スクロールして項目を見つける
scroll(direction: "down", distance: 300)
semantic_snapshot() → item now visible → ref "s_5"
tap_widget(ref: "s_5")
identifier で画面外のフィールドを表示する
reveal_search(query: "greeting_input_field", matchBy: "identifier", direction: "down", maxAttempts: 4)
→ returns ref "s_14", snapshotId 2, match, attempts trace
enter_text(ref: "s_14", snapshotId: 2, text: "hello")
ウィジェットが表示されるまで待つ
wait_for(predicate: {kind: "text", text: "Welcome"}, timeoutMs: 8000)
→ returns fresh snapshot when text appears
tap_widget(ref: <ref from wait_for snapshot>)
ルートに移動する
navigate(action: "push", route: "/settings", arguments: {tab: "account"})
semantic_snapshot() → fresh refs in the new screen
コード変更後にホットリロードする
hot_reload_and_capture()
→ screenshot + semantic snapshot + errors in one call
戻るハードウェアボタンを押す

press_key に Back キーはありません。Navigator の pop には navigate(action: "pop")、ダイアログには handle_dialog(action: "dismiss")、デスクトップでは press_key(key: "Escape") を使用します。

navigate(action: "pop")

ツールリファレンス

tap_widget

ref でウィジェットをタップします。ref • string • 必須。snapshotId • integer • 任意。connection • object • 任意。

{"name": "tap_widget", "arguments": {"ref": "s_3", "snapshotId": 7}}

戻り値:{"via": "semantic_action", "ref": "s_3"} — 失敗:stale_snapshot、ref_not_found

long_press

ref でウィジェットを長押しします。ref • string • 必須。snapshotId • integer • 任意。connection • object • 任意。

{"name": "long_press", "arguments": {"ref": "s_2"}}

戻り値:{"via": "semantic_action"} — 失敗:stale_snapshot、ref_not_found

enter_text

テキストフィールドにテキストを入力します。入力前にタップしてフォーカスします。ref • string • 必須。text • string • 必須。snapshotId • integer • 任意。connection • object • 任意。

{"name": "enter_text", "arguments": {"ref": "s_1", "text": "[email protected]"}}

戻り値:{"via": "editable_state", "verified": true, "appliedText": "..."}。フィールドに何も保持されなかった場合、text_not_applied、空の appliedText、元に戻された値を示す restoredText が返され、失敗となります。失敗:stale_snapshot、ref_not_found、text_not_applied

reveal_search

画面外にある可能性のあるセマンティックなターゲットを検索します。query • 文字列 • 必須。matchBy • 文字列 • 任意(text|identifier|label|value|hint、デフォルトは text)。direction • 文字列 • 任意(up|down|left|right、デフォルトは down)。maxAttempts • 整数 • 任意 • 最大 10。distance • 数値 • 任意。connection • オブジェクト • 任意。

{"name": "reveal_search", "arguments": {"query": "greeting_input_field", "matchBy": "identifier", "direction": "down", "maxAttempts": 4}}

戻り値:{"ref": "s_14", "snapshotId": 2, "match": {...}, "attempts": [...]} — 失敗:missing_query、target_not_found、scroll_blocked、target_not_actionable。identifier は大文字と小文字を区別して完全一致します。それ以外のモードでは、大文字と小文字を区別せず部分一致を判定します。identifier で見つからなかった場合は、検索した画面で公開された識別子の数を示す identifiersSeen と、最も近い識別子を示す nearIdentifiers が返されます。そのため、panel.tab が見つからない場合は panel.tab.overview / panel.tab.jobs が返され、代わりに渡すことができます。

fill_form

テキスト入力をまとめて実行します。1 回の呼び出しで複数のフィールドに入力します。最初の失敗で停止します。snapshotId は最初のフィールドでのみ検証されます。fields • {ref, text} の配列 • 必須。snapshotId • 整数 • 任意。connection • オブジェクト • 任意。

{"name": "fill_form", "arguments": {"fields": [{"ref":"s_1","text":"user"},{"ref":"s_2","text":"pass"}], "snapshotId": 5}}

戻り値:{"filled": 2} — 失敗:stale_snapshot、ref_not_found

scroll

コンテンツを表示するためにスクロールします。"down" は下のコンテンツを表示します(指を上にスワイプします)。direction • 文字列 • 必須(up|down|left|right)。ref • 文字列 • 任意 — スクロールするリスト、またはリスト内の任意のノードです。指定しない場合は、画面中央の下にあるリストがスクロールします。distance • 数値 • 任意 • デフォルトは 300。リストがオフセットを受け付ける場合は指定した値がそのまま適用され、それ以外の場合はビューポート 1 ページ分に丸められます。snapshotId • 整数 • 任意。connection • オブジェクト • 任意。

{"name": "scroll", "arguments": {"direction": "down", "ref": "s_0", "distance": 500}}

戻り値:{"via": "semantic_action", "scrollBefore": 0.0, "scrollAfter": 500.0, "distance": 500.0}。測定可能な場合は常に scrollBefore / scrollAfter が返されます。distance は正確なオフセットとポインタースクロールの経路で返されます。スクロール位置を持たずにスクロールアクションを通知するノード(カスタムの Semantics(onScrollUp: ...))では、そのアクションが実行され、success: true、verified: false、unmeasured: true が返されます。失敗:ref_not_found、stale_snapshot、no_scrollable_at_point、no_scroll_movement、unsupported_scroll_action、semantics_owner_unavailable

swipe

高速フリング。同じ方向モデルを scroll として使用します。常に Tier 2 のポインターイベントを使用します。direction • 文字列 • 必須。ref • 文字列 • 任意。distance • 数値 • 任意 • デフォルトは 300。snapshotId • 整数 • 任意。connection • オブジェクト • 任意。

{"name": "swipe", "arguments": {"direction": "left", "ref": "s_4"}}

移動を測定できる場合の戻り値:{"via": "pointer_events", "scrollBefore": 0.0, "scrollAfter": 420.0}。有限のスクロールオフセットを取得できない場合、ジェスチャーをディスパッチすると success: true、verified: false、measurementReason が返されます。失敗:ref_not_found、stale_snapshot、no_scroll_movement、web_gesture_not_supported

drag

あるウィジェットから別のウィジェットへドラッグします。常に Tier 2 です。fromRef • 文字列 • 必須。toRef • 文字列 • 必須。snapshotId • 整数 • 任意。connection • オブジェクト • 任意。

{"name": "drag", "arguments": {"fromRef": "s_2", "toRef": "s_7"}}

戻り値:{"via": "pointer_events"} — 失敗:ref_not_found、web_gesture_not_supported

hover

マウスホバーを合成します。デスクトップ/ウェブのみ — モバイルにはホバーの概念がありません。ref • 文字列 • 必須。snapshotId • 整数 • 任意。connection • オブジェクト • 任意。

{"name": "hover", "arguments": {"ref": "s_5"}}

戻り値:{"via": "pointer_events"} — 失敗:ref_not_found、モバイルでのプラットフォームエラー

focus_widget

ウィジェットにキーボードフォーカスを与え、後続の press_key がそのウィジェットに届くようにします。スナップショットにノードのセマンティックな focus アクションがある場合はそれを使用し、ない場合は ref の範囲内にあるフォーカス可能なウィジェットを使用します(Flutter がフォーカスアクションを公開していない iOS では、これが唯一の方法です)。ホバー状態は維持されます。ref • string • 必須。snapshotId • integer • 任意。connection • object • 任意。

{"name": "focus_widget", "arguments": {"ref": "s_5"}}

戻り値:{"via": "semantic_action" | "focus_node", "verified": true, "verifiedBy": "semantics_flag" | "focus_node", "focusMoved": bool} — success: true は、フレームワークの反映後に対象がフォーカス状態を報告していることを意味します。focusMoved: false は、すでにフォーカスされていたことを意味します。フォーカスノード経由の場合、focusedNow には ref 内で実際にフォーカスを受けたコントロールが示されます。失敗:ref_not_found、stale_ref、stale_snapshot、target_disabled、focus_not_exposed(ref の背後にフォーカス可能な要素がありません)、focus_refused(要求は送信されましたが、対象がフォーカス状態を報告していません。現在のフォーカス先は focusedNow に示されます)

press_key

キーの押下(down+up)を合成します。受け付けるキー:Enter Escape Tab Backspace Delete Space ArrowUp ArrowDown ArrowLeft ArrowRight および単一の ASCII 文字(a-z 0-9)。key • string • 必須。ctrl/shift/alt/meta • boolean • 任意 • デフォルト false。connection • object • 任意。

{"name": "press_key", "arguments": {"key": "Enter"}}

戻り値:{"key": "Enter", "handled": bool} — handled は、どちらかのディスパッチ段階(ハードウェアキーボードハンドラーまたはフォーカスチェーン)がメインのキー押下イベントを処理したかどうかを示します。false でもキー解放イベントや修飾キーイベントの処理状況は示しません。clearedStaleModifiers / releasedStuckModifiers には、この呼び出しで解放する必要があった修飾キーが示されます。以前のキーストロークで修飾キーが押されたままになっていると、通常のショートカットがすべて一致しなくなるため、この呼び出しでは状態を正規化し、その旨を報告します。失敗:unknown_key

wait_for

UI の述語を待機し、新しいセマンティックスナップショットを返します。述語:{kind:"text",text} | {kind:"noText",text} | {kind:"time",ms} | {kind:"stable",stableWindowMs}。stable はフレームごとにサンプリングし、セマンティクスツリーが指定された実時間のあいだ変化しなかった場合に一致します。一致時には stableFor.sampledFrames と stableFor.elapsedMs が報告されます。stableWindowMs は timeoutMs(デフォルト 5000)未満である必要があります。実現不可能な予算を指定した場合は、直ちに invalid_predicate で失敗します。predicate • object • 必須。timeoutMs • integer • 任意 • デフォルト 5000 • 最大 30000。connection • object • 任意。

{"name": "wait_for", "arguments": {"predicate": {"kind": "text", "text": "Dashboard"}, "timeoutMs": 8000}}

戻り値:新しいセマンティックスナップショット — 失敗:timeout、invalid_predicate

navigate

登録済み Navigator を操作します。アプリで MCPToolkitBinding.instance.navigatorKey = key を設定する必要があります。action • string • 必須(push|pop|popUntil)。route • string • push/popUntil では必須。arguments • object • 任意(push 用)。connection • object • 任意。

{"name": "navigate", "arguments": {"action": "push", "route": "/profile", "arguments": {"userId": "42"}}}

戻り値:{"action": "push", "route": "/profile"}。名前のないルートが生成された場合、スタックは変更されても要求された名前を確認できないため、success: true、verified: false、via: "stack_changed_unnamed_route" が返されます。pop はスタックの success を読み取り、maybePop の結果を handled として報告します。PopScope(canPop: false) の下にあるページでは、handled: true、success: false、nothing_popped が返されます。失敗:navigator_not_configured、route_not_found

handle_dialog

最前面のポップアップ/ダイアログルートを閉じます。サポートされるのは action: "dismiss" のみです。アプリで MCPToolkitBinding.instance に navigatorKey = key を設定する必要があります。action • string • 必須("dismiss" でなければなりません)。connection • object • 任意。

{"name": "handle_dialog", "arguments": {"action": "dismiss"}}

戻り値:{"success": true, "handled": true, "routeType": "DialogRoute<void>"}。success はルートスタックから読み取られます。handled は maybePop の応答です。そのため、PopScope(canPop: false) の下にあるダイアログでは、handled: true、success: false、dialog_declined_pop が返されます。失敗:navigator_not_registered、no_popup_route、dialog_declined_pop

hot_reload_flutter

アプリをホットリロードします。状態は維持されます。force • ブール値 • 任意 • デフォルト false(ソースに変更がなくてもリロードします)。connection • オブジェクト • 任意。

{"name": "hot_reload_flutter", "arguments": {}}

戻り値:"Hot reload completed" + レポート JSON — 失敗時:vm_not_connected、compilation_error

hot_restart_flutter

完全に再起動します。アプリの状態は維持されません。必須パラメーターはありません。connection • オブジェクト • 任意。

{"name": "hot_restart_flutter", "arguments": {}}

戻り値:{"report": {"type": "Success", "success": true}} — 失敗時:vm_not_connected

hot_reload_and_capture

ホットリロード後、スクリーンショット、セマンティクス、エラーを1回の呼び出しで取得します。compress • ブール値 • デフォルト true。includeSemantics • ブール値 • デフォルト true。includeErrors • ブール値 • デフォルト true。errorsCount • 整数 • デフォルト 4。connection • オブジェクト • 任意。

{"name": "hot_reload_and_capture", "arguments": {"includeErrors": true}}

戻り値:スクリーンショット(base64)+ セマンティックスナップショット + エラー — 失敗時:vm_not_connected、compilation_error

パターン

ナビゲーション後は、tap_widget の前に必ず wait_for を使う

navigate(action: "push") の後、新しいルートのウィジェットはまだツリーにありません。テキスト述語を指定して wait_for を使い、遷移先が描画されたことを確認してから、スナップショットを取得して操作します。

navigate(action: "push", route: "/checkout")
wait_for(predicate: {kind: "text", text: "Order Summary"}, timeoutMs: 5000)
semantic_snapshot() → tap target widgets
複数回の enter_text より fill_form を優先する

enter_text を呼び出すたびに、VM とのラウンドトリップが発生します。fill_form はすべてのフィールドとテキストのペアを1回の呼び出しで送信します。snapshotId は1回だけ(最初のフィールドで)確認されます。フィールドが2つ以上あるフォームでは、常に fill_form を優先してください。

# Avoid: 2 round-trips
enter_text(ref: "s_1", text: "Alice")
enter_text(ref: "s_2", text: "secret")

# Prefer: 1 round-trip
fill_form(fields: [{ref: "s_1", text: "Alice"}, {ref: "s_2", text: "secret"}])
hot_reload_* の後は、続行する前に新しいツリーを待つ

ホットリロードは非同期に完了します。semantic_snapshot を呼び出す前に、wait_for(predicate: {kind:"stable", stableWindowMs:300}) を使ってツリーが安定したことを確認してください。または、リロード後のスナップショットを直接返す hot_reload_and_capture を使います。

hot_reload_flutter()
wait_for(predicate: {kind: "stable", stableWindowMs: 300})
semantic_snapshot() → interact with reloaded widgets

# Or in one call (preferred):
hot_reload_and_capture() → screenshot + semantics + errors already post-reload

コメント