nodered-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/dist/api.d.ts +13 -0
- package/dist/api.js +55 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +51 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Node-RED MCP
|
|
2
|
+
|
|
3
|
+
MCP server for Node-RED: flows, nodes. Works with any instance.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
export NODERED_URL=http://localhost:1880
|
|
9
|
+
export NODERED_TOKEN=token_if_adminauth_on
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
node dist/index.js
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The server uses stdio, so it can be connected to Claude Desktop, Cursor, VS Code, MCP Inspector, or another compatible MCP client.
|
|
16
|
+
|
|
17
|
+
## Tools at a glance
|
|
18
|
+
|
|
19
|
+
- `list_flows`: Flow tabs with ids.
|
|
20
|
+
- `get_flow`: Node count and type breakdown.
|
|
21
|
+
|
|
22
|
+
## Limits and privacy
|
|
23
|
+
|
|
24
|
+
This project is intentionally narrow. It should be treated as a practical helper, not a complete certification or security audit. Check the implementation and the returned data before using it with sensitive material. Everything stays on your network.
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export declare class NodeRedError extends Error {
|
|
2
|
+
}
|
|
3
|
+
type Raw = Record<string, any>;
|
|
4
|
+
export interface Flow {
|
|
5
|
+
id: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
nodes: number;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function listFlows(): Promise<Flow[]>;
|
|
11
|
+
export declare function getFlow(id: string): Promise<Raw[]>;
|
|
12
|
+
export declare function formatFlow(id: string, nodes: Raw[]): string;
|
|
13
|
+
export {};
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-RED Admin HTTP API client. Points at any instance via NODERED_URL
|
|
3
|
+
* (default http://localhost:1880). If adminAuth is on, set NODERED_TOKEN
|
|
4
|
+
* (Bearer) — get one by logging into the editor once.
|
|
5
|
+
* Docs: https://nodered.org/docs/api/admin/methods/
|
|
6
|
+
*/
|
|
7
|
+
const BASE = (process.env.NODERED_URL ?? "http://localhost:1880").replace(/\/+$/, "");
|
|
8
|
+
export class NodeRedError extends Error {
|
|
9
|
+
}
|
|
10
|
+
function headers() {
|
|
11
|
+
const h = { "User-Agent": "nodered-mcp/1.0", Accept: "application/json" };
|
|
12
|
+
if (process.env.NODERED_TOKEN)
|
|
13
|
+
h.Authorization = `Bearer ${process.env.NODERED_TOKEN}`;
|
|
14
|
+
return h;
|
|
15
|
+
}
|
|
16
|
+
async function getJson(path) {
|
|
17
|
+
let res;
|
|
18
|
+
try {
|
|
19
|
+
res = await fetch(`${BASE}${path}`, {
|
|
20
|
+
headers: headers(),
|
|
21
|
+
signal: AbortSignal.timeout(20000),
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new NodeRedError(`Node-RED at ${BASE} is unreachable. Set NODERED_URL.`);
|
|
26
|
+
}
|
|
27
|
+
if (res.status === 401 || res.status === 403)
|
|
28
|
+
throw new NodeRedError("Node-RED refused (401/403). Set NODERED_TOKEN.");
|
|
29
|
+
if (res.status === 404)
|
|
30
|
+
throw new NodeRedError("Not found.");
|
|
31
|
+
if (!res.ok)
|
|
32
|
+
throw new NodeRedError(`Node-RED error ${res.status}`);
|
|
33
|
+
return (await res.json());
|
|
34
|
+
}
|
|
35
|
+
export async function listFlows() {
|
|
36
|
+
const rows = await getJson("/flows");
|
|
37
|
+
return rows
|
|
38
|
+
.filter((f) => f.type === "tab")
|
|
39
|
+
.map((f) => ({ id: String(f.id), label: f.label, nodes: 0, disabled: f.disabled }));
|
|
40
|
+
}
|
|
41
|
+
export async function getFlow(id) {
|
|
42
|
+
if (!id.trim())
|
|
43
|
+
throw new NodeRedError("Flow id is empty.");
|
|
44
|
+
const nodes = await getJson(`/flow/${encodeURIComponent(id.trim())}`);
|
|
45
|
+
return Array.isArray(nodes) ? nodes : [];
|
|
46
|
+
}
|
|
47
|
+
export function formatFlow(id, nodes) {
|
|
48
|
+
if (nodes.length === 0)
|
|
49
|
+
return `Flow ${id}: no nodes.`;
|
|
50
|
+
const kinds = {};
|
|
51
|
+
for (const n of nodes)
|
|
52
|
+
kinds[String(n.type ?? "?")] = (kinds[String(n.type ?? "?")] ?? 0) + 1;
|
|
53
|
+
const top = Object.entries(kinds).sort((a, b) => b[1] - a[1]).slice(0, 8);
|
|
54
|
+
return [`Flow ${id} (${nodes.length} nodes):`, ...top.map(([k, v]) => `- ${k} x${v}`)].join("\n");
|
|
55
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createServer } from "./server.js";
|
|
3
|
+
async function main() {
|
|
4
|
+
const server = createServer();
|
|
5
|
+
const transport = new StdioServerTransport();
|
|
6
|
+
await server.connect(transport);
|
|
7
|
+
console.error("MCP server running on stdio");
|
|
8
|
+
}
|
|
9
|
+
main().catch((err) => {
|
|
10
|
+
console.error("Fatal error:", err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { formatFlow, getFlow, listFlows, NodeRedError } from "./api.js";
|
|
4
|
+
const text = (t) => ({ content: [{ type: "text", text: t }] });
|
|
5
|
+
const textError = (t) => ({ content: [{ type: "text", text: t }], isError: true });
|
|
6
|
+
const READ_ONLY = { readOnlyHint: true, openWorldHint: true };
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "nodered-mcp",
|
|
10
|
+
version: "1.0.0",
|
|
11
|
+
});
|
|
12
|
+
server.registerTool("list_flows", {
|
|
13
|
+
title: "List flows",
|
|
14
|
+
description: "Node-RED flow tabs with ids and labels. Set NODERED_URL for non-default hosts.",
|
|
15
|
+
inputSchema: z.object({}),
|
|
16
|
+
annotations: READ_ONLY,
|
|
17
|
+
}, async () => {
|
|
18
|
+
try {
|
|
19
|
+
const rows = await listFlows();
|
|
20
|
+
if (rows.length === 0)
|
|
21
|
+
return text("No flows.");
|
|
22
|
+
return text(rows.map((f, i) => `${i + 1}. [${f.id}] ${f.label ?? "(unnamed)"}${f.disabled ? " (disabled)" : ""}`).join("\n"));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
return textError(errorMessage(e));
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
server.registerTool("get_flow", {
|
|
29
|
+
title: "Get flow",
|
|
30
|
+
description: "One flow: node count plus node-type breakdown.",
|
|
31
|
+
inputSchema: z.object({
|
|
32
|
+
id: z.string().describe("Flow tab id (use list_flows)"),
|
|
33
|
+
}),
|
|
34
|
+
annotations: READ_ONLY,
|
|
35
|
+
}, async ({ id }) => {
|
|
36
|
+
try {
|
|
37
|
+
return text(formatFlow(id, await getFlow(id)));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
return textError(errorMessage(e));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
45
|
+
function errorMessage(e) {
|
|
46
|
+
if (e instanceof NodeRedError)
|
|
47
|
+
return `Error: ${e.message}`;
|
|
48
|
+
if (e instanceof Error)
|
|
49
|
+
return `Error: ${e.message}`;
|
|
50
|
+
return `Error: ${String(e)}`;
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nodered-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP server for Node-RED: flows, nodes. Works with any instance.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"mcpName": "io.github.mrfentmen/nodered-mcp",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/mrfentmen/awesome-mcps.git"
|
|
10
|
+
},
|
|
11
|
+
"bin": {
|
|
12
|
+
"nodered-mcp": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.js",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"start": "node dist/index.js",
|
|
21
|
+
"dev": "rm -rf dist && tsc -p tsconfig.json && node dist/index.js",
|
|
22
|
+
"inspect": "npx @modelcontextprotocol/inspector node dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"mcp",
|
|
26
|
+
"node-red",
|
|
27
|
+
"flows",
|
|
28
|
+
"api"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
33
|
+
"zod": "^3.23.8"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"typescript": "^5.6.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=20"
|
|
41
|
+
}
|
|
42
|
+
}
|