@gmag11/nodered-mcp-server 1.0.1
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/LICENSE +201 -0
- package/README.md +162 -0
- package/index.js +133 -0
- package/package.json +58 -0
- package/resources/skills/nodered-flow-builder/SKILL.md +659 -0
- package/resources/skills/nodered-flow-layout/SKILL.md +395 -0
- package/resources/skills/nodered-flowfuse-dashboard/SKILL.md +941 -0
- package/resources/skills/nodered-fundamentals/SKILL.md +323 -0
- package/resources/skills/nodered-jsonata/SKILL.md +1039 -0
- package/resources/skills/nodered-mustache/SKILL.md +588 -0
- package/resources/skills/nodered-node-reference/SKILL.md +1020 -0
- package/resources/skills/nodered-node-reference/examples/common.json +113 -0
- package/resources/skills/nodered-node-reference/examples/network.json +107 -0
- package/resources/skills/nodered-node-reference/examples/parser.json +147 -0
- package/resources/skills/nodered-node-reference/examples/sequence.json +141 -0
- package/resources/skills/nodered-node-reference/examples/storage.json +104 -0
- package/resources/skills/nodered-patterns/SKILL.md +414 -0
- package/resources/skills/nodered-patterns/examples/error-handler.json +72 -0
- package/resources/skills/nodered-patterns/examples/http-endpoint.json +42 -0
- package/resources/skills/nodered-patterns/examples/mqtt-subscriber.json +47 -0
- package/resources/skills/nodered-patterns/examples/timer-flow.json +50 -0
- package/resources/skills/nodered-subflows/SKILL.md +261 -0
- package/resources/skills/nodered-uibuilder/SKILL.md +500 -0
- package/src/auth/api-key-verifier.js +36 -0
- package/src/auth/composite-verifier.js +59 -0
- package/src/auth/config.js +106 -0
- package/src/auth/oauth-clients-store.js +107 -0
- package/src/auth/oauth-provider.js +149 -0
- package/src/auth/oauth-token-store.js +312 -0
- package/src/nodered/auth.js +158 -0
- package/src/nodered/client.js +199 -0
- package/src/nodered/comms-client.js +500 -0
- package/src/renderer/colors.js +161 -0
- package/src/renderer/geometry.js +115 -0
- package/src/renderer/html-builder.js +571 -0
- package/src/renderer/index.js +51 -0
- package/src/renderer/ir-builder.js +161 -0
- package/src/renderer/layout.js +126 -0
- package/src/renderer/mermaid-builder.js +109 -0
- package/src/renderer/svg-builder.js +228 -0
- package/src/schemas/responses.js +283 -0
- package/src/server.js +844 -0
- package/src/skills/loader.js +84 -0
- package/src/staging-store.js +258 -0
- package/src/tools/add-nodes-to-group.js +216 -0
- package/src/tools/connect-nodes.js +115 -0
- package/src/tools/constants.js +45 -0
- package/src/tools/create-flow.js +87 -0
- package/src/tools/create-node.js +126 -0
- package/src/tools/create-subflow-instance.js +123 -0
- package/src/tools/create-subflow.js +101 -0
- package/src/tools/delete-context.js +60 -0
- package/src/tools/delete-flow.js +81 -0
- package/src/tools/delete-group.js +116 -0
- package/src/tools/delete-node.js +73 -0
- package/src/tools/delete-subflow.js +103 -0
- package/src/tools/deploy.js +94 -0
- package/src/tools/disconnect-nodes.js +158 -0
- package/src/tools/export-flow.js +161 -0
- package/src/tools/export-subflow.js +78 -0
- package/src/tools/flow-utils.js +376 -0
- package/src/tools/get-config-nodes.js +86 -0
- package/src/tools/get-context.js +76 -0
- package/src/tools/get-flow-diagram.js +99 -0
- package/src/tools/get-flow-nodes.js +116 -0
- package/src/tools/get-flows.js +74 -0
- package/src/tools/get-node-detail.js +77 -0
- package/src/tools/get-node-type-detail.js +92 -0
- package/src/tools/get-palette-nodes.js +63 -0
- package/src/tools/get-staging-status.js +34 -0
- package/src/tools/get-subflow-detail.js +110 -0
- package/src/tools/get-subflows.js +105 -0
- package/src/tools/import-flow.js +310 -0
- package/src/tools/inject-message.js +117 -0
- package/src/tools/install-node.js +31 -0
- package/src/tools/read-debug-messages.js +155 -0
- package/src/tools/refresh-staging.js +62 -0
- package/src/tools/remove-nodes-from-group.js +162 -0
- package/src/tools/render-staging.js +69 -0
- package/src/tools/response-utils.js +42 -0
- package/src/tools/search-nodes.js +134 -0
- package/src/tools/uninstall-node.js +31 -0
- package/src/tools/update-flow.js +95 -0
- package/src/tools/update-group.js +77 -0
- package/src/tools/update-node.js +132 -0
- package/src/tools/update-subflow.js +84 -0
- package/src/transport/http.js +252 -0
- package/src/transport/stdio.js +16 -0
- package/src/transport/ws-server.js +223 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-context
|
|
3
|
+
*
|
|
4
|
+
* Reads a context variable from a node, flow, or global scope in Node-RED
|
|
5
|
+
* via the Admin API. Supports reading a single key or all keys in a scope.
|
|
6
|
+
*
|
|
7
|
+
* Note: In-memory context values are lost when Node-RED restarts.
|
|
8
|
+
*/
|
|
9
|
+
import { formatSuccess } from './response-utils.js';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
import { ANN_READONLY } from './constants.js';
|
|
13
|
+
import { GenericObjectSchema } from '../schemas/responses.js';
|
|
14
|
+
/**
|
|
15
|
+
* Build the API path for a context GET request.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} scope - 'node' | 'flow' | 'global'
|
|
18
|
+
* @param {string|undefined} id - Node or flow UUID (required for node/flow scopes)
|
|
19
|
+
* @param {string|undefined} key - Optional context key to read
|
|
20
|
+
* @returns {string} The API path (with optional ?key= query string)
|
|
21
|
+
*/
|
|
22
|
+
export function buildGetContextPath(scope, id, key) {
|
|
23
|
+
const base = scope === 'global' ? '/context/global' : `/context/${scope}/${id}`;
|
|
24
|
+
return key ? `${base}/${encodeURIComponent(key)}` : base;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Transform the raw Node-RED context API response into the tool result shape.
|
|
29
|
+
*
|
|
30
|
+
* Single-key query: returns { [key]: value }
|
|
31
|
+
* All-keys query: returns { [key]: value, ... } (the raw object as-is)
|
|
32
|
+
*
|
|
33
|
+
* @param {string|undefined} key - The requested key (if any)
|
|
34
|
+
* @param {unknown} rawResponse - Raw value returned by the Node-RED API
|
|
35
|
+
* @returns {object}
|
|
36
|
+
*/
|
|
37
|
+
export function transformGetContextResponse(key, rawResponse) {
|
|
38
|
+
if (key) {
|
|
39
|
+
return { [key]: rawResponse ?? null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// All-keys query: rawResponse is already an object of key-value pairs
|
|
43
|
+
return rawResponse ?? {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Handler for the get-context MCP tool.
|
|
48
|
+
*
|
|
49
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
50
|
+
* @param {object} params - Validated input parameters
|
|
51
|
+
* @param {string} params.scope - 'node' | 'flow' | 'global'
|
|
52
|
+
* @param {string} [params.id] - Node or flow UUID (required for node/flow scopes)
|
|
53
|
+
* @param {string} [params.key] - Context key to read (omit to read all keys)
|
|
54
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
55
|
+
*/
|
|
56
|
+
export async function handleGetContext(client, params) {
|
|
57
|
+
const { scope, id, key } = params;
|
|
58
|
+
|
|
59
|
+
// Validate: id is required for node and flow scopes
|
|
60
|
+
if ((scope === 'node' || scope === 'flow') && !id) {
|
|
61
|
+
throw new Error(`id is required for scope "${scope}". Provide the node or flow UUID to target. For global scope, omit the id parameter.`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const path = buildGetContextPath(scope, id, key);
|
|
65
|
+
const rawResponse = await client.request('GET', path);
|
|
66
|
+
const result = transformGetContextResponse(key, rawResponse);
|
|
67
|
+
|
|
68
|
+
return formatSuccess(result);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const getContextDefinition = {
|
|
72
|
+
name: 'get-context',
|
|
73
|
+
annotations: ANN_READONLY,
|
|
74
|
+
outputSchema: GenericObjectSchema,
|
|
75
|
+
handler: handleGetContext,
|
|
76
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-flow-diagram
|
|
3
|
+
*
|
|
4
|
+
* Returns a Mermaid flowchart (flowchart TD) representing the topology of nodes
|
|
5
|
+
* within a specific Node-RED flow, with filtering and pagination support.
|
|
6
|
+
* Delegates Mermaid generation to the shared renderer module.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ANN_READONLY } from './constants.js';
|
|
10
|
+
import { FlowDiagramResponseSchema } from '../schemas/responses.js';
|
|
11
|
+
import { buildIR } from '../renderer/ir-builder.js';
|
|
12
|
+
import { buildMermaid } from '../renderer/mermaid-builder.js';
|
|
13
|
+
import {
|
|
14
|
+
getFlowNodes,
|
|
15
|
+
applyFilters,
|
|
16
|
+
paginate,
|
|
17
|
+
} from './flow-utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Transform a raw /flows response into a paginated Mermaid diagram for a given flow.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} rawResponse - Response from GET /flows (v2 format: { rev, flows })
|
|
23
|
+
* @param {string} flowId - ID of the tab or subflow to diagram
|
|
24
|
+
* @param {object} [options]
|
|
25
|
+
* @param {boolean} [options.disabledOnly]
|
|
26
|
+
* @param {string} [options.nodeType]
|
|
27
|
+
* @param {string} [options.fromNodeId]
|
|
28
|
+
* @param {'downstream'|'upstream'|'both'} [options.direction='both']
|
|
29
|
+
* @param {number} [options.offset=0]
|
|
30
|
+
* @param {number} [options.limit=50]
|
|
31
|
+
* @returns {{ flowId: string, diagram: string, totalCount: number, offset: number, limit: number, hasMore: boolean }}
|
|
32
|
+
* @throws {Error} If flowId not found, or fromNodeId not found in flow
|
|
33
|
+
*/
|
|
34
|
+
export function transformFlowDiagram(rawResponse, flowId, options = {}) {
|
|
35
|
+
const {
|
|
36
|
+
disabledOnly,
|
|
37
|
+
nodeType,
|
|
38
|
+
fromNodeId,
|
|
39
|
+
direction = 'both',
|
|
40
|
+
offset = 0,
|
|
41
|
+
limit = 50,
|
|
42
|
+
} = options;
|
|
43
|
+
|
|
44
|
+
const allNodes = rawResponse.flows || [];
|
|
45
|
+
|
|
46
|
+
// Get all nodes belonging to this flow (validates flowId exists)
|
|
47
|
+
const flowNodes = getFlowNodes(allNodes, flowId);
|
|
48
|
+
|
|
49
|
+
// Apply filters
|
|
50
|
+
const filtered = applyFilters(flowNodes, { disabledOnly, nodeType, fromNodeId, direction });
|
|
51
|
+
|
|
52
|
+
// Paginate
|
|
53
|
+
const page = paginate(filtered, offset, limit);
|
|
54
|
+
|
|
55
|
+
// Build IR and delegate Mermaid generation to shared renderer
|
|
56
|
+
const ir = buildIR(page.items);
|
|
57
|
+
const diagram = buildMermaid(ir);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
flowId,
|
|
61
|
+
diagram,
|
|
62
|
+
totalCount: page.totalCount,
|
|
63
|
+
offset: page.offset,
|
|
64
|
+
limit: page.limit,
|
|
65
|
+
hasMore: page.hasMore,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Handler for the get-flow-diagram MCP tool.
|
|
71
|
+
*
|
|
72
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
73
|
+
* @param {object} params - Validated input parameters
|
|
74
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
75
|
+
*/
|
|
76
|
+
export async function handleGetFlowDiagram(staging, params) {
|
|
77
|
+
const flows = await staging.getFlows();
|
|
78
|
+
const result = transformFlowDiagram({ flows }, params.flowId, params);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: 'text',
|
|
84
|
+
text: `Mermaid diagram for flow "${result.flowId}" (nodes ${result.offset + 1}–${result.offset + (result.totalCount === 0 ? 0 : Math.min(result.limit, result.totalCount - result.offset))} of ${result.totalCount}${result.hasMore ? ', hasMore: true' : ''}):\n\n\`\`\`mermaid\n${result.diagram}\n\`\`\``,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
structuredContent: {
|
|
88
|
+
mermaid: result.diagram,
|
|
89
|
+
nodeCount: result.totalCount,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const getFlowDiagramDefinition = {
|
|
95
|
+
name: 'get-flow-diagram',
|
|
96
|
+
annotations: ANN_READONLY,
|
|
97
|
+
outputSchema: FlowDiagramResponseSchema,
|
|
98
|
+
handler: handleGetFlowDiagram,
|
|
99
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-flow-nodes
|
|
3
|
+
*
|
|
4
|
+
* Returns a paginated, filterable list of nodes within a specific Node-RED flow,
|
|
5
|
+
* with metadata and sanitized configuration (large text fields excluded).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getFlowNodes,
|
|
10
|
+
sanitizeNodeConfig,
|
|
11
|
+
applyFilters,
|
|
12
|
+
paginate,
|
|
13
|
+
} from './flow-utils.js';
|
|
14
|
+
import { formatSuccess } from './response-utils.js';
|
|
15
|
+
|
|
16
|
+
import { ANN_READONLY } from './constants.js';
|
|
17
|
+
import { FlowNodesResponseSchema } from '../schemas/responses.js';
|
|
18
|
+
/**
|
|
19
|
+
* Transform a raw /flows response into a paginated list of nodes for a given flow.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} rawResponse - Response from GET /flows (v2 format: { rev, flows })
|
|
22
|
+
* @param {string} flowId - ID of the tab or subflow to inspect
|
|
23
|
+
* @param {object} [options]
|
|
24
|
+
* @param {boolean} [options.disabledOnly] - Return only disabled nodes
|
|
25
|
+
* @param {string} [options.nodeType] - Return only nodes of this type
|
|
26
|
+
* @param {string} [options.fromNodeId] - Filter to connected subgraph from this node ID
|
|
27
|
+
* @param {'downstream'|'upstream'|'both'} [options.direction='both'] - Traversal direction
|
|
28
|
+
* @param {number} [options.offset=0] - Pagination offset
|
|
29
|
+
* @param {number} [options.limit=50] - Pagination limit
|
|
30
|
+
* @returns {{ flowId: string, nodes: object[], totalCount: number, offset: number, limit: number, hasMore: boolean }}
|
|
31
|
+
* @throws {Error} If flowId not found, or fromNodeId not found in flow
|
|
32
|
+
*/
|
|
33
|
+
export function transformFlowNodes(rawResponse, flowId, options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
disabledOnly,
|
|
36
|
+
nodeType,
|
|
37
|
+
fromNodeId,
|
|
38
|
+
direction = 'both',
|
|
39
|
+
offset = 0,
|
|
40
|
+
limit = 50,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
const allNodes = rawResponse.flows || [];
|
|
44
|
+
|
|
45
|
+
// Get all nodes belonging to this flow (validates flowId exists)
|
|
46
|
+
const flowNodes = getFlowNodes(allNodes, flowId);
|
|
47
|
+
|
|
48
|
+
// Apply filters (subgraph, disabled, type)
|
|
49
|
+
const filtered = applyFilters(flowNodes, { disabledOnly, nodeType, fromNodeId, direction });
|
|
50
|
+
|
|
51
|
+
// Paginate
|
|
52
|
+
const page = paginate(filtered, offset, limit);
|
|
53
|
+
|
|
54
|
+
// Shape each node: top-level metadata + sanitized config
|
|
55
|
+
const nodes = page.items.map((node) => {
|
|
56
|
+
// Group nodes have a distinct shape: no wires, but style + member nodes array
|
|
57
|
+
if (node.type === 'group') {
|
|
58
|
+
return {
|
|
59
|
+
id: node.id,
|
|
60
|
+
type: 'group',
|
|
61
|
+
name: node.name || '',
|
|
62
|
+
disabled: node.d === true,
|
|
63
|
+
g: null,
|
|
64
|
+
x: node.x,
|
|
65
|
+
y: node.y,
|
|
66
|
+
w: node.w,
|
|
67
|
+
h: node.h,
|
|
68
|
+
style: node.style || {},
|
|
69
|
+
nodes: node.nodes || [],
|
|
70
|
+
config: {}, // no blocklisted fields on group nodes
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
id: node.id,
|
|
76
|
+
type: node.type,
|
|
77
|
+
name: node.name || '',
|
|
78
|
+
disabled: node.d === true,
|
|
79
|
+
g: node.g || null,
|
|
80
|
+
x: node.x,
|
|
81
|
+
y: node.y,
|
|
82
|
+
wires: node.wires || [],
|
|
83
|
+
config: sanitizeNodeConfig(node),
|
|
84
|
+
};
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
flowId,
|
|
89
|
+
nodes,
|
|
90
|
+
totalCount: page.totalCount,
|
|
91
|
+
offset: page.offset,
|
|
92
|
+
limit: page.limit,
|
|
93
|
+
hasMore: page.hasMore,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Handler for the get-flow-nodes MCP tool.
|
|
99
|
+
*
|
|
100
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
101
|
+
* @param {object} params - Validated input parameters
|
|
102
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
103
|
+
*/
|
|
104
|
+
export async function handleGetFlowNodes(staging, params) {
|
|
105
|
+
const flows = await staging.getFlows();
|
|
106
|
+
const result = transformFlowNodes({ flows }, params.flowId, params);
|
|
107
|
+
|
|
108
|
+
return formatSuccess(result);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const getFlowNodesDefinition = {
|
|
112
|
+
name: 'get-flow-nodes',
|
|
113
|
+
annotations: ANN_READONLY,
|
|
114
|
+
outputSchema: FlowNodesResponseSchema,
|
|
115
|
+
handler: handleGetFlowNodes,
|
|
116
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-flows
|
|
3
|
+
*
|
|
4
|
+
* Returns a summarized list of flow tabs from the connected
|
|
5
|
+
* Node-RED instance, optimized for LLM consumption.
|
|
6
|
+
* Use get-subflows for subflow definitions.
|
|
7
|
+
*/
|
|
8
|
+
import { formatSuccess } from './response-utils.js';
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
import { ANN_READONLY } from './constants.js';
|
|
12
|
+
import { FlowSummarySchema } from '../schemas/responses.js';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
/**
|
|
15
|
+
* Transform the raw Node-RED /flows response into an LLM-friendly summary.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} rawResponse - Response from GET /flows (v2 format: { rev, flows })
|
|
18
|
+
* @returns {Array<{ id: string, label: string, type: string, disabled: boolean, nodeCount: number, nodeTypes: string[] }>}
|
|
19
|
+
*/
|
|
20
|
+
export function transformFlows(rawResponse) {
|
|
21
|
+
const allNodes = rawResponse.flows || [];
|
|
22
|
+
|
|
23
|
+
// Identify flows: tabs only (subflows are handled by get-subflows)
|
|
24
|
+
const flows = allNodes.filter(
|
|
25
|
+
(node) => node.type === 'tab'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Group child nodes by their parent flow (z property)
|
|
29
|
+
const childrenByFlow = new Map();
|
|
30
|
+
for (const node of allNodes) {
|
|
31
|
+
if (node.z) {
|
|
32
|
+
if (!childrenByFlow.has(node.z)) {
|
|
33
|
+
childrenByFlow.set(node.z, []);
|
|
34
|
+
}
|
|
35
|
+
childrenByFlow.get(node.z).push(node);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return flows.map((flow) => {
|
|
40
|
+
const children = childrenByFlow.get(flow.id) || [];
|
|
41
|
+
const uniqueTypes = [...new Set(children.map((n) => n.type))];
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
id: flow.id,
|
|
45
|
+
label: flow.label || flow.name || '',
|
|
46
|
+
type: flow.type,
|
|
47
|
+
disabled: flow.disabled || false,
|
|
48
|
+
locked: flow.locked || false,
|
|
49
|
+
info: flow.info || '',
|
|
50
|
+
nodeCount: children.length,
|
|
51
|
+
nodeTypes: uniqueTypes,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Handler for the get-flows MCP tool.
|
|
58
|
+
*
|
|
59
|
+
* @param {import('../staging-store.js').StagingStore} staging
|
|
60
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
61
|
+
*/
|
|
62
|
+
export async function handleGetFlows(staging) {
|
|
63
|
+
const flows = await staging.getFlows();
|
|
64
|
+
const result = transformFlows({ flows });
|
|
65
|
+
|
|
66
|
+
return formatSuccess({ flows: result });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const getFlowsDefinition = {
|
|
70
|
+
name: 'get-flows',
|
|
71
|
+
annotations: ANN_READONLY,
|
|
72
|
+
outputSchema: z.object({ flows: z.array(FlowSummarySchema) }),
|
|
73
|
+
handler: handleGetFlows,
|
|
74
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-node-detail
|
|
3
|
+
*
|
|
4
|
+
* Returns the full detail of a single Node-RED node by its ID, including
|
|
5
|
+
* large text fields (func, template, etc.) that are excluded from get-flow-nodes.
|
|
6
|
+
*
|
|
7
|
+
* Also queries the /credentials/:type/:id endpoint to include credential
|
|
8
|
+
* metadata: field names and whether each password-type field is set.
|
|
9
|
+
* Password values are never exposed — only `has_<field>: true/false` is returned.
|
|
10
|
+
*/
|
|
11
|
+
import { formatSuccess } from './response-utils.js';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
import { ANN_READONLY } from './constants.js';
|
|
15
|
+
import { GenericObjectSchema } from '../schemas/responses.js';
|
|
16
|
+
/**
|
|
17
|
+
* Find a node by ID in the raw /flows response and return all its fields.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} rawResponse - Response from GET /flows (v2 format: { rev, flows })
|
|
20
|
+
* @param {string} nodeId - ID of the node to retrieve
|
|
21
|
+
* @returns {object} The full node object
|
|
22
|
+
* @throws {Error} If no node with the given ID is found
|
|
23
|
+
*/
|
|
24
|
+
export function transformNodeDetail(rawResponse, nodeId) {
|
|
25
|
+
const allNodes = rawResponse.flows || [];
|
|
26
|
+
const node = allNodes.find((n) => n.id === nodeId);
|
|
27
|
+
|
|
28
|
+
if (!node) {
|
|
29
|
+
throw new Error(`Node '${nodeId}' not found. Use search-nodes with the node name or get-flow-nodes to list nodes in a flow.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return node;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handler for the get-node-detail MCP tool.
|
|
37
|
+
*
|
|
38
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
39
|
+
* @param {object} params - Validated input parameters
|
|
40
|
+
* @param {string} params.nodeId - ID of the node to retrieve
|
|
41
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
42
|
+
*/
|
|
43
|
+
export async function handleGetNodeDetail(staging, client, params) {
|
|
44
|
+
const flows = await staging.getFlows();
|
|
45
|
+
const node = transformNodeDetail({ flows }, params.nodeId);
|
|
46
|
+
|
|
47
|
+
// Fetch credential metadata if the node type may have credentials.
|
|
48
|
+
// The /credentials/:type/:id endpoint returns field names and
|
|
49
|
+
// has_<field>: true/false for password-type fields (never real values).
|
|
50
|
+
let credentialMetadata = null;
|
|
51
|
+
try {
|
|
52
|
+
const credResponse = await client.request(
|
|
53
|
+
'GET',
|
|
54
|
+
`/credentials/${encodeURIComponent(node.type)}/${encodeURIComponent(node.id)}`,
|
|
55
|
+
);
|
|
56
|
+
if (credResponse && typeof credResponse === 'object' && Object.keys(credResponse).length > 0) {
|
|
57
|
+
credentialMetadata = credResponse;
|
|
58
|
+
}
|
|
59
|
+
} catch {
|
|
60
|
+
// Node type has no credentials registered, or editor is disabled.
|
|
61
|
+
// credentialMetadata stays null — the response simply won't include _credentials.
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Build the result, adding _credentials metadata when available
|
|
65
|
+
const result = credentialMetadata
|
|
66
|
+
? { ...node, _credentials: credentialMetadata }
|
|
67
|
+
: node;
|
|
68
|
+
|
|
69
|
+
return formatSuccess(result);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const getNodeDetailDefinition = {
|
|
73
|
+
name: 'get-node-detail',
|
|
74
|
+
annotations: ANN_READONLY,
|
|
75
|
+
outputSchema: GenericObjectSchema,
|
|
76
|
+
handler: handleGetNodeDetail,
|
|
77
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-node-type-detail
|
|
3
|
+
*
|
|
4
|
+
* Returns detailed information about a specific node type installed in the
|
|
5
|
+
* Node-RED palette, including its configuration parameters.
|
|
6
|
+
*/
|
|
7
|
+
import TurndownService from 'turndown';
|
|
8
|
+
import { formatSuccess } from './response-utils.js';
|
|
9
|
+
|
|
10
|
+
import { ANN_READONLY } from './constants.js';
|
|
11
|
+
import { GenericObjectSchema } from '../schemas/responses.js';
|
|
12
|
+
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract the help HTML for a specific node type from the GET /nodes HTML response.
|
|
16
|
+
*
|
|
17
|
+
* The HTML response contains blocks of the form:
|
|
18
|
+
* <script type="text/html" data-help-name="<type>">...</script>
|
|
19
|
+
*
|
|
20
|
+
* @param {string} html - Full HTML body from GET /nodes with Accept: text/html
|
|
21
|
+
* @param {string} typeName - The node type name to extract documentation for
|
|
22
|
+
* @returns {string|null} Inner HTML of the help block, or null if not found
|
|
23
|
+
*/
|
|
24
|
+
export function extractHelpHtml(html, typeName) {
|
|
25
|
+
// Escape special regex characters in the type name
|
|
26
|
+
const escaped = typeName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
27
|
+
const pattern = new RegExp(
|
|
28
|
+
`<script[^>]+data-help-name=["']${escaped}["'][^>]*>([\\s\\S]*?)<\\/script>`,
|
|
29
|
+
'i'
|
|
30
|
+
);
|
|
31
|
+
const match = html.match(pattern);
|
|
32
|
+
return match ? turndown.turndown(match[1].trim()) : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Search the raw GET /nodes response for a specific type name and return its node set.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<object>} rawResponse - Array of node set objects from GET /nodes
|
|
39
|
+
* @param {string} typeName - The node type to look up (e.g. "inject")
|
|
40
|
+
* @returns {object} The raw node set object from the API
|
|
41
|
+
* @throws {Error} If the type is not found in any installed node set
|
|
42
|
+
*/
|
|
43
|
+
export function findNodeType(rawResponse, typeName) {
|
|
44
|
+
for (const nodeSet of rawResponse) {
|
|
45
|
+
if ((nodeSet.types || []).includes(typeName)) {
|
|
46
|
+
return nodeSet;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
throw new Error(`Node type '${typeName}' not found. Use get-palette-nodes to list all installed node types, or install-node to add a missing package.`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Handler for the get-node-type-detail MCP tool.
|
|
55
|
+
*
|
|
56
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
57
|
+
* @param {object} params
|
|
58
|
+
* @param {string} params.type - The node type name to look up
|
|
59
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
60
|
+
*/
|
|
61
|
+
export async function handleGetNodeTypeDetail(client, params) {
|
|
62
|
+
const [rawResponse, helpHtml] = await Promise.all([
|
|
63
|
+
client.request('GET', '/nodes'),
|
|
64
|
+
client.requestText('GET', '/nodes'),
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
let nodeSet;
|
|
68
|
+
try {
|
|
69
|
+
nodeSet = findNodeType(rawResponse, params.type);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
return {
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'text',
|
|
75
|
+
text: err.message,
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const help = extractHelpHtml(helpHtml, params.type);
|
|
82
|
+
const result = { ...nodeSet, help };
|
|
83
|
+
|
|
84
|
+
return formatSuccess(result);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const getNodeTypeDetailDefinition = {
|
|
88
|
+
name: 'get-node-type-detail',
|
|
89
|
+
annotations: ANN_READONLY,
|
|
90
|
+
outputSchema: GenericObjectSchema,
|
|
91
|
+
handler: handleGetNodeTypeDetail,
|
|
92
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-palette-nodes
|
|
3
|
+
*
|
|
4
|
+
* Returns a paginated list of node sets from the Node-RED palette,
|
|
5
|
+
* exactly as returned by the GET /nodes API.
|
|
6
|
+
*/
|
|
7
|
+
import { formatSuccess } from './response-utils.js';
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import { ANN_READONLY } from './constants.js';
|
|
11
|
+
import { PaletteNodesResponseSchema } from '../schemas/responses.js';
|
|
12
|
+
const MAX_LIMIT = 200;
|
|
13
|
+
const DEFAULT_LIMIT = 50;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Apply pagination to an array of node sets.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array<object>} nodes - Array of node set objects from GET /nodes
|
|
19
|
+
* @param {number} offset - 0-based pagination offset (default 0)
|
|
20
|
+
* @param {number} limit - Max items to return (default 50, max 200)
|
|
21
|
+
* @returns {{ offset: number, limit: number, total: number, nodes: Array<object> }}
|
|
22
|
+
*/
|
|
23
|
+
export function paginateNodes(nodes, offset = 0, limit = DEFAULT_LIMIT) {
|
|
24
|
+
const clampedLimit = Math.min(Math.max(1, limit), MAX_LIMIT);
|
|
25
|
+
const total = nodes.length;
|
|
26
|
+
|
|
27
|
+
const start = offset;
|
|
28
|
+
const slice = start >= total ? [] : nodes.slice(start, start + clampedLimit);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
offset,
|
|
32
|
+
limit: clampedLimit,
|
|
33
|
+
total,
|
|
34
|
+
nodes: slice,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Handler for the get-palette-nodes MCP tool.
|
|
40
|
+
*
|
|
41
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
42
|
+
* @param {object} params
|
|
43
|
+
* @param {number} [params.offset]
|
|
44
|
+
* @param {number} [params.limit]
|
|
45
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
46
|
+
*/
|
|
47
|
+
export async function handleGetPaletteNodes(client, params) {
|
|
48
|
+
const rawResponse = await client.request('GET', '/nodes');
|
|
49
|
+
const result = paginateNodes(
|
|
50
|
+
rawResponse,
|
|
51
|
+
params.offset ?? 0,
|
|
52
|
+
params.limit ?? DEFAULT_LIMIT,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return formatSuccess(result);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const getPaletteNodesDefinition = {
|
|
59
|
+
name: 'get-palette-nodes',
|
|
60
|
+
annotations: ANN_READONLY,
|
|
61
|
+
outputSchema: PaletteNodesResponseSchema,
|
|
62
|
+
handler: handleGetPaletteNodes,
|
|
63
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: get-staging-status
|
|
3
|
+
*
|
|
4
|
+
* Returns the current staging state: pending change count, dirty node IDs,
|
|
5
|
+
* dirty flow IDs, and whether the staging is deployed (no pending changes).
|
|
6
|
+
*
|
|
7
|
+
* Use this to inspect what's pending before deciding to deploy or to
|
|
8
|
+
* verify that a deploy was successful.
|
|
9
|
+
*/
|
|
10
|
+
import { formatSuccess } from './response-utils.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import { ANN_READONLY } from './constants.js';
|
|
14
|
+
import { StagingSummarySchema } from '../schemas/responses.js';
|
|
15
|
+
/**
|
|
16
|
+
* Handler for the get-staging-status MCP tool.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../staging-store.js').StagingStore} staging
|
|
19
|
+
* @returns {() => Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
20
|
+
*/
|
|
21
|
+
export function handleGetStagingStatus(staging) {
|
|
22
|
+
return () => {
|
|
23
|
+
const summary = staging.getStagingSummary();
|
|
24
|
+
|
|
25
|
+
return formatSuccess(summary);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const getStagingStatusDefinition = {
|
|
30
|
+
name: 'get-staging-status',
|
|
31
|
+
annotations: ANN_READONLY,
|
|
32
|
+
outputSchema: StagingSummarySchema,
|
|
33
|
+
handler: handleGetStagingStatus,
|
|
34
|
+
};
|