@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,500 @@
1
+ ---
2
+ name: nodered-uibuilder
3
+ version: "7.7.3"
4
+ description: >-
5
+ Architecture and communication patterns for node-red-contrib-uibuilder v7.7.3,
6
+ a versatile bridge between Node-RED flows and custom web frontends.
7
+ Covers the single-node bridge model, filesystem-based frontend structure,
8
+ Socket.IO bidirectional messaging, framework integration patterns
9
+ (Vue, React, Svelte, vanilla JS), and common recipes. Docs:
10
+ https://totallyinformation.github.io/node-red-contrib-uibuilder/
11
+ tools:
12
+ - install-node
13
+ - create-node
14
+ - update-node
15
+ - get-node-type-detail
16
+ - get-node-detail
17
+ - inject-message
18
+ - read-debug-messages
19
+ - refresh-staging
20
+ - deploy
21
+ ---
22
+
23
+ # Node-RED UIBuilder — Architecture & Patterns
24
+
25
+ Comprehensive reference for **node-red-contrib-uibuilder** v7.7.3, a versatile bridge that connects Node-RED flows to custom web frontends via Socket.IO. Unlike Dashboard 2.0 (widget-based), uibuilder gives you a single Node-RED node that acts as a bidirectional bridge to a **full web application** stored on the filesystem. You write the frontend code yourself using any framework — uibuilder handles the real-time communication.
26
+
27
+ > **⚠️ Staging reminder:** All create-node/update-node calls stage changes locally. After adding or configuring a uibuilder node, call `deploy()` to push to Node-RED. The uibuilder node serves frontend files only after deploy.
28
+
29
+ > **Prerequisites:** Read `nodered-fundamentals` first for core vocabulary. The frontend code is edited outside the MCP domain — MCP tools manage only the Node-RED side (the uibuilder node itself). Use the Node-RED editor or a local file editor to modify frontend files.
30
+
31
+ > **Frontend scope:** This skill covers the bridge layer — Node-RED communication patterns and the `msg._ui` protocol. For full frontend framework tutorials (Vue, React, Svelte), consult the official docs at https://totallyinformation.github.io/node-red-contrib-uibuilder/.
32
+
33
+ ---
34
+
35
+ ## Architecture
36
+
37
+ UIBuilder uses a **single-node bridge** model. One `uibuilder` node in Node-RED connects to one web application via Socket.IO:
38
+
39
+ ```
40
+ ┌─────────────────────────────────────────────────────────┐
41
+ │ Node-RED Runtime │
42
+ │ ┌──────────┐ ┌──────────────┐ ┌───────────────┐ │
43
+ │ │ inject/ │───▶│ uibuilder │◀───│ function/debug│ │
44
+ │ │ sensor │ │ node │ │ (process │ │
45
+ │ │ │◀───│ │───▶│ responses) │ │
46
+ │ └──────────┘ └──────┬───────┘ └───────────────┘ │
47
+ │ │ │
48
+ │ Socket.IO │
49
+ │ (websocket) │
50
+ └─────────────────────────┼───────────────────────────────┘
51
+
52
+ ┌───────────┴───────────┐
53
+ │ Frontend App │
54
+ │ (on filesystem) │
55
+ │ │
56
+ │ ~/.node-red/ │
57
+ │ uibuilder/ │
58
+ │ <url>/ │
59
+ │ src/ │
60
+ │ index.html │
61
+ │ index.js │
62
+ │ index.css │
63
+ └───────────────────────┘
64
+ ```
65
+
66
+ **Key architectural points:**
67
+ - **One uibuilder node = one web app.** Each node serves its own URL path and has its own frontend directory.
68
+ - **Frontend lives on the filesystem** at `~/.node-red/uibuilder/<url>/src/`. This is where `index.html`, `index.js`, and `index.css` live.
69
+ - **Communication is bidirectional.** NR flows send data to the frontend via Socket.IO. The frontend sends data back to NR flows via Socket.IO.
70
+ - **MCP handles ONLY the Node-RED side.** You create/configure the uibuilder node with MCP tools. The frontend code is written separately outside the MCP domain.
71
+ - **No build step required.** The uibuilder library (`uibuilder.esm.js` or `uibuilder.iife.js`) is served directly to the browser. You can use any framework or none at all.
72
+
73
+ **Supported frameworks (all optional):** Vanilla JavaScript, Vue.js, React, Svelte, and any other framework that can run in a browser. UIBuilder is framework-agnostic.
74
+
75
+ ---
76
+
77
+ ## Setup & Configuration
78
+
79
+ ### Installation
80
+
81
+ Install the uibuilder package via MCP:
82
+ ```
83
+ install-node(packageName: "node-red-contrib-uibuilder")
84
+ ```
85
+
86
+ ### Creating a UIBuilder Node
87
+
88
+ ```
89
+ create-node(type: "uibuilder", name: "My Web App", properties: {
90
+ url: "my-app",
91
+ name: "My Web App"
92
+ })
93
+ ```
94
+
95
+ After deploy, uibuilder creates the frontend directory at `~/.node-red/uibuilder/my-app/src/` with template files.
96
+
97
+ ### Key Properties
98
+
99
+ | Property | Type | Description |
100
+ |----------|------|-------------|
101
+ | `url` | string | URL path for the frontend. Access at `http://<host>:1880/uibuilder/<url>/`. Also the directory name under `~/.node-red/uibuilder/` |
102
+ | `name` | string | Display name for the node in the Node-RED editor |
103
+ | `topic` | string | Default `msg.topic` for messages emitted by this node |
104
+ | `forwardInputMessages` | boolean | If true, incoming messages on the input are forwarded to the frontend automatically |
105
+
106
+ ### UIBuilder Node I/O
107
+
108
+ - **Input (left port):** Messages sent here are forwarded to the frontend via Socket.IO (if `forwardInputMessages` is true). You can also construct `msg._ui` commands here.
109
+ - **Output (right port):** Messages sent from the frontend via `uibuilder.send()` appear here as standard Node-RED `msg` objects. Wire this to function/debug nodes to process.
110
+
111
+ ### Frontend Directory Structure
112
+
113
+ After deploy with a new `url`, uibuilder creates:
114
+ ```
115
+ ~/.node-red/uibuilder/<url>/
116
+ ├── src/
117
+ │ ├── index.html ← Main HTML file (edit this)
118
+ │ ├── index.js ← Your frontend JavaScript (edit this)
119
+ │ └── index.css ← Your styles (edit this)
120
+ ├── package.json
121
+ └── ...
122
+ ```
123
+
124
+ The `index.html` auto-loads the uibuilder client library. You edit the files in `src/` to build your UI.
125
+
126
+ ---
127
+
128
+ ## Communication Patterns
129
+
130
+ ### Node-RED → Frontend (msg._ui Protocol)
131
+
132
+ The `msg._ui` protocol is the standard way to control the frontend DOM from Node-RED. Set `msg._ui` to an array of command objects. Each command has a `method` and method-specific parameters.
133
+
134
+ **Common _ui methods:**
135
+
136
+ | Method | Required Fields | Description |
137
+ |--------|----------------|-------------|
138
+ | `addToDom` | `selector`, `data` | Insert HTML/content into the DOM at the specified CSS selector. Optional: `position` (`"last"`, `"first"`, `"before"`, `"after"`) |
139
+ | `update` | `selector`, `data` | Replace the inner content of the element at the CSS selector |
140
+ | `remove` | `selector` | Remove the element at the CSS selector from the DOM |
141
+ | `notify` | `data` | Show a browser notification. `data` can be string message or `{ message, type, timeout }` |
142
+ | `navigate` | `data` | Navigate to a URL. `data` is the URL string |
143
+
144
+ **Example — add content to the page:**
145
+ ```javascript
146
+ msg._ui = [
147
+ {
148
+ method: "addToDom",
149
+ selector: "#output",
150
+ data: "<div class='alert'>Temperature: 23.5°C</div>",
151
+ position: "last"
152
+ }
153
+ ]
154
+ return msg;
155
+ // Send this from any node (function, inject, etc.) to the uibuilder node's input
156
+ ```
157
+
158
+ **Example — update existing element:**
159
+ ```javascript
160
+ msg._ui = [
161
+ {
162
+ method: "update",
163
+ selector: "#status",
164
+ data: "System running — last update: " + new Date().toLocaleTimeString()
165
+ }
166
+ ]
167
+ return msg;
168
+ ```
169
+
170
+ **Example — show notification:**
171
+ ```javascript
172
+ msg._ui = [
173
+ {
174
+ method: "notify",
175
+ data: {
176
+ message: "⚠️ Alert: Temperature exceeds threshold",
177
+ type: "warning",
178
+ timeout: 5000
179
+ }
180
+ }
181
+ ]
182
+ return msg;
183
+ ```
184
+
185
+ **Example — multiple commands:**
186
+ ```javascript
187
+ msg._ui = [
188
+ { method: "update", selector: "#temp", data: "23.5°C" },
189
+ { method: "update", selector: "#humidity", data: "65%" },
190
+ { method: "addToDom", selector: "#log", data: "<li>Reading recorded</li>", position: "first" }
191
+ ]
192
+ return msg;
193
+ ```
194
+
195
+ **Wiring pattern — NR → frontend:**
196
+ ```
197
+ inject/function → [construct msg._ui] → uibuilder node (input)
198
+ ```
199
+
200
+ ### Frontend → Node-RED (uibuilder.send)
201
+
202
+ The frontend sends messages back to Node-RED using the `uibuilder.send()` function. Messages arrive at the uibuilder node's **output port** as standard `msg` objects.
203
+
204
+ **Minimal frontend JavaScript (vanilla JS, in index.js):**
205
+ ```javascript
206
+ // Listen for messages from Node-RED
207
+ uibuilder.onChange('msg', function(msg) {
208
+ console.log('Received from NR:', msg);
209
+
210
+ // Do something with the data
211
+ document.getElementById('output').innerHTML =
212
+ '<pre>' + JSON.stringify(msg, null, 2) + '</pre>';
213
+ });
214
+
215
+ // Send a message to Node-RED
216
+ function sendToNR(data) {
217
+ uibuilder.send({
218
+ topic: 'user-action',
219
+ payload: data
220
+ });
221
+ }
222
+
223
+ // Example: send on button click
224
+ document.getElementById('myButton').addEventListener('click', function() {
225
+ sendToNR({ action: 'button_clicked', timestamp: Date.now() });
226
+ });
227
+ ```
228
+
229
+ **Receiving in Node-RED:**
230
+ ```
231
+ uibuilder node (output) → function/debug → ...
232
+ // msg.payload = { action: 'button_clicked', timestamp: 1234567890 }
233
+ // msg.topic = 'user-action'
234
+ ```
235
+
236
+ **Wiring pattern — frontend → NR:**
237
+ ```
238
+ uibuilder node (output port) → function (process user data) → ...
239
+ ```
240
+
241
+ ### Standard Messages from UIBuilder
242
+
243
+ UIBuilder automatically sends certain messages to the frontend on connection:
244
+ - **`msg.topic: 'uibuilder/connected'`** — Sent when the Socket.IO connection is established. Contains client info.
245
+ - **`msg.topic: 'uibuilder/disconnected'`** — Sent when the connection is lost.
246
+
247
+ These can be used to show connection status in the UI.
248
+
249
+ ---
250
+
251
+ ## Framework Integration
252
+
253
+ UIBuilder works with any frontend framework. The key is that the uibuilder client library (`uibuilder.esm.js` for ES modules or `uibuilder.iife.js` for script tags) must be loaded before your framework code.
254
+
255
+ ### Vanilla JavaScript (No Framework)
256
+
257
+ The simplest setup. The uibuilder client is auto-loaded in the template `index.html`:
258
+
259
+ ```html
260
+ <!-- index.html — vanilla JS template -->
261
+ <!DOCTYPE html>
262
+ <html>
263
+ <head>
264
+ <meta charset="utf-8">
265
+ <title>My UIBuilder App</title>
266
+ <link rel="stylesheet" href="./index.css">
267
+ </head>
268
+ <body>
269
+ <div id="app">
270
+ <h1>Node-RED Data</h1>
271
+ <div id="output"></div>
272
+ <button id="sendBtn">Send to NR</button>
273
+ </div>
274
+
275
+ <!-- uibuilder client (IIFE version) -->
276
+ <script src="../uibuilder/vendor/socket.io/socket.io.min.js"></script>
277
+ <script src="../uibuilder/uibuilder.iife.min.js"></script>
278
+ <script src="./index.js"></script>
279
+ </body>
280
+ </html>
281
+ ```
282
+
283
+ ### Vue.js
284
+
285
+ Use the ESM version of the uibuilder client. The uibuilder client library detects Vue if `window.Vue` is available.
286
+
287
+ ```javascript
288
+ // In your Vue app's entry point
289
+ import '../uibuilder/uibuilder.esm.min.js'; // load first
290
+
291
+ // Check if Vue is detected
292
+ if (uibuilder.get('isVue')) {
293
+ console.log('Vue version:', uibuilder.get('vueVersion'));
294
+ }
295
+
296
+ // Use uibuilder within Vue components
297
+ export default {
298
+ mounted() {
299
+ uibuilder.onChange('msg', (msg) => {
300
+ this.serverData = msg.payload;
301
+ });
302
+ },
303
+ methods: {
304
+ sendToNR() {
305
+ uibuilder.send({ topic: 'vue-event', payload: this.formData });
306
+ }
307
+ }
308
+ }
309
+ ```
310
+
311
+ ### React
312
+
313
+ Load the uibuilder client as a module:
314
+
315
+ ```javascript
316
+ // In your React app
317
+ import '../uibuilder/uibuilder.esm.min.js';
318
+
319
+ function App() {
320
+ useEffect(() => {
321
+ uibuilder.onChange('msg', (msg) => {
322
+ console.log('Data from NR:', msg);
323
+ });
324
+
325
+ return () => {
326
+ // cleanup if needed
327
+ };
328
+ }, []);
329
+
330
+ const handleClick = () => {
331
+ uibuilder.send({ topic: 'react-event', payload: { clicked: true } });
332
+ };
333
+
334
+ return <button onClick={handleClick}>Send to NR</button>;
335
+ }
336
+ ```
337
+
338
+ ### Svelte
339
+
340
+ ```javascript
341
+ // In your Svelte component
342
+ import '../uibuilder/uibuilder.esm.min.js';
343
+ import { onMount } from 'svelte';
344
+
345
+ let serverData = '';
346
+
347
+ onMount(() => {
348
+ uibuilder.onChange('msg', (msg) => {
349
+ serverData = msg.payload;
350
+ });
351
+ });
352
+
353
+ function sendToNR() {
354
+ uibuilder.send({ topic: 'svelte-event', payload: { value: serverData } });
355
+ }
356
+ ```
357
+
358
+ > **Official framework guides:** The snippets above are minimal connection examples. For full integration guides, build tooling setup, and best practices for each framework, see the official documentation: https://totallyinformation.github.io/node-red-contrib-uibuilder/#/ui-frameworks/README
359
+
360
+ ---
361
+
362
+ ## Recipes
363
+
364
+ ### Recipe: Real-Time Data Display
365
+
366
+ **Goal:** Display live sensor data pushed from Node-RED to a web page.
367
+
368
+ **NR Side:**
369
+ 1. Install and create uibuilder node:
370
+ ```
371
+ install-node(packageName: "node-red-contrib-uibuilder")
372
+ uibId = create-node(type: "uibuilder", name: "Live Data", properties: { url: "live-data" })
373
+ ```
374
+ 2. Create an inject to simulate sensor data:
375
+ ```
376
+ injectId = create-node(type: "inject", name: "Sensor", properties: {
377
+ payload: "23.5",
378
+ payloadType: "num",
379
+ topic: "temperature",
380
+ repeat: "2"
381
+ })
382
+ ```
383
+ 3. Wire inject → uibuilder:
384
+ ```
385
+ connect-nodes(fromNodeId: injectId, outputPort: 0, toNodeId: uibId)
386
+ deploy()
387
+ ```
388
+
389
+ **Frontend Side (edit `~/.node-red/uibuilder/live-data/src/index.js`):**
390
+ ```javascript
391
+ uibuilder.onChange('msg', function(msg) {
392
+ document.getElementById('temp').textContent = msg.payload + '°C';
393
+ document.getElementById('time').textContent = new Date().toLocaleTimeString();
394
+ });
395
+ ```
396
+
397
+ **Frontend HTML (`index.html`):**
398
+ ```html
399
+ <h1>Live Sensor Data</h1>
400
+ <p>Temperature: <span id="temp">--</span></p>
401
+ <p>Last update: <span id="time">--</span></p>
402
+ ```
403
+
404
+ Access at `http://<host>:1880/uibuilder/live-data/`.
405
+
406
+ ### Recipe: Form Submission to NR and Response Back
407
+
408
+ **Goal:** User submits a form on the web page, NR processes it, and sends a confirmation back.
409
+
410
+ **NR Side:**
411
+ 1. Create uibuilder node (as above).
412
+ 2. Create a function to process form data:
413
+ ```
414
+ fnId = create-node(type: "function", name: "Process Form", properties: {
415
+ func: "// msg.payload from frontend = { name: '...', message: '...' }\nconst data = msg.payload;\n\n// Process the data (e.g., save to DB, send email)\nconst success = data.name && data.message;\n\n// Send response back to the frontend\nmsg._ui = [{\n method: 'update',\n selector: '#response',\n data: success \n ? '<div class=\"success\">Thank you, ' + data.name + '!</div>'\n : '<div class=\"error\">Please fill all fields.</div>'\n}];\nreturn msg;",
416
+ outputs: 1
417
+ })
418
+ ```
419
+ 3. Wire: uibuilder output → function → uibuilder input:
420
+ ```
421
+ connect-nodes(fromNodeId: uibId, outputPort: 0, toNodeId: fnId)
422
+ connect-nodes(fromNodeId: fnId, outputPort: 0, toNodeId: uibId)
423
+ deploy()
424
+ ```
425
+
426
+ **Frontend Side (`index.js`):**
427
+ ```javascript
428
+ document.getElementById('submitBtn').addEventListener('click', function() {
429
+ const name = document.getElementById('name').value;
430
+ const message = document.getElementById('message').value;
431
+
432
+ uibuilder.send({
433
+ topic: 'form-submission',
434
+ payload: { name: name, message: message }
435
+ });
436
+ });
437
+
438
+ // Listen for response from NR
439
+ uibuilder.onChange('msg', function(msg) {
440
+ if (msg._ui) {
441
+ // _ui commands update the DOM automatically
442
+ // The #response div will show success/error
443
+ }
444
+ });
445
+ ```
446
+
447
+ **Frontend HTML:**
448
+ ```html
449
+ <input id="name" placeholder="Your name">
450
+ <textarea id="message" placeholder="Your message"></textarea>
451
+ <button id="submitBtn">Send</button>
452
+ <div id="response"></div>
453
+ ```
454
+
455
+ ### Recipe: NR as WebSocket Relay for External Data
456
+
457
+ **Goal:** Receive data from an external WebSocket/MQTT source, relay it to all connected frontend clients in real time.
458
+
459
+ **NR Side:**
460
+ 1. Create uibuilder node:
461
+ ```
462
+ uibId = create-node(type: "uibuilder", name: "Relay", properties: { url: "relay" })
463
+ ```
464
+ 2. Wire external data source → function → uibuilder input:
465
+ ```
466
+ // External source (mqtt in, http request, etc.) → function
467
+ connect-nodes(fromNodeId: "<sourceId>", outputPort: 0, toNodeId: "<fnId>")
468
+ connect-nodes(fromNodeId: "<fnId>", outputPort: 0, toNodeId: uibId)
469
+ ```
470
+ 3. Function node to format data for frontend:
471
+ ```javascript
472
+ // Format the incoming data for frontend display
473
+ msg._ui = [
474
+ { method: "addToDom", selector: "#feed", data: "<li>" + JSON.stringify(msg.payload) + "</li>", position: "first" }
475
+ ];
476
+ return msg;
477
+ ```
478
+ 4. Deploy.
479
+
480
+ **Frontend Side (`index.html`):**
481
+ ```html
482
+ <h1>Live Data Feed</h1>
483
+ <ul id="feed"></ul>
484
+ ```
485
+
486
+ All connected clients receive the relayed data in real time.
487
+
488
+ ---
489
+
490
+ ## References
491
+
492
+ - **Official Documentation:** https://totallyinformation.github.io/node-red-contrib-uibuilder/
493
+ - **Quick Start Guide:** https://totallyinformation.github.io/node-red-contrib-uibuilder/#/quick-start
494
+ - **Frontend Client Docs (functions):** https://totallyinformation.github.io/node-red-contrib-uibuilder/#/client-docs/functions
495
+ - **Controlling from Node-RED:** https://totallyinformation.github.io/node-red-contrib-uibuilder/#/client-docs/controlling-from-nr
496
+ - **Zero-code UI Elements:** https://totallyinformation.github.io/node-red-contrib-uibuilder/#/zero-code-elements/
497
+ - **GitHub Repository:** https://github.com/TotallyInformation/node-red-contrib-uibuilder
498
+ - **npm Package:** `node-red-contrib-uibuilder` (documented at v7.7.3)
499
+
500
+ > **Version note:** This skill documents `node-red-contrib-uibuilder` v7.7.3. The `msg._ui` protocol and client API are stable but may have additions in newer versions. For the latest API details, consult the official docs.
@@ -0,0 +1,36 @@
1
+ /**
2
+ * API Key verifier implementing the MCP SDK's token verification interface.
3
+ *
4
+ * Used with requireBearerAuth() middleware. Validates tokens against
5
+ * the MCP_API_KEY environment variable.
6
+ */
7
+
8
+ import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
9
+
10
+ /**
11
+ * Creates an API key token verifier.
12
+ *
13
+ * @param {string} apiKey - The expected API key from MCP_API_KEY env var
14
+ * @returns {{ verifyAccessToken: (token: string) => Promise<AuthInfo> }}
15
+ */
16
+ export function createApiKeyVerifier(apiKey) {
17
+ return {
18
+ /**
19
+ * Verify a bearer token against the configured API key.
20
+ *
21
+ * @param {string} token - The bearer token from the Authorization header
22
+ * @returns {Promise<import('./types.js').AuthInfo>}
23
+ * @throws {InvalidTokenError} if the token does not match
24
+ */
25
+ async verifyAccessToken(token) {
26
+ if (token === apiKey) {
27
+ return {
28
+ clientId: 'api-key',
29
+ scopes: ['*'],
30
+ expiresAt: Number.POSITIVE_INFINITY,
31
+ };
32
+ }
33
+ throw new InvalidTokenError('Invalid API key');
34
+ },
35
+ };
36
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Composite token verifier that tries API key first, then falls through to OAuth.
3
+ *
4
+ * Used as the single verifier passed to the SDK's requireBearerAuth() middleware.
5
+ * Supports three modes:
6
+ * - API key only: fast path comparison, no file I/O
7
+ * - OAuth only: delegates to the OAuth provider's verifyAccessToken
8
+ * - Both: API key first (fast path), then OAuth
9
+ */
10
+
11
+ import { InvalidTokenError } from '@modelcontextprotocol/sdk/server/auth/errors.js';
12
+
13
+ /**
14
+ * Creates a composite verifier from the auth configuration.
15
+ *
16
+ * @param {object} opts
17
+ * @param {string|null} opts.apiKey - API key from config (null = disabled)
18
+ * @param {import('@modelcontextprotocol/sdk/server/auth/provider.js').OAuthTokenVerifier|null} opts.oauthVerifier - OAuth verifier (null = disabled)
19
+ * @returns {import('@modelcontextprotocol/sdk/server/auth/provider.js').OAuthTokenVerifier}
20
+ */
21
+ export function createCompositeVerifier({ apiKey, oauthVerifier }) {
22
+ return {
23
+ /**
24
+ * Verify a bearer token.
25
+ *
26
+ * Tries API key match first (fast, no I/O), then falls through to OAuth.
27
+ *
28
+ * @param {string} token
29
+ * @returns {Promise<import('./types.js').AuthInfo>}
30
+ */
31
+ async verifyAccessToken(token) {
32
+ // Fast path: API key
33
+ if (apiKey) {
34
+ if (token === apiKey) {
35
+ return {
36
+ token,
37
+ clientId: 'api-key',
38
+ scopes: ['*'],
39
+ expiresAt: Number.POSITIVE_INFINITY,
40
+ };
41
+ }
42
+ // API key is configured but token doesn't match.
43
+ // If OAuth is also configured, fall through to it.
44
+ // Otherwise, reject immediately.
45
+ if (!oauthVerifier) {
46
+ throw new InvalidTokenError('Invalid API key');
47
+ }
48
+ }
49
+
50
+ // Fall through to OAuth
51
+ if (oauthVerifier) {
52
+ return oauthVerifier.verifyAccessToken(token);
53
+ }
54
+
55
+ // No verifier available — defensive, should not happen
56
+ throw new InvalidTokenError('No authentication method configured');
57
+ },
58
+ };
59
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * MCP server authentication configuration.
3
+ *
4
+ * Reads and validates all auth-related environment variables.
5
+ * Centralizes validation so invalid config is caught at startup,
6
+ * not at request time.
7
+ */
8
+
9
+ /**
10
+ * Validates and returns the frozen auth configuration.
11
+ *
12
+ * @returns {Readonly<AuthConfig>}
13
+ */
14
+ export function loadAuthConfig() {
15
+ const apiKey = (process.env.MCP_API_KEY || '').trim() || null;
16
+ const oauthEnabled = process.env.MCP_OAUTH_ENABLED === 'true';
17
+ const oauthIssuerUrl = (process.env.MCP_OAUTH_ISSUER_URL || '').trim() || null;
18
+ const oauthClientsFile = (process.env.MCP_OAUTH_CLIENTS_FILE || '').trim() || null;
19
+ const oauthTokensFile = (process.env.MCP_OAUTH_TOKENS_FILE || '').trim() || null;
20
+
21
+ // Validate OAuth configuration when enabled
22
+ if (oauthEnabled) {
23
+ if (!oauthIssuerUrl) {
24
+ throw new Error(
25
+ 'MCP_OAUTH_ENABLED is true but MCP_OAUTH_ISSUER_URL is not set. ' +
26
+ 'Set MCP_OAUTH_ISSUER_URL to the public base URL of this server ' +
27
+ '(e.g., https://nodered-mcp.example.com).'
28
+ );
29
+ }
30
+
31
+ let issuer;
32
+ try {
33
+ issuer = new URL(oauthIssuerUrl);
34
+ } catch {
35
+ throw new Error(
36
+ `MCP_OAUTH_ISSUER_URL is not a valid URL: "${oauthIssuerUrl}"`
37
+ );
38
+ }
39
+
40
+ // Allow http://localhost for development
41
+ if (issuer.protocol !== 'https:' &&
42
+ issuer.hostname !== 'localhost' &&
43
+ issuer.hostname !== '127.0.0.1') {
44
+ throw new Error(
45
+ `MCP_OAUTH_ISSUER_URL must use HTTPS in production. Got: "${oauthIssuerUrl}". ` +
46
+ 'http://localhost is allowed for development.'
47
+ );
48
+ }
49
+
50
+ // RFC 8414: issuer URL must not have fragment or query
51
+ if (issuer.hash) {
52
+ throw new Error(
53
+ `MCP_OAUTH_ISSUER_URL must not have a fragment: "${oauthIssuerUrl}"`
54
+ );
55
+ }
56
+ if (issuer.search) {
57
+ throw new Error(
58
+ `MCP_OAUTH_ISSUER_URL must not have a query string: "${oauthIssuerUrl}"`
59
+ );
60
+ }
61
+ }
62
+
63
+ const config = Object.freeze({
64
+ /** @type {string|null} API key for Bearer token auth. null = auth disabled. */
65
+ apiKey,
66
+
67
+ /** @type {boolean} Whether OAuth 2.0 authorization server is enabled. */
68
+ oauthEnabled,
69
+
70
+ /** @type {string|null} Base issuer URL for OAuth metadata. Required when oauthEnabled. */
71
+ oauthIssuerUrl,
72
+
73
+ /** @type {string|null} Path to JSON file storing OAuth clients. */
74
+ oauthClientsFile,
75
+
76
+ /** @type {string|null} Path to JSON file storing OAuth tokens. */
77
+ oauthTokensFile,
78
+ });
79
+
80
+ // Determine the display auth mode string
81
+ const mode = getAuthModeLabel(config);
82
+
83
+ return { config, mode };
84
+ }
85
+
86
+ /**
87
+ * Returns a human-readable label for the current auth configuration.
88
+ *
89
+ * @param {Readonly<AuthConfig>} config
90
+ * @returns {string}
91
+ */
92
+ function getAuthModeLabel(config) {
93
+ const parts = [];
94
+ if (config.apiKey) parts.push('api-key');
95
+ if (config.oauthEnabled) parts.push('oauth');
96
+ return parts.length > 0 ? parts.join(' + ') : 'none';
97
+ }
98
+
99
+ /**
100
+ * @typedef {object} AuthConfig
101
+ * @property {string|null} apiKey
102
+ * @property {boolean} oauthEnabled
103
+ * @property {string|null} oauthIssuerUrl
104
+ * @property {string|null} oauthClientsFile
105
+ * @property {string|null} oauthTokensFile
106
+ */