1. 从一个"反直觉"的痛点说起:为什么我要让 AI 去读功耗计
做 IoT 硬件开发的朋友大概率都经历过这种场景:板子跑起来了,功能也正常,但续航就是不对劲。你怀疑是某个外设在偷偷耗电,于是搬出功耗计,盯着屏幕上跳动的电流曲线看半天,手动记录、手动对比、手动分析。整个过程枯燥、重复,而且一旦测试用例多了,人眼根本看不过来。
我手上有一台IoT Power,就是那种能实时采集电压、电流、功率的小型功耗分析设备,平时用来测低功耗设备的休眠电流、峰值电流、平均功耗。它本身挺好用,配套软件也能出曲线图。但问题在于——每次我想让 AI 帮我分析数据,都得自己先导出 CSV,再复制粘贴给模型,来回折腾。AI 明明有很强的数据分析能力,却因为拿不到实时数据而只能"隔岸观火"。
于是就有了这个项目:给 IoT Power 写一个 MCP 服务端,让 AI 能够自己"看"功耗计的数据。
这里要先解释一下MCP。MCP 全称 Model Context Protocol,是一套让 AI 模型(尤其是 AI Agent)能够标准化调用外部工具、读取外部资源的协议。你可以把它理解成"AI 世界的 USB 接口"——只要你的服务端实现了 MCP 协议,任何支持 MCP 的 AI 客户端都能即插即用地调用你的能力。它解决的核心问题是:AI 不再只能靠对话上下文里的信息干活,而是能主动去"够"到外部世界的数据和工具。
这个项目适合谁看?三类人:一是做 IoT、嵌入式、低功耗测试的工程师,你们会直接受益;二是想入门 MCP 服务端开发的开发者,这是一个非常典型的"把硬件能力暴露给 AI"的案例;三是对 AI Agent 落地感兴趣的人,你能看到一个真实、可跑通的工具集成思路。
接下来我会把整个项目的设计思路、协议细节、代码实现、踩坑经验全部摊开讲,尽量做到你照着做就能复现。
2. 先想清楚 MCP 服务端到底要暴露什么能力
动手写代码之前,最忌讳的就是直接开干。MCP 服务端不是"把设备所有功能都包一遍"就完事,而是要站在 AI 的视角想:AI 需要什么,才能完成功耗分析这件事?
2.1 MCP 的三种核心原语:Tools、Resources、Prompts
MCP 协议里,服务端能向客户端暴露三类东西,理解它们的区别是设计的第一步:
- Tools(工具):AI 可以主动调用的函数,有输入参数、有返回值。适合"执行一个动作",比如"开始采集""停止采集""设置采样率"。
- Resources(资源):AI 可以读取的数据,通常是只读的,通过 URI 标识。适合"读取一份数据",比如"当前功耗读数""最近一段时间的采样记录"。
- Prompts(提示模板):预定义的提示词模板,帮用户快速发起某类任务。适合"封装常见分析流程"。
很多人一开始会把所有东西都塞进 Tools,这其实是个误区。读数据用 Resources,做动作才用 Tools,这样 AI 客户端在展示和调用时语义更清晰。比如"获取当前功率"更适合做成 Resource,因为它是幂等的读取操作;而"开始一次 30 秒的采集"是典型的 Tool,因为它有副作用、会改变设备状态。
2.2 针对 IoT Power 的能力清单设计
结合 IoT Power 的实际能力,我最终确定了这样一份能力清单:
| 类型 | 名称 | 作用 | 关键参数 |
|---|---|---|---|
| Tool | start_capture | 启动一次功耗采集 | 时长、采样率 |
| Tool | stop_capture | 停止当前采集 | 无 |
| Tool | set_sample_rate | 调整采样率 | 采样率 Hz |
| Resource | power://current | 当前实时读数 | 无 |
| Resource | power://session/latest | 最近一次采集的完整数据 | 无 |
| Prompt | analyze_power | 功耗分析提示模板 | 数据摘要 |
这份清单的设计逻辑是:让 AI 能完成"采集—读取—分析"的完整闭环。AI 先调用start_capture采一段数据,再通过 Resource 读取结果,最后用 Prompt 里的模板组织分析。整个过程不需要人干预,这才是"让 AI 自己看功耗计"的真正含义。
2.3 为什么不做成"一个大而全"的工具
我一开始也想过,干脆做一个analyze_power_consumption工具,内部把采集、读取、分析全干了,AI 只调一次就行。但实测下来这个思路有问题:AI 失去了中间过程的控制权。比如它想调整采样率再采一次,或者想只采 5 秒看看瞬时峰值,大而全的工具就僵住了。
把粒度拆细,让 AI 自己编排调用顺序,反而更灵活。这也是 MCP 设计哲学里很重要的一点:服务端提供原子能力,编排交给 Agent。当然,如果你确实有高频固定流程,用 Prompt 模板封装一下就好,不必牺牲 Tools 的灵活性。
3. 通信层选型:stdio 还是 HTTP,这是个真问题
MCP 服务端和客户端之间怎么通信,是绕不开的第一个技术决策。MCP 官方支持多种传输方式,最常用的是stdio(标准输入输出)和HTTP/SSE。选错了,后面会很难受。
3.1 stdio 传输:简单,但和硬件进程绑得死
stdio 模式下,MCP 客户端会把服务端当成一个子进程启动,通过标准输入输出收发 JSON-RPC 消息。优点是零网络配置、启动即用、安全性好(不开放端口)。对于本地开发、单机使用场景,这是最省事的选择。
但 IoT Power 这个场景有个特殊性:设备是通过串口或 USB 连接的。如果服务端进程被客户端反复启停,串口句柄的打开/关闭就会变得很频繁,某些驱动下容易出现"设备被占用"的问题。而且 stdio 模式下服务端生命周期完全由客户端控制,你想让它常驻后台持续采集就不太方便。
3.2 HTTP/SSE 传输:适合常驻,但要处理并发
HTTP 模式下,服务端是一个独立常驻的进程,监听某个端口,客户端通过 HTTP 请求和 SSE(Server-Sent Events)流来通信。好处是服务端可以长期运行,持续采集数据,多个客户端也能同时连接。对于"让 AI 随时读取功耗"这种需求,常驻服务端显然更合适。
代价是要处理并发访问。功耗计是独占资源,如果两个客户端同时调用start_capture,就会冲突。我的做法是在服务端加一把设备锁,任何涉及设备操作的请求都要先获取锁,拿不到就返回"设备忙"的错误。这个细节后面会展开。
3.3 我的最终选择与理由
综合考虑,我选了HTTP/SSE 作为主传输方式,同时保留 stdio 作为轻量模式。日常用 HTTP 常驻,方便 AI 随时读数据;需要快速验证或做单次分析时,用 stdio 起一个临时进程,不污染环境。
这里有个实操心得:HTTP 模式下一定要给服务端加健康检查接口。因为设备可能被拔掉、串口可能断开,服务端进程却还活着。加一个/health端点,返回设备连接状态,AI 客户端在调用前可以先探一下,避免拿到一堆超时错误。
4. 把 IoT Power 的串口协议翻译成 MCP 工具
这一节是项目的核心:怎么把功耗计的底层通信,包装成 AI 能理解的工具。
4.1 先摸清 IoT Power 的通信协议
IoT Power 通常通过串口(UART)或 USB CDC 与上位机通信,指令一般是简单的文本或二进制帧。以常见的文本协议为例,可能长这样:
发送: MEASURE?\n 返回: V=3.301,I=0.0123,P=0.0406\n不同固件版本指令可能不同,第一步永远是拿官方文档或抓包确认指令格式。我踩过的坑是:想当然按某个开源项目的指令写,结果设备固件版本不一样,返回格式完全不同,白白调了半天。
确认协议后,把它封装成一个 Python 类,负责串口打开、指令发送、响应解析:
import serial import time class IoTPower: def __init__(self, port, baudrate=115200, timeout=1): self.ser = serial.Serial(port, baudrate, timeout=timeout) self.lock = threading.Lock() def _send(self, cmd): with self.lock: self.ser.reset_input_buffer() self.ser.write((cmd + "\n").encode()) line = self.ser.readline().decode().strip() return line def read_measurement(self): line = self._send("MEASURE?") # 解析 V=..,I=..,P=.. parts = dict(p.split("=") for p in line.split(",")) return { "voltage": float(parts["V"]), "current": float(parts["I"]), "power": float(parts["P"]), }注意那个self.lock——串口是独占资源,多线程访问必须加锁,否则会出现指令和响应错位,读到的数据张冠李戴。这个坑我在早期版本里踩得很惨,表现为"偶尔读到离谱的电流值",排查了很久才发现是并发问题。
4.2 用 MCP SDK 定义工具
MCP 官方提供了 Python SDK(mcp包),定义工具非常直观。以start_capture为例:
from mcp.server import Server from mcp.types import Tool, TextContent import json app = Server("iot-power-mcp") power = IoTPower("/dev/ttyUSB0") @app.list_tools() async def list_tools(): return [ Tool( name="start_capture", description="启动一次功耗采集,返回采集到的样本摘要", inputSchema={ "type": "object", "properties": { "duration_sec": {"type": "number", "description": "采集时长(秒)"}, "sample_rate_hz": {"type": "number", "description": "采样率(Hz)"} }, "required": ["duration_sec"] } ), # ... 其他工具 ] @app.call_tool() async def call_tool(name, arguments): if name == "start_capture": duration = arguments["duration_sec"] rate = arguments.get("sample_rate_hz", 10) samples = power.capture(duration, rate) summary = summarize(samples) return [TextContent(type="text", text=json.dumps(summary))]这里有个关键设计点:工具返回的不能是原始几万个采样点,而是摘要。AI 的上下文窗口有限,你塞几万行数据进去,既浪费 token 又干扰分析。我的做法是返回统计摘要(最大/最小/平均功率、峰值时刻、采样点数),如果 AI 需要原始数据,再通过 Resource 按需读取。
4.3 工具描述怎么写,AI 才用得对
description字段不是写给人看的注释,而是写给 AI 看的说明书。写得含糊,AI 就会乱调。我的经验是:
- 明确说明什么时候该用这个工具,比如"当你需要测量设备在一段时间内的平均功耗时使用"。
- 参数描述要写清单位、取值范围、默认值,比如"采样率,单位 Hz,建议 1-1000,默认 10"。
- 说明返回什么,让 AI 知道调用后能拿到什么。
我对比过:把描述写详细之后,AI 调用工具的正确率明显提升,尤其是参数填错的情况大幅减少。这算是 MCP 开发里一个容易被忽视但收益很高的细节。
5. 让 AI 读懂数据:Resource 与 Prompt 的配合
工具负责"采",Resource 和 Prompt 负责"读"和"分析",三者配合才能形成闭环。
5.1 Resource 的 URI 设计与数据组织
MCP 的 Resource 用 URI 标识,我设计了这样一套:
power://current:当前瞬时读数,JSON 格式。power://session/latest:最近一次采集的完整数据。power://session/{id}:按 ID 访问历史采集记录。
实现上,服务端维护一个内存中的会话列表(也可以落盘),每次start_capture生成一个 session,记录时间戳、参数、采样数据。Resource 读取时按 URI 返回对应内容:
@app.read_resource() async def read_resource(uri): if uri == "power://current": data = power.read_measurement() return json.dumps(data) if uri.startswith("power://session/"): sid = uri.split("/")[-1] session = store.get(sid) return json.dumps(session.to_dict())为什么要区分 current 和 session?因为它们的时效性和数据量完全不同。current是高频读取的实时值,数据量小;session是历史记录,可能很大。分开设计,AI 就能根据需求选择,避免每次都拉一大堆数据。
5.2 Prompt 模板:把分析套路固化下来
功耗分析其实有一套固定套路:先看平均功耗判断整体水平,再看峰值找异常,最后看波形有没有周期性波动。我把这套流程写成了一个 Prompt 模板:
@app.list_prompts() async def list_prompts(): return [Prompt( name="analyze_power", description="对一次功耗采集结果做结构化分析", arguments=[PromptArgument(name="session_id", required=True)] )] @app.get_prompt() async def get_prompt(name, arguments): sid = arguments["session_id"] return GetPromptResult(messages=[ PromptMessage( role="user", content=TextContent( type="text", text=f"请分析功耗会话 {sid} 的数据:1) 计算平均功耗;" f"2) 找出峰值及其时刻;3) 判断是否存在周期性波动;" f"4) 给出可能的耗电原因。数据可通过 power://session/{sid} 读取。" ) ) ])这样用户只要选这个 Prompt,AI 就会自动按套路分析,不用每次手写提示词。Prompt 的价值在于把领域知识沉淀下来,让不熟悉功耗分析的人也能得到专业结果。
5.3 一个完整的 AI 分析流程演示
把上面这些串起来,一次典型的交互是这样的:
- 用户对 AI 说:"帮我测一下这块板子待机 30 秒的功耗,看看有没有异常。"
- AI 调用
start_capture(duration_sec=30, sample_rate_hz=100)。 - 服务端采集 30 秒,返回摘要:平均功率 0.042W,峰值 0.31W 出现在第 12 秒。
- AI 读取
power://session/latest拿到详细数据。 - AI 分析后回复:"平均功耗 42mW 偏高,第 12 秒有 310mW 的尖峰,疑似某个外设周期性唤醒,建议检查……"
整个过程用户只说了一句话,剩下的 AI 全包了。这就是 MCP 服务端的价值——把硬件能力变成 AI 可调用的"感官"。
6. 踩坑实录:那些文档里不会写的坑
项目能跑通是一回事,跑得稳是另一回事。下面这些坑都是我实际踩过的,分享出来帮你省时间。
6.1 串口句柄泄漏与设备占用
最开始我用 stdio 模式,每次客户端启动都 new 一个IoTPower,退出时忘了关串口。跑几次之后,设备就报"resource busy",只能拔插 USB 才能恢复。根因是串口句柄没释放。
解决方案有两个层面:一是给IoTPower加close()方法,并在服务端退出钩子里调用;二是改用 HTTP 常驻模式,进程只开一次串口,从根上避免频繁开关。我最后两个都做了,双保险。
6.2 采样率设置过高导致数据错乱
IoT Power 的串口波特率是有限的。我一开始把采样率设到 1000Hz,结果数据开始出现错位、丢帧。算一下就明白了:假设每帧数据 30 字节,1000Hz 就是 30000 字节/秒,而 115200 波特率理论也就约 11520 字节/秒,带宽根本不够。
所以设置采样率时一定要先算带宽:采样率 × 单帧字节数 < 波特率 / 10(留余量)。超过这个值,要么降采样率,要么提高波特率。这个计算我在服务端做成了校验,参数超限直接拒绝,避免用户设了个跑不动的值。
6.3 AI 把工具调用参数填错
即使描述写得很清楚,AI 偶尔还是会把duration_sec填成字符串"30",或者把采样率填成负数。服务端必须做参数校验,不能假设 AI 一定填对。我的做法是用 JSON Schema 严格约束类型,同时在代码里再兜一层:
def validate_capture_args(args): duration = args.get("duration_sec") if not isinstance(duration, (int, float)) or duration <= 0 or duration > 3600: raise ValueError("duration_sec 必须是 0-3600 之间的数字") rate = args.get("sample_rate_hz", 10) if not isinstance(rate, (int, float)) or rate <= 0 or rate > 1000: raise ValueError("sample_rate_hz 必须是 0-1000 之间的数字") return duration, rate校验失败时返回清晰的错误信息,AI 看到后往往能自己纠正重试。别小看错误信息的质量,它直接影响 AI 的自愈能力。
6.4 长采集任务把客户端"卡死"
如果 AI 调用start_capture(duration_sec=600),服务端同步阻塞 10 分钟,客户端会一直等,体验极差,还可能超时断开。长任务必须异步化。
我的方案是:start_capture立即返回一个 session_id,采集在后台线程进行;AI 通过轮询power://session/{id}的status字段(running/done)来获知进度。这样既不阻塞,AI 也能随时查询。对于 MCP 这种请求-响应模型,把长任务拆成"提交 + 查询"两步是通用解法。
7. 稳定性与扩展:让这个服务端能长期用下去
跑通 demo 只是开始,真正要用起来,还得考虑稳定性和扩展性。
7.1 设备断连的自动重连
设备被拔掉、串口异常断开是常态。服务端不能一断就崩,要能自动重连。我的做法是加一个后台守护线程,定期检查串口状态,发现断开就尝试重新打开,并记录重连次数。同时power://current这类 Resource 在设备不可用时返回明确的错误状态,而不是抛异常。
这里有个细节:重连要有退避策略。如果设备一直不在,每秒重试一次会刷爆日志。我用的是指数退避,从 1 秒开始,最多退到 30 秒一次。
7.2 数据持久化:内存不够用怎么办
内存里存 session 数据,服务端一重启就全没了。对于需要长期记录功耗的场景,得落盘。我选的是SQLite,轻量、无需额外服务、单文件。每次采集完成后写一条记录,包含时间戳、参数、摘要和原始数据(原始数据可以压缩存储)。
查询时通过power://session/{id}读取,也可以扩展一个list_sessions工具,让 AI 能按时间范围检索历史记录。有了持久化,AI 就能做趋势分析,比如"对比这周和上周的待机功耗",价值比单次测量大得多。
7.3 安全边界:别让服务端变成"万能钥匙"
HTTP 模式下服务端监听端口,就得考虑安全。虽然这是本地工具,但也不能裸奔。我的做法:
- 默认只监听
127.0.0.1,不对外网开放。 - 加一个简单的 token 鉴权,客户端请求头带上 token 才放行。
- 对工具调用做频率限制,防止 AI 陷入死循环疯狂调用。
尤其是频率限制,非常必要。我遇到过 AI 因为一次分析失败,反复重试start_capture,几秒钟内发起几十次采集请求。加了限流之后,这种情况就被挡住了。
7.4 还能怎么扩展
这个服务端的骨架搭好之后,扩展空间很大。几个我考虑过的方向:
- 多设备支持:URI 里带上设备 ID,
power://device/{id}/current,同时管理多台功耗计。 - 触发式采集:设定阈值,当电流超过某个值时自动开始记录,适合抓偶发尖峰。
- 与测试框架集成:把 MCP 工具接到自动化测试流程里,每次跑完用例自动采集功耗并生成报告。
- 数据可视化:服务端生成波形图,通过 Resource 返回图片,AI 可以直接"看"图分析。
这些扩展的共同点是:都建立在"原子能力 + AI 编排"这个架构之上,不需要推翻重来。这也是我一开始坚持把粒度拆细的原因——架构对了,扩展就是加法。
8. 我个人的几点实操体会
写这个 MCP 服务端的过程中,有几个体会比较深,分享给准备动手的朋友。
第一,先跑通最小闭环,再谈优化。我一开始想一步到位,把多设备、持久化、可视化全做了,结果卡在串口通信上好几天。后来退回来,先让"采集—读取—分析"这条最短路径跑通,再逐步加功能,效率高得多。
第二,MCP 服务端的本质是"翻译"。它把硬件的、底层的、非标准的能力,翻译成 AI 能理解的标准化接口。翻译得好不好,直接决定 AI 用得顺不顺。所以工具描述、参数设计、错误信息这些"软"的东西,值得花时间打磨。
第三,AI 不是万能的,服务端要兜底。别指望 AI 每次都填对参数、每次都不超时。服务端该校验的校验、该限流的限流、该异步的异步,把 AI 当成一个"聪明但会犯错的调用方"来对待,系统才稳。
最后分享一个小技巧:调试 MCP 服务端时,可以用官方的 Inspector 工具,它能可视化地列出所有 Tools、Resources、Prompts,还能手动调用测试。比对着日志猜问题高效太多。等你的服务端接上真正的 AI 客户端,看着它自己调工具、读数据、给出分析结论的那一刻,会觉得前面这些折腾都值了。