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