@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,223 @@
1
+ /**
2
+ * Lightweight WebSocket Server
3
+ *
4
+ * Implements a minimal WebSocket server using Node.js built-in `http` module —
5
+ * no external dependencies (no `ws`, no Socket.IO). Handles RFC 6455
6
+ * handshake, message framing, and broadcasting to connected clients.
7
+ *
8
+ * Used by the staging visualization HTML page to receive live updates
9
+ * whenever the StagingStore mutates.
10
+ *
11
+ * @module transport/ws-server
12
+ */
13
+
14
+ import { createHash } from 'crypto';
15
+
16
+ /**
17
+ * WebSocket magic GUID per RFC 6455.
18
+ */
19
+ const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
20
+
21
+ /**
22
+ * Opcodes per RFC 6455.
23
+ */
24
+ const OP_TEXT = 0x1;
25
+ const OP_CLOSE = 0x8;
26
+ const OP_PING = 0x9;
27
+ const OP_PONG = 0xA;
28
+
29
+ /**
30
+ * Minimal WebSocket server attached to an existing Node.js HTTP server.
31
+ */
32
+ export class WSServer {
33
+ /** @type {Set<import('http').IncomingMessage>} */
34
+ #clients = new Set();
35
+
36
+ /** @type {import('http').Server | null} */
37
+ #httpServer = null;
38
+
39
+ /** @type {ReturnType<typeof setInterval> | null} */
40
+ #coalesceTimer = null;
41
+
42
+ /** @type {object | null} */
43
+ #pendingMessage = null;
44
+
45
+ /** @type {() => Promise<object> | null} */
46
+ #getCurrentState = null;
47
+
48
+ /**
49
+ * Attach to an existing HTTP server and start listening for upgrades.
50
+ *
51
+ * @param {import('http').Server} httpServer - Existing HTTP server
52
+ * @param {() => Promise<object>} getCurrentState - Async function returning current staging state { flows, dirtyNodeIds, dirtyFlowIds }
53
+ */
54
+ attach(httpServer, getCurrentState) {
55
+ this.#httpServer = httpServer;
56
+ this.#getCurrentState = getCurrentState;
57
+
58
+ httpServer.on('upgrade', (req, socket, head) => {
59
+ if (req.url === '/staging-ws') {
60
+ this.#handleUpgrade(req, socket, head);
61
+ } else {
62
+ socket.destroy();
63
+ }
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Broadcast a staging update to all connected clients.
69
+ * Coalesces rapid updates within a 100ms window into a single message.
70
+ *
71
+ * @param {object} data - { flows, dirtyNodeIds, dirtyFlowIds }
72
+ */
73
+ broadcast(data) {
74
+ const message = JSON.stringify({
75
+ type: 'staging-update',
76
+ flows: data.flows,
77
+ dirtyNodeIds: [...(data.dirtyNodeIds || [])],
78
+ dirtyFlowIds: [...(data.dirtyFlowIds || [])],
79
+ });
80
+
81
+ // Coalesce: if a message was sent recently, wait
82
+ if (this.#coalesceTimer) {
83
+ this.#pendingMessage = message;
84
+ return;
85
+ }
86
+
87
+ this.#sendToAll(message);
88
+
89
+ // Arm coalescing timer
90
+ this.#coalesceTimer = setTimeout(() => {
91
+ this.#coalesceTimer = null;
92
+ if (this.#pendingMessage) {
93
+ const pending = this.#pendingMessage;
94
+ this.#pendingMessage = null;
95
+ this.#sendToAll(pending);
96
+ }
97
+ }, 100);
98
+ }
99
+
100
+ /**
101
+ * Send data to all connected clients. Removes dead sockets.
102
+ *
103
+ * @param {string} data - String message to send
104
+ */
105
+ #sendToAll(data) {
106
+ for (const socket of this.#clients) {
107
+ try {
108
+ this.#sendFrame(socket, data);
109
+ } catch {
110
+ this.#clients.delete(socket);
111
+ try { socket.destroy(); } catch { /* ignore */ }
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Handle HTTP upgrade request — perform WebSocket handshake.
118
+ */
119
+ #handleUpgrade(req, socket, head) {
120
+ const key = req.headers['sec-websocket-key'];
121
+ if (!key) {
122
+ socket.destroy();
123
+ return;
124
+ }
125
+
126
+ // Compute accept key per RFC 6455
127
+ const acceptKey = createHash('sha1')
128
+ .update(key + WS_GUID)
129
+ .digest('base64');
130
+
131
+ // Send handshake response
132
+ socket.write(
133
+ 'HTTP/1.1 101 Switching Protocols\r\n' +
134
+ 'Upgrade: websocket\r\n' +
135
+ 'Connection: Upgrade\r\n' +
136
+ 'Sec-WebSocket-Accept: ' + acceptKey + '\r\n' +
137
+ '\r\n'
138
+ );
139
+
140
+ this.#clients.add(socket);
141
+
142
+ // Send initial state to the new client
143
+ if (this.#getCurrentState) {
144
+ this.#getCurrentState().then((state) => {
145
+ const initMsg = JSON.stringify({
146
+ type: 'staging-update',
147
+ flows: state.flows,
148
+ dirtyNodeIds: [...(state.dirtyNodeIds || [])],
149
+ dirtyFlowIds: [...(state.dirtyFlowIds || [])],
150
+ });
151
+ try {
152
+ this.#sendFrame(socket, initMsg);
153
+ } catch { /* socket may have closed */ }
154
+ }).catch(() => { /* ignore */ });
155
+ }
156
+
157
+ // Handle incoming data (currently we just consume ping/pong/close)
158
+ socket.on('data', (buffer) => {
159
+ try {
160
+ const opcode = buffer[0] & 0x0f;
161
+ if (opcode === OP_CLOSE) {
162
+ this.#clients.delete(socket);
163
+ try { socket.destroy(); } catch { /* ignore */ }
164
+ } else if (opcode === OP_PING) {
165
+ // Respond with pong
166
+ const pongFrame = Buffer.alloc(2 + (buffer.length - 2));
167
+ pongFrame[0] = 0x8a; // FIN + PONG opcode
168
+ pongFrame[1] = buffer.length - 2; // payload length
169
+ buffer.copy(pongFrame, 2, 2);
170
+ try { socket.write(pongFrame); } catch { /* ignore */ }
171
+ }
172
+ // TEXT frames from client are ignored (read-only visualization)
173
+ } catch { /* malformed frame — ignore */ }
174
+ });
175
+
176
+ socket.on('close', () => {
177
+ this.#clients.delete(socket);
178
+ });
179
+
180
+ socket.on('error', () => {
181
+ this.#clients.delete(socket);
182
+ try { socket.destroy(); } catch { /* ignore */ }
183
+ });
184
+ }
185
+
186
+ /**
187
+ * Send a text frame per RFC 6455.
188
+ *
189
+ * @param {import('net').Socket} socket
190
+ * @param {string} data - Payload string
191
+ */
192
+ #sendFrame(socket, data) {
193
+ const payload = Buffer.from(data, 'utf8');
194
+ const length = payload.length;
195
+
196
+ let frame;
197
+ let offset;
198
+
199
+ if (length < 126) {
200
+ frame = Buffer.alloc(2 + length);
201
+ frame[0] = 0x81; // FIN + TEXT opcode
202
+ frame[1] = length;
203
+ offset = 2;
204
+ } else if (length < 65536) {
205
+ frame = Buffer.alloc(4 + length);
206
+ frame[0] = 0x81;
207
+ frame[1] = 126;
208
+ frame[2] = (length >> 8) & 0xff;
209
+ frame[3] = length & 0xff;
210
+ offset = 4;
211
+ } else {
212
+ frame = Buffer.alloc(10 + length);
213
+ frame[0] = 0x81;
214
+ frame[1] = 127;
215
+ // 64-bit length in network byte order
216
+ frame.writeBigUInt64BE(BigInt(length), 2);
217
+ offset = 10;
218
+ }
219
+
220
+ payload.copy(frame, offset);
221
+ socket.write(frame);
222
+ }
223
+ }