Skip to main content
Glama
olykov

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

by olykov

Node-RED MCP Server

@olykov/node-red-mcp-server is an upstream-first fork of node-red-contrib-mcp-server@1.1.5 for MCP tool runtimes. The scoped name keeps an accidental install of the upstream package from replacing it. Requires Node-RED 5.0 or later and Node.js 22.9 or later.

The package exposes Node-RED flows as endpoint-scoped MCP servers with read-only admin helpers and package-level OAuth support.

Scope

Included nodes:

  • mcp-flow-server

  • mcp-tool-registry

  • mcp-server-metrics

  • mcp-runtime config node

  • mcp-redis config node

  • mcp-auth config node

Local extensions:

  • Runtime config nodes for local MCP listener settings.

  • Endpoint nodes for logical MCP names and paths.

  • Endpoint-scoped and shared tool registration.

  • Automatic provider scopes in _meta.securitySchemes.

  • Optional read-only get_flow tool gated by runtime admin settings and an exact endpoint path.

  • OIDC-backed MCP authorization configuration.

  • Memory or Redis-backed storage for short-lived auth state and opaque access tokens.

  • Bearer token enforcement for protected MCP endpoints.

  • Authorization-code flow bridge with PKCE S256, OIDC ID token validation, and userinfo claim extraction.

  • Tests for the read-only admin boundary and flow-server execution path.

Not included yet:

  • Verified interoperability with hosted MCP clients.

  • Removed local compatibility nodes from earlier fork revisions.

Related MCP server: nr-mcp

Architecture

Node-RED runs MCP tools and exposes MCP endpoints from this package. Authorization routes are registered by the package on the same MCP runtime port; they are not modeled as Node-RED HTTP-in flows.

Expected boundary:

MCP client -> Node-RED MCP package auth layer -> Node-RED MCP flow server -> Node-RED flows

Installation

From a Git reference:

cd ~/.node-red
npm install git+ssh://git@github.com/olykov/node-red-contrib-mcp-server.git#<commit>

For local development:

cd /path/to/node-red-contrib-mcp-server
npm install
npm test
npm link
cd ~/.node-red
npm link @olykov/node-red-mcp-server

Upgrading to 3.2

tools/list now requires a token. Clients that listed tools anonymously must sign in first; Claude Code and Codex do this on the HTTP 401. To roll back, pin the 3.1.0 commit.

Upgrading to 3.1

  1. Set the mcp-runtime Base URL. Endpoints no longer derive OAuth URLs from request headers and do not start without it.

  2. Give every mcp-flow-server at least one Group. An endpoint without groups no longer admits every identity-provider user; it does not start.

  3. Check Client Hosts: entries now match exactly. Use *.example.com to allow subdomains. Client metadata documents must contain a client_id equal to their URL.

  4. Users sign in once more: sign-in now binds the upstream state to the browser with a cookie, so sign-ins started before the upgrade are rejected.

Upgrading to 3.0

3.0 renames the package and changes three contracts. Before deploying it:

  1. Uninstall node-red-contrib-mcp-server from the Node-RED user directory and install @olykov/node-red-mcp-server; the node types keep their names.

  2. Rename editor-supplied environment variables to the MCP_ prefix, for example REDIS_URL to MCP_REDIS_URL, in the runtime environment and in the mcp-redis and mcp-auth configuration nodes. A configuration with another name is reported as invalid and its endpoints do not start.

  3. Open every mcp-tool-registry node and select its endpoint again: tools now bind by MCP path, not by node ID.

  4. The mcp-flow-server output now carries only mcp-tool-execute messages; the mcp-server-started, mcp-server-stopped and status replies were removed, and the runtime no longer serves /health. Endpoint state is shown in the node status.

Existing sessions end once after the upgrade because session storage is no longer keyed by the auth node ID; users sign in again.

Flow Server Extensions

mcp-runtime owns the local HTTP listener: port, base URL and optional admin API settings. Endpoints start automatically on Node-RED's flows:started event. Responses do not enable cross-origin browser access.

Base URL is required. It is the address clients and their browsers use to reach the MCP runtime, directly or through a proxy. It may be VPN-only; public internet exposure is not required. Changing that address requires updating the runtime configuration, provider callback registration and client endpoint URLs, not OAuth code. The runtime listener currently serves HTTP. HTTPS must terminate outside that listener; removing TLS termination entirely requires separate HTTPS runtime support. This is independent of whether the endpoint is VPN-only or internet-facing.

mcp-redis defines storage for short-lived authorization state and opaque access tokens. Memory mode is for local development only. Redis-backed modes are intended for shared or restarted runtimes.

mcp-auth selects its active identity provider using the Microsoft Entra ID or OIDC tab. Each provider keeps separate client settings and secrets. Shared settings select storage, client metadata hosts and token TTLs. Provider scopes are fixed in code, not editable fields. Secrets are stored as Node-RED credentials or read from environment variables. Authorization state and one-time authorization codes expire after a fixed 300 seconds. Auth configurations have no separate enable/disable switch.

Set an explicit shared OAuth slug, for example team-login. It must start with a lowercase letter and contain only lowercase letters, digits or hyphens (1-63 characters). Register https://mcp.example.test/oauth/team-login/callback with the selected provider. Authorize, token and authorization-server metadata routes use the same slug, not the Node-RED node ID. Keep the slug when replacing an auth node; the registered callback stays unchanged and, with the same provider settings, existing sessions continue. Separate auth configurations cannot share a slug on the same listener or base URL. Multiple endpoints can share one auth configuration. There is no fallback or alias for node-ID routes.

OIDC uses an HTTPS issuer URL, client ID and UserInfo claim names; its base scopes are fixed to openid profile email groups. Microsoft uses a tenant GUID and application client GUID; its tenant-specific v2 issuer, openid profile email scopes and roles claim are fixed. Microsoft permissions come only from the verified ID token; at least one assigned app role is required. Endpoint Groups (separated by commas or spaces) must match the role values used for that endpoint. No Graph directory permissions are needed.

Authorization state, codes and access tokens are isolated by auth node, provider, issuer, client ID, claim settings and OAuth slug, even when storage is shared. Switching provider/configuration requires a new login. Existing configurations must explicitly select a provider and be saved; there is no legacy-provider inference. Switching back to an identical configuration can reuse its unexpired tokens; switching is not a token-revocation mechanism.

Client metadata hosts must be allow-listed. Entries match the host exactly; *.example.com allows its subdomains. This prevents the authorization endpoint from fetching arbitrary user-provided URLs during client metadata validation. Private and loopback addresses (including bracketed IPv6 and IPv4-mapped forms) are rejected, documents are limited to 8 KiB, and the document's client_id must equal its URL. Host names are not resolved, so allow only hosts you trust.

mcp-flow-server defines one logical MCP endpoint: MCP name, HTTP path and allowed groups. Both runtime and OAuth auth configurations are required. There is no public or inherited authorization mode.

One runtime owns one local port. Multiple endpoints may share that runtime port when their MCP paths differ.

Tool execution request emitted by the flow server:

msg.topic = 'mcp-tool-execute';
msg.payload = { toolName, arguments, executionId };

Tool response returned to the same flow server node:

msg.topic = 'mcp-tool-response';
msg.payload = { executionId, result };

result can be a standard MCP result object. Plain strings and plain objects are normalized into text responses.

The mcp-flow-server output emits only mcp-tool-execute messages. Return matching mcp-tool-response messages to the node input. The input also accepts start, stop and restart commands, which produce no output; endpoint state is shown in the node status and tool telemetry comes from mcp-server-metrics.

mcp-server-metrics is a source node with no input and one output. Select the same mcp-runtime used by the MCP endpoints, then connect its output to metric writers. Use one metrics node per runtime. It emits one mcp-tool-telemetry message for each tools/call request across those endpoints, including failed calls and authentication failures. Its payload contains endpoint, tool, user, client, mode, status, durationMs, responseBytes, scannedNodes, and cached. user is the caller's e-mail (subject when no e-mail is known) and client the MCP client (claude, codex, chatgpt or the client host); unauthenticated calls report unknown. Unknown and unauthenticated tool names are reported as unknown to bound metric cardinality. user is personal data: use it as a metric label only while the set of users is small and the metrics store is access-controlled. It does not poll Node-RED or expose a metrics route.

Configuration Notes

Tool execution always requires OAuth and checks endpoint roles/groups. Provider scopes are advertised automatically in securitySchemes and _meta.securitySchemes; custom endpoint/tool scope policies are not supported. offline_access is advertised only when refresh is configured.

mcp-tool-registry requires an explicit endpoint choice. A specific endpoint is stored as its MCP path, so the binding survives copy, import and node replacement; endpoints with the same path on different runtimes share the tool. Select Shared (All MCPs) only for tools intentionally exposed on every endpoint in the same Node-RED runtime.

Editor-supplied environment variable names (URL Env, Password Env, Secret Env) must start with MCP_, so a flow editor cannot point a configuration at other process secrets such as NODE_RED_CREDENTIAL_SECRET.

The authorization server issuer is the Base URL origin. Authorization responses carry it as iss (RFC 9207). Keep one mcp-auth configuration per runtime port: the root /.well-known/oauth-authorization-server document answers only while a single configuration serves the port.

Deploy with Modified Flows and keep each MCP endpoint and its tool flows on their own tab. Stopping an endpoint fails its pending tool calls (the flow restarts with it); requests already being answered, such as get_flow or a token exchange, finish in the background, and the port stays open while any endpoint on it is running.

Each endpoint runs at most 32 tool calls at once; further calls get JSON-RPC error -32000 until one finishes. Tool arguments are not validated against the Input Schema, so the flow must check its own input.

Admin tools expose read-only get_flow only when the selected runtime has Admin Port, Admin Token, and Admin Endpoint Path configured, and the endpoint path exactly matches that Admin Endpoint Path.

Inspecting Node-RED flows

get_flow returns compact structuredContent and a matching text result. It never returns a raw tab export.

Arguments

Result

Admin API read

none, or mode: "tabs"

Paginated tab IDs, labels, disabled state and node counts

In-process runtime index

id, or mode: "tab_summary", id

Groups, wired chains, key nodes, counts

/flow/:id

mode: "group", id, groupId

Group members and their direct wires

/flow/:id

mode: "chain", id, nodeId

Wired component containing the node

/flow/:id

mode: "node", id, nodeId

Selected node and direct incoming/outgoing wires

/flow/:id

mode: "subflows"

Definition index

/flow/global

mode: "subflow", subflowId

Definition and contained node index

/flow/global

mode: "subflow", subflowId, id

Definition and usages on one specified tab

/flow/global, /flow/:id

mode: "configs", id

Paginated config-node IDs, types and names for one tab

/flow/:id

mode: "configs", subflowId

Paginated config-node index for one subflow definition

/flow/global

mode: "configs"

Paginated global config-node index

/flow/global

mode: "config", configId with optional id or subflowId

Ordinary properties of one config node in the selected scope

/flow/global or /flow/:id

mode: "node_configs", nodeId with id or subflowId

Config references of one node, including the property name and scope

/flow/:id, /flow/global for tabs; /flow/global for subflows

Use offset from meta.nextOffset to continue a paginated response. A chain follows direct wires within one tab; link nodes expose target IDs but are not traversed into other tabs. Node details include only an allowlist of identifiers and labels. includeCode: true works only with mode: "node" and returns at most 10,000 Function code characters.

Use includeConfig: true with mode: "node" to inspect supported execution settings:

Node type

Settings

inject

Payload, topic, properties and scheduling

link in, link out, link call

Links, output mode, call type and timeout as applicable

switch

Property/type, rules, check-all, repair and outputs

catch

Scoped node IDs and uncaught setting

mongodb4

Operation mode, collection, operation, output, timeout and ID handling

mcp-flow-server

Route and allowed groups

mcp-tool-registry

Description, input/output schemas, endpoint, behavior and world access

Settings retain their configured types; absent fields are not filled with inferred defaults. node.configSupported identifies supported types. Values over 32 KiB, excessive nested structures or settings beyond the 64 KiB config budget are omitted, listed in configOmittedProperties, and set meta.truncated. Nested values are limited to 16 levels and 2,000 visited values per request. Credentials and arbitrary top-level properties are excluded. Named secret keys inside supported structures, including JSON schema strings, are replaced with [REDACTED] and identified in configRedactedProperties; invalid JSON schema text is omitted. Literal strings may still contain secrets stored outside credential fields: this is not a general-purpose secret detector. Default node, tab, group and chain inspection remain compact. Config references remain available through node_configs; no credential API is called.

Config-node details retain ordinary property types and values. Node-RED's flow API excludes declared credential values; an unexpected credentials container is replaced with [REDACTED] defensively. No credential API is called. Named secret keys (at any depth), user information in URLs and all env values are replaced with [REDACTED] and listed in redactedProperties. A property over 32 KiB is listed in omittedProperties instead of being returned; config properties are paginated by 40 fields and 64 KiB of values.

get_flow never requests the full /flows export. The tab index walks Node-RED's in-memory configuration once and caches only compact tab metadata until the next runtime deploy; it does not serialize or transfer full flows. meta.cached indicates whether the index was reused. The index excludes undeployed editor changes. Tab detail modes read /flow/:id; subflow modes read /flow/global and optionally the specified tab. These endpoints still make Node-RED prepare the selected flow before the palette applies its response limits. Responses cap lists at 40 items and edges at 80; Admin API reads have a 15-second timeout and a 32 MiB response limit. Tab indexing stops above 100,000 configuration nodes; graph inspection stops above 20,000 nodes or 100,000 wire visits. meta reports the source, scan count, returned node count and truncation.

Only the handshake (initialize, notifications/initialized) is anonymous. tools/list, tools/call and every other method require a valid token; without one the endpoint answers HTTP 401 with WWW-Authenticate pointing to the protected resource metadata, which starts sign-in in MCP clients (MCP authorization spec). Tools advertise securitySchemes and the matching _meta.securitySchemes.

All tool calls validate credentials before flow or admin execution. Missing, invalid or expired tokens return HTTP 401 with WWW-Authenticate. Endpoint role and resource failures remain HTTP 403. Unresolved required auth configuration fails closed, and an invalid mcp-auth configuration is reported in the log and stops its endpoints from starting. GET on an MCP path requires authentication (HTTP 401 with WWW-Authenticate) and otherwise answers 405; the legacy SSE stream was removed, so only JSON-RPC POST is served. If the provider returns an error instead of a code, the callback redirects that error to the client redirect_uri (RFC 6749 4.1.2.1). Storage failures on OAuth routes return 503 temporarily_unavailable and are logged; responses never include internal error text. Client support for an in-chat OAuth linking UI must be verified separately; emitting the challenge does not guarantee that UI.

The authorization endpoint requires PKCE S256, validates the client metadata host allow-list, checks the exact redirect URI against the client metadata document, delegates login to the configured OIDC issuer, validates the returned ID token through JWKS, and issues short-lived opaque MCP access tokens.

The RFC 8252 loopback exception permits a different port for a registered HTTP callback on 127.0.0.1, [::1] or localhost when CIMD declares native or omits application_type. The host, path and query must remain identical; credentials, fragments and invalid ports are rejected by this exception. Explicitly non-native clients retain exact matching. The token endpoint always requires the exact URI, including the port, used in the authorization request. This relaxation is a deliberate deviation from exact matching: Claude Code publishes http://localhost/callback and http://127.0.0.1/callback without application_type and listens on a random port.

PKCE code_challenge must be 43 base64url characters and code_verifier 43-128 RFC 7636 characters. The upstream state is bound to the browser that started sign-in with an HttpOnly, SameSite=Lax cookie (__Host- prefixed on HTTPS); a callback without that cookie is rejected. Request bodies are limited (JSON 256 KiB, form 16 KiB). The package has no request rate limiter; apply one in the reverse proxy for internet-facing deployments.

Refresh Tokens

Refresh is enabled only when the selected provider's Refresh Key credential contains a 32-byte key encoded as 64 hexadecimal characters. Each provider tab has its own password field; generate a key with openssl rand -hex 32. Keep it stable across restarts and out of packages/logs. Replacing it requires new sign-ins for existing refresh sessions. Without it, discovery advertises only the authorization-code grant. Clients requesting offline_access receive a separate opaque MCP refresh token; the upstream provider credential stays encrypted on the server with AES-256-GCM, bound to the auth configuration namespace.

Default and maximum lifetimes are 900 seconds for access, 604800 seconds of refresh inactivity and 1814400 seconds from initial sign-in for the absolute session. Shorter values can be configured for testing. Each successful refresh resets only the inactivity window; neither rotation nor continuous use extends the absolute deadline. Access issuance is capped to the remaining session lifetime.

The token endpoint accepts grant_type=refresh_token, refresh_token, client_id, and optional resource/scope. Client, resource and endpoint remain bound; scope can be narrowed but never increased. Refresh tokens are hashed in storage, rotated atomically and checked for reuse. Reuse grace is fixed at 30 seconds and is not configurable in the editor. Within grace, an identical retry receives the same token pair without calling the provider again. The cached response is encrypted with AES-256-GCM and bound to the namespace, session and previous token hash. Its storage TTL is bounded by grace and the original access/session expiry; returned expires_in decreases with the remaining access lifetime. Scope changes cannot reuse a response issued for a different scope set.

Concurrent requests poll shared storage every 25 milliseconds for at most five seconds. If the first request has not finished, the waiter receives HTTP 503 temporarily_unavailable; it does not revoke the session. Reuse outside grace, client/resource/endpoint mismatches and definitive provider authorization or identity/policy failures revoke the session and return invalid_grant.

Network errors, timeouts, provider 429/5xx and unavailable discovery/JWKS return HTTP 503 temporarily_unavailable, with a validated upstream Retry-After when present. The claim is restored using compare-and-set, without extending expiry. A provider invalid_client or unauthorized_client response (for example an expired client secret) is a configuration error, not a user failure: it also returns 503, keeps the session and logs provider_config at error level. Once a provider response has been verified, an encrypted pending result permits retrying interrupted access storage or rotation without refreshing the provider again. Claims have a 60-second ownership deadline so retries can recover after an interrupted worker or a failed rollback; stale workers cannot commit rotation. A permanently unavailable storage backend cannot guarantee claim recovery. Microsoft permits reuse of its previous provider refresh credential after a lost response; a generic OIDC provider that consumes it immediately may still require sign-in after an ambiguous network failure.

Grace is a bounded replay-detection tradeoff: possession of the old bearer token and its public client bindings can retrieve the cached pair during this window. Grace is fixed at 30 seconds and is not configurable. Refresh outcomes are logged as success, transient, provider_config, unknown, revoked, replay or grace_reuse (see Audit Log), never with credential payloads.

Every new rotation contacts the selected provider. Microsoft requires a freshly signed ID token with the expected issuer, audience, tenant, subject and current app roles. For OIDC providers, current UserInfo groups are checked, and a returned ID token is validated when present. No cached role/group fallback is used. Account and role administration remain the identity administrator's responsibility.

Use the existing Redis-backed auth storage for persistence and protect its network access, credentials and backups. Memory storage loses sessions on restart. Local tests use synthetic providers and a Redis adapter double; real provider, Redis restart and native client silent-refresh acceptance are separate rollout checks. No callback or proxy route change is needed for the refresh grant.

Audit Log

Security events are written to the Node-RED log as one JSON line each, through the node that handled them. Lines never contain tokens, authorization codes, PKCE values or tool arguments; sessions appear only as a 12-character hash.

event

When

Main fields

oauth.sign_in

callback completes or is refused

outcome, reason, subject, client, endpoint

oauth.token

authorization code exchange

outcome, reason, subject, client, session

oauth.refresh

refresh grant

outcome, subject, client, session

mcp.auth

rejected token or insufficient access (missing tokens are not logged)

reason, subject, client, ip

mcp.tool_call

every tools/call

outcome, tool, subject, client, session, durationMs

mcp.command

start, stop, restart input

command, endpoint

The flow also receives the caller in msg.mcp (subject, email, groups, client) with each mcp-tool-execute message. Retention and forwarding of the Node-RED log belong to the deployment.

Environment Variables

Variable

Used by

Notes

MCP_REDIS_URL (default name)

mcp-redis in env mode

Any MCP_* name can be configured.

MCP_*

mcp-redis Password Env, mcp-auth Secret Env

Names outside MCP_ are rejected.

NODE_RED_ADMIN_API_TOKEN

mcp-runtime admin tools

Overrides the Admin Token credential when set. Give it read-only Node-RED permissions; get_flow only issues GET requests.

Rollback

Production pins the palette by commit. To roll back, pin the previous commit and redeploy, then undo the configuration that the newer version required:

  • 3.2 -> 3.1: no configuration change.

  • 3.1 -> 3.0: no configuration change is required; Base URL, Groups and exact Client Hosts remain valid. Users sign in again.

  • 3.0 -> 2.x: install node-red-contrib-mcp-server instead of @olykov/node-red-mcp-server, restore REDIS_URL in mcp-redis, re-select registry endpoints (2.x binds by node ID), and sign in again.

Verification

Run before commit:

npm run lint
npm run coverage
npm pack --dry-run
npm audit --omit=dev

npm run coverage runs the tests and fails below 90% lines, 80% branches or 85% functions. GitHub Actions (.github/workflows/ci.yml) runs the same checks on Node.js 22 and 24 for every push to main, release tag and pull request.

Run source scans before publishing to confirm that sensitive material and environment-specific names are absent.

Upstream Updates

Keep upstream as the base. Pull new upstream releases into a candidate branch, then reapply the documented local patch set and run the verification suite.

License

MIT. See LICENSE.

Related MCP Connectors

Related MCP Servers