@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,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared description fragments used across multiple tool definitions.
|
|
3
|
+
*
|
|
4
|
+
* Centralizing these avoids copy-paste drift when the staging model,
|
|
5
|
+
* deploy workflow, or common warnings need updating.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Appended to tool descriptions for tools that stage changes locally. */
|
|
9
|
+
export const STAGING_WARNING =
|
|
10
|
+
'⚠️ STAGING: Changes are NOT live until you call `deploy`. Check the `staging` field in the response.';
|
|
11
|
+
|
|
12
|
+
/** Used by tools that require an explicit deploy call. */
|
|
13
|
+
export const DEPLOY_REQUIRED =
|
|
14
|
+
'Changes are NOT deployed to Node-RED until you call the `deploy` tool.';
|
|
15
|
+
|
|
16
|
+
// ── MCP Annotation constants ──────────────────────────────────────
|
|
17
|
+
// Tool modules import these to include in their definition exports.
|
|
18
|
+
|
|
19
|
+
export const ANN_READONLY = {
|
|
20
|
+
readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true,
|
|
21
|
+
};
|
|
22
|
+
export const ANN_MUTATION = {
|
|
23
|
+
readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false,
|
|
24
|
+
};
|
|
25
|
+
export const ANN_DESTRUCTIVE = {
|
|
26
|
+
readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false,
|
|
27
|
+
};
|
|
28
|
+
export const ANN_DEPLOY = {
|
|
29
|
+
readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false,
|
|
30
|
+
};
|
|
31
|
+
export const ANN_INJECT = {
|
|
32
|
+
readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true,
|
|
33
|
+
};
|
|
34
|
+
export const ANN_READ_DEBUG = {
|
|
35
|
+
readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true,
|
|
36
|
+
};
|
|
37
|
+
export const ANN_INSTALL = {
|
|
38
|
+
readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true,
|
|
39
|
+
};
|
|
40
|
+
export const ANN_UNINSTALL = {
|
|
41
|
+
readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true,
|
|
42
|
+
};
|
|
43
|
+
export const ANN_REFRESH = {
|
|
44
|
+
readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false,
|
|
45
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: create-flow
|
|
3
|
+
*
|
|
4
|
+
* Creates a new Node-RED flow tab with the given label and optional properties.
|
|
5
|
+
* Stages the change locally — call `deploy` to push to Node-RED.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
|
|
10
|
+
import { formatSuccess } from './response-utils.js';
|
|
11
|
+
import { ANN_MUTATION } from './constants.js';
|
|
12
|
+
import { CreateFlowResponseSchema } from '../schemas/responses.js';
|
|
13
|
+
/**
|
|
14
|
+
* Assemble the POST /flow request body.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} label
|
|
17
|
+
* @param {boolean|undefined} disabled
|
|
18
|
+
* @param {string|undefined} info
|
|
19
|
+
* @param {Array<{name: string, value: string, type: string}>|undefined} env
|
|
20
|
+
* @returns {object}
|
|
21
|
+
*/
|
|
22
|
+
export function buildCreateFlowPayload(label, disabled, info, env) {
|
|
23
|
+
return {
|
|
24
|
+
label,
|
|
25
|
+
disabled: disabled ?? false,
|
|
26
|
+
info: info ?? '',
|
|
27
|
+
env: env ?? [],
|
|
28
|
+
nodes: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Apply a create-flow mutation to the flows array.
|
|
34
|
+
*
|
|
35
|
+
* Creates a new tab node with the given properties and appends it to the
|
|
36
|
+
* flows array. No HTTP — pure data transformation.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} rawResponse - Wrapper with `flows` array
|
|
39
|
+
* @param {string} label - Display label for the new flow tab
|
|
40
|
+
* @param {boolean} [disabled=false]
|
|
41
|
+
* @param {string} [info='']
|
|
42
|
+
* @param {Array} [env=[]]
|
|
43
|
+
* @returns {{ updatedFlows: object[], currentState: object }}
|
|
44
|
+
*/
|
|
45
|
+
export function applyCreateFlow(rawResponse, label, disabled = false, info = '', env = []) {
|
|
46
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
47
|
+
|
|
48
|
+
const newTab = {
|
|
49
|
+
id: randomUUID(),
|
|
50
|
+
type: 'tab',
|
|
51
|
+
label,
|
|
52
|
+
disabled,
|
|
53
|
+
info,
|
|
54
|
+
env,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const updatedFlows = [...flows, newTab];
|
|
58
|
+
|
|
59
|
+
return { updatedFlows, currentState: newTab };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Handler for the create-flow MCP tool.
|
|
64
|
+
*
|
|
65
|
+
* @param {import('../staging-store.js').StagingStore} staging
|
|
66
|
+
* @param {object} params
|
|
67
|
+
* @param {string} params.label
|
|
68
|
+
* @param {boolean} [params.disabled]
|
|
69
|
+
* @param {string} [params.info]
|
|
70
|
+
* @param {Array<{name: string, value: string, type: string}>} [params.env]
|
|
71
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
72
|
+
*/
|
|
73
|
+
export async function handleCreateFlow(staging, params) {
|
|
74
|
+
const { currentState } = await staging.applyMutation((rawResponse) => {
|
|
75
|
+
return applyCreateFlow(rawResponse, params.label, params.disabled, params.info, params.env);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const data = { flowId: currentState.id, currentState, staging: staging.getStagingSummary() };
|
|
79
|
+
return formatSuccess(data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const createFlowDefinition = {
|
|
83
|
+
name: 'create-flow',
|
|
84
|
+
annotations: ANN_MUTATION,
|
|
85
|
+
outputSchema: CreateFlowResponseSchema,
|
|
86
|
+
handler: handleCreateFlow,
|
|
87
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: create-node
|
|
3
|
+
*
|
|
4
|
+
* Creates a new node of any installed palette type in a specified Node-RED flow.
|
|
5
|
+
* Generates a UUID for the node ID, assembles the node object, appends it to the
|
|
6
|
+
* flows, and deploys. Returns nodeId and currentState.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ INJECT NODE GOTCHA: The inject node in Node-RED 5.x requires ALL fields
|
|
9
|
+
* (repeat, crontab, once, onceDelay, topic, props) to be explicitly set in
|
|
10
|
+
* `properties`, even if their value is the default empty/false. Omitting any
|
|
11
|
+
* of these fields causes the Node-RED editor to display a red-triangle error
|
|
12
|
+
* on the node, even though the inject works functionally. See
|
|
13
|
+
* nodered-node-reference skill for the complete field set.
|
|
14
|
+
*
|
|
15
|
+
* Credential handling (via normalizeCredentials from flow-utils.js):
|
|
16
|
+
* When creating configuration nodes (e.g. mqtt-broker, http-proxy),
|
|
17
|
+
* credential fields like `username`, `password`, `key`, `token`, etc.
|
|
18
|
+
* are automatically nested under a `credentials` sub-object to match
|
|
19
|
+
* Node-RED's credential storage model.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { randomUUID } from 'crypto';
|
|
23
|
+
import { normalizeCredentials } from './flow-utils.js';
|
|
24
|
+
|
|
25
|
+
import { formatSuccess } from './response-utils.js';
|
|
26
|
+
import { ANN_MUTATION } from './constants.js';
|
|
27
|
+
import { CreateNodeResponseSchema } from '../schemas/responses.js';
|
|
28
|
+
/**
|
|
29
|
+
* Build a new node object with structural fields set and properties merged in.
|
|
30
|
+
* Strips `id`, `z`, and `wires` from `properties` if the caller accidentally
|
|
31
|
+
* includes them — the tool controls those fields.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} type - Palette node type (e.g. "function", "debug")
|
|
34
|
+
* @param {string} flowId - ID of the flow (tab or subflow) to place the node in
|
|
35
|
+
* @param {object} properties - Optional extra fields to merge onto the node
|
|
36
|
+
* @param {number} x - X position (default 300)
|
|
37
|
+
* @param {number} y - Y position (default 200)
|
|
38
|
+
* @returns {object} New node object
|
|
39
|
+
*/
|
|
40
|
+
export function buildNewNode(type, flowId, properties, x, y) {
|
|
41
|
+
// Strip structural fields the caller must not override
|
|
42
|
+
const { id: _id, z: _z, wires: _wires, ...safeProperties } = properties;
|
|
43
|
+
|
|
44
|
+
// Normalize credentials: move credential fields like username, password,
|
|
45
|
+
// key, token, etc. into a `credentials` sub-object for config nodes.
|
|
46
|
+
// Pass null for node since this is a new node (no existing credentials).
|
|
47
|
+
const normalizedProperties = normalizeCredentials(safeProperties, null);
|
|
48
|
+
|
|
49
|
+
// Nodes that have no output ports by default get wires: [].
|
|
50
|
+
// All other nodes default to 1 output port: wires: [[]].
|
|
51
|
+
const zeroOutputTypes = new Set(['debug', 'comment']);
|
|
52
|
+
const defaultWires = zeroOutputTypes.has(type) ? [] : [[]];
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
id: randomUUID(),
|
|
56
|
+
type,
|
|
57
|
+
z: flowId,
|
|
58
|
+
x,
|
|
59
|
+
y,
|
|
60
|
+
wires: defaultWires,
|
|
61
|
+
...normalizedProperties,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Apply the create-node operation to the flows array.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} rawResponse - Raw GET /flows response (must contain `flows` array)
|
|
69
|
+
* @param {string} type - Palette node type
|
|
70
|
+
* @param {string} flowId - ID of the target flow tab or subflow
|
|
71
|
+
* @param {object} properties - Extra properties to merge onto the node
|
|
72
|
+
* @param {number} x - X position
|
|
73
|
+
* @param {number} y - Y position
|
|
74
|
+
* @returns {{ updatedFlows: object[], currentState: object }}
|
|
75
|
+
*/
|
|
76
|
+
export function applyCreateNode(rawResponse, type, flowId, properties, x, y) {
|
|
77
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
78
|
+
|
|
79
|
+
// Verify the target flow exists
|
|
80
|
+
const targetFlow = flows.find(
|
|
81
|
+
(n) => (n.type === 'tab' || n.type === 'subflow') && n.id === flowId,
|
|
82
|
+
);
|
|
83
|
+
if (!targetFlow) {
|
|
84
|
+
throw new Error(`Flow '${flowId}' not found. Use get-flows to list available flow tabs and subflows.`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Reject locked flows
|
|
88
|
+
if (targetFlow.locked) {
|
|
89
|
+
throw new Error(`Flow '${flowId}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const newNode = buildNewNode(type, flowId, properties, x, y);
|
|
93
|
+
const updatedFlows = [...flows, newNode];
|
|
94
|
+
|
|
95
|
+
return { updatedFlows, currentState: newNode };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Handler for the create-node MCP tool.
|
|
100
|
+
*
|
|
101
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
102
|
+
* @param {object} params
|
|
103
|
+
* @param {string} params.type
|
|
104
|
+
* @param {string} params.flowId
|
|
105
|
+
* @param {object} [params.properties={}]
|
|
106
|
+
* @param {number} [params.x=300]
|
|
107
|
+
* @param {number} [params.y=200]
|
|
108
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
109
|
+
*/
|
|
110
|
+
export async function handleCreateNode(staging, client, params) {
|
|
111
|
+
const { type, flowId, properties = {}, x = 300, y = 200 } = params;
|
|
112
|
+
|
|
113
|
+
const { currentState } = await staging.applyMutation((rawResponse) => {
|
|
114
|
+
return applyCreateNode(rawResponse, type, flowId, properties, x, y);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const data = { nodeId: currentState.id, currentState, staging: staging.getStagingSummary() };
|
|
118
|
+
return formatSuccess(data);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const createNodeDefinition = {
|
|
122
|
+
name: 'create-node',
|
|
123
|
+
annotations: ANN_MUTATION,
|
|
124
|
+
outputSchema: CreateNodeResponseSchema,
|
|
125
|
+
handler: handleCreateNode,
|
|
126
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: create-subflow-instance
|
|
3
|
+
*
|
|
4
|
+
* Creates a new instance of an existing subflow inside a flow tab.
|
|
5
|
+
* Auto-sizes output wires to match the subflow's output port count.
|
|
6
|
+
* Validates that both the subflow and the target flow exist.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
|
|
11
|
+
import { formatSuccess } from './response-utils.js';
|
|
12
|
+
import { ANN_MUTATION } from './constants.js';
|
|
13
|
+
import { CreateSubflowInstanceResponseSchema } from '../schemas/responses.js';
|
|
14
|
+
/**
|
|
15
|
+
* Build a subflow instance node object.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} subflowId - ID of the subflow definition
|
|
18
|
+
* @param {string} flowId - ID of the target flow tab
|
|
19
|
+
* @param {string|undefined} name - Display name for the instance
|
|
20
|
+
* @param {Array<{name: string, value: string, type: string}>|undefined} env - Env vars
|
|
21
|
+
* @param {number} outputCount - Number of output ports (from subflow.out.length)
|
|
22
|
+
* @param {number} x - X position
|
|
23
|
+
* @param {number} y - Y position
|
|
24
|
+
* @returns {object} New instance node
|
|
25
|
+
*/
|
|
26
|
+
export function buildSubflowInstance(subflowId, flowId, name, env, outputCount, x, y) {
|
|
27
|
+
// Auto-size wires: one empty array per output port
|
|
28
|
+
const wires = Array.from({ length: outputCount }, () => []);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
id: randomUUID(),
|
|
32
|
+
type: `subflow:${subflowId}`,
|
|
33
|
+
z: flowId,
|
|
34
|
+
name: name || '',
|
|
35
|
+
env: env || [],
|
|
36
|
+
x,
|
|
37
|
+
y,
|
|
38
|
+
wires,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply the create-subflow-instance operation to the flows array.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} rawResponse - Raw GET /flows response
|
|
46
|
+
* @param {string} subflowId - ID of the subflow definition
|
|
47
|
+
* @param {string} flowId - ID of the target flow tab
|
|
48
|
+
* @param {string|undefined} name - Display name
|
|
49
|
+
* @param {Array<{name: string, value: string, type: string}>|undefined} env - Env vars
|
|
50
|
+
* @param {number} x - X position
|
|
51
|
+
* @param {number} y - Y position
|
|
52
|
+
* @returns {{ updatedFlows: object[], currentState: object }}
|
|
53
|
+
* @throws {Error} If subflow or flow does not exist, or flow is locked
|
|
54
|
+
*/
|
|
55
|
+
export function applyCreateSubflowInstance(rawResponse, subflowId, flowId, name, env, x, y) {
|
|
56
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
57
|
+
|
|
58
|
+
// Validate subflow exists
|
|
59
|
+
const subflow = flows.find(
|
|
60
|
+
(n) => n.type === 'subflow' && n.id === subflowId,
|
|
61
|
+
);
|
|
62
|
+
if (!subflow) {
|
|
63
|
+
throw new Error(`Subflow '${subflowId}' not found. Use get-subflows to list available subflow definitions.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Validate target flow tab exists
|
|
67
|
+
const targetFlow = flows.find(
|
|
68
|
+
(n) => n.type === 'tab' && n.id === flowId,
|
|
69
|
+
);
|
|
70
|
+
if (!targetFlow) {
|
|
71
|
+
throw new Error(`Flow '${flowId}' not found. Use get-flows to list available flow tabs.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Reject locked flows
|
|
75
|
+
if (targetFlow.locked) {
|
|
76
|
+
throw new Error(`Flow '${flowId}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const outputCount = Array.isArray(subflow.out) ? subflow.out.length : 0;
|
|
80
|
+
const newNode = buildSubflowInstance(subflowId, flowId, name, env, outputCount, x, y);
|
|
81
|
+
const updatedFlows = [...flows, newNode];
|
|
82
|
+
|
|
83
|
+
return { updatedFlows, currentState: newNode };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Handler for the create-subflow-instance MCP tool.
|
|
88
|
+
*
|
|
89
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
90
|
+
* @param {object} params
|
|
91
|
+
* @param {string} params.subflowId
|
|
92
|
+
* @param {string} params.flowId
|
|
93
|
+
* @param {string} [params.name]
|
|
94
|
+
* @param {Array<{name: string, value: string, type: string}>} [params.env]
|
|
95
|
+
* @param {number} [params.x=200]
|
|
96
|
+
* @param {number} [params.y=200]
|
|
97
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
98
|
+
*/
|
|
99
|
+
export async function handleCreateSubflowInstance(staging, client, params) {
|
|
100
|
+
const { subflowId, flowId, name, env, x = 200, y = 200 } = params;
|
|
101
|
+
|
|
102
|
+
const { currentState } = await staging.applyMutation((rawResponse) => {
|
|
103
|
+
return applyCreateSubflowInstance(
|
|
104
|
+
rawResponse,
|
|
105
|
+
subflowId,
|
|
106
|
+
flowId,
|
|
107
|
+
name,
|
|
108
|
+
env,
|
|
109
|
+
x,
|
|
110
|
+
y,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const data = { nodeId: currentState.id, currentState, staging: staging.getStagingSummary() };
|
|
115
|
+
return formatSuccess(data);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const createSubflowInstanceDefinition = {
|
|
119
|
+
name: 'create-subflow-instance',
|
|
120
|
+
annotations: ANN_MUTATION,
|
|
121
|
+
outputSchema: CreateSubflowInstanceResponseSchema,
|
|
122
|
+
handler: handleCreateSubflowInstance,
|
|
123
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: create-subflow
|
|
3
|
+
*
|
|
4
|
+
* Creates a new empty subflow definition (type: "subflow").
|
|
5
|
+
* The subflow can then be populated with internal nodes using
|
|
6
|
+
* create-node (with flowId = subflowId) and connect-nodes.
|
|
7
|
+
* Ports are defined via update-subflow.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { randomUUID } from 'crypto';
|
|
11
|
+
|
|
12
|
+
import { formatSuccess } from './response-utils.js';
|
|
13
|
+
import { ANN_MUTATION } from './constants.js';
|
|
14
|
+
import { CreateSubflowResponseSchema } from '../schemas/responses.js';
|
|
15
|
+
/**
|
|
16
|
+
* Build a new subflow definition node.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} name - Subflow display name
|
|
19
|
+
* @param {string|undefined} info - Markdown description
|
|
20
|
+
* @param {string|undefined} category - Palette category
|
|
21
|
+
* @param {string|undefined} color - Palette color
|
|
22
|
+
* @param {string|undefined} icon - Palette icon
|
|
23
|
+
* @param {object[]|undefined} inPorts - Input port definitions
|
|
24
|
+
* @param {object[]|undefined} outPorts - Output port definitions
|
|
25
|
+
* @returns {object} New subflow node
|
|
26
|
+
*/
|
|
27
|
+
export function buildSubflowDefinition(name, info, category, color, icon, inPorts, outPorts) {
|
|
28
|
+
const node = {
|
|
29
|
+
id: randomUUID(),
|
|
30
|
+
type: 'subflow',
|
|
31
|
+
name,
|
|
32
|
+
info: info || '',
|
|
33
|
+
in: inPorts || [],
|
|
34
|
+
out: outPorts || [],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Add optional palette metadata only if provided
|
|
38
|
+
if (category) node.category = category;
|
|
39
|
+
if (color) node.color = color;
|
|
40
|
+
if (icon) node.icon = icon;
|
|
41
|
+
|
|
42
|
+
return node;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Apply the create-subflow operation to the flows array.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} rawResponse - Raw GET /flows response
|
|
49
|
+
* @param {string} name - Subflow name
|
|
50
|
+
* @param {string|undefined} info - Description
|
|
51
|
+
* @param {string|undefined} category - Palette category
|
|
52
|
+
* @param {string|undefined} color - Palette color
|
|
53
|
+
* @param {string|undefined} icon - Palette icon
|
|
54
|
+
* @param {object[]|undefined} inPorts - Input port definitions
|
|
55
|
+
* @param {object[]|undefined} outPorts - Output port definitions
|
|
56
|
+
* @returns {{ updatedFlows: object[], currentState: object }}
|
|
57
|
+
*/
|
|
58
|
+
export function applyCreateSubflow(rawResponse, name, info, category, color, icon, inPorts, outPorts) {
|
|
59
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
60
|
+
|
|
61
|
+
const newNode = buildSubflowDefinition(name, info, category, color, icon, inPorts, outPorts);
|
|
62
|
+
const updatedFlows = [...flows, newNode];
|
|
63
|
+
|
|
64
|
+
return { updatedFlows, currentState: newNode };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Handler for the create-subflow MCP tool.
|
|
69
|
+
*
|
|
70
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
71
|
+
* @param {object} params
|
|
72
|
+
* @param {string} params.name
|
|
73
|
+
* @param {string} [params.info]
|
|
74
|
+
* @param {string} [params.category]
|
|
75
|
+
* @param {string} [params.color]
|
|
76
|
+
* @param {string} [params.icon]
|
|
77
|
+
* @param {object[]} [params.in]
|
|
78
|
+
* @param {object[]} [params.out]
|
|
79
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
80
|
+
*/
|
|
81
|
+
export async function handleCreateSubflow(staging, client, params) {
|
|
82
|
+
const { name, info, category, color, icon } = params;
|
|
83
|
+
const inPorts = params.in;
|
|
84
|
+
const outPorts = params.out;
|
|
85
|
+
|
|
86
|
+
const { currentState } = await staging.applyMutation((rawResponse) => {
|
|
87
|
+
return applyCreateSubflow(
|
|
88
|
+
rawResponse, name, info, category, color, icon, inPorts, outPorts,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const data = { subflowId: currentState.id, currentState, staging: staging.getStagingSummary() };
|
|
93
|
+
return formatSuccess(data);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const createSubflowDefinition = {
|
|
97
|
+
name: 'create-subflow',
|
|
98
|
+
annotations: ANN_MUTATION,
|
|
99
|
+
outputSchema: CreateSubflowResponseSchema,
|
|
100
|
+
handler: handleCreateSubflow,
|
|
101
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: delete-context
|
|
3
|
+
*
|
|
4
|
+
* Deletes a context variable from a node, flow, or global scope in Node-RED
|
|
5
|
+
* via the Admin API (DELETE /context/{scope}/{id}/{var_name}).
|
|
6
|
+
*
|
|
7
|
+
* Note: In-memory context values are lost when Node-RED restarts anyway,
|
|
8
|
+
* but this tool explicitly removes a key from any configured store.
|
|
9
|
+
*/
|
|
10
|
+
import { formatSuccess } from './response-utils.js';
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
import { ANN_DESTRUCTIVE } from './constants.js';
|
|
14
|
+
import { DeleteContextResponseSchema } from '../schemas/responses.js';
|
|
15
|
+
/**
|
|
16
|
+
* Build the API path for a context DELETE request.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} scope - 'node' | 'flow' | 'global'
|
|
19
|
+
* @param {string|undefined} id - Node or flow UUID (required for node/flow scopes)
|
|
20
|
+
* @param {string} key - The context variable name to delete
|
|
21
|
+
* @returns {string} The API path
|
|
22
|
+
*/
|
|
23
|
+
export function buildDeleteContextPath(scope, id, key) {
|
|
24
|
+
const base = scope === 'global' ? '/context/global' : `/context/${scope}/${id}`;
|
|
25
|
+
return `${base}/${encodeURIComponent(key)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Handler for the delete-context MCP tool.
|
|
30
|
+
*
|
|
31
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
32
|
+
* @param {object} params - Validated input parameters
|
|
33
|
+
* @param {string} params.scope - 'node' | 'flow' | 'global'
|
|
34
|
+
* @param {string} [params.id] - Node or flow UUID (required for node/flow scopes)
|
|
35
|
+
* @param {string} params.key - Context key to delete
|
|
36
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
37
|
+
*/
|
|
38
|
+
export async function handleDeleteContext(client, params) {
|
|
39
|
+
const { scope, id, key } = params;
|
|
40
|
+
|
|
41
|
+
// Validate: id is required for node and flow scopes
|
|
42
|
+
if ((scope === 'node' || scope === 'flow') && !id) {
|
|
43
|
+
throw new Error(`id is required for scope "${scope}". Provide the node or flow UUID to target. For global scope, omit the id parameter.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const path = buildDeleteContextPath(scope, id, key);
|
|
47
|
+
await client.request('DELETE', path);
|
|
48
|
+
|
|
49
|
+
const result = { scope, key, deleted: true };
|
|
50
|
+
if (id) result.id = id;
|
|
51
|
+
|
|
52
|
+
return formatSuccess(result);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const deleteContextDefinition = {
|
|
56
|
+
name: 'delete-context',
|
|
57
|
+
annotations: ANN_DESTRUCTIVE,
|
|
58
|
+
outputSchema: DeleteContextResponseSchema,
|
|
59
|
+
handler: handleDeleteContext,
|
|
60
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: delete-flow
|
|
3
|
+
*
|
|
4
|
+
* Deletes an existing Node-RED flow tab by ID along with all its child nodes.
|
|
5
|
+
* Stages the change locally — call `deploy` to push to Node-RED.
|
|
6
|
+
* Returns the full previous state (including nodes) before deletion.
|
|
7
|
+
* Refuses to delete a locked flow.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { formatSuccess } from './response-utils.js';
|
|
11
|
+
|
|
12
|
+
import { ANN_DESTRUCTIVE } from './constants.js';
|
|
13
|
+
import { DeleteFlowResponseSchema } from '../schemas/responses.js';
|
|
14
|
+
/**
|
|
15
|
+
* Apply a delete-flow mutation to the flows array.
|
|
16
|
+
*
|
|
17
|
+
* Removes the flow tab and all nodes with `z === flowId`.
|
|
18
|
+
* No HTTP — pure data transformation.
|
|
19
|
+
*
|
|
20
|
+
* @param {object} rawResponse - Wrapper with `flows` array
|
|
21
|
+
* @param {string} flowId - ID of the flow tab to delete
|
|
22
|
+
* @returns {{ updatedFlows: object[], previousState: object|null }}
|
|
23
|
+
* @throws {Error} If flow not found, is the last remaining flow, or is locked
|
|
24
|
+
*/
|
|
25
|
+
export function applyDeleteFlow(rawResponse, flowId) {
|
|
26
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
27
|
+
|
|
28
|
+
const tabIndex = flows.findIndex((n) => n.type === 'tab' && n.id === flowId);
|
|
29
|
+
if (tabIndex === -1) {
|
|
30
|
+
throw new Error(`Flow '${flowId}' not found. Use get-flows to list available flow tabs.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const tab = flows[tabIndex];
|
|
34
|
+
|
|
35
|
+
if (tab.locked) {
|
|
36
|
+
throw new Error(`Flow '${flowId}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Guard: Node-RED requires at least one flow tab to exist
|
|
40
|
+
const tabCount = flows.filter((n) => n.type === 'tab').length;
|
|
41
|
+
if (tabCount <= 1) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
'Cannot delete the last flow — at least one flow tab must exist. Use get-flows to confirm how many tabs remain, or create-flow to add a new tab before deleting this one.'
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Collect all child nodes
|
|
48
|
+
const children = flows.filter((n) => n.z === flowId);
|
|
49
|
+
const removedIds = new Set([flowId, ...children.map((n) => n.id)]);
|
|
50
|
+
|
|
51
|
+
const previousState = { tab, nodes: children };
|
|
52
|
+
const updatedFlows = flows.filter((n) => !removedIds.has(n.id));
|
|
53
|
+
|
|
54
|
+
return { updatedFlows, previousState };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Handler for the delete-flow MCP tool.
|
|
59
|
+
*
|
|
60
|
+
* @param {import('../staging-store.js').StagingStore} staging
|
|
61
|
+
* @param {object} params
|
|
62
|
+
* @param {string} params.flowId
|
|
63
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
64
|
+
*/
|
|
65
|
+
export async function handleDeleteFlow(staging, params) {
|
|
66
|
+
const { flowId } = params;
|
|
67
|
+
|
|
68
|
+
const { previousState } = await staging.applyMutation((rawResponse) => {
|
|
69
|
+
return applyDeleteFlow(rawResponse, flowId);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const data = { flowId, previousState, staging: staging.getStagingSummary() };
|
|
73
|
+
return formatSuccess(data);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const deleteFlowDefinition = {
|
|
77
|
+
name: 'delete-flow',
|
|
78
|
+
annotations: ANN_DESTRUCTIVE,
|
|
79
|
+
outputSchema: DeleteFlowResponseSchema,
|
|
80
|
+
handler: handleDeleteFlow,
|
|
81
|
+
};
|