22-tool MCP server for Node-RED flow management with Pydantic models, incremental flow patching, layout linting, and flexible auth (token, basic, OAuth2). 137 tests, full ruff compliance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
7.6 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
What This Is
Node-RED MCP is a FastMCP server that exposes Node-RED flow management as MCP tools. It communicates with a Node-RED instance via the Admin HTTP API, allowing language models to read, create, update, and delete flows, manage nodes, trigger inject nodes, and inspect runtime settings. Unlike the original TypeScript node-red-mcp-server, this is a Python implementation using the mailmcp project conventions (FastMCP, Pydantic models, async httpx client).
Commands
make all # format + lint + test
make check # lint + test (no format)
make test # uv run pytest tests/ -v
make lint # uv run ruff check src/ tests/
make format # uv run ruff format src/ tests/
make fix # uv run ruff check --fix --unsafe-fixes src/ tests/
Run a single test: uv run pytest tests/test_models.py -v
Run with coverage: uv run pytest tests/ -v --cov=src/nodered_mcp --cov-report=term-missing
Always use uv run to execute Python tools — never bare python or pytest.
Start the server: uv run nodered-mcp
Architecture
src/nodered_mcp/
├── server.py # FastMCP server, config loading, client init, tool imports
├── config.py # Pydantic Settings (env vars: NODE_RED_URL, NODE_RED_TOKEN, NODE_RED_API_VERSION, auth fields)
├── client.py # NodeRedClient: async httpx wrapper with auth
├── models/
│ ├── base.py # BaseApiModel with from_api() classmethod
│ ├── flow.py # Node, FlowTab, Flow, FlowState
│ ├── node.py # NodeModule, NodeSet
│ ├── responses.py # FlowList, FlowSummary, FlowCreateResult, Settings, DiagnosticsResult
│ └── layout.py # LayoutIssue, LayoutReport (lint results)
└── tools/
├── flows.py # 12 flow tools (get_flows, update_flows, list_tabs, etc.) + auto-lint on mutations
├── nodes.py # 6 node tools (inject, get_nodes, find_nodes_by_type, etc.)
├── settings.py # 2 settings tools (get_settings, get_diagnostics)
├── layout.py # 1 layout tool (lint_flow) + pure lint helpers
└── utility.py # 1 api_help tool (static reference)
Layer Diagram
Tools (thin @mcp.tool() functions)
↓
Client (async httpx wrapper, returns raw dicts)
↓
Node-RED Admin HTTP API
Tools convert raw dicts to Pydantic models using Model.from_api().
Key Pattern: Model Layering
Base models define the canonical schema. All models inherit from BaseApiModel:
BaseApiModel (from_api → model_validate)
├── Node (extra="allow" for type-specific fields)
├── FlowTab (tab workspace)
├── Flow (complete flow with nodes/configs/subflows)
├── FlowState (start/stop state)
├── NodeSet (installed node module)
├── NodeModule (node module info)
├── Response wrappers (FlowList, FlowSummary, Settings, DiagnosticsResult, etc.)
└── Layout models (LayoutIssue, LayoutReport)
Tools accept/return base types. No provider hierarchy needed (single backend).
Key Pattern: Client Singleton
NodeRedClient is a global singleton managed by get_client() / reset_client():
# Tools call get_client()
client = get_client()
data = await client.get_flows()
return FlowList.from_api(data)
# Tests call reset_client() to get fresh instances
await reset_client() # closes old client to prevent connection leaks
Similar to mailmcp's get_provider_manager() pattern, but simpler (no provider abstraction).
Key Pattern: Authentication
Three auth methods configured via NODE_RED_AUTH_METHOD:
| Method | Env Vars | Use Case |
|---|---|---|
token (default) |
NODE_RED_TOKEN |
Static bearer token |
basic |
NODE_RED_AUTH_USERNAME, NODE_RED_AUTH_PASSWORD |
Forward auth proxy (e.g. authentik) |
oauth |
NODE_RED_AUTH_USERNAME, NODE_RED_AUTH_PASSWORD, NODE_RED_OAUTH_TOKEN_URL, NODE_RED_OAUTH_CLIENT_ID |
OAuth2 client credentials grant |
- token: Sets
Authorization: Bearer {token}header (existing behavior, backward compatible) - basic: Uses
httpx.BasicAuth— proxy validates credentials, sets cookies - oauth: Lazy token fetch via client credentials grant, cached with 30s expiry buffer, single 401 retry
Key Pattern: Heterogeneous Flow Arrays
GET /flows returns a flat array mixing tabs (type=tab), subflows (type=subflow), and nodes (all other types). Tools must filter by type before parsing:
# Correct
flows_data = await client.get_flows()
tabs = [item for item in flows_data if item.get("type") == "tab"]
return FlowTab.from_api(tabs)
# Wrong - crashes on non-tab items
flows_data = await client.get_flows()
return FlowTab.from_api(flows_data) # ERROR
Client returns the raw list[dict]; type-aware parsing happens in tool helpers.
Tool List (22 tools)
Flow Tools (12)
get_flows— GET /flows → FlowListupdate_flows— POST /flows (deployment_type header)get_flow— GET /flow/:id → Flowupdate_flow— PUT /flow/:id → strlist_tabs— GET /flows, filter type=tab → list[FlowTab]create_flow— POST /flow → FlowCreateResultdelete_flow— DELETE /flow/:id → strget_flows_state— GET /flows/state → FlowStateset_flows_state— POST /flows/state → FlowStateget_flows_formatted— GET /flows, format client-side → FlowSummaryvisualize_flows— GET /flows, format as markdown → strpatch_flow— GET /flow/:id, apply ops, PUT /flow/:id → str
Node Tools (6)
inject— POST /inject/:id → strget_nodes— GET /nodes → list[NodeSet]get_node_info— GET /nodes/:module → NodeModuletoggle_node_module— PUT /nodes/:module → NodeModulefind_nodes_by_type— GET /flows, filter by type → list[Node]search_nodes— GET /flows, search by query → list[Node]
Settings Tools (2)
get_settings— GET /settings → Settingsget_diagnostics— GET /diagnostics → DiagnosticsResult
Layout Tools (1)
lint_flow— GET /flow/:id, run layout checks → LayoutReport
Utility Tools (1)
api_help— static reference → str
Conventions (see conventions.md for full details)
- All models inherit BaseApiModel, never plain
BaseModelor raw dicts - Never override
from_api()— usevalidation_alias,@field_validator,@model_validatorinstead - Transformation hierarchy:
validation_alias(key renames) →@field_validator(type conversions) →@model_validator(cross-field structural reshaping only) - MCP tools return Pydantic models, raise exceptions on failure (no error dicts). Exception: simple confirmation strings for inject, delete, visualize.
- Tools are thin wrappers — business logic lives in client or helper functions
- Client returns raw dicts — tools convert to models via
Model.from_api() - Heterogeneous flow arrays — filter by
typebefore callingfrom_api() - Node model uses
extra="allow"— Node-RED nodes have many type-specific fields - Client singleton —
get_client()/reset_client()manage global instance;reset_client()closes old client - Settings uses
validation_alias—http_node_root: str = Field("", validation_alias="httpNodeRoot")
Quality Gates
All must pass: ruff check, pytest (22 tools covered), server starts cleanly via uv run nodered-mcp.