@olykov/node-red-contrib-mcp-server-readonly
Exposes Node-RED flows as callable MCP tools and provides a read-only admin API tool to inspect flow tabs and their node counts.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@olykov/node-red-contrib-mcp-server-readonlylist my Node-RED flow tabs and node counts"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
@olykov/node-red-contrib-mcp-server-readonly
Generic Model Context Protocol (MCP) server nodes for Node-RED: expose any flow as an MCP tool behind an OAuth-protected endpoint, with optional read-only Node-RED admin flow inspection. No home-automation or other domain coupling — this is a bare building block for turning Node-RED flows into MCP tools that AI assistants (Claude, Codex, etc.) can call.
Breaking change in 0.5.0 — public client (PKCE) only. Client secrets and the node-side redirect URI allowlist are gone: the open client-registration endpoint handed any configured secret to every caller, and redirect URIs are validated by the identity provider at
/authorizeanyway. Migration: switch the IdP client to public with PKCE (a still-confidential client fails token exchange withinvalid_client), make sure the MCP client callback URLs are whitelisted at the IdP, and if the node warns about a stored secret, open its config, click Done, and deploy to delete it. MCP clients connected before the upgrade may have cached the old registration — remove and re-add the server in the client if sign-in misbehaves.
Nodes
mcp-server(config node) — hosts a standalone MCP JSON-RPC endpoint atPOST /mcp/<path>, OAuth 2.0 protected-resource discovery (RFC 9728), authorization-server discovery (RFC 8414) proxying a real OIDC identity provider, and a dynamic client registration shim, so OAuth-aware MCP clients (e.g. Claude.ai) can self-register and authenticate. Multiplemcp-servernodes can coexist, each with its own path and its own independent auth configuration.mcp-in— defines one MCP tool (name, description, JSON-Schema parameters, and an optional per-tool access gate). When an MCP client calls the tool, the node emits a message carrying the call arguments; wire the rest of the flow to do the actual work. The arguments inmsg.payloadare untrusted caller input — the JSON schema is documentation for the model, not validation — so the flow must validate and escape them before use in shell commands, file paths, URLs or queries.mcp-out— resolves a pending tool call. Wire the end of your flow here withmsg._mcpCallIdintact (from the originatingmcp-inmessage) andmsg.payloadset to the result.
A single mcp-in → ... → mcp-out chain is one MCP tool. Build as many chains as you want
against the same mcp-server node to expose a whole toolset.
Admin read-only API tools
Enable Admin read-only API tools on an mcp-server node to additionally expose one tool that operates
on Node-RED's own Admin HTTP API, gated by a configurable JWT claim (default: groups
contains admin):
get_flow— lists all flow tabs (id, label, node count), or returns the full JSON of one tab when called with anid.
Configuring an mcp-server node
General: name,
path(→ registersPOST /mcp/<path>), the publicServer URLthis Node-RED instance is reachable at, optional server name/instructions shown to the model, and an optional hostname filter (see below).Auth: an OIDC
Identity providerissuer URL (required — endpoints auto-discovered from/.well-known/openid-configuration, with PocketID-style fallback paths; leaving this empty produces a broken OAuth discovery document with relative-path endpoints and no working auth, so the editor won't let you deploy without it), a client id (the IdP client must be public with PKCE — client secrets are no longer supported, and redirect URIs are configured and validated at the IdP only), scopes, token audience, an optional local debug token that bypasses the IdP entirely for local testing (put any placeholder URL in Identity provider and rely on the debug token — it's never contacted when the debug token matches; thegroupsclaim the debug user gets is configurable so the access gates can be tested locally too), and theAccess claim/Server accessgate (see below).Admin: enable/disable admin read-only API tools, admin token (for the Node-RED Admin API), admin API port, and the
Read-only accessgate that additionally restricts just the read-only admin tools.
Access control
One claim name, many value lists. Access claim on the Auth tab (default groups) names the
single JWT claim every gate matches against. Every other authorization field is a comma-separated
any-of list of that claim's values — media, ops passes if the claim contains at least one of
them. An empty list imposes no restriction.
Nested claims are addressed with a dotted path, for providers that don't put roles at the top
level of the token: realm_access.roles reads Keycloak's realm roles, and any depth works. A key
that exists literally always wins, so a claim genuinely named with a dot in it still resolves to
itself. Only strings and arrays of strings match — pointing the claim at a container object grants
nothing rather than matching by accident.
Field | Where | Restricts |
| mcp-server, Auth tab | every tool on this server |
| mcp-in | that one tool, additionally |
| mcp-server, Admin tab |
|
The lists are combined with AND. Reaching a tool means clearing the server's list and that
tool's own list. Admin read-only API tools are not a special case — their field is simply the
tool list for 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] → AEveryone with a valid token still connects — initialize always succeeds — but tools a caller
can't reach are hidden from tools/list and from the initialize instructions. A direct
tools/call on one of them is refused as an MCP tool result with isError: true and an
explanatory message (not a raw JSON-RPC protocol error), so the reason reaches the calling model
instead of being collapsed into a generic "tool execution failed".
The client axis: required scope
The lists above answer what may this user do. Required scope answers a
different question — what was this client authorized to do on the user's behalf — and the two
are checked with AND.
They are not interchangeable. A group says who is at the keyboard; a scope says how much of that person's authority was delegated to the software holding the token. Collapse them into one field and only one gets consulted: a client granted a read-only scope, driven by someone who may write, would write. The client's grant has to bound the user's rights, not be ignored.
The required scope is added to scopes_supported automatically, so there is nothing to repeat in the scopes field, and it is named in the WWW-Authenticate challenge on a 401.
The scope claim is read the way OAuth defines it
(RFC 6749 §3.3): a space-delimited
string, or an array if your provider sends one. The claim name is not configurable because it is standardised; scp is read as a fallback for
Microsoft Entra and Okta. The field itself is a comma-separated any-of list. Empty means no constraint, so an install that
never fills it in is unaffected; a configured scope the token does not carry is refused,
including when the token has no scope claim at all.
Upgrading: the admin gate no longer has its own claim-name field — it matches against the Auth tab's
Access claimlike everything else. If you had set a different claim name for admin tools, move that value to the Auth tab or adjust the admin list accordingly. A value that literally contains a comma is now read as a list rather than one literal string. The gate fields were also relabelled (Required claim/Required value→Access claim/Server access/Admin access); the underlying settings are unchanged, so existing flows keep working untouched.
Protocol
The endpoint speaks MCP protocol version 2024-11-05 over plain HTTP POST — every request is
one JSON-RPC message, every response one JSON body. initialize, tools/list, tools/call and
ping are supported; there is no SSE/streaming GET channel and no server-initiated messages.
This is the subset today's OAuth-capable MCP clients (e.g. Claude) actually use against a tools-only
server. The advertised version is intentionally pinned rather than echoing the client's offer.
Hostname filtering
Off by default. When Only serve requests for this hostname is enabled, the node only answers
requests whose Host header matches the hostname in its Server URL. This lets several
mcp-server nodes share the same path on one Node-RED instance, each answering only its own
virtual host — useful behind a reverse proxy that fronts multiple hostnames for one Node-RED
backend. Leave it off for a single server, or when a reverse proxy rewrites the Host header.
Reverse proxy
Each mcp-server node is its own OAuth resource — unlike a single shared MCP endpoint, every
instance registers its own discovery and registration routes, scoped under its path. For
a node with path: docker and Server URL: https://mcp.example.com, these six routes exist:
Method & path | Purpose |
| The JSON-RPC MCP endpoint (bearer-token protected) |
| Resource metadata (RFC 9728), path-inserted form |
| Resource metadata (RFC 9728), RFC 8414 form |
| Auth-server metadata (RFC 8414), path-inserted form |
| Auth-server metadata (RFC 8414), RFC 8414 form |
| Dynamic client registration shim |
Client ID Metadata Documents (CIMD). MCP 2026-07-28 deprecates dynamic client registration in favour of CIMD, where a client's id is the HTTPS URL of a metadata document it hosts itself. This node advertises client_id_metadata_document_supported by mirroring what your IdP's discovery document says — it is never configured here, because it is the IdP that resolves the client id, and this server is in no position to promise support the IdP doesn't have. Discovery is fetched once and cached for the lifetime of the node, so enabling or disabling CIMD on the IdP is picked up at the next Node-RED restart or deploy — not live.
The DCR shim is off by default and should stay off. It exists for one situation: a client that cannot use CIMD, talking to an IdP that cannot do DCR itself. When it is on, this server advertises itself as the authorization server so that the registration endpoint is discoverable — which also means the iss your IdP returns will not match the issuer the client recorded, and a client enforcing RFC 9207 (required by MCP 2026-07-28) will refuse to finish the flow. With it off, clients are sent straight to the IdP and must use CIMD or a pre-registered client ID. A node configured before this switch existed keeps the shim on, since that is what it has been doing.
Both mechanisms stay available on purpose. Clients pick in the spec's order — pre-registered, then CIMD, then DCR — so a client without CIMD support keeps using the registration shim exactly as before. Which mechanism each client took is readable from the log: MCP CIMD client authenticated: <url> the first time a CIMD client is seen after a restart, and MCP DCR fallback for a client that registered even though the IdP advertises CIMD. Between them the two lines account for every client that reaches the server.
Tokens from a CIMD client carry that document URL as their audience rather than your pre-registered client id, and they are accepted whenever the IdP advertises CIMD. This node keeps no second allowlist of its own, so the IdP's list of accepted metadata documents is the boundary — any CIMD client on it can reach this server, with the claim gate as the remaining check.
Both well-known forms are advertised because different MCP clients probe different ones —
expose both. Since every instance's routes share the /mcp/<path> and /.well-known/*/mcp/<path>
shapes, one set of wildcard rules covers every current and future mcp-server node (as long
as they're all reachable through the same domain/upstream) — no reverse-proxy change needed when
adding a new path. Example, using
Caddy via caddy-docker-proxy
labels:
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 itself 404s any path that isn't an actual registered route, so the wildcard doesn't
expose anything beyond what each deployed mcp-server node already registers. If a path
needs to be reachable on a different domain than the others, give it its own caddy_N site
block (or combine with hostname filtering above).
What the identity provider needs to support (same requirements as lib/mcp-auth.js):
An OIDC provider with discovery — endpoints are read from
‹issuerUrl›/.well-known/openid-configuration, falling back to PocketID's path layout if discovery is unavailable.JWT access tokens signed with a key published on the provider's JWKS (tokens are verified locally; opaque/introspection-only access tokens are not supported).
A public client with PKCE (S256), grant types
authorization_code+refresh_token, and the MCP client's redirect URI(s) whitelisted (for Claude.ai:https://claude.ai/api/mcp/auth_callback). Redirect URIs are configured and validated at the identity provider only — the node no longer keeps its own allowlist, so IdP wildcard support (e.g. PocketID's) works as-is. Client secrets are no longer supported: the open client-registration endpoint handed any configured secret to every caller, so it could never actually be secret. If a secret is still stored from an earlier version it is ignored with a warning — switch the IdP client to public, then open the node's config, click Done, and deploy to delete the stored secret and clear the warning.
Tested with Caddy (reverse proxy) + PocketID (identity provider) + Claude.ai and Hermes (MCP clients). Any spec-compliant OIDC provider issuing JWT access tokens, behind any reverse proxy that forwards the routes above, should work the same way.
Examples
See examples/ for nine ready-to-import flows (Jellyfin, Calibre, Docker,
Music Assistant, Radarr, iRobot/rest980, Overseerr, Sonarr, Spotify), each with its own
mcp-server node (server description pre-filled, Server URL/Identity provider left
blank for you to fill in) and mcp-in/mcp-out tools — a good reference for wiring up
your own tools.
Development
npm install
npm testLicense
ISC
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