@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,116 @@
|
|
|
1
|
+
import { ANN_DESTRUCTIVE } from './constants.js';
|
|
2
|
+
import { DeleteGroupResponseSchema } from '../schemas/responses.js';
|
|
3
|
+
/**
|
|
4
|
+
* MCP tool: delete-group
|
|
5
|
+
*
|
|
6
|
+
* Permanently removes a Node-RED group. By default, all member nodes are
|
|
7
|
+
* also deleted. Set `deleteMembers: false` to strip group membership and
|
|
8
|
+
* keep the nodes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Apply the delete-group operation to the flows array.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} rawResponse - Raw GET /flows response (must contain `flows` array)
|
|
15
|
+
* @param {string} groupId - ID of the group to delete
|
|
16
|
+
* @param {object} [options]
|
|
17
|
+
* @param {boolean} [options.deleteMembers=true] - Whether to also delete member nodes
|
|
18
|
+
* @returns {{ updatedFlows: object[], previousState: { group: object, members: object[] } }}
|
|
19
|
+
*/
|
|
20
|
+
export function applyDeleteGroup(rawResponse, groupId, options = {}) {
|
|
21
|
+
const { deleteMembers = true } = options;
|
|
22
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
23
|
+
|
|
24
|
+
// Find the group
|
|
25
|
+
const groupIndex = flows.findIndex(
|
|
26
|
+
(n) => n.type === 'group' && n.id === groupId,
|
|
27
|
+
);
|
|
28
|
+
if (groupIndex === -1) {
|
|
29
|
+
throw new Error(`Group '${groupId}' not found. Use get-flow-nodes to list groups in the parent flow, or search-nodes with type: "group" to find it.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const group = flows[groupIndex];
|
|
33
|
+
|
|
34
|
+
// Check parent flow lock
|
|
35
|
+
if (group.z) {
|
|
36
|
+
const parentFlow = flows.find(
|
|
37
|
+
(n) => (n.type === 'tab' || n.type === 'subflow') && n.id === group.z,
|
|
38
|
+
);
|
|
39
|
+
if (parentFlow?.locked) {
|
|
40
|
+
throw new Error(`Flow '${group.z}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Collect member nodes for previousState
|
|
45
|
+
const memberIds = group.nodes || [];
|
|
46
|
+
const members = flows.filter(
|
|
47
|
+
(n) => memberIds.includes(n.id),
|
|
48
|
+
);
|
|
49
|
+
const previousState = {
|
|
50
|
+
group: { ...group },
|
|
51
|
+
members: members.map((m) => ({ ...m })),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let updatedFlows = [...flows];
|
|
55
|
+
|
|
56
|
+
if (deleteMembers && memberIds.length > 0) {
|
|
57
|
+
// Delete all member nodes
|
|
58
|
+
updatedFlows = updatedFlows.filter(
|
|
59
|
+
(n) => !memberIds.includes(n.id),
|
|
60
|
+
);
|
|
61
|
+
} else if (!deleteMembers && memberIds.length > 0) {
|
|
62
|
+
// Strip g from all members, keep the nodes
|
|
63
|
+
updatedFlows = updatedFlows.map((n) => {
|
|
64
|
+
if (memberIds.includes(n.id)) {
|
|
65
|
+
const { g: _g, ...rest } = n;
|
|
66
|
+
return rest;
|
|
67
|
+
}
|
|
68
|
+
return n;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Delete the group node itself
|
|
73
|
+
updatedFlows = updatedFlows.filter((n) => n.id !== groupId);
|
|
74
|
+
|
|
75
|
+
return { updatedFlows, previousState };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Handler for the delete-group MCP tool.
|
|
80
|
+
*
|
|
81
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
82
|
+
* @param {object} params
|
|
83
|
+
* @param {string} params.groupId
|
|
84
|
+
* @param {boolean} [params.deleteMembers=true]
|
|
85
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
86
|
+
*/
|
|
87
|
+
export async function handleDeleteGroup(staging, client, params) {
|
|
88
|
+
const { groupId, deleteMembers } = params;
|
|
89
|
+
|
|
90
|
+
const result = await staging.applyMutation((rawResponse) => {
|
|
91
|
+
return applyDeleteGroup(rawResponse, groupId, { deleteMembers });
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const responseData = {
|
|
95
|
+
groupId,
|
|
96
|
+
previousState: result.previousState,
|
|
97
|
+
staging: staging.getStagingSummary(),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: 'text',
|
|
104
|
+
text: JSON.stringify(responseData, null, 2),
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
structuredContent: responseData,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const deleteGroupDefinition = {
|
|
112
|
+
name: 'delete-group',
|
|
113
|
+
annotations: ANN_DESTRUCTIVE,
|
|
114
|
+
outputSchema: DeleteGroupResponseSchema,
|
|
115
|
+
handler: handleDeleteGroup,
|
|
116
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: delete-node
|
|
3
|
+
*
|
|
4
|
+
* Removes an existing node from a Node-RED flow by nodeId.
|
|
5
|
+
* Node-RED automatically cleans up dangling wire references on deploy.
|
|
6
|
+
* Refuses to delete nodes in locked flows.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatSuccess } from './response-utils.js';
|
|
10
|
+
|
|
11
|
+
import { ANN_DESTRUCTIVE } from './constants.js';
|
|
12
|
+
import { DeleteNodeResponseSchema } from '../schemas/responses.js';
|
|
13
|
+
/**
|
|
14
|
+
* Apply the delete-node operation to the flows array.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} rawResponse - Raw GET /flows response (must contain `flows` array)
|
|
17
|
+
* @param {string} nodeId - ID of the node to remove
|
|
18
|
+
* @returns {{ updatedFlows: object[], previousState: object }}
|
|
19
|
+
*/
|
|
20
|
+
export function applyDeleteNode(rawResponse, nodeId) {
|
|
21
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
22
|
+
|
|
23
|
+
// Find the node to delete
|
|
24
|
+
const nodeIndex = flows.findIndex((n) => n.id === nodeId);
|
|
25
|
+
if (nodeIndex === -1) {
|
|
26
|
+
throw new Error(`Node '${nodeId}' not found. Use search-nodes with the node name or get-flow-nodes to list nodes in the parent flow.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const node = flows[nodeIndex];
|
|
30
|
+
|
|
31
|
+
// Check parent flow lock
|
|
32
|
+
const parentFlowId = node.z;
|
|
33
|
+
if (parentFlowId) {
|
|
34
|
+
const parentFlow = flows.find(
|
|
35
|
+
(n) => (n.type === 'tab' || n.type === 'subflow') && n.id === parentFlowId,
|
|
36
|
+
);
|
|
37
|
+
if (parentFlow?.locked) {
|
|
38
|
+
throw new Error(`Flow '${parentFlowId}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const previousState = { ...node };
|
|
43
|
+
const updatedFlows = flows.filter((n) => n.id !== nodeId);
|
|
44
|
+
|
|
45
|
+
return { updatedFlows, previousState };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Handler for the delete-node MCP tool.
|
|
50
|
+
*
|
|
51
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
52
|
+
* @param {object} params
|
|
53
|
+
* @param {string} params.nodeId
|
|
54
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
export async function handleDeleteNode(staging, client, params) {
|
|
58
|
+
const { nodeId } = params;
|
|
59
|
+
|
|
60
|
+
const { previousState } = await staging.applyMutation((rawResponse) => {
|
|
61
|
+
return applyDeleteNode(rawResponse, nodeId);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const data = { nodeId, previousState, staging: staging.getStagingSummary() };
|
|
65
|
+
return formatSuccess(data);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const deleteNodeDefinition = {
|
|
69
|
+
name: 'delete-node',
|
|
70
|
+
annotations: ANN_DESTRUCTIVE,
|
|
71
|
+
outputSchema: DeleteNodeResponseSchema,
|
|
72
|
+
handler: handleDeleteNode,
|
|
73
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: delete-subflow
|
|
3
|
+
*
|
|
4
|
+
* Deletes a subflow definition, its internal nodes, and optionally
|
|
5
|
+
* its instances. Returns previousState for undo support.
|
|
6
|
+
* Refuses to delete a locked subflow.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { formatSuccess } from './response-utils.js';
|
|
10
|
+
|
|
11
|
+
import { ANN_DESTRUCTIVE } from './constants.js';
|
|
12
|
+
import { DeleteSubflowResponseSchema } from '../schemas/responses.js';
|
|
13
|
+
/**
|
|
14
|
+
* Collect the full previous state of a subflow before deletion.
|
|
15
|
+
*
|
|
16
|
+
* @param {object[]} flows - All nodes from the flows array
|
|
17
|
+
* @param {string} subflowId - ID of the subflow to collect
|
|
18
|
+
* @returns {{ definition: object|undefined, internalNodes: object[], instances: object[] }}
|
|
19
|
+
* @throws {Error} If subflowId does not match any type: "subflow" node
|
|
20
|
+
*/
|
|
21
|
+
export function collectSubflowState(flows, subflowId) {
|
|
22
|
+
const definition = flows.find(
|
|
23
|
+
(n) => n.type === 'subflow' && n.id === subflowId,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (!definition) {
|
|
27
|
+
throw new Error(`Subflow '${subflowId}' not found. Use get-subflows to list available subflow definitions.`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (definition.locked) {
|
|
31
|
+
throw new Error(`Subflow '${subflowId}' is locked. This subflow is locked (read-only). Use get-subflow-detail to inspect it without modifying.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const internalNodes = flows.filter(
|
|
35
|
+
(n) => n.z === subflowId,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const instanceType = `subflow:${subflowId}`;
|
|
39
|
+
const instances = flows.filter(
|
|
40
|
+
(n) => n.type === instanceType,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { definition, internalNodes, instances };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove a subflow and related nodes from the flows array.
|
|
48
|
+
*
|
|
49
|
+
* @param {object[]} flows - All nodes from the flows array
|
|
50
|
+
* @param {string} subflowId - ID of the subflow to delete
|
|
51
|
+
* @param {boolean} deleteInstances - Whether to also delete instances
|
|
52
|
+
* @returns {{ updatedFlows: object[], previousState: object }}
|
|
53
|
+
* @throws {Error} If subflow not found or is locked
|
|
54
|
+
*/
|
|
55
|
+
export function applyDeleteSubflow(flows, subflowId, deleteInstances) {
|
|
56
|
+
const previousState = collectSubflowState(flows, subflowId);
|
|
57
|
+
|
|
58
|
+
// Build set of IDs to remove
|
|
59
|
+
const idsToRemove = new Set();
|
|
60
|
+
idsToRemove.add(subflowId);
|
|
61
|
+
|
|
62
|
+
for (const node of previousState.internalNodes) {
|
|
63
|
+
idsToRemove.add(node.id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (deleteInstances) {
|
|
67
|
+
for (const node of previousState.instances) {
|
|
68
|
+
idsToRemove.add(node.id);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const updatedFlows = flows.filter((n) => !idsToRemove.has(n.id));
|
|
73
|
+
|
|
74
|
+
return { updatedFlows, previousState };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Handler for the delete-subflow MCP tool.
|
|
79
|
+
*
|
|
80
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
81
|
+
* @param {object} params
|
|
82
|
+
* @param {string} params.subflowId
|
|
83
|
+
* @param {boolean} [params.deleteInstances=true]
|
|
84
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
85
|
+
*/
|
|
86
|
+
export async function handleDeleteSubflow(staging, client, params) {
|
|
87
|
+
const { subflowId, deleteInstances = true } = params;
|
|
88
|
+
|
|
89
|
+
const { previousState } = await staging.applyMutation((rawResponse) => {
|
|
90
|
+
const flows = rawResponse.flows || [];
|
|
91
|
+
return applyDeleteSubflow(flows, subflowId, deleteInstances);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const data = { subflowId, previousState, staging: staging.getStagingSummary() };
|
|
95
|
+
return formatSuccess(data);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const deleteSubflowDefinition = {
|
|
99
|
+
name: 'delete-subflow',
|
|
100
|
+
annotations: ANN_DESTRUCTIVE,
|
|
101
|
+
outputSchema: DeleteSubflowResponseSchema,
|
|
102
|
+
handler: handleDeleteSubflow,
|
|
103
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { ANN_DEPLOY } from './constants.js';
|
|
2
|
+
import { DeployResponseSchema } from '../schemas/responses.js';
|
|
3
|
+
/**
|
|
4
|
+
* MCP tool: deploy
|
|
5
|
+
*
|
|
6
|
+
* Sends all staged (undeployed) flow changes to the Node-RED runtime.
|
|
7
|
+
* Supports three deploy types: full, flows, and nodes.
|
|
8
|
+
*
|
|
9
|
+
* By default, deploys only modified nodes (`nodes` deploy) which is the
|
|
10
|
+
* least disruptive — only changed nodes are restarted.
|
|
11
|
+
*
|
|
12
|
+
* After a successful deploy, the staging store syncs with the Node-RED
|
|
13
|
+
* backend to obtain the latest rev and state.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Handler for the deploy MCP tool.
|
|
18
|
+
*
|
|
19
|
+
* @param {import('../staging-store.js').StagingStore} staging
|
|
20
|
+
* @returns {(params: { deployType?: string }) => Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
21
|
+
*/
|
|
22
|
+
export function handleDeploy(staging) {
|
|
23
|
+
return async (params) => {
|
|
24
|
+
const { deployType = 'nodes' } = params || {};
|
|
25
|
+
|
|
26
|
+
// Validate deploy type
|
|
27
|
+
const validTypes = ['full', 'flows', 'nodes'];
|
|
28
|
+
if (!validTypes.includes(deployType)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid deploy type "${deployType}". Use one of: ${validTypes.join(', ')}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const summaryBefore = staging.getStagingSummary();
|
|
35
|
+
|
|
36
|
+
if (!staging.hasPendingChanges()) {
|
|
37
|
+
const noPendingData = {
|
|
38
|
+
success: true,
|
|
39
|
+
message: 'No pending changes to deploy.',
|
|
40
|
+
staging: summaryBefore,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
content: [
|
|
45
|
+
{
|
|
46
|
+
type: 'text',
|
|
47
|
+
text: JSON.stringify(noPendingData, null, 2),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
structuredContent: noPendingData,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
await staging.deploy(deployType);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
const msg = err.message || '';
|
|
58
|
+
if (msg.includes('version_mismatch') || msg.includes('409')) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
'Deploy failed: version mismatch. The flows have been modified externally ' +
|
|
61
|
+
'(e.g., via the Node-RED editor). Your staged changes have been discarded. ' +
|
|
62
|
+
'Call refresh-staging to sync with the latest server state, then re-apply your changes and deploy again.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const summaryAfter = staging.getStagingSummary();
|
|
69
|
+
|
|
70
|
+
const deployData = {
|
|
71
|
+
success: true,
|
|
72
|
+
deployType,
|
|
73
|
+
previousPendingChanges: summaryBefore.pendingChanges,
|
|
74
|
+
staging: summaryAfter,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
content: [
|
|
79
|
+
{
|
|
80
|
+
type: 'text',
|
|
81
|
+
text: JSON.stringify(deployData, null, 2),
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
structuredContent: deployData,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const deployDefinition = {
|
|
90
|
+
name: 'deploy',
|
|
91
|
+
annotations: ANN_DEPLOY,
|
|
92
|
+
outputSchema: DeployResponseSchema,
|
|
93
|
+
handler: handleDeploy,
|
|
94
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool: disconnect-nodes
|
|
3
|
+
*
|
|
4
|
+
* Removes wires from a node's output ports.
|
|
5
|
+
* Supports three modes:
|
|
6
|
+
* 1. Single: remove one specific wire (outputPort + toNodeId)
|
|
7
|
+
* 2. Clear-port: remove all wires from an output port (clearPort=true, toNodeId omitted)
|
|
8
|
+
* 3. Batch: remove multiple wires via connections array
|
|
9
|
+
* Errors if a wire does not exist. Refuses to mutate nodes in locked flows.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { formatSuccess } from './response-utils.js';
|
|
13
|
+
|
|
14
|
+
import { ANN_MUTATION } from './constants.js';
|
|
15
|
+
import { WireChangeResponseSchema } from '../schemas/responses.js';
|
|
16
|
+
/**
|
|
17
|
+
* Apply wire removal in the flows array.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} rawResponse - Raw GET /flows response (must contain `flows` array)
|
|
20
|
+
* @param {string} fromNodeId - ID of the source node
|
|
21
|
+
* @param {number} [outputPort=0] - Output port index (0-based) — ignored in batch mode
|
|
22
|
+
* @param {string} [toNodeId] - ID of the target node to disconnect — ignored in batch/clear-port mode
|
|
23
|
+
* @param {boolean} [clearPort=false] - If true and toNodeId is absent, clear all wires from outputPort
|
|
24
|
+
* @param {Array<{ outputPort: number, toNodeId: string }>} [connections] - Batch removal entries
|
|
25
|
+
* @returns {{ updatedFlows: object[], previousWires: string[][], currentWires: string[][] }}
|
|
26
|
+
*/
|
|
27
|
+
export function applyDisconnect(rawResponse, fromNodeId, outputPort = 0, toNodeId, clearPort = false, connections) {
|
|
28
|
+
const flows = rawResponse.flows ?? rawResponse;
|
|
29
|
+
|
|
30
|
+
// Find source node
|
|
31
|
+
const fromIndex = flows.findIndex((n) => n.id === fromNodeId);
|
|
32
|
+
if (fromIndex === -1) {
|
|
33
|
+
throw new Error(`Node '${fromNodeId}' not found. Use search-nodes with the node name or get-flow-nodes to list nodes in the parent flow.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fromNode = flows[fromIndex];
|
|
37
|
+
|
|
38
|
+
// Check parent flow lock
|
|
39
|
+
const parentFlowId = fromNode.z;
|
|
40
|
+
if (parentFlowId) {
|
|
41
|
+
const parentFlow = flows.find(
|
|
42
|
+
(n) => (n.type === 'tab' || n.type === 'subflow') && n.id === parentFlowId,
|
|
43
|
+
);
|
|
44
|
+
if (parentFlow?.locked) {
|
|
45
|
+
throw new Error(`Flow '${parentFlowId}' is locked. This flow is locked (read-only). Use get-flow-nodes to inspect its nodes without modifying them.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const previousWires = (fromNode.wires ?? []).map((port) => [...port]);
|
|
50
|
+
|
|
51
|
+
// Determine mode: batch > clear-port > single
|
|
52
|
+
if (connections) {
|
|
53
|
+
// --- Batch mode ---
|
|
54
|
+
// Validate all wires exist before removing any (atomicity)
|
|
55
|
+
for (const entry of connections) {
|
|
56
|
+
const port = entry.outputPort;
|
|
57
|
+
const portWires = previousWires[port] ?? [];
|
|
58
|
+
if (!portWires.includes(entry.toNodeId)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Wire from '${fromNodeId}'[${port}] to '${entry.toNodeId}' does not exist. Use get-node-detail to inspect the current wiring of the source node.`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Build new wires — deep copy and remove all target wires
|
|
66
|
+
const newWires = previousWires.map((port) => [...port]);
|
|
67
|
+
for (const entry of connections) {
|
|
68
|
+
const port = entry.outputPort;
|
|
69
|
+
if (newWires[port]) {
|
|
70
|
+
newWires[port] = newWires[port].filter((id) => id !== entry.toNodeId);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const currentWires = newWires;
|
|
75
|
+
const updatedNode = { ...fromNode, wires: currentWires };
|
|
76
|
+
const updatedFlows = flows.map((n, i) => (i === fromIndex ? updatedNode : n));
|
|
77
|
+
|
|
78
|
+
return { updatedFlows, previousWires, currentWires };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (clearPort && !toNodeId) {
|
|
82
|
+
// --- Clear-port mode ---
|
|
83
|
+
const newWires = previousWires.map((port, i) => {
|
|
84
|
+
if (i === outputPort) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [...port];
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const currentWires = newWires;
|
|
91
|
+
const updatedNode = { ...fromNode, wires: currentWires };
|
|
92
|
+
const updatedFlows = flows.map((n, i) => (i === fromIndex ? updatedNode : n));
|
|
93
|
+
|
|
94
|
+
return { updatedFlows, previousWires, currentWires };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Single-wire mode ---
|
|
98
|
+
// Check the wire actually exists
|
|
99
|
+
const portConnections = previousWires[outputPort];
|
|
100
|
+
if (!portConnections || !portConnections.includes(toNodeId)) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`Wire from '${fromNodeId}'[${outputPort}] to '${toNodeId}' does not exist. Use get-node-detail to inspect the current wiring of the source node.`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Build new wires — deep copy and remove the target
|
|
107
|
+
const newWires = previousWires.map((port, i) => {
|
|
108
|
+
if (i === outputPort) {
|
|
109
|
+
return port.filter((id) => id !== toNodeId);
|
|
110
|
+
}
|
|
111
|
+
return [...port];
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const currentWires = newWires;
|
|
115
|
+
const updatedNode = { ...fromNode, wires: currentWires };
|
|
116
|
+
const updatedFlows = flows.map((n, i) => (i === fromIndex ? updatedNode : n));
|
|
117
|
+
|
|
118
|
+
return { updatedFlows, previousWires, currentWires };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Handler for the disconnect-nodes MCP tool.
|
|
123
|
+
*
|
|
124
|
+
* @param {ReturnType<import('../nodered/client.js').createNodeRedClient>} client
|
|
125
|
+
* @param {object} params
|
|
126
|
+
* @param {string} params.fromNodeId
|
|
127
|
+
* @param {number} [params.outputPort=0]
|
|
128
|
+
* @param {string} [params.toNodeId]
|
|
129
|
+
* @param {boolean} [params.clearPort=false]
|
|
130
|
+
* @param {Array<{ outputPort: number, toNodeId: string }>} [params.connections]
|
|
131
|
+
* @returns {Promise<{ content: Array<{ type: string, text: string }> }>}
|
|
132
|
+
*/
|
|
133
|
+
export async function handleDisconnectNodes(staging, client, params) {
|
|
134
|
+
const { fromNodeId, outputPort = 0, toNodeId, clearPort = false, connections } = params;
|
|
135
|
+
|
|
136
|
+
const { previousWires, currentWires } = await staging.applyMutation(
|
|
137
|
+
(rawResponse) => {
|
|
138
|
+
return applyDisconnect(
|
|
139
|
+
rawResponse,
|
|
140
|
+
fromNodeId,
|
|
141
|
+
outputPort,
|
|
142
|
+
toNodeId,
|
|
143
|
+
clearPort,
|
|
144
|
+
connections,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const data = { fromNodeId, previousWires, currentWires, staging: staging.getStagingSummary() };
|
|
150
|
+
return formatSuccess(data);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const disconnectNodesDefinition = {
|
|
154
|
+
name: 'disconnect-nodes',
|
|
155
|
+
annotations: ANN_MUTATION,
|
|
156
|
+
outputSchema: WireChangeResponseSchema,
|
|
157
|
+
handler: handleDisconnectNodes,
|
|
158
|
+
};
|