Hi,
Antropic recently introduce MCP streamable-http protocol. Is there anybody out there can create an example flow with NR.
There is a contrib node named node-red-contrib-mcp-protocol
but it has issues. It is meant to connect to an MCP server but I could not get it to work.
For what purpose are you asking here though?
To make Node-RED an MCP Server? MCP Client? MCP Host?
If its a server, is it to provide custom tools (tools generated at runtime in node-red flows that an MCP client can query? or to programmatically access/modify Node-RED flows? or to serve info/data like "what flow file is this node-red running"?
etc.
Thanks for the reply. I wasn’t able to get the node-red-contrib-mcp-protocol
node working either. What I’m really looking for is a basic example of how to implement an MCP server using Node-RED. Even a simple "add tool" example would be enough to help me understand the flow and how the protocol is expected to work in this context.
You will find FastMCP
far easier (it uses The official Typescript SDK for Model Context Protocol under the hood)
Here is a demo to get you going
[{"id":"fd8aaf75d0b12524","type":"function","z":"f2a7cc4cf78e0527","name":"\"my-mcp-server\"","func":"/// nothing to see here - checkout the On Start and On Stop tabs","outputs":0,"timeout":0,"noerr":0,"initialize":"const { FastMCP, Tool, Content } = fastmcp\nconst { z } = zod\nnode.warn({z,FastMCP});\n\nconst PORT = 8080\nconst SERVER_NAME = \"my-mcp-server\"\n\nconst GreetingSchema = z.object({\n // name with min len of 1 and max len of 100\n name: z.string().min(1).max(100)\n});\n\n/**\n * MCP tool for greeting the user\n * @param name The name of the user\n */\nconst greeterTool = {\n name: 'greet_person',\n description: 'Greet person by name',\n parameters: GreetingSchema,\n annotations: {\n title: 'Greet Person',\n readOnlyHint: true\n },\n execute: async ({ name }) => {\n try {\n console.log('Fetching greeting for person:', name);\n const greetingMessage = `Hi there ${name}! Welcome to MCP Server running in Node-RED!`;\n return { text: greetingMessage, type: 'text' };\n } catch (error) {\n throw error;\n }\n },\n};\n\ntry {\n\n const transportType = 'sse'\n const server = new FastMCP({\n name: SERVER_NAME,\n version: '1.0.0'\n });\n\n server.addTool(greeterTool);\n context.set('server', server)\n\n server.start({\n transportType,\n sse: {\n endpoint: '/sse',\n port: PORT,\n },\n });\n const info = `http://<ip>:${PORT}/sse`\n node.status({ fill: \"green\", shape: \"dot\", text: `${SERVER_NAME} is running at ${info}` });\n\n} catch (error) {\n node.status({fill:\"red\",shape:\"ring\",text:\"Failed to start server\"});\n node.error(`Failed to start server ${SERVER_NAME}: ${error.message}`)\n}\n","finalize":"let server = context.get('server')\nif (server?.stop) {\n node.warn(\"Shutting down MCP Server\");\n server.stop()\n server = null\n}\n","libs":[{"var":"fastmcp","module":"fastmcp"},{"var":"zod","module":"zod"}],"x":1620,"y":320,"wires":[]}]
[
{
"id": "ebd9e8f81e63a3e5",
"type": "function",
"z": "c1a5f1f03dbac4de",
"name": "\"my-mcp-server\"",
"func": "/// nothing to see here - checkout the On Start and On Stop tabs",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "const { FastMCP, Tool, Content } = fastmcp;\nconst { z } = zod;\n\nconst PORT = 8080;\nconst SERVER_NAME = \"my-mcp-server\";\n\n// === Define tool metadata ===\nconst toolDefinitions = [\n {\n name: \"get_weather\",\n description: \"Get current temperature for a given location.\",\n schema: z.object({\n location: z.string().describe(\"City and country e.g. Bogotá, Colombia\")\n })\n },\n {\n name: \"send_email\",\n description: \"Send an email to a specified recipient with a message body.\",\n schema: z.object({\n to: z.string().describe(\"Email address of the recipient.\"),\n body: z.string().describe(\"The content of the email body.\")\n })\n }\n];\n\n// === Create tools from definitions ===\nconst tools = toolDefinitions.map(def => ({\n name: def.name,\n description: def.description,\n parameters: def.schema,\n annotations: {\n title: def.name.replace(/_/g, ' ').replace(/\\b\\w/g, l => l.toUpperCase()),\n readOnlyHint: true\n },\n execute: async (args) => {\n return new Promise((resolve, reject) => {\n try {\n const msg = {\n payload: {\n id: Date.now(),\n name: def.name,\n arguments: args\n },\n _mcpDone: resolve,\n _mcpFail: reject\n };\n node.send([msg, null]); // Output to dispatcher flow\n } catch (err) {\n reject(err);\n }\n });\n }\n}));\n\ntry {\n const transportType = 'sse';\n\n const server = new FastMCP({\n name: SERVER_NAME,\n version: '1.0.0'\n });\n\n // Register tools\n for (const tool of tools) {\n server.addTool(tool);\n }\n\n context.set('server', server);\n\n server.start({\n transportType,\n sse: {\n endpoint: '/sse',\n port: PORT,\n },\n });\n\n const info = `http://<ip>:${PORT}/sse`;\n node.status({ fill: \"green\", shape: \"dot\", text: `${SERVER_NAME} running at ${info}` });\n\n} catch (error) {\n node.status({ fill: \"red\", shape: \"ring\", text: \"Failed to start server\" });\n node.error(`Failed to start server ${SERVER_NAME}: ${error.message}`);\n}\n",
"finalize": "let server = context.get('server')\nif (server?.stop) {\n node.warn(\"Shutting down MCP Server\");\n server.stop()\n server = null\n}\n",
"libs": [
{
"var": "fastmcp",
"module": "fastmcp"
},
{
"var": "zod",
"module": "zod"
}
],
"x": 810,
"y": 440,
"wires": [
[
"5ba9094de54738db"
]
]
},
{
"id": "5ba9094de54738db",
"type": "function",
"z": "c1a5f1f03dbac4de",
"name": "Prepare dispatch",
"func": "// each msg.payload is one tool-call object\nmsg.callId = msg.payload.id;\nmsg.args = msg.payload.arguments;\nmsg.target = msg.payload.name; // drives the dynamic Link Call\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1050,
"y": 440,
"wires": [
[
"29dcde5c9a626a57"
]
]
},
{
"id": "29dcde5c9a626a57",
"type": "link call",
"z": "c1a5f1f03dbac4de",
"name": "Dispatch (dynamic)",
"links": [],
"linkType": "dynamic",
"timeout": "30",
"x": 1330,
"y": 440,
"wires": [
[]
]
},
{
"id": "6e928b60e13c562d",
"type": "link in",
"z": "c1a5f1f03dbac4de",
"name": "get_weather",
"links": [],
"x": 755,
"y": 640,
"wires": [
[
"2330e7aef7e4978a"
]
]
},
{
"id": "2330e7aef7e4978a",
"type": "function",
"z": "c1a5f1f03dbac4de",
"name": "stub get_weather",
"func": "const location = msg.args?.location || 'unknown';\nconst result = `Weather in ${location}: sunny 25°C`;\n\nif (msg._mcpDone) {\n msg._mcpDone({ text: result, type: \"text\" });\n}\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 955,
"y": 640,
"wires": [
[
"73abc3922f507f3d"
]
]
},
{
"id": "73abc3922f507f3d",
"type": "link out",
"z": "c1a5f1f03dbac4de",
"name": "return get_weather",
"mode": "return",
"links": [],
"x": 1155,
"y": 640,
"wires": []
},
{
"id": "e9857db914735441",
"type": "link in",
"z": "c1a5f1f03dbac4de",
"name": "send_email",
"links": [],
"x": 755,
"y": 740,
"wires": [
[
"8cbcce9b477e879c"
]
]
},
{
"id": "8cbcce9b477e879c",
"type": "function",
"z": "c1a5f1f03dbac4de",
"name": "stub send_email",
"func": "const to = msg.args?.to || 'nobody';\nconst body = msg.args?.body || '';\nconst result = `Pretend email sent to ${to} with body: \"${body}\"`;\n\nif (msg._mcpDone) {\n msg._mcpDone({ text: result, type: \"text\" });\n}\nreturn msg;",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 955,
"y": 740,
"wires": [
[
"783f93e0c906606a"
]
]
},
{
"id": "783f93e0c906606a",
"type": "link out",
"z": "c1a5f1f03dbac4de",
"name": "return send_email",
"mode": "return",
"links": [],
"x": 1155,
"y": 740,
"wires": []
}
]