
写这篇文章的起因是我最近在一个鸿蒙设备接入项目里被MCP协议折腾了整整一周。网上关于MCP的资料很多但大部分都在讲“这是什么”极少有人讲“在鸿蒙上到底怎么把流程跑通”。我梳理了一下自己的实战记录把从MCP原理、服务端搭建到鸿蒙设备作为客户端接入、真实调用工具的整个链路都写清楚包括我踩过的坑和最后沉淀下来的方案希望能给正在做MCP协议与鸿蒙设备接入开发的朋友省点时间。1. MCP协议到底是什么为什么鸿蒙开发需要它1.1 MCP协议的核心原理与定位MCP全称Model Context Protocol是2024年底开始快速流行的一个开放协议。它的定位非常直接统一AI模型与外部工具、数据源之间的通信方式。你可以把MCP理解为AI世界的“USB-C接口”——在此之前模型想调用一个天气接口、查一次数据库、操作一下设备每个都要单独写一套集成逻辑不同开发框架之间还不能复用。MCP出现之后模型侧和工具侧只要各自实现一遍协议就能互相发现、互相调用。从技术架构上看MCP采用Client-Server模型MCP Client运行在AI应用或智能体一侧负责与模型交互MCP Server运行在工具提供方一侧把工具和数据源包装成标准接口。两者之间通过JSON-RPC 2.0格式的消息通信消息类型分三类请求Request发起方期待响应比如Client请求Server执行某个工具。响应Response对请求的返回值。通知Notification单向消息不需要回复比如Server通知Client“工具列表已更新”。相比传统REST APIMCP最大区别是“能力发现与动态协商”。传统API是写死接口文档调用方按文档拼请求MCP则让Client在运行时先问Server“你有什么工具、参数长什么样”然后动态组装调用模型甚至可以通过自然语言完成工具选择。这种设计对鸿蒙这类多设备、多形态、能力差异极大的生态尤其有价值。1.2 为什么鸿蒙设备接入MCP是趋势鸿蒙系统从手机、平板延伸到车机、智能家居、工业设备每类设备都有自己的能力——手机有摄像头、平板有手写笔、智能音箱有麦克风。如果AI应用想“指挥”这些设备干活传统做法是每接入一个设备写一个插件。但设备能力成千上万写不过来。MCP提供了一种标准化思路设备把能力暴露成MCP ServerAI应用统一通过MCP Client去发现和调用。这样设备侧开发一次所有支持MCP的AI应用都能用模型侧也不需要针对每类设备做特殊适配。我在鸿蒙项目里实践下来这套模式特别适合做“AI助手 原生能力控制”“智能家居中央控制”“设备间协同任务”这类场景。2. 全链路方案设计——从服务端到鸿蒙客户端的架构拆解2.1 整体架构与接入方式选型做鸿蒙设备接入MCP第一步不是写代码而是确定一条完整的链路。我的方案分为三层MCP Server层用Python FastMCP框架快速实现负责暴露工具、处理JSON-RPC请求。传输层采用HTTPSSE方式部署在局域网或云端。鸿蒙侧没有类似Node.js的stdio运行环境不能用本地子进程方式连接MCP Server所以HTTPSSE或WebSocket是更实际的选择。鸿蒙Client层基于ArkTS实现封装MCP协议握手、工具发现、工具调用最终接入业务界面。这里有一个很重要的问题为什么不让鸿蒙设备直接做MCP Server我的理由是大部分鸿蒙设备处于NAT或局域网内外部AI应用无法主动建连。相比之下设备作为Client主动连接Server更稳定。但如果你做的是开发者工具类应用设备侧做Server也不是不行后面会单独聊。2.2 传输层选型对比鸿蒙环境下的MCP传输方案我实际对比了三种传输方式优点缺点适用场景HTTP请求/响应实现简单鸿蒙原生网络API直接支持无法实现服务端主动推送工具调用频率低、无需订阅通知HTTPSSE流式支持服务端推送符合MCP标准传输要求需要管理长连接生命周期工具状态变化通知、流式结果返回WebSocket双向实时通信鸿蒙WebSocket API成熟需要自行设计overlay协议MCP标准对其支持仍在完善中高频双向通信、设备控制台、实时日志我最终选择了HTTPSSE既符合MCP官方标准鸿蒙侧实现成本也可以接受。如果你的场景只是“点一个按钮调用一个工具”纯HTTP就行不用给自己加复杂度。2.3 通信协议细节拆解成功接人的关键MCP的通信看似只是JSON-RPC但真正让一个Client接入成功有几个协议细节必须吃透。会话初始化InitializeClient连接后第一件事就是发送initialize请求核心字段是protocolVersion、clientCapabilities和clientInfo。Server会返回protocolVersion协商结果。这里容易踩坑的坑是协议版本建议拉高到最新如2025年发布的多个版本两边版本不匹配会直接握手失败。我在鸿蒙上调试时就反复遇到版本不匹配后来把Server端的SDK升级到最新版问题才消失。能力协商Capabilities Negotiation握手成功后的第二件事是发送notifications/initialized通知并设置好tools、resources等能力。这一步要多说一句客户端向服务端声明自己支持的能力服务端会据此决定返回哪些工具。如果Client没有声明某个capabilityServer可能不会返回对应工具。工具发现tools/listClient发送tools/list请求Server返回工具名称、描述、JSON Schema的输入参数定义。这个接口是动态的——你在Server端新增一个工具客户端不需要改代码重新拉一次列表就有了。这也是MCP相比传统API最舒服的地方。工具调用tools/callClient发送tools/call请求带上工具名和参数Server执行后返回结构化结果。注意result是JSON格式可以包含多段内容比如文本加图片的混合结果。下面是一个standard的tools/call请求示例{ jsonrpc: 2.0, id: 2, method: tools/call, params: { name: get_device_status, arguments: { device_id: living_room_light_01 } } }响应里最重要的content字段它里面的schema是协议定义的ContentBlock结构——可以是文本块也可以是图片块、资源链接块。HUAWEI这边对异常情况处理比较严格如果你的参数类型跟工具定义的JSON Schema不符Server会返回一个JSON-RPC错误而不是尝试自我纠错。所以写客户端时参数类型一定要严格校验一遍再发出。3. 鸿蒙设备接入实操从零写一个MCP Client这一节是我这篇文章的核心我会把完整实现流程写出来。基于DevEco Studio 5.0、API 12以上、ArkTS语言。3.1 环境准备与工程创建开发前你要先确认三件事DevEco Studio版本建议使用5.0及以上API 12以上老版本对ArkTS并发和多线程支持较弱。真机或模拟器MCP调用涉及网络长连接建议直接用真机调试模拟器网络模式有时会有额外干扰。MCP Server环境Python 3.10安装fastmcp和uvicorn。工程创建后先在module.json5里申请网络权限{ module: { name: entry, requestPermissions: [ { name: ohos.permission.INTERNET } ] } }顺便说一句鸿蒙应用若只是访问HTTP明文地址不需要额外申请明文流量权限不像Android那么严格但如果访问的是IP加端口需要在“网络安全配置文件”中设置信任的域名或IP这个很容易被忽略。3.2 核心代码实现MCP Client封装我在鸿蒙端用ArkTS写了一个精简但完整的MCP Client。核心思路是用ohos.net.http的HttpClient发送HTTP请求同时用WebSocket模块接收SSE事件流。为了简化先把SSE解析器封装好再写MCP会话管理器。第一步封装HTTPSSE传输层import { http } from kit.NetworkKit; import { webSocket } from kit.NetworkKit; export class Transport { private serverUrl: string ; private ws!: webSocket.WebSocket; private messageCallback: (data: string) void () {}; constructor(url: string) { this.serverUrl url; } // 建立WebSocket连接以接收SSE流 async connect(): Promisevoid { await this.setupSSE(); } // 发送JSON-RPC请求HTTP POST async sendRequest(request: object): Promiseobject { const httpRequest http.createHttp(); const response await httpRequest.request( this.serverUrl, { method: http.RequestMethod.POST, header: { Content-Type: application/json, Accept: application/json, text/event-stream }, extraData: JSON.stringify(request), connectTimeout: 10000, readTimeout: 30000 } ); if (response.responseCode ! 200) { throw new Error(HTTP error: ${response.responseCode}); } const data JSON.parse(response.result as string); return data; } // 处理服务端推送的SSE消息 private handleSSEMessage(data: string): void { const lines data.split(\n); for (const line of lines) { if (line.startsWith(data:)) { const payload line.substring(5).trim(); if (payload [DONE]) continue; this.messageCallback(payload); } } } onMessage(callback: (data: string) void): void { this.messageCallback callback; } }注意HTTP长连接在鸿蒙上有个小陷阱——http.RequestMethod.POST请求设置了readTimeout后如果SSE流长时间没有新数据客户端会先超时断开。我用的是长轮询的应对方式每次POST请求返回后客户端立刻发起下一次请求模拟流式接收效果必要时才升级到WebSocket。第二步MCP会话管理器下面这个类是MCP协议的核心负责initialize握手、tools/list发现、tools/call调用import { Transport } from ./Transport; export class MCPClient { private transport: Transport; private requestId: number 0; private initialized: boolean false; private tools: Tool[] []; private pendingRequests: Mapnumber, (value: any) void new Map(); constructor(serverUrl: string) { this.transport new Transport(serverUrl); this.transport.onMessage((data) this.handleServerPush(data)); } // MCP握手流程 async initialize(): Promisevoid { const response await this.transport.sendRequest({ jsonrpc: 2.0, id: this.nextId(), method: initialize, params: { protocolVersion: 2025-03-26, capabilities: { tools: {} }, clientInfo: { name: harmony-mcp-client, version: 1.0.0 } } }); if (response.result) { // 协商版本以服务端返回的版本为准 const negotiatedVersion response.result.protocolVersion; // 发送初始化完成通知 await this.transport.sendRequest({ jsonrpc: 2.0, method: notifications/initialized, params: {} }); this.initialized true; console.info(MCP initialized, negotiated version: ${negotiatedVersion}); } } // 获取服务端所有可用工具 async listTools(): PromiseTool[] { if (!this.initialized) { throw new Error(Client not initialized); } const response await this.transport.sendRequest({ jsonrpc: 2.0, id: this.nextId(), method: tools/list, params: {} }); this.tools response.result.tools; return this.tools; } // 调用指定工具 async callTool(name: string, args: object): Promiseobject { if (!this.initialized) { throw new Error(Client not initialized); } const response await this.transport.sendRequest({ jsonrpc: 2.0, id: this.nextId(), method: tools/call, params: { name: name, arguments: args } }); return response.result; } private handleServerPush(data: string): void { // 处理服务端主动推送的消息 try { const parsed JSON.parse(data); if (parsed.method notifications/tools/list_changed) { this.refreshTools(); } } catch (e) { console.error(Failed to parse server push message); } } private async refreshTools(): Promisevoid { try { await this.listTools(); console.info(Tools refreshed after server push); } catch (e) { console.error(Failed to refresh tools); } } private nextId(): number { return this.requestId; } }这个类是通用的不绑定具体业务。后续不管对接多简单的工具还是多复杂的Agent逻辑底层通信都通过它完成。第三步在业务页面中调用写一个简单的页面加载完成后连接MCP Server获取工具列表点击按钮触发工具调用Entry Component struct MCPDemoPage { State tools: string[] []; State status: string 未连接; private mcpClient: MCPClient | null null; private serverUrl: string http://192.168.1.100:8000; aboutToAppear() { this.connectServer(); } private async connectServer() { this.status 连接中...; try { this.mcpClient new MCPClient(this.serverUrl); await this.mcpClient.initialize(); const toolList await this.mcpClient.listTools(); this.tools toolList.map(t t.name); this.status 已连接; } catch (e) { this.status 连接失败: (e as Error).message; } } build() { Column({ space: 16 }) { Text(this.status) .fontSize(18) .fontWeight(FontWeight.Bold) List() { ForEach(this.tools, (tool: string) { ListItem() { Row({ space: 12 }) { Text(tool) .fontSize(16) .layoutWeight(1) Button(调用) .onClick(() this.callTool(tool)) } .padding(16) .backgroundColor(#f0f0f0) .borderRadius(8) } }, (tool: string) tool) } .layoutWeight(1) } .padding(16) } private async callTool(toolName: string) { if (!this.mcpClient) return; try { const result await this.mcpClient.callTool(toolName, { // 根据实际工具定义调整参数 query: 今天天气如何 }); AlertDialog.show({ message: JSON.stringify(result), confirm: { value: OK } }); } catch (e) { AlertDialog.show({ message: 调用失败: (e as Error).message, confirm: { value: OK } }); } } }连接失败的情况不要慌90%是网络权限或IP地址问题后面会详细说排查方法。3.3 完整示例让鸿蒙App调用一个天气查询工具为了让上面代码跑起来我写一个最简单的MCP Server暴露一个返回模拟天气数据的工具。Server侧用FastMCP几行代码搞定from fastmcp import FastMCP import random mcp FastMCP(WeatherServer) mcp.tool() def get_weather(city: str, date: str today) - str: 获取指定城市的天气情况 temperature random.randint(-5, 35) return f{city} {date} 天气晴温度{temperature}℃ if __name__ __main__: mcp.run(transporthttp)启动后Server默认监听在8000端口支持/mcp路径的POST请求和SSE连接。在鸿蒙客户端里把serverUrl设置为http://你的电脑IP:8000/mcp就能走通全链路。额外补充一点FastMCP的HTTP模式下默认暴露两个端点/sse和/mcpstdin/HTTP/WebSocket等模式可切换。如果你使用transporthttp它同时支持HTTP POST和SSE GET。我建议Client直接POST到/mcp同时通过WebSocket或轮询接收服务端消息。对于只需要工具调用的场景用GET /sse监听服务端主动更新就够用了。3.4 编译打包与真机调试经验这块坑最多我踩了大概半天。总结下来四个关键点网络权限即使你只在模拟器上调试也要在module.json5里加上INTERNET权限否则报“Network request failed”时日志里根本看不出是权限问题。明文HTTP限制鸿蒙默认对明文HTTP有安全限制。如果有条件给Server配HTTPS证书没条件的话在network_security_config.json里临时放开对某个IP的明文访问。但注意版本发布前务必收紧。超时设置如果是首次启动连接建议在页面加载时异步初始化MCP Client不要阻塞主线程。另外把HTTP的connectTimeout和readTimeout调到至少15秒以上MCP Server冷启动往往会慢。日志排查不要光看应用日志抓取hilog时加上网络过滤关键词比如hilog -t NetworkKit | grep ERROR能快速定位是TCP连接失败、TLS握手失败还是HTTP响应超时。4. 不止于Client鸿蒙设备作为MCP Server的探索很多人觉得鸿蒙设备只能做MCP Client这个理解其实窄了。现实中很多场景恰恰需要设备侧反向暴露能力。我就遇到过两个项目一个是家居中控屏想向AI助手暴露“开关灯”“调色温”等能力另一个是工业触摸屏需要被车间管理系统实时读取运行参数。这两种都非常适合让设备本身充当MCP Server。4.1 设备侧做Server的三种方式方式一轻量HTTP Server形式。在鸿蒙App里内嵌一个轻量HTTP服务模块接收MCP JSON-RPC请求执行对应能力。优点是通用性好云端或其他设备可以直接连接缺点是外部网络访问内网设备需要额外做端口映射或内网穿透。方式二本地IPC形式。设备能力以系统服务方式注册MCP Server进程通过IPC与该系统服务通信。这类“服务发现方法调用”的架构在鸿蒙分布式场景中很适合。设备能力通过系统服务接口暴露MCP Server包装后对外提供。但这种方式对系统裁剪和权限管理要求很高不是所有设备都预置了可编程的服务管理框架。方式三BLE/局域网自发现形式。设备通过BLE广播MCP Server地址附近的手机或电脑自动发现并连接。适合短距离、低功耗场景。鸿蒙的分布式软总线能力正好可以在这层做文章——设备通过软总线组网应用层再用MCP做AI能力互通。4.2 轻量MCU设备如何实现这个其实要分情况。对于内存小于256KB的MCU直接跑MCP不太现实——MCP的JSON-RPC解析和Schema校验对资源要求不低。我的建议是MCU通过预定义协议把数据上报给一个网关比如鸿蒙开发板网关侧用MCP Server把数据包装成标准工具。真正的“端侧跑MCP”需要设备至少具备TCP/IP协议栈、可运行轻量JSON库很多带RTOS的设备可以做到。如果要在MCU侧硬跑可以考虑降级方案只实现一个极简子集比如支持initialize和tools/call两个核心方法工具描述用预定义静态JSON字符串不动态加载Schema。这样至少能跑通“收到命令—执行—回传结果”的链路。4.3 设备-云端-设备闭环场景最后分享一个我觉得很典型的设计。以智能家居为例家里的灯光、空调等设备通过MQTT或BLE接入一个鸿蒙智能网关。网关运行MCP Server把“打开灯”“调温度”“查询能耗”暴露成tools。用户手机上的AI助手运行MCP Client向网关发起调用完成一句话控制。更进一步你还可以在云端再搭一个MCP Server把网关的Server作为自己的工具之一这样AI Agent就能在云侧编排多个家庭的设备。整体链路就变成AI应用 → 云端MCP Server → 网关MCP Server → 鸿蒙设备能力。每一层都标准、可替换这是MCP协议最值得投入的理由。5. 常见问题与排查技巧实录这部分是从我实际调试过程中整理的每一条都真实踩过或验证过。5.1 连接与握手失败现象原因解决方案initialize请求无响应Server未启动或端口错误用浏览器访问http://IP:8000/mcp确认服务可达再排查鸿蒙端URL握手后出现protocol version错误MCP协议版本不匹配将Server端SDK升级到最新版或把Client的protocolVersion改为服务端支持的版本initialized通知后连接被断开Server要求先完成能力协商检查Client是否发送了tools capability声明服务端要求严格时必须补上5.2 JSON-RPC消息格式错误MCP对消息格式要求很严我遇到过两个高频问题id字段重复或缺失MCP要求请求id必须唯一响应必须带对应id。我最初用固定id导致第二笔请求直接失败。解决方法就是用一个自增计数器。content字段结构不对tools/call的返回值是结构化内容块不是裸字符串。比如返回“今天天气晴”也必须包在content数组里。否则鸿蒙端解析会拿到一个意外结构只能在日志里排查。5.3 鸿蒙网络权限与SSL证书问题如果你用的是自签名的HTTPS Server鸿蒙默认会拒绝TLS连接。最简单的开发阶段做法是给Server配置一个正规证书比如用Lets Encrypt或者临时在鸿蒙工程里配置豁免域名。但注意这只是开发期手段生产环境无论怎么都应该用受信证书。另外模拟器和真机对网络环境的处理有差别。我遇到过模拟器能联网、真机连不上的情况原因是模拟器默认走宿主机网络真机则需要跟Server在同一局域网或者做端口转发。5.4 打包hap后无法访问网络这个问题最隐蔽。打包成hap并安装到真机后网络权限、配置文件都检查过仍然调用失败。最后发现原因是serverUrl写死成了localhost或127.0.0.1——在模拟器里这样能通但真机上这两个地址指向设备自己根本找不到局域网里的MCP Server。我的经验是所有网络地址全部通过配置文件或云端下发不要写死在代码里。另外如果你在真机上调试时Server和手机不在同一网段也会直接连接失败。先把两端都接到同一个路由器下确认能ping通再往下查。6. 我在实际项目中的最终体会整套链路跑通后我最大的感受是MCP协议本身不难难的是把各个端的环境、权限、传输细节都照顾到。鸿蒙生态因为还处于快速迭代期官方对MCP这类外部协议的支持还不像Web生态那么顺手但它的网络API、并发模型和ArkTS类型系统完全足够支撑一个完整的MCP客户端。如果接下来你要上手做类似项目我建议按这个顺序推进先写一个Python的最小MCP Server并手动用curl验证通信再在鸿蒙工程里逐步实现握手、listTools、callTool每一步确认通过再进入下一步。不要一上来就追求完整的Agent框架——MCP的价值恰恰在于“把接口做标准”而标准的建设必须一个字节一个字节地验证。最后分享一个我压箱底的小技巧鸿蒙端调试MCP时把Server地址做成可动态配置的并在页面上留一个“重新连接”和“刷新工具”按钮。因为MCP Server的工具列表可以动态变化而鸿蒙App一旦初始化后不会自动感知这种变化。有了这两个按钮你在调业务逻辑时不用反复重启App开发效率会高很多。这套经验在我近期的两个鸿蒙AI项目里都派上了大用场。