Node-RED MCP 插件实践指南:从安装到常见问题解析
引言
在物联网(IoT)和大型语言模型(LLM)的融合浪潮中,模型上下文协议(Model Context Protocol, MCP)作为连接 AI 模型与外部工具(Tools)的桥梁,正变得越来越重要。Node-RED 以其强大的可视化流程编排能力,在 IoT 领域备受欢迎。将两者结合,可以极大地简化和加速 AI Agent 应用的开发。
本文将详细介绍一个增强版的 node-red-contrib-mcp-server
插件。该插件基于 DanEdens/node-red-contrib-mcp-server 进行二次开发,修复了原版的一些问题,并完整实现了 MCP 核心协议,特别解决了与 Dify、Claude 等平台集成时的关键痛点。
- 项目地址: github.com/xunberg/nod…NPM 官方页面: flows.nodered.org/node/node-r…
插件安装
你可以通过本地安装 .tgz
包的方式来使用最新版本。
注意:最新的插件包
.tgz
文件可以在项目发布页或通过其他指定渠道获取。以下命令假设你已下载了node-red-contrib-mcp-server-1.1.5.tgz
文件。
# 1. 进入你的 Node-RED 用户目录cd ~/.node-red# 2. 安装插件包# 请确保 tgz 文件的路径正确npm install /path/to/your/node-red-contrib-mcp-server-1.1.5.tgz
安装完成后,重启 Node-RED 即可在节点面板中找到 MCP 相关的节点。
关键注意事项 (避坑指南)
在实践中,我们总结了三个最容易出错的关键点,提前了解它们可以帮你节省大量调试时间。
1. function
节点中 msg
对象的处理
一个核心要点是:在 mcp-flow-server
节点的 router
分支后的 function
节点中,应避免直接修改或覆盖传入的 msg
对象。
原因分析:插件内部需要通过 msg.payload.executionId
来追踪和匹配工具的请求与响应。如果你在 function
节点中直接 msg = { payload: 'new data' }
,会丢失原始的 executionId
等重要信息,导致后续的响应无法被正确识别,流程中断。
插件内部的响应处理逻辑如下,它强依赖于 executionId
的匹配:
const responseHandler = (msg) => { // ... if (msg.topic === 'mcp-tool-response' && msg.payload.executionId === executionMsg.payload.executionId) { // ... 只有 executionId 匹配,才会继续处理 resolve(msg.payload.result); }};
正确做法:只修改 msg.payload
,并保留其他 msg
上的属性,或者返回一个包含原始 executionId
的新对象。
// 错误示例: 这会覆盖整个 msg 对象// msg.payload = command + terminator;// return msg;// 正确示例: 返回一个新对象,同时传递 executionIdconst command = "SI";const terminator = "\r\n";return { payload: command + terminator, executionId: msg.payload.executionId // 关键:手动传递 executionId};
2. 严格遵循 MCP Tools Response 规范
另一个常见的陷阱是,在流程的最后构建返回给客户端的 MCP Response
时,未能严格遵守 MCP 的 Tools response
规范。
后果:即使你的 Node-RED 流程运行正常,但如果响应格式不正确,MCP 客户端(如 Dify、Claude Desktop)也无法解析,最终导致调用失败。
正确格式:请务必参考 MCP 官方文档中关于 Tool Result 的定义。一个标准的响应 msg
应该类似这样:
// 假设 data 是你要返回的业务数据对象let data = { 'success': true, 'weight': 100.5, 'unit': 'g'};// 1. 将业务数据转换为 MCP content 格式let final_result = { type: "text", text: JSON.stringify(data) // text 字段必须是字符串};// 2. 构建完整的 mcp-tool-responsereturn { topic: 'mcp-tool-response', payload: { executionId: msg.executionId, // 确保 executionId 被正确传递 result: { content: [final_result] // content 是一个数组 } }};
3. 完整实现 notifications/initialized
事件
此插件的增强版特别实现了 notifications/initialized
事件处理。这是一个至关重要的步骤,尤其是在与正式的 MCP 客户端(如 Dify、Claude)集成时。
问题描述:如果缺少对 initialized
通知事件的处理,你可能会发现工具在 MCP 官方的 Inspector 开发工具中可以调试成功,但一旦部署到 Dify 等生产平台,就会持续调用失败。
原因分析:根据 MCP 生命周期规范,在客户端完成工具列表的初始化后,会向服务端发送一个 notifications/initialized
通知。服务端收到此通知后,整个 MCP 会话才被认为是完全建立的。可以将其理解为一种**“握手”机制**,确认双方都已准备就绪。如果服务端不响应或不处理此事件,客户端可能会认为连接尚未成功建立,从而拒绝后续的工具调用。
此增强版插件已在内部处理了该逻辑,你无需额外配置,这确保了与主流平台的兼容性。
示例流程参考
以下提供一个完整的 Node-RED 流程示例,用于演示如何通过 MCP 调用一个模拟的电子天平设备来获取重量。你可以直接复制下方的 JSON 代码,并通过 Node-RED 的 导入 功能使用。
流程概览:
流程 JSON:
[ { "id": "80b5156c46267c5f", "type": "tab", "label": "balance_mcp_demo", "disabled": false, "info": "", "env": [] }, { "id": "7eb1cb484e18fc02", "type": "mcp-flow-server", "z": "80b5156c46267c5f", "name": "balance_mcp_server", "serverName": "node-red-mcp-server", "serverPort": "8098", "autoStart": true, "enableCors": true, "x": 140, "y": 260, "wires": [ [ "81c4a70952a36fad" ] ] }, { "id": "81c4a70952a36fad", "type": "switch", "z": "80b5156c46267c5f", "name": "router", "property": "payload.toolName", "propertyType": "msg", "rules": [ { "t": "eq", "v": "get_weight_tool", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 270, "y": 360, "wires": [ [ "b38089ccf7b3ddd5" ] ] }, { "id": "b38089ccf7b3ddd5", "type": "function", "z": "80b5156c46267c5f", "name": "构建报文", "func": "// 在这里设置你要发送的指令\n// !!!请根据你的设备手册修改此处的指令 !!!\nconst command = \"SI\"; // 假设指令是 SIR\nconst terminator = \"\r\n\"; // 假设结束符是 回车+换行\n\n// 将指令设置到 msg.payload 中,同时传递 executionId\n// 这是关键点1的正确实践\nreturn {\n payload: command + terminator,\n executionId: msg.payload.executionId\n};", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 460, "y": 260, "wires": [ [ "151d7cd3604d126e", "e4b1c6c50b0cd82d" ] ] }, { "id": "151d7cd3604d126e", "type": "serial request", "z": "80b5156c46267c5f", "d": true, "name": "天平设备(真实)", "serial": "bec6f76384cb6abf", "x": 720, "y": 260, "wires": [ [ "d2b4ee66cd8ae1a2" ] ] }, { "id": "d2b4ee66cd8ae1a2", "type": "function", "z": "80b5156c46267c5f", "name": "通用解析函数 ", "func": "let rawData = msg.payload;\n\nif (typeof rawData !== 'string') {\n rawData = String(rawData);\n}\n\nlet parts = rawData.trim().split(/\\s+/);\n\nlet data = {\n 'success': false,\n 'weight': 0,\n 'message': '',\n \"rawData\": rawData\n}\n\n// 尝试从 "S S 100.00 g" 这样的格式中解析重量\nif (parts.length >= 2) {\n let weight = parseFloat(parts[parts.length - 2]);\n if (!isNaN(weight)) {\n data.weight = weight;\n data.success = true;\n data.message = 'Data parsed successfully.';\n }\n}\n\nif (!data.success) {\n const errorMsg = \"无法解析数据: \" + rawData;\n node.warn(errorMsg);\n data.message = errorMsg;\n}\n\n// 将解析结果放到 payload 中,以便下一个节点处理\nmsg.payload = data;\nreturn msg;", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 920, "y": 320, "wires": [ [ "f17847e75b84da19" ] ] }, { "id": "f17847e75b84da19", "type": "function", "z": "80b5156c46267c5f", "name": "构建 MCP Response", "func": "// 这是关键点2的正确实践\n\n// 1. 将解析后的数据对象,转换为 MCP content 格式\nconst final_result = {\n type: \"text\",\n text: JSON.stringify(msg.payload)\n};\n\n// 2. 构建符合 MCP 规范的完整响应\nreturn {\n topic: 'mcp-tool-response',\n payload: {\n executionId: msg.executionId,\n result: {\n content: [final_result] \n }\n }\n};", "outputs": 1, "timeout": "", "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 420, "y": 80, "wires": [ [ "7eb1cb484e18fc02" ] ] }, { "id": "7c9566851a3ef722", "type": "mcp-tool-registry", "z": "80b5156c46267c5f", "name": "register_get_weight_tool", "toolName": "get_weight_tool", "toolDescription": "Obtain the weighing results of the balance equipment. (获取天平设备的称重结果)", "toolSchema": "{\n \"type\": \"object\",\n \"properties\": {\n \"device_name\": {\n \"type\": \"string\",\n \"description\": \"the name of balance device\"\n }\n },\n \"required\": []\n}", "autoRegister": true, "x": 210, "y": 560, "wires": [ [] ] }, { "id": "e4b1c6c50b0cd82d", "type": "function", "z": "80b5156c46267c5f", "name": "模拟设备返回", "func": "// 这个节点用于测试,模拟串口设备返回的数据\nmsg.payload= \"S S 100.00 g\"\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 710, "y": 380, "wires": [ [ "d2b4ee66cd8ae1a2" ] ] }, { "id": "bec6f76384cb6abf", "type": "serial-port", "name": "天平", "serialport": "/dev/tty.usbserial-14140", "serialbaud": "9600", "databits": 8, "parity": "none", "stopbits": 1, "waitfor": "", "dtr": "none", "rts": "none", "cts": "none", "dsr": "none", "newline": "1000", "bin": "false", "out": "time", "addchar": "", "responsetimeout": 10000 }]
总结
通过本文的介绍,相信您已经对如何使用这个增强版的 Node-RED MCP 插件有了清晰的认识。总结一下核心要点:
- 谨慎处理
msg
对象,确保 executionId
在流程中正确传递。严格遵循 MCP 响应规范,保证客户端能正确解析结果。理解 initialized
事件的重要性,它是确保与生产级平台稳定连接的关键。希望这份指南能帮助你顺利地在 Node-RED 上构建强大的 MCP 工具服务。如果你在使用过程中遇到任何问题,或有改进建议,欢迎在 GitHub 项目中提交 Issue 或 Pull Request。