2025 年 5 月時点 Streamable HTTP MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモ

2025 年 5 月時点 Streamable HTTP MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモ

2025 年 5 月時点 Streamable HTTP MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモです。

背景

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

2025 年 5 月時点 stdio 標準入出力 MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモ でうまくいった仕組みを、Streamable HTTP MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するものにしてみます。

こんな仕組みです。

足し算ツールを使うだけの Streamable HTTP MCP クライアントを TypeScript で作る

足し算ツールを使うだけの Streamable HTTP MCP クライアントを TypeScript で作ります。ツール推定はせずに「2+3」と打ち込んだら足し算をするものです。

2025 年 5 月時点 stdio 標準入出力 MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携するメモ では Claude Desktop で行っていましたが、まだ Streamable HTTP MCP サーバーへつながるクライアントに仕組みが広まってない 2025 年 5 月ごろなので TypeScript SDK で自作します。

フォルダ準備

mkdir mcp-client-streamable-http

まず mcp-client-streamable-http というフォルダを作ります。

cd mcp-client-streamable-http

フォルダに移動します。

npm 初期設定

npm init -y

npm の初期設定をします。

このようなファイル構成になりました。

{
  "name": "mcp-client-streamable-http",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

package.json ができあがりました。

"type": "module",
  "scripts": {
    "mcp-client-streamable-http": "node --no-warnings --loader ts-node/esm mcp-client-streamable-http.ts"
  }

package.json に type module で ESM で動くようにして、mcp-client-streamable-http で、このあと作成する mcp-client-streamable-http.ts のプログラムを TypeScript で動作させるコマンドを準備しておきます。

モジュールのインストール

npm install @modelcontextprotocol/sdk

と、

npm install --save-dev typescript ts-node @types/node

でモジュールをインストールします。

tsconfig.json の準備

tsconfig.json もフォルダ内に準備します。

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "types": ["node"],
    "strict": true
  },
  "include": ["**/*.ts"]
}

これらの作業を終えて package.json は以下のようになりました。

{
  "name": "mcp-client-streamable-http",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "mcp-client-streamable-http": "node --no-warnings --loader ts-node/esm mcp-client-streamable-http.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0"
  },
  "devDependencies": {
    "@types/node": "^22.15.21",
    "ts-node": "^10.9.2",
    "typescript": "^5.8.3"
  }
}

mcp-client-streamable-http.ts に MCP クライアントのプログラムを作成

mcp-client-streamable-http.ts で MCP クライアントのプログラムを作成します。

modelcontextprotocol/typescript-sdk: The official Typescript SDK for Model Context Protocol servers and clients の Writing MCP Clients を参考に作りました。

内容は以下の通りです。

// mcp-client-streamable-http.ts
import readline from "readline";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

async function main() {
    const transport = new StreamableHTTPClientTransport(
        new URL("http://localhost:3000/mcp"),
        { sessionId: "stateful-001" }
    );

    const client = new Client({
        name: "add-cli",
        version: "1.0.0",
    });
    await client.connect(transport);

    const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

    console.log("============================================================");
    console.log("足し算ツールのみ呼び出し MCPクライアント Streamable HTTP 方式");
    console.log("例: 3+4 の形式で入力してください");
    console.log("============================================================");

    rl.setPrompt("> ");
    rl.prompt();

    rl.on("line", async (line) => {
        const m = line.trim().match(/^(\d+)\s*\+\s*(\d+)$/);
        if (!m) {
            console.log("例: 3+4 の形式で入力してください");
        } else {
            const a = Number(m[1]);
            const b = Number(m[2]);

            console.log(`${a}${b} を足します。\n`);

            try {
                const { content } = await client.callTool({
                    name: "add",
                    arguments: { a, b }
                });

                console.log(`MCP サーバーからの結果 : ${JSON.stringify(content)}\n`);

                if (Array.isArray(content)) {
                    content.forEach((c) => {
                        if (c.type === "text") {
                            console.log(`答え : ${c.text}`);
                        }
                    });
                }
            } catch (e) {
                console.error("ツール呼び出しに失敗:", e);
            }
        }

        rl.prompt();
    });

    rl.on("close", () => {
        transport.close?.();
        process.exit(0);
    });
}

main().catch((e) => {
    console.error(e);
    process.exit(1);
});

現状のファイル構成

このようなファイル構成になっています。

ブリッジする Streamable HTTP MCP サーバーを TypeScript で作る

Node.js v18.19.0 で動かしています。fetch 関数を素直に動かすためです。これ以上のバージョンであれば fetch 関数は問題なく動きます。

フォルダ準備

cd ..

一旦、プロジェクトフォルダに戻ります。

mkdir streamable-server-node-red-bridge

まず streamable-server-node-red-bridge というフォルダを作ります。

cd streamable-server-node-red-bridge

フォルダに移動します。

npm 初期設定

npm init -y

npm の初期設定をします。

このようなファイル構成になりました。

{
  "name": "streamable-server-node-red-bridge",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

package.json ができあがりました。

"type": "module",
  "scripts": {
    "streamable-server-node-red-bridge": "node --no-warnings --loader ts-node/esm streamable-server-node-red-bridge.ts"
  }

package.json に type module で ESM で動くようにして、streamable-server-node-red-bridge で、このあと作成する streamable-server-node-red-bridge.ts のプログラムを TypeScript で動作させるコマンドを準備しておきます。

モジュールのインストール

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

こちらをもとに、

npm install @modelcontextprotocol/sdk zod express

npm install -D typescript ts-node @types/node @types/express

を実行してモジュールの準備をします。

streamable-server-node-red-bridge.ts 用意

こちらの記事の Running Your Server の stdio のソースを元に足し算のツールの入り口と Node-RED への実際の足し算の処理をつなげる streamable-server-node-red-bridge.ts で用意します。

// streamable-server-node-red-bridge.ts
import express, { Request, Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';

const app = express();
app.use(express.json());

// ─── サーバー/トランスポートをセッションIDで管理 ───
type Session = {
  server: McpServer;
  transport: StreamableHTTPServerTransport;
};
const sessions: Record<string, Session> = {};

// ─── MCP サーバー(ツール登録) ───
function makeServer(): McpServer {
  const server = new McpServer({ name: 'Demo', version: '1.0.0' });
  server.tool(
    'add',
    { a: z.number(), b: z.number() },
    async ({ a, b }) => {
      // Node-RED ブリッジ
      const res = await fetch('http://127.0.0.1:1880/add', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ a, b }),
      });
      if (!res.ok) {
        throw new Error(`Node-RED error: ${res.statusText}`);
      }
      const json = (await res.json()) as { result: number };
      return { content: [{ type: 'text', text: String(json.result) }] };
    }
  );
  return server;
}

// ─── 共通:リクエストを捌く関数 ───
async function handleMcp(
  req: Request,
  res: Response,
  body: any | undefined
) {
  // セッションヘッダー
  const sessionIdHeader = req.headers['mcp-session-id'] as string | undefined;
  const initRequest = body && isInitializeRequest(body);

  let session: Session;

  if (sessionIdHeader && sessions[sessionIdHeader]) {
    // ── 既存セッションを再利用 ──
    session = sessions[sessionIdHeader]!;
  }
  else if (initRequest) {
    // ── 新規ステートフルセッションの初期化 ──
    const server = makeServer();
    const transport = new StreamableHTTPServerTransport({
      // ランダムUUIDでセッションIDを発行
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (sid) => {
        sessions[sid] = { server, transport };
      },
    });
    // 切断時にクリーンアップ
    transport.onclose = () => {
      const sid = transport.sessionId;
      if (sid) delete sessions[sid];
    };

    // 初回は connect が必要
    await server.connect(transport);
    session = { server, transport };
  }
  else {
    // ── ステートレスフォールバック ──
    const server = makeServer();
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });
    // リクエスト完了で即クローズ
    res.on('close', () => {
      transport.close();
      server.close();
    });
    await server.connect(transport);
    session = { server, transport };
  }

  // ── 実際のハンドリング ──
  await session.transport.handleRequest(req, res, body);
}

// ── ルーティング ──
// POST: initialize / tools/list / tools/call など
app.post('/mcp', async (req, res) => {
  try {
    await handleMcp(req, res, req.body);
  } catch (err) {
    console.error('POST /mcp error:', err);
    if (!res.headersSent) {
      res
        .status(500)
        .json({ jsonrpc: '2.0', error: { code: -32603, message: 'Internal error' }, id: null });
    }
  }
});
// GET: SSE ストリーム開始
app.get('/mcp', async (req, res) => {
  try {
    await handleMcp(req, res, undefined);
  } catch (err) {
    console.error('GET /mcp error:', err);
    if (!res.headersSent) {
      res.status(500).send('SSE error');
    }
  }
});
// DELETE: SSE セッション終了
app.delete('/mcp', async (req, res) => {
  try {
    await handleMcp(req, res, undefined);
  } catch (err) {
    console.error('DELETE /mcp error:', err);
    if (!res.headersSent) {
      res.status(500).send('Close error');
    }
  }
});

// サーバー起動
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`&#x2705; MCP server running at http://localhost:${PORT}/mcp`);
});

tsconfig.json

tsconfig.json も準備して、この後、TypeScript を動かす設定もしておきます。

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "types": ["node"],
    "strict": true
  },
  "include": ["**/*.ts"]
}

これらの作業を終えて package.json は以下のようになりました。

{
  "name": "streamable-server-node-red-bridge",
  "version": "1.0.0",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "streamable-server-node-red-bridge": "node --no-warnings --loader ts-node/esm streamable-server-node-red-bridge.ts"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.12.0",
    "express": "^5.1.0",
    "zod": "^3.25.28"
  },
  "devDependencies": {
    "@types/express": "^5.0.2",
    "@types/node": "^22.15.21",
    "ts-node": "^10.9.2",
    "typescript": "^5.8.3"
  }
}

現状のファイル構成

このようなファイル構成になっています。

Node-RED でフローを作る

Node-RED については別途インストールを済ませて起動している前提です。

このようなフローを作りました。

stdio 標準入出力 MCP サーバーから足し算ツールの情報を受け付ける http in ノードです。

POST リクエストで /add というパスから stdio 標準入出力 MCP サーバーから足し算する値を受信します。

実際の足し算をする function ノードです。

// 足し算
const result = msg.payload.a + msg.payload.b;

msg.payload = {};
msg.payload.result = result;

return msg;

ごくシンプルに足し算をして result 値に入れて返しています。

http response ノードは、デフォルトの設定のままでシンプルに返答します。

今回の JSON フローデータです。

[{"id":"c764f7c40134fe66","type":"http in","z":"6b422e4d3cdf58d8","name":"","url":"/add","method":"post","upload":false,"swaggerDoc":"","x":140,"y":140,"wires":[["9ae546d31023c8e0","76fd73c7e1f6391f"]]},{"id":"e69eb256b6f3ad6c","type":"http response","z":"6b422e4d3cdf58d8","name":"","statusCode":"","headers":{},"x":550,"y":140,"wires":[]},{"id":"9ae546d31023c8e0","type":"function","z":"6b422e4d3cdf58d8","name":"足し算","func":"// 足し算\nconst result = msg.payload.a + msg.payload.b;\n\nmsg.payload = {};\nmsg.payload.result = result;\n\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":330,"y":140,"wires":[["e69eb256b6f3ad6c"]]},{"id":"76fd73c7e1f6391f","type":"debug","z":"6b422e4d3cdf58d8","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":340,"y":100,"wires":[]}]

動かしてみる

さきほどの Node-RED は http://127.0.0.1:1880/ で起動しています。

Node-RED へブリッジする MCP サーバーを起動

ターミナルを新しく起動して、

cd streamable-server-node-red-bridge

MCP サーバーのフォルダに移動して

npm run streamable-server-node-red-bridge

を実行します。

MCP サーバーが起動しました。

MCP クライアントを起動

ターミナルを新しく起動して、

cd mcp-client-streamable-http

MCP クライアントのフォルダに移動して

npm run mcp-client-streamable-http

を実行します。

MCP クライアントが起動しました。

足し算ツールのみ呼び出しでツール推定は考えなくて良いものです。数値+数値 の形式で入力します。

3+4

と入力しました。

Node-RED 側に無事にデータが送られて計算処理がされます。

MCP クライアントから MCP サーバーがブリッジして Node-RED に届くまでの流れです。

実行結果が返ってきて足し算結果が無事に表示されました。

Node-RED が計算した結果を MCP サーバーが受け取って JSONRPC 形式で MCP クライアントに返答するの流れです。

あくまで 2025 年 5 月時点ですが Streamable HTTP MCP サーバーをブリッジにして Node-RED が MCP クライアントと連携することができました!