tools: add physics-sanity + Docker MCP scaffolding + tools/README
- tools/physics-sanity/ — JS library of cross-node balance helpers
(mass / hydraulic / hydraulic-power / oxygen-transfer / energy) with
7 unit tests + a CLI demo. Designed for `require()` from per-node
integration tests where shape-based unit tests miss physically-
impossible plant states.
- tools/docker-compose.yml + tools/mcp/{node-red-admin,influxdb,browser}
scaffolding — placeholder Dockerfiles + a ROADMAP.md for the Node-RED
admin MCP. Compose file is the target shape for the Q3-2026 migration
to the central MCP server; the per-service Dockerfile stays in this
repo as the canonical definition either way. Implementations are TODO.
- tools/README.md — top-level tooling index; documents the CI order for
running every tool on a PR.
- .gitignore: ignore tools/.env (developer-specific MCP endpoints).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
97
tools/mcp/README.md
Normal file
97
tools/mcp/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# EVOLV MCP services — Docker stack
|
||||
|
||||
Three MCP services Claude Code uses to close real loops during EVOLV work:
|
||||
|
||||
| Service | What it does | Tools exposed |
|
||||
|---|---|---|
|
||||
| `mcp-node-red-admin` | Wraps the Node-RED HTTP admin API | `getFlows`, `postFlow`, `getFlow`, `inject`, `listNodes`, `restartFlow` |
|
||||
| `mcp-influxdb` | Queries the telemetry bucket | `query`, `assertSeriesExists`, `assertRecentWrite`, `listMeasurements` |
|
||||
| `mcp-browser` | Headless Playwright against the FlowFuse dashboard | `loadDashboard`, `screenshot`, `consoleLogs`, `waitForChart`, `getChartData` |
|
||||
|
||||
## Why these
|
||||
|
||||
Each closes a verification loop Claude currently cannot close:
|
||||
|
||||
- **Node-RED admin** — today Claude pushes flows via raw `curl`; the
|
||||
MCP lets Claude deploy + fire injects + read live state in one
|
||||
conversational turn.
|
||||
- **InfluxDB** — today Claude cannot verify "did the telemetry land?"
|
||||
beyond reading source. The MCP closes the loop after a deploy.
|
||||
- **Browser** — today Claude cannot see the rendered dashboard. The
|
||||
MCP catches the failure mode behind the η-null crash + the
|
||||
blank-ui-chart bug + the editor pile-up bug at the only layer where
|
||||
they're visible.
|
||||
|
||||
## Migration plan
|
||||
|
||||
These run locally **now** (we're in the middle of an infra migration).
|
||||
Once the central MCP server is provisioned (target Q3 2026), each
|
||||
service moves to shared infra by lifting the entry from this
|
||||
`docker-compose.yml` plus the matching `tools/mcp/<name>/Dockerfile`
|
||||
and pointing every developer's Claude Code at the central endpoint
|
||||
instead of `localhost`. **The compose file stays here as the canonical
|
||||
definition.**
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# build the three images (one-off, ~3 min)
|
||||
cd tools
|
||||
docker compose --profile mcp build
|
||||
|
||||
# start them
|
||||
docker compose --profile mcp up -d
|
||||
|
||||
# wire Claude Code to them — add to your user-level .mcp.json
|
||||
{
|
||||
"mcpServers": {
|
||||
"evolv-node-red-admin": { "type": "stdio", "command": "docker", "args": ["exec", "-i", "evolv-mcp-node-red-admin", "node", "server.mjs"] },
|
||||
"evolv-influxdb": { "type": "stdio", "command": "docker", "args": ["exec", "-i", "evolv-mcp-influxdb", "node", "server.mjs"] },
|
||||
"evolv-browser": { "type": "stdio", "command": "docker", "args": ["exec", "-i", "evolv-mcp-browser", "node", "server.mjs"] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The repo-level `.mcp.json` is deliberately **not** committed (each
|
||||
developer has different host endpoints / tokens). Use a user-level
|
||||
config or `~/.claude.json`.
|
||||
|
||||
## Required environment
|
||||
|
||||
`tools/.env` (gitignored) with:
|
||||
|
||||
```dotenv
|
||||
NODE_RED_HOST=http://host.docker.internal:1880
|
||||
NODE_RED_TOKEN=… # optional, only if Node-RED has admin auth on
|
||||
INFLUX_URL=http://host.docker.internal:8086
|
||||
INFLUX_TOKEN=…
|
||||
INFLUX_ORG=wbd
|
||||
INFLUX_BUCKET=telemetry
|
||||
DASHBOARD_URL=http://host.docker.internal:1880/dashboard
|
||||
```
|
||||
|
||||
## Status
|
||||
|
||||
| Service | Dockerfile | Server impl | Status |
|
||||
|---|---|---|---|
|
||||
| `mcp-node-red-admin` | placeholder | **TODO** — see `mcp/node-red-admin/ROADMAP.md` | not runnable yet |
|
||||
| `mcp-influxdb` | placeholder | **TODO** | not runnable yet |
|
||||
| `mcp-browser` | placeholder | **TODO** — wrap `@playwright/test` | not runnable yet |
|
||||
|
||||
The compose file is the **target shape**. The Dockerfile + server
|
||||
implementation per service is a follow-up (each is ~200–400 LOC of MCP
|
||||
protocol + the wrapped client). When a service lands, flip its row
|
||||
above to `runnable` and remove the placeholder.
|
||||
|
||||
## When to use these — required reading
|
||||
|
||||
`CLAUDE.md` § "Tooling (Docker-first, local now, central later)" lists
|
||||
the operating doctrine: **always prefer these tools over ad-hoc
|
||||
curl/grep/manual checks**. Each tool exists because of a specific bug
|
||||
class we've already paid for. Skipping them re-opens those bugs.
|
||||
|
||||
## Future: OPC-UA / PLC MCP
|
||||
|
||||
Out of scope for this round; will be revisited later. When added it
|
||||
follows the same pattern: `tools/mcp/opcua/` with its own Dockerfile
|
||||
and a row in this README.
|
||||
5
tools/mcp/browser/Dockerfile
Normal file
5
tools/mcp/browser/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
# placeholder — see ../README.md status table
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN echo 'console.error("mcp-browser not yet implemented"); process.exit(1);' > server.mjs
|
||||
CMD ["node", "server.mjs"]
|
||||
5
tools/mcp/influxdb/Dockerfile
Normal file
5
tools/mcp/influxdb/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
# placeholder — see ../README.md status table
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN echo 'console.error("mcp-influxdb not yet implemented"); process.exit(1);' > server.mjs
|
||||
CMD ["node", "server.mjs"]
|
||||
5
tools/mcp/node-red-admin/Dockerfile
Normal file
5
tools/mcp/node-red-admin/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
# placeholder — see ROADMAP.md
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
RUN echo 'console.error("mcp-node-red-admin not yet implemented — see ROADMAP.md"); process.exit(1);' > server.mjs
|
||||
CMD ["node", "server.mjs"]
|
||||
50
tools/mcp/node-red-admin/ROADMAP.md
Normal file
50
tools/mcp/node-red-admin/ROADMAP.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# mcp-node-red-admin — implementation roadmap
|
||||
|
||||
Status: placeholder. Compose entry exists, server impl is TODO.
|
||||
|
||||
## Goal
|
||||
|
||||
An MCP stdio server that wraps the Node-RED admin HTTP API, exposing the
|
||||
following tools to Claude Code:
|
||||
|
||||
| Tool | Wraps | Use case |
|
||||
|---|---|---|
|
||||
| `node_red_get_flows` | `GET /flows` | Read deployed flow JSON |
|
||||
| `node_red_post_flow` | `POST /flow/:id` (single tab) | Deploy one tab without nuking others |
|
||||
| `node_red_replace_flows` | `POST /flows` (bulk) | Replace the entire flow set |
|
||||
| `node_red_inject` | `POST /inject/:nodeId` | Fire an inject node by id |
|
||||
| `node_red_list_nodes` | `GET /nodes` | Discover registered node types |
|
||||
| `node_red_restart_flow` | `DELETE` + redeploy | Force a restart of one tab |
|
||||
|
||||
## Sketch
|
||||
|
||||
```js
|
||||
// tools/mcp/node-red-admin/server.mjs
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
|
||||
const NODE_RED_HOST = process.env.NODE_RED_HOST;
|
||||
const TOKEN = process.env.NODE_RED_TOKEN;
|
||||
|
||||
const server = new Server({ name: 'evolv-node-red-admin', version: '0.1.0' }, { capabilities: { tools: {} } });
|
||||
server.setRequestHandler(/* listTools */);
|
||||
server.setRequestHandler(/* callTool — fetch NODE_RED_HOST + tool's endpoint */);
|
||||
await server.connect(new StdioServerTransport());
|
||||
```
|
||||
|
||||
## Dockerfile sketch
|
||||
|
||||
```dockerfile
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package.json server.mjs ./
|
||||
RUN npm install
|
||||
CMD ["node", "server.mjs"]
|
||||
```
|
||||
|
||||
## When to build it
|
||||
|
||||
After we've shipped enough EVOLV flow work that "deploy + fire inject +
|
||||
read state in one turn" becomes the dominant inner loop. Today the
|
||||
`curl` pattern still works for one-off deploys; the MCP earns its
|
||||
keep when the loop runs 5+ times per session.
|
||||
Reference in New Issue
Block a user