前段时间我在研究 Flutter 面向 OpenHarmony 的跨端适配时,遇到一个很有意思的需求:把 AI 大模型的能力(准确说是 MCP 协议,Model Context Protocol)通过 Flutter 应用接入 OpenHarmony 设备。社区里的方案大多是 Android/iOS 的,OpenHarmony 这边的资料几乎是空白。当时我手里正好要做一个基于大模型交互的智能代理引擎,涉及工具调用、上下文管理、多轮对话,于是就把 mcp_dart 这个三方库完整对接了一次,跑通了 flutter_flutter 分支在 HarmonyOS NEXT/ohos 平台上的能力链路。
这篇文章我会把整个过程拆开讲:MCP 协议到底是什么、mcp_dart 的代码结构为什么适合 OpenHarmony、平台通道怎么做、智能代理引擎的 Tool 注册和调用流程怎么串,以及真机上最容易翻车的几个细节。内容偏实操,所有步骤都是我踩过坑之后整理出来的,适合正在做 Flutter for OpenHarmony 应用、或者想把大模型 Agent 能力塞进鸿蒙设备的朋友参考。
1. 为什么偏偏是“Flutter + OpenHarmony + MCP”这个组合
1.1 OpenHarmony 的 Flutter 生态并没有想象中那么“空白”
很多人一听到 OpenHarmony 适配 Flutter,第一反应是“三方库肯定一堆不能用”。这个判断对一半。OpenHarmony 的 Flutter 分支(社区常说的 flutter_flutter 或者厂商维护的 ohos 分支)确实和标准 Flutter SDK 有差异,但它保留了完整的 Dart 运行时、Flutter 引擎的渲染管线、Platform Channel 机制,这意味着:只要逻辑不依赖 Android/iOS 原生 SDK,绝大多数纯 Dart 三方库是零修改直接可用的。
mcp_dart 就属于这一类。它是一个纯 Dart 实现的 MCP 客户端/服务端库,底层走的是 WebSocket、HTTP、stdio 这类通用通道,不依赖任何 Android 系统 API,也没有 iOS 的 Framework 依赖。从理论上说,只要 OpenHarmony 的 Flutter 分支能跑 dart:io,这个库就能跑。我在实际验证中也确认了这一点,但有几个细节需要处理,后面专门讲。
1.2 MCP 在 AI 应用里的角色:不是模型本身,而是模型的“万能插头”
MCP(Model Context Protocol)经常被误解成“又一个 AI 接口封装”。其实它的定位很清晰:统一 AI 应用与外部工具、数据源之间的通信方式。类比一下,USB-C 接口统一了充电、数据传输、视频输出;MCP 就是 AI 能力的 USB-C,让大模型可以通过统一协议调用本地文件、数据库、计算器、硬件传感器、企业 API 等。
在 OpenHarmony 设备上做智能代理引擎,MCP 的价值尤其明显。OpenHarmony 设备形态杂——有轻量带屏设备、有标准带屏设备、有富媒体设备,它们的系统能力暴露方式不同。如果用传统方式,每接一个能力就要写一套原生适配;有了 MCP 层,AI 应用只需要理解一套工具描述协议,设备能力以 MCP Tool 的方式暴露出来,模型侧按需调用即可。
1.3 mcp_dart 在 OpenHarmony 上的定位分析
mcp_dart 是 MCP 官方 Dart SDK 的社区实现,它主要解决三件事:
- 协议编解码:把 MCP 的 JSON-RPC 2.0 消息格式化成标准二进制/文本帧,或者反向解析。
- 传输抽象:提供 Transport 接口,实现 stdio、SSE(Server-Sent Events)、WebSocket 等通道。
- 会话管理:维护 client/server 两端的 session,包括 initialize 握手、工具列表同步、消息分发。
这个分层设计对 OpenHarmony 移植非常有利。因为 OpenHarmony 的 Flutter 分支对 dart:io 的支持和标准 Dart 基本一致,mcp_dart 的传输层可以直接用 WebSocket 走 TCP,不需要动协议层。实际要做适配的关键点反而在 Flutter 层——如何把 Dart 侧收到的工具调用请求交给鸿蒙原生侧执行,这是 Platform Channel 要做的事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. MCP 协议与 mcp_dart:先搞清楚我们要适配的是什么
2.1 握手流程里的几个关键消息
MCP 的会话建立不是简单地“连上就行”,它有一个明确的初始化流程,mcp_dart 的 ClientSession 封装了这些逻辑。稳住这几个消息,后续接入就顺了:
initialize:客户端告诉服务端自己的协议版本、client 名称、能力(如是否支持工具调用)。initialized:客户端确认初始化完成,开始正常通信。tools/list:客户端向服务端请求可用的工具列表,返回结果是 JSON Schema 格式的工具描述。tools/call:客户端请求执行某个工具,传入参数由模型生成并校验。
在 OpenHarmony 场景里,tools/list 的内容其实是动态的:不同设备暴露的工具不同。比如带屏设备可能暴露“屏幕亮度调节”“音量控制”,传感器设备暴露“读取温度”“获取加速度”。实现时,这些工具列表必须由鸿蒙原生侧动态返回,不能写死在 Dart 层。
2.2 mcp_dart 的传输层选型:为什么我推荐 WebSocket
mcp_dart 支持多种传输方式,但 OpenHarmony 上我强烈建议优先走 WebSocket,理由有三:
- stdio 传输在 OpenHarmony 上不是首选,因为 Flutter 应用运行在沙箱进程里,你很难 spawn 一个外部 MCP server 进程并管理它的生命周期。鸿蒙的原子化服务能力模型和传统桌面不同,进程管理受限较多。
- SSE 传输可以,但不如 WebSocket 省心,SSE 是单向推送,MCP 请求响应是双向的,需要额外搭一条上行通道,代码复杂度更高。
- WebSocket 是双工通道,天然契合 JSON-RPC,请求和响应通过消息 ID 对应,时序问题最少。
实际项目中,WebSocket server 可以跑在设备本地的 loopback 端口,也可以跑在远端服务器。前期调试建议本地起服务,方便抓包。等代理引擎的逻辑稳定了,再往远端迁。
2.3 mcp_dart 的客户端创建逻辑
创建一个 MCP 客户端,核心代码如下,注意几个参数的含义:
dart复制import 'package:mcp_dart/mcp_dart.dart';
Future<ClientSession> createMcpSession({
required String wsUrl,
required String clientName,
required String clientVersion,
}) async {
// 1. 创建 WebSocket 传输
final transport = WebSocketTransport(
uri: Uri.parse(wsUrl),
protocols: ['mcp'], // MCP 协议子协议标识,服务端会据此识别
);
// 2. 初始化客户端
final client = McpClient(
transport: transport,
capabilities: ClientCapabilities(
tools: ToolCapabilities(
listChanged: true, // 支持工具列表动态变化
),
),
);
// 3. 握手
final session = await client.connect(
clientInfo: ImplementationInfo(
name: clientName,
version: clientVersion,
),
);
return session;
}
连接建立后,session.listTools() 会返回工具列表,session.callTool(toolName, arguments) 会触发一次工具调用。但在正式使用前,需要补一个异常处理:OpenHarmony 设备网络栈和桌面端有差异,WebSocket 握手机制可能因为 TLS 证书、代理设置等原因失败,所以超时时间建议给足,至少 10 秒以上。
3. 从 main.dart 到设备端:OpenHarmony 适配层的完整链路
3.1 Platform Channel 里到底该传什么
mcp_dart 跑起来了,但它只是 Dart 侧的一个会话对象。真正要让“模型理解设备能力、用户操作映射到设备功能”,必须打通和 OpenHarmony 原生侧的通道。Flutter 与 OpenHarmony 原生通信的方式和 Android 类似,也是 MethodChannel + EventChannel。
我定义的通道结构:
dart复制class McpOhosBridge {
static const _methodChannel = MethodChannel('mcp_dart/ohos_bridge');
static const _eventChannel = EventChannel('mcp_dart/ohos_events');
// Dart -> 鸿蒙原生:请求工具列表或执行工具
static Future<Map<String, dynamic>> invokeTool(
String toolName,
Map<String, dynamic> args,
) async {
try {
final result = await _methodChannel.invokeMapMethod<String, dynamic>(
'invokeTool',
{
'tool': toolName,
'arguments': args,
},
);
return result ?? {};
} on PlatformException catch (e) {
throw McpToolExecutionException(
toolName: toolName,
message: e.message ?? 'Tool execution failed',
);
}
}
// 鸿蒙原生 -> Dart:主动上报工具列表变化、异步事件
static Stream<dynamic> get eventStream => _eventChannel.receiveBroadcastStream();
}
这里有个设计要点:通道里走的是“工具调用请求”,而不是“工具执行结果”。也就是说,Dart 侧的智能代理引擎把模型生成的参数通过 MethodChannel 交给鸿蒙原生,原生侧真正去调用系统能力(比如调节音量、查日历、发通知),再把结果以结构化数据返回。这样模型的推理能力和设备的原子能力彻底解耦。
3.2 鸿蒙原生侧的执行器实现
OpenHarmony 侧,我用的是 Stage 模型下 ExtensionAbility 的能力。注册 Flutter 引擎时,植入自定义 MethodChannel 处理器。
typescript复制// MainAbility.ts (OpenHarmony / HarmonyOS NEXT)
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
export default class MainAbility extends UIAbility {
private mcpToolExecutor: McpToolExecutor;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
super.onCreate(want, launchParam);
this.mcpToolExecutor = new McpToolExecutor(this.context);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err, data) => {
if (err.code) {
console.error(`Failed to load content: ${err.message}`);
return;
}
});
// 拿到 Flutter 引擎注册通道
const flutterEngine = FlutterEngineManager.getInstance().getEngine('default_engine');
flutterEngine?.getMethodChannel('mcp_dart/ohos_bridge')?.setMethodCallHandler((call, result) => {
if (call.method === 'invokeTool') {
const { tool, arguments: args } = call.arguments as {
tool: string;
arguments: Record<string, Object>;
};
try {
const output = this.mcpToolExecutor.execute(tool, args);
result.success(output);
} catch (e) {
result.error('TOOL_EXEC_ERROR', (e as Error).message, null);
}
}
});
}
}
McpToolExecutor 是工具注册表,每个工具对应一个 handler。例如“获取设备信息”工具:
typescript复制// McpToolExecutor.ets
import { common } from '@kit.AbilityKit';
import { deviceInfo } from '@kit.BasicServicesKit';
export class McpToolExecutor {
private context: common.UIAbilityContext;
private toolRegistry: Map<string, (args: Record<string, Object>) => Object>;
constructor(context: common.UIAbilityContext) {
this.context = context;
this.toolRegistry = new Map();
// 注册内置工具
this.toolRegistry.set('getDeviceInfo', this.getDeviceInfo.bind(this));
this.toolRegistry.set('setBrightness', this.setBrightness.bind(this));
// ... 可根据业务动态注册
}
execute(toolName: string, args: Record<string, Object>): Object {
const handler = this.toolRegistry.get(toolName);
if (!handler) {
throw new Error(`Tool not found: ${toolName}`);
}
return handler(args);
}
private getDeviceInfo(args: Record<string, Object>): Object {
return {
deviceType: deviceInfo.deviceType,
hardwareModel: deviceInfo.hardwareModel,
osFullName: deviceInfo.oSFullName,
displayVersion: deviceInfo.displayVersion,
udid: deviceInfo.udid,
};
}
private setBrightness(args: Record<string, Object>): Object {
// 调用系统亮度接口,具体 API 按 SDK 版本调整
return { success: true, brightness: args['level'] };
}
}
工具注册表的设计是“可插拔”的,新增设备能力只需要在新模块里调用 register('xxx', handler),不需要改动引擎代码。这个对智能代理引擎的迭代很重要,因为你每轮调研都可能发现新设备能力可接入。
3.3 事件通道:异步推送设备的“主动性行为”
MethodChannel 是请求-响应模式,适合工具调用。但智能代理引擎还需要一种推送机制——比如用户按了物理按键、传感器触发异常、应用进入后台,这些事件需要主动通知 Dart 侧,让模型感知环境变化。这个用 EventChannel 实现。
鸿蒙原生侧推送事件:
typescript复制// 通过 FlutterEngine 的 EventChannel 发送事件
const eventSink = await FlutterEngineManager.getInstance()
.getEngine('default_engine')
?.createEventChannel('mcp_dart/ohos_events');
eventSink?.success({
type: 'sensor_triggered',
data: {
sensor: 'accelerometer',
x: 0.01,
y: 9.8,
z: 0.02,
},
});
Dart 侧订阅:
dart复制void listenDeviceEvents() {
McpOhosBridge.eventStream.listen((event) {
if (event is Map) {
_agentEngine.handleDeviceEvent(event);
}
}, onError: (Object error) {
// 事件流异常时建议重建监听
});
}
这里容易踩一个坑:EventChannel 的订阅在 Flutter 页面销毁后会被系统回收。如果你的代理引擎运行在后台(比如语音助手场景),需要在引擎层保持订阅引用,最好的方式是在 MainAbility 或容器级 Component 级别建立订阅,而不是在某个页面的 State 里。
4. 把工具、模型和 MCP 串起来:一个最小可跑的智能代理引擎
4.1 Agent 循环:模型推理与工具执行的分工
智能代理引擎的核心是一个循环:
- 把用户输入 + 系统提示词 + 工具列表描述发给大模型。
- 模型返回两种结果之一:要么是最终回复文本,要么是一个工具调用请求(tool_call)。
- 如果是工具调用,Agent 引擎通过 mcp_dart 的
callTool调用 MCP 工具,把结果拼到对话上下文里,继续走第 1 步。 - 直到模型输出不再需要调用工具,把最终回复返回给用户。
mcp_dart 在这个循环里负责的是“MCP 客户端的部分”。模型本身可能有自己的 SDK(比如 OpenAI SDK、DashScope SDK),Agent 引擎负责连接两者。
伪代码:
dart复制class AgentEngine {
final McpClientSession mcpSession;
final LlmClient llm;
Future<String> chat(String userInput) async {
List<Map<String, dynamic>> messages = [
{'role': 'system', 'content': _systemPrompt},
{'role': 'user', 'content': userInput},
];
// 获取 MCP 工具列表
final tools = await mcpSession.listTools();
for (var round = 0; round < _maxIterations; round++) {
final llmResponse = await llm.chat(
messages: messages,
tools: tools.map((t) => t.toJsonSchema()).toList(),
);
if (llmResponse.hasToolCalls) {
for (final toolCall in llmResponse.toolCalls) {
// TODO: 工具名和参数要校验
final result = await mcpSession.callTool(
toolCall.name,
toolCall.arguments,
);
messages.add({
'role': 'tool',
'tool_call_id': toolCall.id,
'content': jsonEncode(result),
});
}
} else {
return llmResponse.content;
}
}
throw AgentMaxIterationException('Agent iterations exceeded');
}
}
这个循环看着简单,实际运行中最容易出问题的点在于工具描述的数据格式。MCP tools/list 返回的 JSON Schema 格式和大模型 API 的 tools 参数格式非常相似,但细节不同(比如部分模型要求 strict: true,部分要求工具描述必须控制在 256 字符内)。建议在 Dart 层写一个 adapter,把 MCP 工具协议格式转成模型 API 的 tool spec 格式。
4.2 MCP Server 和工具发现的联动:让模型感知“设备上有哪些能力”
OpenHarmony 设备不是跑大模型的主力硬件(算力受限),所以标准的部署架构是:设备端跑 Flutter 应用,远端跑大模型,MCP Server 可以放设备端也可以放云端。
我推荐前期把 MCP Server 放在远端,要接入的设备能力通过鸿蒙原生工具执行器暴露。等需要低延迟场景(比如离线命令控制)时,再把轻量 MCP Server 集成进 OpenHarmony 应用内,通过 loopback WebSocket 和 Flutter 通信。
tools/list 必须动态生成。不能只注册“设备信息”“调亮度”这种系统工具,还要结合应用业务注入业务工具。比如做一个“智能家居管家”应用,就要注入“查家里设备状态”“控制空调温度”“打开窗帘”这些工具,这些工具的数据源可能来自鸿蒙的分布式软总线,也可能来自云端 API。动态生成的代码类似:
dart复制List<ToolSpec> buildTools() {
return [
ToolSpec(
name: 'query_device_status',
description: '查询智能家居设备实时状态',
inputSchema: {
'type': 'object',
'properties': {
'deviceId': {'type': 'string', 'description': '设备唯一标识'},
},
'required': ['deviceId'],
},
handler: (args) => _homeApi.queryStatus(args['deviceId']),
),
// ...
];
}
模型只有看到明确的工具描述,才知道自己“能做什么、什么时候该调用”。工具描述写得好不好,直接决定 Agent 的效果。我见过太多项目卡在“模型就是不调用工具”或者“工具参数乱传”,多数是因为描述不够具体、参数约束不够严格。
4.3 工具调用的权限与安全护栏
接大模型应用时,工具权限怎么控制,这是绕不开的问题。如果模型被越权调用工具,后果很严重。我在 OpenHarmony 场景里做了三层防护:
- 工具白名单:鸿蒙原生侧只允许执行明确注册过的工具,未注册的调用直接拒绝。
- 参数校验层:Dart 侧和鸿蒙侧都做 JSON Schema 校验,防止模型生成畸形参数击穿底层 API。
- 高危操作二次确认:比如“发送短信”“删除文件”“调节系统设置”这类操作,工具返回一个
confirmationRequired标记,Agent 引擎收到后先问用户“确认执行吗”,用户确认后才真正调用。
dart复制if (toolSpec.requiresConfirmation) {
final confirmed = await _showConfirmationDialog(toolSpec, args);
if (!confirmed) {
return {
'status': 'cancelled',
'message': 'User denied the tool execution.',
};
}
}
这里补充一个实践心得:把“工具执行的合法性判断”放在鸿蒙原生侧比放在 Dart 侧更安全。因为 Dart 侧的代码可以通过热更新替换,而鸿蒙原生侧的可信度更高。我在 McpToolExecutor 里把需要二次确认的工具单独标记,execute() 方法先走 preCheck,再走真正执行逻辑。
5. 真机验证与避坑清单:这几个细节我建议你提前绕开
5.1 Flutter 引擎版本与 mcp_dart 的 dart:io 兼容性
OpenHarmony 的 Flutter 分支对 dart:io 的支持不完全等同于标准 Flutter。具体来说,SecureSocket、HttpClient 在 OpenHarmony 上的实现有差异,尤其是自签名证书场景。mcp_dart 的 WebSocket Transport 底层依赖 WebSocket.connect(),如果 MCP Server 的 TLS 证书未受信任,连接会直接失败。
我的解决办法:初期调试用明文 WebSocket(ws://),跑通后再换 wss:// 并在鸿蒙侧信任对应 CA。OpenHarmony 的网络安全配置和 Android 的 networkSecurityConfig 类似,但配置入口不同,需要熟悉鸿蒙的资源文件和网络安全策略。
5.2 EventChannel 的线程模型问题
Flutter 的 EventChannel 回调线程默认是平台主线程,在 OpenHarmony 上也一样。如果你的工具执行是耗时任务(比如调用云 API、查询数据库),不要在收到 MethodChannel 调用后同步执行,否则会卡 UI。正确做法是:MethodChannel 调用先回一个“任务已接收”,异步任务完成后再通过另一个通道回传结果,或者采用 EventChannel 推送结果。
我采用的模式:
typescript复制// 鸿蒙侧异步执行
flutterEngine?.getMethodChannel('mcp_dart/ohos_bridge')?.setMethodCallHandler((call, result) => {
if (call.method === 'invokeTool') {
result.success({'status': 'accepted', 'taskId': task.id});
// 异步执行
this.executeAsync(task).then((out) => {
eventSink?.success({'taskId': task.id, 'status': 'done', 'result': out});
});
}
});
Dart 侧维护一个 Completer 映射,收到 taskId 时挂起任务,收到 done 事件时 complete。效果和原生异步完全一致。
5.3 mcp_dart 的三方依赖冲突:最常见但最容易被忽视
mcp_dart 依赖了几个常用 Dart 包:web_socket_channel、json_annotation、collection、meta 等。在 OpenHarmony 的 Flutter 工程里接入时,如果你之前引入了其他版本的低级库,pub solve 时很容易冲突。尤其是 web_socket_channel,版本不同 API 差异很大,mcp_dart 要求 2.4.x 以上,而一些老的 flutter 插件锁定了 2.2.x。
如果遇到冲突,不要硬改 mcp_dart 的依赖约束,优先降级你的业务插件版本。因为 mcp_dart 是持续维护的,跟着它的依赖版本走,后续升级省心。
5.4 工具列表过长的 Token 管理
智能代理引擎每次请求都要携带完整工具列表,如果工具数量上去了(比如 30+ 个工具),工具描述的 token 开销会很大。模型输入端压力和响应延迟都会上升。
我做了一个工具分片机制:根据用户意图先粗筛工具子集,只把可能相关的工具描述发给模型。
dart复制List<ToolSpec> filterToolsByIntent(String userInput, List<ToolSpec> allTools) {
// 通过关键词匹配或小模型意图分类,返回子集
// 兜底策略:至少返回 5 个核心工具
}
这个在低算力设备上收益明显。毕竟 OpenHarmony 很多设备的内存和网络带宽有限,token 越少,首字延迟越低。
5.5 真机调试时最容易忽略的权限声明
OpenHarmony 应用如果要访问网络(连接远端 MCP Server),必须在 module.json5 里声明 ohos.permission.INTERNET。如果还要读取设备位置、使用摄像头,权限声明一个都不能少。这个和 Android 的 AndroidManifest 权限声明类似,但位置和格式不同,经常有从 Android 转过来的同学漏掉。
另外,如果你在鸿蒙设备上通过 127.0.0.1 连接本地 MCP Server,默认是允许的;但如果你想连接同一局域网的其他设备上的 MCP Server,可能需要额外的“局域网通信”权限。具体哪个版本开始有这个限制,不同 SDK 版本有差异,实测下来还是一个一个试最靠谱,我打印了权限错误日志后逐条补声明。
6. 从“能跑”到“好用”:mcp_dart + OpenHarmony 的演进方向
6.1 多 Agent 协作:一个 MCP Server 服务多个 Flutter 页面
我前面实现的是单 Agent 引擎,但如果应用内多个页面(首页语音助手、设置页控制面板、隐私页查询权限列表)都需要访问 MCP 工具,不应该每个页面各建一个 MCP session。我改成单例模式,整个应用共享一个 AgentEngine,各页面通过消息总线传递请求。这样每个工具调用状态统一管理,也方便做请求计数和限流。
6.2 对话上下文的持久化:mcp_dart + OpenHarmony KV Store
OpenHarmony 提供分布式 KV Store(键值型数据库),把 Agent 的多轮对话上下文存进去之后,应用重启可以恢复会话。而且 KV Store 本身支持分布式同步,同一个用户在不同设备上(手机、平板、智能屏)可以共享会话历史。这套组合比传统的关系型数据库更适合智能代理:数据结构简单,读写快,天然支持设备间迁移。
我在项目里把上下文的序列化格式定为 JSON,每个 session 对应一个 key,内容是 messages 数组。注意控制单条消息的大小,模型上下文窗口和 KV Store 单条目大小都有限制,建议超过阈值时做摘要压缩。
6.3 离线场景:MCP + 端侧小模型的方案预研
OpenHarmony 设备大量存在弱网甚至离线场景,如果完全依赖云端大模型,Agent 就成了摆设。我预研过两个方向:
- 端侧小模型做意图识别:先跑一个轻量分类模型,判断用户请求是否需要调用工具、调用什么工具,再把具体参数生成交给云端模型。这样即使断网,也能执行部分预设工具(比如本地控制类)。
- MCP Server 端集成轻量推理引擎:把一个小模型直接包进 OpenHarmony 应用,通过 MCP 的
sampling能力暴露给客户端。这块 mcp_dart 还在开发中,但方向明确。
我个人的结论是:端侧小模型负责“决策”,云端大模型负责“生成”,两者通过 MCP 协议衔接,是 OpenHarmony 设备上 Agent 最务实的架构。
6.4 分布式软总线的接入:让工具调用跨设备流转
OpenHarmony 的杀手级能力是分布式软总线——多设备可以组成一个超级终端。在 MCP 的语境里,这意味着:A 设备的 Agent 可以调用 B 设备的工具。
比如你在手机上说“把客厅投影仪亮度调低”,手机上的 Agent 通过 MCP 调用“调亮度”工具,但真正的执行端是客厅的投影仪。这个场景的实现路径是:手机 MCP Client -> 远端 MCP Server -> 设备通信中间层 -> 投影仪上的工具执行器。工具链路上多了一跳,但协议不变。
mcp_dart 的 client/server 模型完全支持这种拓扑,前提是每个设备的工具注册中心都上报自己设备的唯一标识(UDID),这样 Agent 在生成工具调用参数时才能指定目标设备。我在 tools/list 返回的 tool 描述里增加了一个 x_device_target 自定义字段,模型看到这个字段就知道该工具可以指定设备。
7. 最后一公里:我在落地过程中总结的三条经验
第一条,工具描述永远比模型聪明。不要指望模型“理解”一个含糊的工具名,工具描述一定要写清楚:“这个工具是什么”“什么时候调用”“参数怎么取值”。我见过太多 Agent 项目死在工具描述上——模型完全不知道什么时候该调工具。mcp_dart 的 inputSchema 支持 JSON Schema 全特性,enum、pattern、description 这些字段都尽量用满。
第二条,测试工具调用要打日志。智能代理引擎的调试链路很长:用户输入 -> 模型推理 -> 工具调用 -> 结果回填 -> 二次推理。缺一个日志环节就不知道在哪里断了。我在 Dart 侧搭了一个轻量日志系统,每次 callTool 都记录工具名、参数、耗时、返回结果摘要;鸿蒙原生侧同样输出执行日志。两端日志关联起来,基本上问题都能快速定位。
第三条,先跑通最小闭环,再考虑花活。接入 mcp_dart 的最优路径是:先做“一个工具 + 一次调用”的最小闭环,验证 Flutter 引擎、Platform Channel、鸿蒙原生执行器、模型 API 整条链路通了,再加工具、加 Agent 能力、加分布式支持。一上来就做复杂架构,调试时五六个环节同时出问题,人会疯掉。
我现在在 OpenHarmony 设备上跑的这个智能代理引擎,MCP 侧已经稳定,Agent 循环也跑通了。如果你也在做相关方向,卡在哪个环节随时可以交流。工具调用、平台通道、协议适配这些细节,每一个都值得先踩一遍再说。
