@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,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
+ };