Model Context Protocol (MCP) over HTTP

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

Code_ThWPjsWLhS

[{"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":[]}]
2 Likes

Thanks for the great Help!


[
    {
        "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": []
    }
]

Hello I was wondering if there is alternative or native answer ?

I want to keep my HTTP or Websocket node, provide the payload to a whitebox (the MCP SDK) then handle tools and send the response back.

  1. I don't want to instanciate an "MCP Server" that will, underhood, open port and do some similar stuff to HTTP or Websocket node. Unless @knolleary states it's official.

  2. I don't want to instanciate a library that underhood run Python, that's horrible architecture.

May be I misunderstood, but it's only JSON exchange over a specific protocole official libraries should split the server part from the logical part ?

The implementation seems really weak:

const server = getServer(); 
const transport: StreamableHTTPServerTransport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
});
  • They build a server object to declare tools and routes, thats fine even if they think in term of pseudo-URL
  • They rely on a transport object that mix server concepts like session and business concepts.

Thanks for clarification

MCP is a protocol and part of its 'nature' is that MCP communicates using SSE (server-send events), if you want to keep using ws/http, you can use tools instead as you can define a tool function to make a request over websockets or http(s) .

Hum, I don't have enought knowledge on that, it seems server-send-events is on top HTTP protocole, so it should work with regular http node ?

you can use tools instead as you can define a tool function to make a request over websockets or http(s)

Hum, I thought you have to declare tool to the MCP server ? like transport.

May be I mis-understood MCP. I thought I can

  • declare an HTTP-in EndPoint receive queries from clients push it to a "MCP Manager"
  • the manager handle the protocole
  • the manager trigger tools, aka a callback function that would do node.send() somehow
  • it should be "easy to call from a node manager.send() something

Ahhhh I think I understand, in order to manager.send() it requires an access to req/res and it is where Node-RED had limits and I need to use node that wrap http in/out protocole.

The terminology is a bit confusing (for me at least) - in the LLM you configure MCP tools - which are MCP 'clients' that connect to 'servers' - this uses JSON-RCP over SSE. See the architecture