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. 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

评论

支持 Markdown