Files
nodered-mcp/CLAUDE.md
Joe Carter 764d123fdb Initial commit: Node-RED MCP server (Python/FastMCP)
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>
2026-02-24 09:19:33 +00:00

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)

  1. get_flows — GET /flows → FlowList
  2. update_flows — POST /flows (deployment_type header)
  3. get_flow — GET /flow/:id → Flow
  4. update_flow — PUT /flow/:id → str
  5. list_tabs — GET /flows, filter type=tab → list[FlowTab]
  6. create_flow — POST /flow → FlowCreateResult
  7. delete_flow — DELETE /flow/:id → str
  8. get_flows_state — GET /flows/state → FlowState
  9. set_flows_state — POST /flows/state → FlowState
  10. get_flows_formatted — GET /flows, format client-side → FlowSummary
  11. visualize_flows — GET /flows, format as markdown → str
  12. patch_flow — GET /flow/:id, apply ops, PUT /flow/:id → str

Node Tools (6)

  1. inject — POST /inject/:id → str
  2. get_nodes — GET /nodes → list[NodeSet]
  3. get_node_info — GET /nodes/:module → NodeModule
  4. toggle_node_module — PUT /nodes/:module → NodeModule
  5. find_nodes_by_type — GET /flows, filter by type → list[Node]
  6. search_nodes — GET /flows, search by query → list[Node]

Settings Tools (2)

  1. get_settings — GET /settings → Settings
  2. get_diagnostics — GET /diagnostics → DiagnosticsResult

Layout Tools (1)

  1. lint_flow — GET /flow/:id, run layout checks → LayoutReport

Utility Tools (1)

  1. api_help — static reference → str

Conventions (see conventions.md for full details)

  • All models inherit BaseApiModel, never plain BaseModel or raw dicts
  • Never override from_api() — use validation_alias, @field_validator, @model_validator instead
  • 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 type before calling from_api()
  • Node model uses extra="allow" — Node-RED nodes have many type-specific fields
  • Client singletonget_client() / reset_client() manage global instance; reset_client() closes old client
  • Settings uses validation_aliashttp_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.