2025 年 5 月時点 Node-RED から node-red-node-daemon を使って stdio 標準入出力 MCP サーバーと連携するメモ
2025 年 5 月時点 Node-RED から node-red-node-daemon を使って stdio 標準入出力 MCP サーバーと連携するメモです。
背景
あくまで2025 年 5 月時点の仕様での試してみたお話です。ここから仕様が変わったり、より良いアプローチが見つかるかもしれません。
- 2025 年 4 月時点 Node-RED からローカルのステートフル MCP サーバーと連携するメモ – 1ft-seabass.jp.MEMO
- 2025年4月時点 Node-RED の HTTP request ノードでローカルのステートレス MCP サーバーと連携するメモ – 1ft-seabass.jp.MEMO
こちらで HTTP リクエストや HTTP ストリーミングをベースにした MCP サーバーと Node-RED のやりとりができたわけですが、ここまで進めていてローカルでやるならシンプルに stdio 標準入出力でできた MCP サーバーでシンプルに行うのもやってみようと実験をしました。
stdio 標準入出力 MCP サーバーを TypeScript で作る
まずは stdio 標準入出力 MCP サーバーを作ります。
Node-RED 実行フォルダの中に mcp フォルダを作成

このように mcp というフォルダを Node-RED を実行しているフォルダの配下に置いておきます。
Node.js の実行として Node-RED は type module 無しで動くんですが、MCP サーバーの環境は TypeScript なども絡んで type module で動くために設定を分けるためにこのようにしています。
以下は mcp フォルダ内で進めていきます
ということで、この mcp フォルダの中に設定を整えていきます。
モジュールのインストール
今回はサーバーとはいえ HTTP サーバーは作らないので express 系は入れません。
こちらをもとに、
npm install @modelcontextprotocol/sdk zod
と
npm install -D typescript ts-node @types/node
を実行してモジュールの準備をします。
stdio-server.ts 用意
こちらの記事の Running Your Server の stdio のソースを元に、足し算のツールを加えたものを stdio-server.ts で用意します。
import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// Create an MCP server
const server = new McpServer({
name: "Demo",
version: "1.0.0"
});
// Add an addition tool
server.tool("add",
{ a: z.number(), b: z.number() },
async ({ a, b }) => ({
content: [{ type: "text", text: String(a + b) }]
})
);
// Add a dynamic greeting resource
server.resource(
"greeting",
new ResourceTemplate("greeting://{name}", { list: undefined }),
async (uri, { name }) => ({
contents: [{
uri: uri.href,
text: `Hello, ${name}!`
}]
})
);
const transport = new StdioServerTransport();
await server.connect(transport);
package.json に "type": "module" 加える
{
"name": "mcp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^22.15.9",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"zod": "^3.24.4"
}
}
package.json はこんな構成です。
"type": "module" を加えます。
npm run で動かせるようにする
package.json の scripts 項目に
"mcp-server": "node --no-warnings --loader ts-node/esm stdio-server.ts",
を TypeScript で stdio-server.ts を実行するコマンドを加えます。
{
"name": "mcp",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"mcp-server": "node --no-warnings --loader ts-node/esm stdio-server.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/node": "^22.15.9",
"ts-node": "^10.9.2",
"typescript": "^5.8.3"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.11.0",
"zod": "^3.24.4"
}
}
こんな風に加えました。
--no-warnings がついている理由は、
(node:67812) ExperimentalWarning: `--experimental-loader` may be removed in the future; instead use `register()`:
--import 'data:text/javascript,import { register } from "node:module"; import { pathToFileURL } from "node:url"; register("ts-node/esm", pathToFileURL("./"));'
(Use `node --trace-warnings ...` to show where the warning was created)
という ExperimentalWarning が、環境によって(といっても私の環境がですが)毎回出てしまうので、消すために入れています。
tsconfig.json
tsconfig.json も準備して、この後、TypeScript を動かす設定もしておきます。
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"types": ["node"],
"strict": true
},
"include": ["**/*.ts"]
}
ここまでのファイル構造

mcp フォルダ内に一通りの設定が出来上がりました。
一旦 stdio で動かしてみる
cd mpc
mpc フォルダに入って npm run 実行してみます。
npm run mcp-server
こちらで先ほど設定した mcp-server を実行します。

無事入力待ちになりました。
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"add","arguments":{"a":5,"b":6}},"id":1}
で add ツールを呼び出して足し算をしてみます。

{"result":{"content":[{"type":"text","text":"11"}]},"jsonrpc":"2.0","id":1}
このように結果が返ってきました。
Node-RED で node-red-node-daemon をインストール
今回のように起動しっぱなしにして、あとからデータを入力したり出力したりする用途は標準の exec ノードでは難しいので、そういった用途に対応できる node-red-node-daemon をインストールします。
(標準の exec ノードでは spawn モードで出力内容をつどつど出力できるものの、spawn 実行後に msg.payload で入力してしまうと、単純なプログラムの起動しなおしになってしまうので、データを入力したり出力したりできないニュアンスです。)

node-red-node-daemon (node) - Node-RED
こちらです。メンテナーは knolleary さん、dceejay さんで標準ノードに近いものなので安心感があります!

メニュー>パレットの管理>パレット>ノードを追加から node-red-node-daemon を検索してインストールします。
Node-RED でフローを作る

このようなフローを作りました。
node-red-node-daemon を使った stdio 標準入出力

node-red-node-daemon ノードで、先ほどの npm run のアクセスと同じことを行ってます。

コマンドは npm として、引数で
--prefix ./mcp run mcp-server
という引数を与えています。
--prefix ./mcp- Node-RED 実行フォルダでなく mcp のサブフォルダを実行場所にするための prefix パラメータです
run mcp-server- あとは mcp フォルダで実行されているように動くので、こちらを加えて同様の実行ができます
その他の設定としては
- デプロイ時にデーモンを自動起動する
- オフ
- こちらは別途自動スタートさせるためにオフにします
- 全てのメッセージ送信に enter を追加する
- オン
- 入力のたびに入力された JSON の内容が送られるようにオンにしています
- 終了またはエラー時にコマンドを再実行する
- オン
というようにしてます。
自動スタートする部分

こちらの起動時に動作する inject ノードと change ノードで msg.start を加えることで自動スタートを実現しています。
データを入力する部分

こちらでデータを入力する部分を行っています。

たとえば tool add と書いてある change ノードでは
{"jsonrpc":"2.0","method":"tools/call","params":{"name":"add","arguments":{"a":5,"b":6}},"id":1}
という値を送っています。
結果表示する部分

結果表示は、素直に通すと、起動時に
> mcp@1.0.0 mcp-server
> node --no-warnings --loader ts-node/esm stdio-server.ts
という内容が来るので JSON パースでエラーになるため、

JSON 簡易判定 {"result": という名前の switch ノードで JSON データの時と簡易分岐させてます。

このように {"result": が含まれるときに JSON 変換して、それ以外の時はそのまま出しています。
JSON フローデータ
今回のフローの JSON データはこちらです。
[{"id":"eb0a6e8e6f610b0a","type":"inject","z":"a0d20da28d63679b","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":340,"y":500,"wires":[["44c2bad8f468aaa2"]]},{"id":"44c2bad8f468aaa2","type":"change","z":"a0d20da28d63679b","name":"tool add","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"params\":{\"name\":\"add\",\"arguments\":{\"a\":5,\"b\":6}},\"id\":1}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":500,"wires":[["f6e429e9fbebe8e4"]]},{"id":"a019bac2927d35a2","type":"debug","z":"a0d20da28d63679b","name":"stderr","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":850,"y":560,"wires":[]},{"id":"236188695d1df68e","type":"debug","z":"a0d20da28d63679b","name":"exit code","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":860,"y":600,"wires":[]},{"id":"42229d0e2c0b39d0","type":"debug","z":"a0d20da28d63679b","name":"stdout","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":500,"wires":[]},{"id":"f6e429e9fbebe8e4","type":"daemon","z":"a0d20da28d63679b","name":"mcp-server","command":"npm","args":"--prefix ./mcp run mcp-server","autorun":false,"cr":true,"redo":true,"op":"string","closer":"SIGKILL","x":690,"y":540,"wires":[["b1f291cd886adf69"],["a019bac2927d35a2"],["236188695d1df68e"]]},{"id":"de7a1ede01fd67e9","type":"inject","z":"a0d20da28d63679b","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":360,"y":580,"wires":[["76d2027a10faf333"]]},{"id":"76d2027a10faf333","type":"change","z":"a0d20da28d63679b","name":"start","rules":[{"t":"set","p":"start","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":580,"wires":[["f6e429e9fbebe8e4"]]},{"id":"b9d3201eb6552ee2","type":"inject","z":"a0d20da28d63679b","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":360,"y":620,"wires":[["4e209c93c728315e"]]},{"id":"4e209c93c728315e","type":"change","z":"a0d20da28d63679b","name":"stop","rules":[{"t":"set","p":"stop","pt":"msg","to":"","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":620,"wires":[["f6e429e9fbebe8e4"]]},{"id":"32a7f33ea7274f9b","type":"inject","z":"a0d20da28d63679b","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":340,"y":440,"wires":[["cf15c6c811a03472"]]},{"id":"cf15c6c811a03472","type":"change","z":"a0d20da28d63679b","name":"listTools","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"jsonrpc\":\"2.0\",\"method\":\"tools/list\",\"params\":{},\"id\":2}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":440,"wires":[["f6e429e9fbebe8e4"]]},{"id":"b1f291cd886adf69","type":"switch","z":"a0d20da28d63679b","name":"JSON 簡易判定 {\"result\":","property":"payload","propertyType":"msg","rules":[{"t":"cont","v":"{\"result\":","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":910,"y":480,"wires":[["22093ad0beeacffc"],["42229d0e2c0b39d0"]]},{"id":"22093ad0beeacffc","type":"json","z":"a0d20da28d63679b","name":"","property":"payload","action":"","pretty":false,"x":970,"y":420,"wires":[["d164792683309bd3"]]},{"id":"d164792683309bd3","type":"debug","z":"a0d20da28d63679b","name":"result","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1090,"y":420,"wires":[]}]
動かしてみる

まず、デプロイすると自動スタートする部分が動作して起動し JSON 以外のデータとして起動ログが文字列で出力されます。

こちらの inject ノードをクリックして、足し算ツールを 5+6 で実行してみます。

11 と MCP サーバーから結果が返ってきました!

こちらの inject ノードをクリックして、ツール一覧を実行してみます。

ツール一覧が返答されます!