2025 年 5 月時点 Node-RED から node-red-node-daemon を使って stdio 標準入出力 MCP サーバーと連携するメモ

2025 年 5 月時点 Node-RED から node-red-node-daemon を使って stdio 標準入出力 MCP サーバーと連携するメモ

2025 年 5 月時点 Node-RED から node-red-node-daemon を使って stdio 標準入出力 MCP サーバーと連携するメモです。

背景

あくまで2025 年 5 月時点の仕様での試してみたお話です。ここから仕様が変わったり、より良いアプローチが見つかるかもしれません。

こちらで 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 系は入れません。

modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients

こちらをもとに、

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 ノードをクリックして、ツール一覧を実行してみます。

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