@olykov/node-red-contrib-mcp-server-readonly
@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 节点构建任意数量的链,即可暴露一整套工具集。
管理只读 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 的领域角色,任意深度均可。字面上存在的键总是优先,因此真正包含点的声明名仍会解析为自身。只有字符串和字符串数组才能匹配——将声明指向容器对象不会授予任何权限,而不是意外匹配。
字段 | 位置 | 限制范围 |
| mcp-server,Auth 标签页 | 此服务器上的每个工具 |
| mcp-in | 该工具,额外限制 |
| mcp-server,Admin 标签页 |
|
这些列表以 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 的节点,存在以下六条路由:
方法及路径 | 用途 |
| JSON-RPC MCP 端点(受 bearer 令牌保护) |
| 资源元数据(RFC 9728),路径插入形式 |
| 资源元数据(RFC 9728),RFC 8414 形式 |
| 授权服务器元数据(RFC 8414),路径插入形式 |
| 授权服务器元数据(RFC 8414),RFC 8414 形式 |
| 动态客户端注册垫片 |
客户端 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许可证
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP server for Argo RPG Platform — connects AI assistants to campaign data via OAuth2
Self-hosted MCP gateway: turn any API, database or MCP server into AI connectors — no code.
MCP Hub: AI service discovery, per-user OAuth, and multi-service workflow orchestration
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/olykov/node-red-contrib-mcp-server-readonly'
If you have feedback or need assistance with the MCP directory API, please join our Discord server