Skip to main content
Glama
olykov

@olykov/node-red-contrib-mcp-server-readonly

by olykov

@olykov/node-red-contrib-mcp-server-readonly

Node-RED 用の汎用 Model Context Protocol (MCP) サーバーノードです。任意のフローを OAuth で保護されたエンドポイントの背後にある MCP ツールとして公開し、オプションで読み取り専用の Node-RED 管理フロー検査も提供します。ホームオートメーションやその他のドメインに依存しない、Node-RED フローを AI アシスタント(Claude、Codex など)が呼び出せる MCP ツールに変換するための最小限の構成要素です。

0.5.0 での破壊的変更 — パブリッククライアント(PKCE)のみ。 クライアントシークレットとノード側のリダイレクト URI 許可リストは廃止されました。オープンなクライアント登録エンドポイントは、設定されたシークレットをすべての呼び出し元に渡していたためです。また、リダイレクト URI は /authorize で ID プロバイダーによって検証されます。移行方法: IdP クライアントを PKCE 付きパブリック に切り替えてください(機密クライアントのままでは、トークン交換が invalid_client で失敗します)。MCP クライアントのコールバック URL が IdP で許可リストに登録されていることを確認し、ノードが保存済みシークレットについて警告した場合は、その設定を開いて 完了 をクリックし、デプロイして削除してください。アップグレード前に接続していた MCP クライアントが古い登録情報をキャッシュしている可能性があるため、サインインが正しく動作しない場合は、クライアントでサーバーを削除して再追加してください。

ノード

  • mcp-server(設定ノード)— スタンドアロンの MCP JSON-RPC エンドポイントを POST /mcp/<path> でホストします。OAuth 2.0 保護リソースディスカバリ(RFC 9728)、実際の OIDC ID プロバイダーをプロキシする認可サーバーディスカバリ(RFC 8414)、および動的クライアント登録シムを提供するため、OAuth 対応の MCP クライアント(例: Claude.ai)が自己登録して認証できます。複数の mcp-server ノードを共存させることができ、それぞれが独自のパスと独立した認証設定を持ちます。

  • mcp-in — 1 つの MCP ツール(名前、説明、JSON-Schema パラメータ、オプションのツール単位アクセスゲート)を定義します。MCP クライアントがツールを呼び出すと、ノードは呼び出し引数を運ぶメッセージを出力します。実際の処理を行うために、フローの残りを配線してください。msg.payload 内の引数は 信頼できない呼び出し元の入力 です。JSON スキーマはモデル向けのドキュメントであり、検証ではありません。そのため、シェルコマンド、ファイルパス、URL、クエリで使用する前に、フローで検証してエスケープする必要があります。

  • mcp-out — 保留中のツール呼び出しを解決します。フローの終端に、msg._mcpCallId を(元の mcp-in メッセージから)保持し、msg.payload に結果を設定して配線します。

1 つの mcp-in → ... → mcp-out チェーンが 1 つの MCP ツールになります。同じ mcp-server ノードに対して必要な数のチェーンを構築して、ツールセット全体を公開できます。

Related MCP server: nr-mcp

管理読み取り専用 API ツール

mcp-server ノードで 管理読み取り専用 API ツール を有効にすると、Node-RED 独自の管理 HTTP API を操作するツールが 1 つ追加で公開されます。これは設定可能な JWT クレーム(デフォルト: groups に admin が含まれる)によってゲートされます。

  • get_flow — すべてのフロータブ(id、ラベル、ノード数)を一覧表示するか、id を指定して呼び出すと 1 つのタブの完全な JSON を返します。

mcp-server ノードの設定

  • 一般: 名前、path(→ POST /mcp/<path> を登録)、この Node-RED インスタンスが到達可能な公開 Server URL、モデルに表示されるオプションのサーバー名/説明、およびオプションの ホスト名フィルター(下記参照)。

  • 認証: OIDC Identity provider の発行者 URL(必須 — エンドポイントは /.well-known/openid-configuration から自動検出され、PocketID スタイルのフォールバックパスもサポート。これを空にすると、相対パスのエンドポイントと動作しない認証を持つ壊れた OAuth ディスカバリドキュメントが生成されるため、エディタはデプロイを許可しません)、クライアント ID(IdP クライアントは PKCE 付きパブリック である必要があります — クライアントシークレットはサポートされなくなり、リダイレクト URI は IdP でのみ設定・検証されます)、スコープ、トークンオーディエンス、IdP を完全にバイパスしてローカルテストを行うためのオプションのローカルデバッグトークン(Identity provider に任意のプレースホルダー URL を入れ、デバッグトークンが一致する場合は決して接触されません。デバッグユーザーが取得する groups クレームは設定可能で、アクセスゲートもローカルでテストできます)、および Access claim / Server access ゲート(下記参照)。

  • 管理: 管理読み取り専用 API ツールの有効/無効、管理トークン(Node-RED 管理 API 用)、管理 API ポート、および読み取り専用管理ツールのみをさらに制限する Read-only access ゲート。

アクセス制御

1 つのクレーム名、複数の値リスト。 認証タブの Access claim(デフォルト: groups)は、すべてのゲートが照合する単一の JWT クレームを指定します。他のすべての認証フィールドは、そのクレームの値のカンマ区切りの any-of リストです。media, ops は、クレームがそれらの少なくとも 1 つを含む場合に通過します。空のリストは制限を課しません。

ネストされたクレーム は、プロバイダーがトークンの最上位にロールを配置しない場合に、ドット区切りのパスで指定できます。realm_access.roles は Keycloak のレルムロールを読み取ります。任意の深さで機能します。リテラルに存在するキーが常に優先されるため、名前にドットが含まれるクレームでも、それ自体として解決されます。文字列と文字列の配列のみが一致します。クレームがコンテナオブジェクトを指している場合、誤って一致することはなく、何も許可されません。

フィールド

場所

制限対象

Server access

mcp-server、認証タブ

このサーバーのすべてのツール

Tool access

mcp-in

その 1 つのツール(追加で)

Read-only access

mcp-server、管理タブ

get_flow(追加で)

リストは AND で結合されます。 ツールに到達するには、サーバーのリスト かつ そのツール自身のリストをクリアする必要があります。管理読み取り専用 API ツールは特別なケースではありません。そのフィールドは単に get_flow のツールリストです。

Access claim: groups     Server access: staff
tool A: (empty)   tool B: media   Admin access: admin

groups=[staff]         → A
groups=[staff, media]  → A, B
groups=[staff, admin]  → A + get_flow
groups=[media]         → nothing            (server list not cleared)
groups=[guest]         → nothing

Server access empty:
groups=[media]         → A, B
groups=[guest]         → A

有効なトークンを持つすべてのユーザーは依然として接続できます — initialize は常に成功します — ただし、呼び出し元が到達できないツールは tools/list と initialize の説明から隠されます。それらの 1 つに対する直接の tools/call は、MCP ツールの結果として isError: true と説明メッセージ付きで拒否されます(生の JSON-RPC プロトコルエラーではありません)。そのため、理由が「ツール実行失敗」という一般的なエラーにまとめられることなく、呼び出し元のモデルに届きます。

クライアント側: 必須スコープ

上記のリストは このユーザーが何をしてよいか に答えます。Required scope は別の質問 — このクライアントがユーザーに代わって何を行うことを承認されたか — に答え、両者は AND でチェックされます。

これらは交換可能ではありません。グループはキーボードの前にいる人物を示し、スコープはその人物の権限のうち、トークンを保持するソフトウェアに委任された量を示します。これらを 1 つのフィールドにまとめると、片方だけが考慮されます。読み取り専用スコープを付与されたクライアントが、書き込み可能な人物によって操作されている場合、書き込みが行われます。クライアントの付与はユーザーの権利を制限するものであり、無視されるべきではありません。

必須スコープは scopes_supported に自動的に追加されるため、スコープフィールドで繰り返す必要はありません。また、401 の WWW-Authenticate チャレンジで名前が示されます。

スコープクレームは OAuth の定義に従って読み取られます(RFC 6749 §3.3): スペース区切りの文字列、またはプロバイダーが配列を送信する場合は配列です。クレーム名は標準化されているため設定できません。Microsoft Entra と Okta のフォールバックとして scp も読み取られます。フィールド自体はカンマ区切りの any-of リストです。空は制約なしを意味するため、入力しないインストールには影響しません。設定されたスコープをトークンが保持していない場合(トークンにスコープクレームがまったくない場合を含む)は拒否されます。

アップグレード時の注意: 管理ゲートには独自のクレーム名フィールドがなくなり、他のすべてと同様に認証タブの Access claim に対して照合されます。管理ツールに 異なる クレーム名を設定していた場合は、その値を認証タブに移動するか、管理リストを調整してください。リテラルにカンマを含む値は、単一の文字列ではなくリストとして読み取られるようになりました。ゲートフィールドのラベルも変更されました(Required claim/Required value → Access claim/Server access/Admin access)。基になる設定は変更されていないため、既存のフローはそのまま動作し続けます。

プロトコル

エンドポイントは、プレーンな HTTP POST で MCP プロトコルバージョン 2024-11-05 を話します。各リクエストは 1 つの JSON-RPC メッセージであり、各レスポンスは 1 つの JSON ボディです。initialize、tools/list、tools/call、ping がサポートされています。SSE/ストリーミングの GET チャネルやサーバー発信メッセージはありません。これは、今日の OAuth 対応 MCP クライアント(例: Claude)がツールのみのサーバーに対して実際に使用するサブセットです。アドバタイズされるバージョンは、クライアントの提案をエコーするのではなく、意図的に固定されています。

ホスト名フィルタリング

デフォルトではオフです。Only serve requests for this hostname を有効にすると、ノードは Host ヘッダーが Server URL のホスト名と一致するリクエストのみに応答します。これにより、複数の mcp-server ノードが 1 つの Node-RED インスタンス上で 同じ path を共有し、それぞれが独自の仮想ホストに応答できます。これは、複数のホスト名を 1 つの Node-RED バックエンドにプロキシするリバースプロキシの背後で役立ちます。単一サーバーの場合、またはリバースプロキシが Host ヘッダーを書き換える場合は、オフのままにしてください。

リバースプロキシ

各 mcp-server ノードは独自の OAuth リソースです。単一の共有 MCP エンドポイントとは異なり、各インスタンスは 独自の ディスカバリおよび登録ルートを登録し、その path の下にスコープされます。path: docker と Server URL: https://mcp.example.com のノードの場合、次の 6 つのルートが存在します。

メソッドとパス

目的

POST /mcp/docker

JSON-RPC MCP エンドポイント(ベアラートークン保護)

GET /mcp/docker/.well-known/oauth-protected-resource

リソースメタデータ(RFC 9728)、パス挿入形式

GET /.well-known/oauth-protected-resource/mcp/docker

リソースメタデータ(RFC 9728)、RFC 8414 形式

GET /mcp/docker/.well-known/oauth-authorization-server

認可サーバーメタデータ(RFC 8414)、パス挿入形式

GET /.well-known/oauth-authorization-server/mcp/docker

認可サーバーメタデータ(RFC 8414)、RFC 8414 形式

POST /mcp/docker/oauth/register

動的クライアント登録シム

クライアント ID メタデータドキュメント(CIMD)。 MCP 2026-07-28 は、動的クライアント登録を CIMD に置き換えます。CIMD では、クライアントの ID は、クライアント自身がホストするメタデータドキュメントの HTTPS URL です。このノードは、IdP のディスカバリドキュメントが示す内容をミラーリングして client_id_metadata_document_supported をアドバタイズします。ここで設定されることはありません。なぜなら、クライアント ID を解決するのは IdP であり、このサーバーは IdP が持っていないサポートを約束できる立場にないからです。ディスカバリはノードの存続期間中 1 回取得されキャッシュされるため、IdP で CIMD を有効または無効にした場合、次の Node-RED 再起動またはデプロイ時に反映されます(ライブではありません)。

DCR シムはデフォルトでオフであり、オフのままにすべきです。 これは、CIMD を使用できないクライアントが、DCR を実行できない IdP と通信するという 1 つの状況のために存在します。オンにすると、このサーバーは それ自体 を認可サーバーとしてアドバタイズし、登録エンドポイントを発見可能にします。これは、IdP が返す iss がクライアントが記録した発行者と一致しないことも意味し、RFC 9207(MCP 2026-07-28 で必須)を強制するクライアントはフローを完了することを拒否します。オフの場合、クライアントは IdP に直接送られ、CIMD または事前登録済みのクライアント ID を使用する必要があります。このスイッチが存在する前に設定されたノードは、これまで行ってきたことと同様に、シムをオンのままにします。

両方のメカニズムは意図的に利用可能なまま残されています。クライアントは仕様の順序(事前登録 → CIMD → DCR)で選択するため、CIMD をサポートしていないクライアントは従来どおり登録シムをそのまま使用し続けます。各クライアントがどのメカニズムを採用したかはログから確認できます。再起動後に CIMD クライアントが初めて確認されたときは MCP CIMD client authenticated: <url>、IdP が CIMD をアドバタイズしているにもかかわらず登録したクライアントは MCP DCR fallback と記録されます。この2つの行で、サーバーに到達するすべてのクライアントを把握できます。

CIMD クライアントからのトークンは、事前登録されたクライアント ID ではなく、そのドキュメント URL をオーディエンスとして保持し、IdP が CIMD をアドバタイズしている限り受け入れられます。このノードは独自の2つ目の許可リストを保持しないため、IdP の受け入れメタデータドキュメントリストが境界となり、リスト上の CIMD クライアントはすべてこのサーバーに到達でき、クレームゲートが残りのチェックとなります。

両方の well-known 形式がアドバタイズされるのは、MCP クライアントによってプローブする形式が異なるためです — 両方を公開してください。すべてのインスタンスのルートは /mcp/<path> と /.well-known/*/mcp/<path> の形状を共有するため、1セットのワイルドカードルールで現在および将来のすべての mcp-server ノードをカバーできます(すべてが同じドメイン/アップストリーム経由で到達可能である限り)— 新しい path を追加してもリバースプロキシの変更は不要です。例として、caddy-docker-proxy ラベル経由の Caddy を使用する場合:

labels:
  caddy_1: mcp.example.com
  caddy_1.reverse_proxy_0: /mcp/* "{{upstreams 1880}}"
  caddy_1.reverse_proxy_1: /.well-known/oauth-protected-resource/mcp/* "{{upstreams 1880}}"
  caddy_1.reverse_proxy_2: /.well-known/oauth-authorization-server/mcp/* "{{upstreams 1880}}"

Node-RED 自体は、実際に登録されたルートではないパスには 404 を返すため、ワイルドカードは各デプロイ済み mcp-server ノードがすでに登録しているもの以外は何も公開しません。path を他のノードとは異なるドメインで到達可能にする必要がある場合は、独自の caddy_N サイトブロックを指定するか(または上記のホスト名フィルタリングと組み合わせて)、対応してください。

ID プロバイダーがサポートする必要があるもの(lib/mcp-auth.js と同じ要件):

  • ディスカバリ対応の OIDC プロバイダー — エンドポイントは ‹issuerUrl›/.well-known/openid-configuration から読み取られ、ディスカバリが利用できない場合は PocketID のパスレイアウトにフォールバックします。

  • プロバイダーの JWKS で公開されたキーで署名された JWT アクセストークン(トークンはローカルで検証されます。不透明/イントロスペクション専用のアクセストークンはサポートされていません)。

  • PKCE (S256) を使用したパブリッククライアント、grant タイプ authorization_code + refresh_token、および MCP クライアントの リダイレクト URI がホワイトリストに登録されていること(Claude.ai の場合: https://claude.ai/api/mcp/auth_callback)。リダイレクト URI は ID プロバイダー側でのみ設定・検証されます。ノードは独自の許可リストを保持しなくなったため、IdP のワイルドカードサポート(PocketID など)はそのまま機能します。クライアントシークレットはサポートされなくなりました。オープンなクライアント登録エンドポイントは、設定されたシークレットをすべての呼び出し元に渡していたため、実際には秘密にすることはできませんでした。以前のバージョンからシークレットがまだ保存されている場合は、警告付きで無視されます — IdP クライアントをパブリックに切り替え、ノードの設定を開いて Done をクリックし、デプロイして保存されたシークレットを削除し、警告を解消してください。

Caddy(リバースプロキシ)+ PocketID(ID プロバイダー)+ Claude.ai および Hermes(MCP クライアント)でテスト済み。JWT アクセストークンを発行する仕様準拠の OIDC プロバイダーで、上記のルートを転送するリバースプロキシの背後にあれば、同様に動作するはずです。

例

examples/ には、インポート可能な9つのフロー(Jellyfin、Calibre、Docker、Music Assistant、Radarr、iRobot/rest980、Overseerr、Sonarr、Spotify)があり、それぞれに独自の mcp-server ノード(サーバー説明は事前入力済み、Server URL/Identity provider は入力用に空白)と mcp-in/mcp-out ツールが含まれています — 独自のツールを配線する際の参考になります。

開発

npm install
npm test

ライセンス

ISC

Related MCP Connectors

Related MCP Servers