关于 flutter-mcp-toolkit-control
用于通过交互方式操作正在运行的 Flutter 应用的技能,适合应用开发与测试工作。它通过语义快照定位控件,并提供交互、导航、等待及热重载等工具。
主要功能
- 01语义快照与控件引用
- 02点击、输入及表单填写
- 03滚动、滑动与拖拽
- 04路由与键盘操作
- 05等待条件及热重载捕获
适用场景
- 01自动填写并提交登录表单
- 02查找并操作屏幕外控件
- 03验证页面导航与对话框
- 04修改 Dart 后热重载并检查界面
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 源文件后进行热重载或热重启。
- 在一轮往返中组合重载、截图和语义信息,以便快速迭代。
选择器
大多数交互工具通过 ref 定位组件——这是一个类似 "s_0" 的短字符串,由 semantic_snapshot 返回。对于可见组件,调用 semantic_snapshot,检查返回的节点,找到正确的 ref,然后传入该 ref。可使用 identifierPrefix 或 subtreeOf(一个 ref 或 identifier)缩小节点集,并通过 fields 将每个节点精简为你要读取的键;无论哪种情况,ref 都对应完整树中的节点。对于具有稳定语义文本或 identifier 的屏幕外目标,使用 reveal_search;它会执行有界的快照 → 匹配 → 滚动循环,并返回最新的 ref/snapshotId。
要检查的快照节点键:
| 查找目标 | 检查字段 | 示例值 |
|---|---|---|
| 按语义 identifier 查找 | identifier | "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 传递给任何交互调用。如果树已更改,调用会返回 stale_snapshot 和两个 ID,提示你重新获取快照。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 键。使用 navigate(action: "pop") 执行 Navigator pop;使用 handle_dialog(action: "dismiss") 关闭对话框;在桌面端使用 press_key(key: "Escape")。
navigate(action: "pop")
工具参考
tap_widget
按 ref 点击组件。ref • 字符串 • 必填。snapshotId • 整数 • 可选。connection • 对象 • 可选。
{"name": "tap_widget", "arguments": {"ref": "s_3", "snapshotId": 7}}
返回:{"via": "semantic_action", "ref": "s_3"} — 失败:stale_snapshot、ref_not_found
long_press
按住组件。ref • 字符串 • 必填。snapshotId • 整数 • 可选。connection • 对象 • 可选。
{"name": "long_press", "arguments": {"ref": "s_2"}}
返回:{"via": "semantic_action"} — 失败:stale_snapshot、ref_not_found
enter_text
在文本字段中输入文本;输入前会先点击字段以使其获得焦点。ref • 字符串 • 必填。text • 字符串 • 必填。snapshotId • 整数 • 可选。connection • 对象 • 可选。
{"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
批量输入文本:一次调用即可填写多个字段。遇到第一个失败时停止。仅对第一个字段验证 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;列表支持偏移时会精确采用该值,否则会按视口页取整。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
模拟鼠标悬停。仅限桌面端 / Web 端 — 移动端没有悬停概念。ref • 字符串 • 必填。snapshotId • 整数 • 可选。connection • 对象 • 可选。
{"name": "hover", "arguments": {"ref": "s_5"}}
返回:{"via": "pointer_events"} — 失败原因:ref_not_found、移动端平台错误
focus_widget
将键盘焦点移至某个小组件,使后续的 press_key 能对其生效。如果快照中列出了节点的语义 focus 操作,则使用该操作;否则,聚焦 ref 边界内可聚焦的小组件(这是 iOS 上唯一可用的方式,因为 Flutter 不会公开焦点操作)。保持已停驻的悬停状态。ref • 字符串 • 必填。snapshotId • 整数 • 可选。connection • 对象 • 可选。
{"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
模拟按键(按下并抬起)。支持:Enter Escape Tab Backspace Delete Space ArrowUp ArrowDown ArrowLeft ArrowRight,以及单个 ASCII 字符(a-z、0-9)。key • 字符串 • 必填。ctrl/shift/alt/meta • 布尔值 • 可选 • 默认 false。connection • 对象 • 可选。
{"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 • 对象 • 必填。timeoutMs • 整数 • 可选 • 默认 5000 • 最大值 30000。connection • 对象 • 可选。
{"name": "wait_for", "arguments": {"predicate": {"kind": "text", "text": "Dashboard"}, "timeoutMs": 8000}}
返回:新的语义快照 — 失败:timeout、invalid_predicate
navigate
驱动已注册的 Navigator。应用中必须设置 MCPToolkitBinding.instance.navigatorKey = key。action • 字符串 • 必填(push|pop|popUntil)。route • 字符串 • push/popUntil 时必填。arguments • 对象 • 可选(用于 push)。connection • 对象 • 可选。
{"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 • 字符串 • 必填(必须为 "dismiss")。connection • 对象 • 可选。
{"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
热重载,然后通过一次调用捕获屏幕截图、语义信息和错误。compress • 布尔值 • 默认值为 true。includeSemantics • 布尔值 • 默认值为 true。includeErrors • 布尔值 • 默认值为 true。errorsCount • 整数 • 默认值为 4。connection • 对象 • 可选。
{"name": "hot_reload_and_capture", "arguments": {"includeErrors": true}}
返回:屏幕截图(base64)+ 语义快照 + 错误 — 失败情况:vm_not_connected、compilation_error
模式
导航后,始终先调用 wait_for,再调用 tap_widget
调用 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
优先使用 fill_form,而不是多次调用 enter_text
每次调用 enter_text 都会单独进行一次 VM 往返。fill_form 会在一次调用中发送所有字段/文本对;snapshotId 仅检查一次(检查第一个字段时)。对于包含 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_* 后,先等待新树就绪再继续
热重载会异步完成。使用 wait_for(predicate: {kind:"stable", stableWindowMs:300}) 确认树已稳定,然后再调用 semantic_snapshot。也可以使用 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