@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.
Files changed (89) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +162 -0
  3. package/index.js +133 -0
  4. package/package.json +58 -0
  5. package/resources/skills/nodered-flow-builder/SKILL.md +659 -0
  6. package/resources/skills/nodered-flow-layout/SKILL.md +395 -0
  7. package/resources/skills/nodered-flowfuse-dashboard/SKILL.md +941 -0
  8. package/resources/skills/nodered-fundamentals/SKILL.md +323 -0
  9. package/resources/skills/nodered-jsonata/SKILL.md +1039 -0
  10. package/resources/skills/nodered-mustache/SKILL.md +588 -0
  11. package/resources/skills/nodered-node-reference/SKILL.md +1020 -0
  12. package/resources/skills/nodered-node-reference/examples/common.json +113 -0
  13. package/resources/skills/nodered-node-reference/examples/network.json +107 -0
  14. package/resources/skills/nodered-node-reference/examples/parser.json +147 -0
  15. package/resources/skills/nodered-node-reference/examples/sequence.json +141 -0
  16. package/resources/skills/nodered-node-reference/examples/storage.json +104 -0
  17. package/resources/skills/nodered-patterns/SKILL.md +414 -0
  18. package/resources/skills/nodered-patterns/examples/error-handler.json +72 -0
  19. package/resources/skills/nodered-patterns/examples/http-endpoint.json +42 -0
  20. package/resources/skills/nodered-patterns/examples/mqtt-subscriber.json +47 -0
  21. package/resources/skills/nodered-patterns/examples/timer-flow.json +50 -0
  22. package/resources/skills/nodered-subflows/SKILL.md +261 -0
  23. package/resources/skills/nodered-uibuilder/SKILL.md +500 -0
  24. package/src/auth/api-key-verifier.js +36 -0
  25. package/src/auth/composite-verifier.js +59 -0
  26. package/src/auth/config.js +106 -0
  27. package/src/auth/oauth-clients-store.js +107 -0
  28. package/src/auth/oauth-provider.js +149 -0
  29. package/src/auth/oauth-token-store.js +312 -0
  30. package/src/nodered/auth.js +158 -0
  31. package/src/nodered/client.js +199 -0
  32. package/src/nodered/comms-client.js +500 -0
  33. package/src/renderer/colors.js +161 -0
  34. package/src/renderer/geometry.js +115 -0
  35. package/src/renderer/html-builder.js +571 -0
  36. package/src/renderer/index.js +51 -0
  37. package/src/renderer/ir-builder.js +161 -0
  38. package/src/renderer/layout.js +126 -0
  39. package/src/renderer/mermaid-builder.js +109 -0
  40. package/src/renderer/svg-builder.js +228 -0
  41. package/src/schemas/responses.js +283 -0
  42. package/src/server.js +844 -0
  43. package/src/skills/loader.js +84 -0
  44. package/src/staging-store.js +258 -0
  45. package/src/tools/add-nodes-to-group.js +216 -0
  46. package/src/tools/connect-nodes.js +115 -0
  47. package/src/tools/constants.js +45 -0
  48. package/src/tools/create-flow.js +87 -0
  49. package/src/tools/create-node.js +126 -0
  50. package/src/tools/create-subflow-instance.js +123 -0
  51. package/src/tools/create-subflow.js +101 -0
  52. package/src/tools/delete-context.js +60 -0
  53. package/src/tools/delete-flow.js +81 -0
  54. package/src/tools/delete-group.js +116 -0
  55. package/src/tools/delete-node.js +73 -0
  56. package/src/tools/delete-subflow.js +103 -0
  57. package/src/tools/deploy.js +94 -0
  58. package/src/tools/disconnect-nodes.js +158 -0
  59. package/src/tools/export-flow.js +161 -0
  60. package/src/tools/export-subflow.js +78 -0
  61. package/src/tools/flow-utils.js +376 -0
  62. package/src/tools/get-config-nodes.js +86 -0
  63. package/src/tools/get-context.js +76 -0
  64. package/src/tools/get-flow-diagram.js +99 -0
  65. package/src/tools/get-flow-nodes.js +116 -0
  66. package/src/tools/get-flows.js +74 -0
  67. package/src/tools/get-node-detail.js +77 -0
  68. package/src/tools/get-node-type-detail.js +92 -0
  69. package/src/tools/get-palette-nodes.js +63 -0
  70. package/src/tools/get-staging-status.js +34 -0
  71. package/src/tools/get-subflow-detail.js +110 -0
  72. package/src/tools/get-subflows.js +105 -0
  73. package/src/tools/import-flow.js +310 -0
  74. package/src/tools/inject-message.js +117 -0
  75. package/src/tools/install-node.js +31 -0
  76. package/src/tools/read-debug-messages.js +155 -0
  77. package/src/tools/refresh-staging.js +62 -0
  78. package/src/tools/remove-nodes-from-group.js +162 -0
  79. package/src/tools/render-staging.js +69 -0
  80. package/src/tools/response-utils.js +42 -0
  81. package/src/tools/search-nodes.js +134 -0
  82. package/src/tools/uninstall-node.js +31 -0
  83. package/src/tools/update-flow.js +95 -0
  84. package/src/tools/update-group.js +77 -0
  85. package/src/tools/update-node.js +132 -0
  86. package/src/tools/update-subflow.js +84 -0
  87. package/src/transport/http.js +252 -0
  88. package/src/transport/stdio.js +16 -0
  89. 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
+ };