背景:MCP 是什么?
先说说核心判断:MCP 这个概念,说白了就是一套开放协议,由 Anthropic 提出来,目的是让 AI 助手能直接调用外部工具和服务。用更直白的方式讲——它是 AI 和你开发环境之间的翻译官。

Flutter 官方顺势推出了 dart mcp-server,这套工具链让 Claude Code 可以直接:
- 获取运行中 App 的 Widget Tree
- 执行热重载 / 热重启
- 查看运行时错误
- 通过 Flutter Driver 操控 UI
这意味着什么?意味着你写代码的时候,AI 不再只是个聊天窗口,而是能真正伸手帮你干活了。
一、环境配置
1. 安装 dart mcp-server
在项目根目录下跑一行命令:
claude mcp add -s project --transport stdio dart -- dart mcp-server
这条命令跑完,项目根目录会自动生成一个 .mcp.json 配置文件:
{"mcpServers": {"dart-mcp-server": {"command": "dart","args": ["mcp-server"],"env": {}}}}
2. 开启 VSCode 支持
接下来,打开 .vscode/settings.json,加一行配置:
{"dart.mcpServer": true}
搞定,现在 VSCode 里的 AI 扩展能直接和 Dart MCP Server 通信了。
3. 集成 Flutter Driver
想要 AI 能操控 UI,还得把 Flutter Driver 集成进来。先在 pubspec.yaml 的 dev 依赖里加上:
dev_dependencies:flutter_driver:sdk: fluttertest: any
然后新建一个 main_driver.dart 文件,作为 Driver 的入口:
import 'package:flutter_driver/driver_extension.dart';import 'lib/main.dart' as app;void main() {enableFlutterDriverExtension(); // 担心污染生产构建?用环境变量控制一下即可app.main();}
这里有个细节:enableFlutterDriverExtension() 如果直接跑在生产 build 里可能会带来安全风险,建议通过 --dart-define 或环境变量来控制是否启用。
二、连接 MCP
启动应用后,终端会打印出 DTD 地址,看起来像这样:
ws://127.0.0.1:62633/P6KDlN4NFYw=
把这个地址告诉 Claude Code,它就会自动连上去:
连接成功!Dart MCP 已就绪
一旦连上,Claude Code 就有了"眼睛"和"手"——它能看到 Widget Tree,也能直接操控 UI 元素。
三、让 AI 直接操控 App
连接成功后,最令人兴奋的部分来了。直接用自然语言下指令试试:
"执行登录操作,账号 15899999999,密码 a123456,勾选协议,点击登录"
Claude Code 会自己把这句话拆解成具体的操作步骤,然后按顺序执行:
waitFor('欢迎登录二手合规通!')→ 确认登录页已经渲染完毕tap(phone_input)→ 点击手机号输入框enterText('15899999999')→ 输入手机号tap(password_input)→ 点击密码输入框enterText('a123456')→ 输入密码tap(AppCheckbox)→ 勾选协议tap('登录')→ 点击登录按钮
整个过程,你连手指头都不用动。当然,为了让 Driver 能精准定位到输入框,需要给对应的 Widget 加上 Key:
// 示例:给 TextField 加 KeyTextField(key: const Key('phone_input'),// ... 其他属性)
四、自动化测试用例
有 Flutter Driver 打底,把操作固化成可重复运行的测试用例,就是顺理成章的事。这部分跟 MCP 本身没有直接关系,用的是纯 flutter_driver,但它和前面的 AI 操控构成了完整的自动化闭环。
测试文件结构
推荐这样组织:
test/└── driver/ └── login_test.dart # 登录页测试,共 8 个用例
setUpAll 模板
写测试之前,先搭好基础模板:
dartsetUpAll(() async {driver = await FlutterDriver.connect();// 等待 runApp() 执行完毕,避免 "No root widget" 错误await driver.waitUntilFirstFrameRasterized();// 等待目标页面渲染完成await driver.waitFor(find.text('欢迎登录二手合规通!'));});
这里有一个容易踩的坑:如果少了 waitUntilFirstFrameRasterized(),Driver 连接时 runApp() 还没执行完,就会报 No root widget is attached。这个错误在文档里不显眼,但实际调试时经常遇到。
登录页 8 个测试用例
下面这张表列出了完整的登录页测试场景:
| 用例 | 场景 | 验证点 |
|---|---|---|
| TC-01 | 手机号为空 | Toast "请输入手机号" |
| TC-02 | 手机号不足 11 位 | Toast "登录手机号格式不正确" |
| TC-03 | 密码为空 | Toast "请输入密码" |
| TC-04 | 密码少于 6 位 | Toast "请输入6-20位密码" |
| TC-05 | 未勾选协议 | Toast "请先阅读并同意..." |
| TC-06 | 点击清除按钮 | 输入框内容清空 |
| TC-07 | 点击协议文字 | 切换勾选状态 |
| TC-09 | 正确账号密码+勾选协议 | 跳转首页(ShellScaffold) |
注意一下,TC-08 原本计划测试点击《用户协议》跳转 WebView,但跳过了。原因很简单:WebView 页面会导致 flutter_driver 的 VM service 连接断开,这是目前已知的限制。
核心测试代码片段
挑两个代表性用例看看具体实现。
TC-06 验证清除按钮:
// TC-06: 清除按钮验证test('TC-06 手机号清除按钮 - 输入后点击清除,输入框变空', () async {await driver.tap(find.byValueKey('phone_input'));await driver.enterText('15899999999');final textBefore = await driver.getText(find.byValueKey('phone_input'));expect(textBefore, '15899999999');await driver.tap(find.byValueKey('phone_clear_btn'));final textAfter = await driver.getText(find.byValueKey('phone_input'));expect(textAfter, '');});
TC-09 完整走通登录流程:
// TC-09: 正常登录test('TC-09 正常登录 - 跳转到首页', () async {await driver.tap(find.byValueKey('phone_input'));await driver.enterText('15899999999');await driver.tap(find.byValueKey('password_input'));await driver.enterText('a123456');await driver.tap(find.byType('AppCheckbox'));await driver.tap(find.text('登录'));await driver.waitFor(find.byType('ShellScaffold'),timeout: const Duration(seconds: 10),);});
五、执行测试
运行命令(3 个参数缺一不可)
跑测试的时候,命令里这三个参数一个都不能少:
NO_PROXY=127.0.0.1,localhost flutter drive --target=main_driver.dart --driver=test/driver/login_test.dart --dart-define-from-file .env.development
| 参数 | 说明 |
|---|---|
--target=main_driver.dart | Driver 入口,这个值是固定的 |
--driver=test/driver/xxx.dart | 指定测试文件的路径 |
--dart-define-from-file | 必须提供,否则 API_HOST 为空会导致 App 崩溃 |
NO_PROXY=127.0.0.1,localhost | macOS 上如果开了袋里,必须加这个环境变量 |
测试结果
跑完以后,输出应该是这样的:
+1: TC-01 手机号为空 - 提示"请输入手机号"+2: TC-02 手机号格式不正确(少于11位)+3: TC-03 密码为空 - 提示"请输入密码"+4: TC-04 密码长度不足(少于6位)+5: TC-05 未勾选协议 - 提示"请先阅读并同意..."+6: TC-06 手机号清除按钮 - 输入后点击清除,输入框变空+7: TC-07 点击协议文字 - 可切换勾选状态+8: TC-09 正常登录 - 跳转到首页All tests passed!
特别说明一下 TC-09:这个测试不仅仅是 UI 层面的验证,它真实调用了后端接口,完整走了一遍完整的业务链路:
获取客户端列表 → 登录 → 获取店铺列表 → 跳转首页
能做到这步,说明这套方案在实际项目中已经可以投入使用了。
六、踩坑总结
调试过程中遇到的几个问题,整理成一张表,省得下次再踩一遍:
| 问题 | 原因 | 解决方案 |
|---|---|---|
Connection closed before full header | macOS 袋里拦截了本地连接 | 加 NO_PROXY=127.0.0.1,localhost |
Invalid argument (baseUrl): Must be a valid URL | 缺少 --dart-define-from-file | 补上环境变量参数 |
No root widget is attached | Driver 连接时 runApp() 还没跑完 | 加 waitUntilFirstFrameRasterized() |
Service connection disposed | WebView 页面导致 VM service 断开 | 跳过含 WebView 的测试用例 |
enter_text 无效果 | 没有先 tap 聚焦输入框 | 先 tap 再 enterText |
这些坑在文档里不一定写得那么清楚,但遇到的时候确实挺折腾人的。
七、总结
Flutter MCP 带来的变化,是把 AI 从"聊天助手"变成了"测试工程师"。它能看懂 Widget Tree,能操控 UI,能帮你写测试用例,还能自动跑测试。
开发阶段,不用反复手动填表单、点按钮;测试阶段,AI 可以自动分析页面结构、补全 Widget Key、生成测试用例,固化下来的测试命令可以直接接进 CI 流水线。
写完登录页之后,直接跟 AI 说一句"帮我测一下登录流程",就够了。
