Skip to main content
Glama

nodered-mcp

An MCP server that reads, queries, and edits a Node-RED flows.json.

License CI Python

About

Node-RED stores every flow, node, wire, and group box in one large JSON file. Editing it by hand — or with jq and sed — is how you end up with dangling wires, groups whose boxes no longer cover their own nodes, and new nodes stacked on top of existing ones.

This server exposes that file to an MCP client as a small set of tools that understand the format. It knows the difference between a flow node and a config node, it can trace a wire path, and it reproduces the Node-RED editor's own geometry so a group box it draws is the box the editor would have drawn.

It is a port of the flows_util.py / layout_util.py pair used to script Node-RED changes in a home-automation repo, generalised so the file path, container name, and restart command are all configuration.

Related MCP server: nr-mcp

Features

  • Query — tabs, groups, orphaned nodes, subflows, referenced Home Assistant entities, and wire traces through a flow.

  • Edit — create, update, delete, rename, and duplicate nodes; wire and unwire them; create, populate, and restyle groups; import and export node sets.

  • Place — claim empty canvas before creating nodes instead of guessing coordinates, lint the canvas for collisions, and repair overlaps.

  • Commit deliberately — edits accumulate in memory and reach disk only when you ask, so a multi-node build lands as one unit.

  • Two guards the underlying scripts never needed: a layout gate that refuses writes which introduce new collisions, and a staleness check that refuses to overwrite a flows.json someone deployed from the browser.

Requirements

  • Python 3.11+

  • A flows.json on the local filesystem

  • Docker on PATH — only for the deploy tool, which copies the file into a container and restarts it

Installation

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

Usage

The flows.json path is the only required setting. There is no sensible default, so the server refuses to start without one.

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

Register with an MCP client

{
  "mcpServers": {
    "nodered": {
      "type": "stdio",
      "command": "uv",
      "args": ["run", "--directory", "/path/to/nodered-mcp", "nodered-mcp"],
      "env": {
        "NODERED_FLOWS_PATH": "/path/to/nodered/data/flows.json"
      }
    }
  }
}

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

Configuration

Every setting resolves CLI flag > environment variable > default.

Flag

Environment variable

Default

Purpose

--flows-path

NODERED_FLOWS_PATH

(required)

Path to flows.json on the host

--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

--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

If Node-RED is managed by something other than plain Docker, point --restart-cmd at it:

NODERED_RESTART_CMD="docker compose restart {container}"

Tools

Seven tools, each dispatching on an op argument.

Tool

Ops

nodered_query

summary, tabs, groups, tab, group, search, ungrouped, orphans, subflows, styles, 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, update_node, delete_node, rename_node, duplicate_node, wire, unwire, import_nodes, export_group

nodered_group

create, add, move_node, rename, set_style, normalize_styles, refit, shift, bounds

nodered_layout

check, free_region, occupied, fix

nodered_session

status, 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.

How it protects the file

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 on disk never block — only ones your edit created. 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

Node-RED rewrites flows.json whenever someone presses Deploy in the browser. The session records (mtime_ns, size) when it loads the file and re-checks before every write. If the file changed underneath you, the commit is refused rather than silently reverting that work. Either reload and redo your edits, or pass force=true.

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 compares equal and slips past the check.

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 seven 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 and populate group boxes
├── placement.py    LayoutMixin    — claim free canvas, measure and refit boxes
├── 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                    # 49 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.

A
license - permissive license
Not graded
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
3Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    B
    maintenance
    Minimal MCP server wrapping the Node-RED admin API, enabling flow management, node installation, and context retrieval via natural language.

View all related MCP servers

Related MCP Connectors

  • Create, browse, remix, collaborate on, and run durable AI workflow nodes from MCP hosts.

  • MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.

  • JSON tools MCP.

View all MCP Connectors

Latest Blog Posts

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/ljmerza/nodered-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server