Skip to main content

nodered-mcp

Stars Version PyPI CI License

Buy Me A Coffee

Explore, edit, and deploy Node-RED flows from an MCP client.

About

An MCP server for Node-RED. It gives an MCP client tools to explore and edit a flow set -- tabs, nodes, wires, groups, subflows and config nodes -- and to deploy the result to the running instance.

It reaches Node-RED over its Admin API, or works directly against a flows.json on disk. Either way the tools understand the format rather than treating it as generic JSON: they tell a flow node from a config node, follow a wire path across tabs, and reproduce the editor's own geometry, so a group box this server draws is the box Node-RED would have drawn.

Features

  • Read tabs, groups, orphaned nodes, subflows, config nodes and who references them, referenced Home Assistant entities, and wire traces through a flow.
  • Create, update, delete, rename, and duplicate nodes. Enable or disable them, wire and unwire them, splice one into an existing wire, or route traffic around it.
  • Create, populate, restyle, and delete groups; create, rename, reorder, and delete tabs; import and export node sets.
  • Claim empty canvas before creating nodes instead of guessing coordinates, lint the canvas for collisions, repair overlaps, and repack a tab's groups into columns instead of one tall stack.
  • Edits accumulate in memory and reach Node-RED only when you commit, so a multi-node build lands as one unit. diff shows what they would change before you commit, and undo walks them back one call at a time.
  • Two guards on every write: a layout gate that refuses writes introducing new collisions, and a staleness check that refuses to overwrite flows someone deployed from the browser in the meantime.

Requirements

  • Python 3.11+ and nothing outside the standard library beyond fastmcp
  • One of: a reachable Node-RED, or a flows.json on this filesystem

That is the whole list. Docker is needed only by the fallback deploy route, which you can ignore entirely if Node-RED's Admin API is reachable.

Installation

No install step -- uvx fetches and runs it:

uvx nodered-mcp --nodered-url http://localhost:1880

Or pip install nodered-mcp. To work on the package itself:

git clone https://github.com/ljmerza/nodered-mcp
cd nodered-mcp
uv sync

Usage

The server needs to know where the flows are. Point it at a running Node-RED and it reads them over the Admin API -- no file access, and Node-RED can be on another machine:

nodered-mcp --nodered-url http://localhost:1880

Or point it at the file:

nodered-mcp --flows-path /path/to/nodered/data/flows.json

With neither, it looks in ~/.node-red/ and uses the flow file it finds there. That covers a stock npm install -g node-red, where the file is named after the hostname (flows_raspberrypi.json) rather than flows.json -- only the Docker image uses the plain name.

Register with an MCP client

{
  "mcpServers": {
    "nodered": {
      "type": "stdio",
      "command": "uvx",
      "args": ["nodered-mcp"],
      "env": {
        "NODERED_URL": "http://localhost:1880"
      }
    }
  }
}

See .mcp.json.example for a fuller example.

Configuration

Every setting resolves CLI flag > environment variable > default.

Flag Environment variable Default Purpose
--nodered-url NODERED_URL (unset) Base URL of the running Node-RED. Reads the flows and deploys over its Admin API
--flows-path NODERED_FLOWS_PATH (discovered) Path to flows.json. Optional when --nodered-url is set
--nodered-token NODERED_TOKEN (unset) Admin API token, if adminAuth is enabled
--read-only NODERED_READ_ONLY false Expose only the read tools. Nothing can edit, save, or deploy
--group-style NODERED_GROUP_STYLE (editor theme) JSON merged over Node-RED's default style for groups this server creates
--transport NODERED_MCP_TRANSPORT stdio stdio, http, or sse
--host / --port NODERED_MCP_HOST / NODERED_MCP_PORT 127.0.0.1 / 8080 Bind address for http and sse

Only used by the fallback deploy route, below:

Flag Environment variable Default Purpose
--container NODERED_CONTAINER nodered Container name used by deploy
--container-flows-path NODERED_CONTAINER_FLOWS_PATH /data/flows.json Path to flows.json inside the container
--restart-cmd NODERED_RESTART_CMD docker restart <container> Restart command; {container} is substituted

Where the flows come from

Set --nodered-url and the flows are read with GET /flows and written back with POST /flows. This host needs no filesystem access to Node-RED at all, so it works against a container, a systemd service, a Home Assistant add-on, or a box across the network, all the same way.

Node-RED stamps each flow revision, and that revision is what guards your edits: this server sends back the rev it loaded, and if anyone deployed in the meantime Node-RED answers 409 and the edit is refused rather than silently reverting their work. session(op='deploy', force=true) overrides it.

Set --flows-path instead and the flows are read from the file. The equivalent guard compares the file against what was loaded, so a Deploy from the browser is caught the same way.

Group styling

Groups this server creates carry no colours, which is what Node-RED itself does -- the editor computes stroke and fill from the active theme's CSS, so a group looks right whether the person opening it uses the light or dark theme. To force a specific look, merge in the keys you care about:

NODERED_GROUP_STYLE='{"fill": "#2e333a", "fill-opacity": "0.75"}'

Read-only mode

--read-only unregisters nodered_edit, nodered_group and nodered_tab entirely, so a client never sees them, and refuses the mutating ops of the other two tools. Useful for pointing a model at a production Node-RED to ask questions about it.

How deploy reaches Node-RED

save only writes flows.json. Node-RED reads that file at startup and not again, so a save on its own changes nothing in the running instance -- and with no local file there is nothing for it to do, so it refuses. deploy is what makes the flows live, and it has two routes.

Admin API (set --nodered-url). The flows are POSTed to Node-RED's own Admin API, the same endpoint the editor uses when you press Deploy. Node-RED swaps them in place, so there is no restart and no downtime, and this host needs no container runtime. If a local flows.json is also configured it is written first, so the file stays in step.

Node credentials are untouched: they live in flows_cred.json, keyed by node id, and a deploy that carries no credentials key leaves them exactly as they were (verified against Node-RED 5.0.4).

If settings.js sets httpAdminRoot, include it (http://localhost:1880/admin). If it sets adminAuth, add a token. Node-RED mints one from your editor credentials, and it expires -- 7 days by default:

curl -s http://localhost:1880/auth/token \
  --data 'client_id=node-red-admin&grant_type=password&scope=*&username=admin&password=secret'
# {"access_token":"...","expires_in":604800,"token_type":"Bearer"}

NODERED_TOKEN="..."

Docker (no --nodered-url). The fallback: docker cp the flows into the container, then run --restart-cmd. This restarts the whole container and needs the Docker CLI plus socket access. Point --restart-cmd at whatever actually manages the container:

NODERED_RESTART_CMD="docker compose restart {container}"

session(op='status') reports which route is configured.

Tools

Eight tools, each dispatching on an op argument.

Tool Ops
nodered_query summary, tabs, groups, tab, group, search, ungrouped, orphans, subflows, styles, configs, entities, inspect, connections, trace
nodered_find_nodes Structured search by tab, type, or name substring
nodered_get_node One node's raw JSON plus its wiring context
nodered_edit create_node, create_config_node, update_node, update_many, delete_node, rename_node, set_enabled, duplicate_node, replace_node, wire, unwire, insert_between, bypass, import_nodes, export_group
nodered_group create, add, move_node, delete, rename, set_style, normalize_styles, refit, shift, bounds, decouple
nodered_tab create, rename, delete, reorder, set_enabled, set_info
nodered_layout check, audit, free_region, occupied, arrange, fix
nodered_session status, diff, undo, save, deploy, reload

A typical build

nodered_query(op="tabs")                                   -> tab ids
nodered_layout(op="free_region", tab_id=TAB, w=800, h=200) -> {"x": 100, "y": 3240}
nodered_edit(op="create_node", tab_id=TAB, node_type="inject",
             name="tick", x=100, y=3240)                   -> node id
nodered_edit(op="create_node", tab_id=TAB, node_type="switch",
             name="gate", x=300, y=3240)                   -> node id
nodered_edit(op="wire", source_id=..., target_id=...)
nodered_group(op="create", name="My Flow", tab_id=TAB, node_ids=[...])
nodered_session(op="save")

Nothing above touches flows.json until the final save.

Arranging a tab

A tab that has been edited for a year drifts into one tall column, because every routine that needed space took the next free spot underneath everything else. arrange repacks it.

nodered_layout(op="audit")                            -> worst tabs first
nodered_layout(op="arrange", tab_id=TAB)              -> the plan, nothing moved
nodered_layout(op="arrange", tab_id=TAB, apply=true)  -> groups repacked
nodered_session(op="save")

Groups are packed into columns, each one going into whichever column is currently shortest. The column count is chosen to land the tab's bounding box nearest target_ratio (1.6 by default, roughly a widescreen viewport). Pass columns to force it. sort picks the placement order: packed (tallest first, densest, but it reorders the tab), current (keeps the existing reading order), or name.

Nodes belonging to no group never move. If the packed block would land on one, the whole block drops below them instead, so a tab with a scratch node parked in the middle arranges around it rather than burying it.

arrange is a dry run unless you pass apply=true, it reports the footprint it will produce before it produces it, and it goes on the undo stack like any other edit.

Decoupling groups

A wire that runs from a node in one group to a node in another pins the two groups to each other: move one and the wire stretches across the tab, so the groups can no longer be arranged independently. decouple swaps every such wire for a link out / link in pair -- the wire now stops at the edge of its own group and is picked up inside the other one.

nodered_group(op="decouple", tab_id=TAB)              -> the crossings, nothing changed
nodered_group(op="decouple", tab_id=TAB, apply=true)  -> one link pair per crossing
nodered_layout(op="arrange", tab_id=TAB, apply=true)  -> now safe to repack
nodered_session(op="save")

One pair per wire, named after the node at the other end (-> compute, tick ->). The link out goes in a column just right of the source group's nodes, the link in just left of the target group's, and a group grows by one column however many wires cross it. Both boxes are refitted afterwards.

Two kinds of wire are left alone. One touching an ungrouped node, because there is no second group to decouple from; and one between a group and its own parent or child, because those move together anyway.

Scope it with tab_id for a whole tab or group_id for the crossings that touch one group. Like arrange it is a dry run unless you pass apply=true, and it goes on the undo stack. Widening the boxes can push a group into a neighbour, which the layout gate would block on save -- the op reports any overlap it introduces, and arrange repacks them.

How it protects your flows

The layout gate

save and deploy lint the canvas before and after your edit, and refuse to write if the edit introduces a new error-level finding:

Finding Severity Meaning
group-overlap error A group box landed on another group box
group-escape error A group box no longer covers its own nodes
stray-in-group warning A node sits inside a group box it isn't a member of
node-overlap warning Two nodes occupy the same space

Problems that already existed never block. Only the ones your edit created do. When the gate fires, the fix is usually one of:

  • nodered_layout(op="free_region") to claim clear canvas, then place there
  • nodered_group(op="refit", group_id=...) to resize a group around its nodes
  • nodered_session(op="save", allow_overlap=true) if the overlap is deliberate

Group geometry is exact: the sizing rules are ported from the Node-RED editor, so a computed box matches what the editor draws. Node geometry is exact apart from label text width, which is approximated from Helvetica metrics. That is why node-level findings are only ever warnings.

The staleness check

Someone pressing Deploy in the browser while you have edits pending would be silently reverted by your commit. Both sources guard against that, each using whatever the source itself can prove.

Admin API. Node-RED stamps every flow revision. The session sends back the rev it loaded, and Node-RED answers 409 if that is no longer current. Its own conflict check, so nothing can slip between the check and the write.

File. The session records (mtime_ns, size) at load and re-checks before every write. Nanoseconds rather than os.path.getmtime: a float epoch only resolves to about a microsecond, so a write landing in the same tick as the load would compare equal and slip past. A stamp mismatch is then confirmed against the file's actual content, because a rewrite that reproduces what you already have is not somebody else's work -- Node-RED persists the flows a deploy just handed it, to the same file, with a fresh mtime.

Either way the commit is refused rather than reverting the other work. Either reload and redo your edits, or pass force=true.

Standalone use

Both engine modules work as libraries and CLIs, independent of MCP.

uv run python -m nodered_mcp.flows summary --flows-path /path/to/flows.json
uv run python -m nodered_mcp.layout --path /path/to/flows.json --fix boxes,move
from nodered_mcp.flows import Flows

f = Flows("/path/to/flows.json")
ox, oy = f.free_region(tab_id, w=1600, h=300)
f.create_node(tab_id, "inject", "tick", x=ox, y=oy)
f.save()

--fix boxes alone makes things worse: refitting grows some boxes so they swallow neighbouring non-member nodes. Run boxes,move together, and read the dry run before passing --apply.

Project layout

src/nodered_mcp/
├── server.py       FastMCP server: the eight tools
├── session.py      in-memory session, stdout capture, staleness guard
├── config.py       CLI flags and environment resolution
├── flows.py        the Flows class, composed from the mixins below
├── constants.py    defaults, the group style, LayoutError
├── reports.py      ReadMixin      — summary, tab, group, search, trace
├── nodes.py        NodeEditMixin  — create/update/delete/wire nodes
├── groups.py       GroupMixin     — create, populate, and delete group boxes
├── tabs.py         TabMixin       — create, rename, reorder, and delete tabs
├── placement.py    LayoutMixin    — claim free canvas, refit boxes, arrange tabs
├── transfer.py     TransferMixin  — import and export node sets
├── persist.py      PersistMixin   — save, deploy, and the layout gate
└── layout.py       canvas geometry and linter, ported from the NR editor

Flows composes the mixins, so the public API stays flat: f.summary(), f.create_node(), f.free_region(), f.save().

Development

uv sync --group dev
uv run pytest                    # 93 tests
uv run ruff check .
uv run ruff format --check .

Tests run against a synthetic fixture in tests/fixtures/, never a real flows file. They cover configuration precedence, the read tools, in-memory-until-save semantics, the layout gate both blocking and overridden, the staleness guard, the deploy command sequence, and that no tool writes to stdout: a stray print would corrupt MCP's stdio framing.

CI runs the same checks through ljmerza/misc-actions.

Contributing

Issues and pull requests are welcome. Please keep ruff check, ruff format, and pytest green.

Acknowledgments

  • Node-RED: the canvas geometry here is ported from its editor client, so group boxes match what the editor draws.
  • FastMCP: the MCP server framework.

License

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nodered_mcp-2.0.0.tar.gz (168.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nodered_mcp-2.0.0-py3-none-any.whl (67.2 kB view details)

Uploaded Python 3

File details

Details for the file nodered_mcp-2.0.0.tar.gz.

File metadata

  • Download URL: nodered_mcp-2.0.0.tar.gz
  • Upload date:
  • Size: 168.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nodered_mcp-2.0.0.tar.gz
Algorithm Hash digest
SHA256 070ca3665133abd97955057f245c16184d0beaed04ec7758cd70797d4cbd253e
MD5 ed22760e8b4a32b8d150744666c4a3ab
BLAKE2b-256 48d8b5c0efd9b0198bb9ad107ecc285228f26a739185d12c43cbb5e6ad1bcbd1

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodered_mcp-2.0.0.tar.gz:

Publisher: release.yml on ljmerza/nodered-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nodered_mcp-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: nodered_mcp-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 67.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for nodered_mcp-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5e8e427b82dc8dfab1695c288f008f72b7e18f3f9890fb1ada497f3c135a31b4
MD5 a477f424b98a24e72409c6ef79a1f7c3
BLAKE2b-256 7a7f8fed36756673bcb44414b9ee7680180617458b6cdb9af889675d4c2aaf56

See more details on using hashes here.

Provenance

The following attestation bundles were made for nodered_mcp-2.0.0-py3-none-any.whl:

Publisher: release.yml on ljmerza/nodered-mcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

2.0.0 This release

2 files

1.2.0

2 files

1.1.0

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page