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)服务器节点:将任意流程作为 MCP 工具暴露在受 OAuth 保护的端点之后,并可选提供 Node-RED 管理流程的只读检查。不绑定家庭自动化或任何其他领域——这是一个纯粹的构建模块,用于将 Node-RED 流程变成 AI 助手(Claude、Codex 等)可以调用的 MCP 工具。

0.5.0 中的破坏性变更——仅支持公共客户端(PKCE)。 客户端密钥和节点端重定向 URI 白名单已被移除:开放的客户端注册端点会把任何已配置的密钥交给每个调用者,而重定向 URI 反正是在身份提供方的 /authorize 处验证的。迁移: 将 IdP 客户端切换为带 PKCE 的公共客户端(仍然保密的客户端会因 invalid_client 而无法完成令牌交换),确保 MCP 客户端回调 URL 在 IdP 处被列入白名单,如果节点警告存在已存储的密钥,请打开其配置,点击完成,然后部署以删除它。升级前已连接的 MCP 客户端可能缓存了旧注册信息——如果登录行为异常,请在客户端中移除并重新添加服务器。

节点

  • mcp-server(配置节点)——在 POST /mcp/<path> 上托管独立的 MCP JSON-RPC 端点,提供 OAuth 2.0 受保护资源发现(RFC 9728)、授权服务器发现(RFC 8414,代理真实的 OIDC 身份提供方),以及动态客户端注册垫片,使支持 OAuth 的 MCP 客户端(例如 Claude.ai)能够自行注册并完成认证。多个 mcp-server 节点可以共存,每个节点有自己的 path 和独立的认证配置。

  • mcp-in —— 定义一个 MCP 工具(名称、描述、JSON-Schema 参数,以及可选的按工具访问门)。当 MCP 客户端调用该工具时,节点会发出携带调用参数的消息;将流程的其余部分连接起来以完成实际工作。msg.payload 中的参数是不可信的调用者输入——JSON schema 只是给模型的文档,而非验证——因此流程在使用这些参数于 shell 命令、文件路径、URL 或查询之前,必须对其进行验证和转义。

  • mcp-out —— 解析一个挂起的工具调用。将流程的末端连接到这里,保持 msg._mcpCallId 完整(来自源头的 mcp-in 消息),并将 msg.payload 设置为结果。

一条 mcp-in → ... → mcp-out 链就是一个 MCP 工具。针对同一个 mcp-server 节点构建任意数量的链,即可暴露一整套工具集。

Related MCP server: nr-mcp

管理只读 API 工具

在 mcp-server 节点上启用 Admin read-only API tools,可额外暴露一个基于 Node-RED 自身 Admin HTTP API 的工具,由可配置的 JWT 声明(默认:groups 包含 admin)进行门控:

  • get_flow —— 列出所有流程标签页(id、label、节点数量),或在使用 id 调用时返回单个标签页的完整 JSON。

配置 mcp-server 节点

  • General:名称、path(→ 注册 POST /mcp/<path>)、此 Node-RED 实例可访问的公共 Server URL、可选的服务器名称/指令(显示给模型),以及可选的主机名过滤器(见下文)。

  • Auth:一个 OIDC Identity provider 签发者 URL(必填——端点从 /.well-known/openid-configuration 自动发现,并支持 PocketID 风格的备用路径;留空会产生一个带有相对路径端点且无有效认证的损坏 OAuth 发现文档,因此编辑器不会让你在缺少它的情况下部署)、客户端 ID(IdP 客户端必须是带 PKCE 的公共客户端——不再支持客户端密钥,重定向 URI 仅在 IdP 处配置和验证)、作用域、令牌受众、一个可选的本地调试令牌(完全绕过 IdP 用于本地测试——在 Identity provider 中放入任何占位 URL 并依赖调试令牌——当调试令牌匹配时永远不会联系 IdP;调试用户获得的 groups 声明是可配置的,因此访问门也可以本地测试),以及 Access claim / Server access 门(见下文)。

  • Admin:启用/禁用管理只读 API 工具、管理令牌(用于 Node-RED Admin API)、管理 API 端口,以及额外限制只读管理工具的 Read-only access 门。

访问控制

一个声明名,多个值列表。 Auth 标签页上的 Access claim(默认 groups)指定了每个门匹配的单个 JWT 声明。所有其他授权字段都是该声明值的逗号分隔任意匹配列表——media, ops 表示声明包含其中至少一个值即通过。空列表表示不施加任何限制。

嵌套声明使用点分路径寻址,适用于不将角色放在令牌顶层的提供方:realm_access.roles 读取 Keycloak 的领域角色,任意深度均可。字面上存在的键总是优先,因此真正包含点的声明名仍会解析为自身。只有字符串和字符串数组才能匹配——将声明指向容器对象不会授予任何权限,而不是意外匹配。

字段

位置

限制范围

Server access

mcp-server,Auth 标签页

此服务器上的每个工具

Tool access

mcp-in

该工具,额外限制

Read-only access

mcp-server,Admin 标签页

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 指令中隐藏。直接对其中一个调用 tools/call 会以 isError: true 和解释性消息的 MCP 工具结果被拒绝(而非原始 JSON-RPC 协议错误),因此原因会到达调用模型,而不是被折叠成通用的“工具执行失败”。

客户端轴:必需作用域

上述列表回答的是 此用户能做什么。Required scope 回答的是另一个问题——此客户端被授权代表用户做什么——两者以 AND 方式检查。

它们不可互换。组声明说明谁在键盘前;作用域说明该人的多少权限被委托给了持有令牌的软件。将它们合并为一个字段只会导致只检查其中一个:一个被授予只读作用域的客户端,由可能具有写权限的人驱动,就会执行写操作。客户端的授权必须约束用户的权利,而不是被忽略。

必需作用域会自动添加到 scopes_supported 中,因此无需在作用域字段中重复,并且会在 401 响应中的 WWW-Authenticate 挑战中命名。

作用域声明按照 OAuth 的定义读取(RFC 6749 §3.3):一个空格分隔的字符串,或者如果提供方发送数组则为数组。声明名不可配置,因为它是标准化的;scp 作为 Microsoft Entra 和 Okta 的备用读取。该字段本身是一个逗号分隔的任意匹配列表。空表示无约束,因此从未填写它的安装不受影响;令牌未携带的已配置作用域会被拒绝,包括令牌完全没有作用域声明的情况。

升级: 管理门不再有自己的声明名字段——它像其他所有字段一样匹配 Auth 标签页的 Access claim。如果你曾为管理工具设置了 不同 的声明名,请将该值移到 Auth 标签页或相应调整管理列表。字面上包含逗号的值现在被读取为列表而不是单个字面字符串。门字段也已重新标记(Required claim/Required value → Access claim/Server access/Admin access);底层设置未变,因此现有流程无需修改即可继续工作。

协议

端点使用 MCP 协议版本 2024-11-05,通过普通 HTTP POST 通信——每个请求是一个 JSON-RPC 消息,每个响应是一个 JSON 主体。支持 initialize、tools/list、tools/call 和 ping;没有 SSE/流式 GET 通道,也没有服务器发起的消息。这是当今支持 OAuth 的 MCP 客户端(例如 Claude)针对纯工具服务器实际使用的子集。广告的版本是故意固定的,而不是回显客户端的提议。

主机名过滤

默认关闭。启用 Only serve requests for this hostname 时,节点仅响应 Host 头与其 Server URL 中的主机名匹配的请求。这允许多个 mcp-server 节点在一个 Node-RED 实例上共享 相同 的 path,每个节点只响应自己的虚拟主机——在反向代理为单个 Node-RED 后端提供多个主机名时很有用。对于单个服务器,或当反向代理重写 Host 头时,请保持关闭。

反向代理

每个 mcp-server 节点都是自己的 OAuth 资源——与单个共享 MCP 端点不同,每个实例都注册自己的发现和注册路由,范围限定在其 path 下。对于 path: docker 和 Server URL: https://mcp.example.com 的节点,存在以下六条路由:

方法及路径

用途

POST /mcp/docker

JSON-RPC MCP 端点(受 bearer 令牌保护)

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,其中客户端的 ID 是其自行托管的元数据文档的 HTTPS URL。此节点通过镜像 IdP 发现文档的内容来通告 client_id_metadata_document_supported——它永远不会在此处配置,因为解析客户端 ID 的是 IdP,而此服务器无法承诺 IdP 不提供的支持。发现信息只获取一次,并在节点生命周期内缓存,因此在 IdP 上启用或禁用 CIMD 会在下次 Node-RED 重启或部署时生效——而不是实时生效。

DCR 垫片默认关闭,应保持关闭。 它只适用于一种情况:一个无法使用 CIMD 的客户端,与一个自身无法执行 DCR 的 IdP 通信。当它开启时,此服务器会通告自身为授权服务器,以便注册端点可被发现——这也意味着您的 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。这两行日志合在一起,涵盖了到达该服务器的每一个客户端。

来自 CIMD 客户端的令牌会将该文档 URL 作为其受众,而不是您预先注册的客户端 ID,并且只要 IdP 通告了 CIMD,这些令牌就会被接受。此节点自身不维护第二个允许列表,因此 IdP 的已接受元数据文档列表就是边界——列表上的任何 CIMD 客户端都可以访问此服务器,而声明门控是剩余的检查。

两种 well-known 形式都会被通告,因为不同的 MCP 客户端会探测不同的形式——请同时暴露两者。由于每个实例的路由都共享 /mcp/<path> 和 /.well-known/*/mcp/<path> 的形状,一组通配符规则即可覆盖当前和未来的所有 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 站点块(或与上面的主机名过滤结合使用)。

身份提供程序需要支持的内容(与 lib/mcp-auth.js 的要求相同):

  • 支持发现功能的 OIDC 提供程序——端点从 ‹issuerUrl›/.well-known/openid-configuration 读取,如果发现不可用,则回退到 PocketID 的路径布局。

  • 使用提供程序 JWKS 上发布的密钥签名的 JWT 访问令牌(令牌在本地验证;不支持不透明/仅限 introspection 的访问令牌)。

  • 一个公共客户端,支持 PKCE (S256)、授权类型 authorization_code + refresh_token,并将 MCP 客户端的重定向 URI 列入白名单(对于 Claude.ai:https://claude.ai/api/mcp/auth_callback)。重定向 URI 仅在身份提供程序处配置和验证——节点不再维护自己的允许列表,因此 IdP 的通配符支持(例如 PocketID 的)可以原样工作。不再支持客户端密钥:开放的客户端注册端点会将任何已配置的密钥交给每个调用者,因此它实际上永远无法保密。如果早期版本仍存储了密钥,它将被忽略并显示警告——请将 IdP 客户端切换为公共,然后打开节点配置,单击“完成”并部署以删除存储的密钥并清除警告。

已使用 Caddy(反向代理)+ PocketID(身份提供程序)+ Claude.ai 和 Hermes(MCP 客户端)进行测试。任何符合规范、签发 JWT 访问令牌的 OIDC 提供程序,只要位于转发上述路由的任何反向代理之后,都应该以相同方式工作。

示例

请参阅 examples/ 获取九个可直接导入的流程(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

许可证

Related MCP Connectors

Related MCP Servers