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>
175 lines
7.6 KiB
Markdown
175 lines
7.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
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()`:
|
|
|
|
```python
|
|
# 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**:
|
|
|
|
```python
|
|
# 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)
|
|
12. `inject` — POST /inject/:id → str
|
|
13. `get_nodes` — GET /nodes → list[NodeSet]
|
|
14. `get_node_info` — GET /nodes/:module → NodeModule
|
|
15. `toggle_node_module` — PUT /nodes/:module → NodeModule
|
|
16. `find_nodes_by_type` — GET /flows, filter by type → list[Node]
|
|
17. `search_nodes` — GET /flows, search by query → list[Node]
|
|
|
|
### Settings Tools (2)
|
|
18. `get_settings` — GET /settings → Settings
|
|
19. `get_diagnostics` — GET /diagnostics → DiagnosticsResult
|
|
|
|
### Layout Tools (1)
|
|
20. `lint_flow` — GET /flow/:id, run layout checks → LayoutReport
|
|
|
|
### Utility Tools (1)
|
|
21. `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 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`.
|