@flowfuse/nr-launcher 2.32.1-b23502c-202607061802.0 → 2.32.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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
#### 2.32.1: Release
|
|
2
|
+
|
|
3
|
+
- fix: Ensure MCP server-side sessions are terminated (#580)
|
|
4
|
+
- ci: Add slack notification to the release pipeline (#578)
|
|
5
|
+
- Replace @node-red/util dependency with local red_util module (#577)
|
|
6
|
+
- Bump multer from 2.1.1 to 2.2.0 (#568)
|
|
7
|
+
|
|
1
8
|
#### 2.32.0: Release
|
|
2
9
|
|
|
3
10
|
- chore: bump @flowfuse/nr-assistant to ^0.17.0 (#574)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* eslint-disable no-useless-escape */
|
|
2
|
+
|
|
3
|
+
// Extracted from @node-red/util/util.js to avoid dependency on @node-red/util package
|
|
4
|
+
|
|
5
|
+
function createError (code, message) {
|
|
6
|
+
const e = new Error(message)
|
|
7
|
+
e.code = code
|
|
8
|
+
return e
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parses a property expression, such as `msg.foo.bar[3]` to validate it
|
|
13
|
+
* and convert it to a canonical version expressed as an Array of property
|
|
14
|
+
* names.
|
|
15
|
+
*
|
|
16
|
+
* For example, `a["b"].c` returns `['a','b','c']`
|
|
17
|
+
*
|
|
18
|
+
* If `msg` is provided, any internal cross-references will be evaluated against that
|
|
19
|
+
* object. Otherwise, it will return a nested set of properties
|
|
20
|
+
*
|
|
21
|
+
* For example, without msg set, 'a[msg.foo]' returns `['a', [ 'msg', 'foo'] ]`
|
|
22
|
+
* But if msg is set to '{"foo": "bar"}', 'a[msg.foo]' returns `['a', 'bar' ]`
|
|
23
|
+
*
|
|
24
|
+
* @param {String} str - the property expression
|
|
25
|
+
* @return {Array} the normalised expression
|
|
26
|
+
* @memberof @node-red/util_util
|
|
27
|
+
*/
|
|
28
|
+
function normalisePropertyExpression (str, msg, toString) {
|
|
29
|
+
// This must be kept in sync with validatePropertyExpression
|
|
30
|
+
// in editor/js/ui/utils.js
|
|
31
|
+
|
|
32
|
+
const length = str.length
|
|
33
|
+
if (length === 0) {
|
|
34
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: zero-length')
|
|
35
|
+
}
|
|
36
|
+
const parts = []
|
|
37
|
+
let start = 0
|
|
38
|
+
let inString = false
|
|
39
|
+
let inBox = false
|
|
40
|
+
let quoteChar
|
|
41
|
+
let v
|
|
42
|
+
for (let i = 0; i < length; i++) {
|
|
43
|
+
const c = str[i]
|
|
44
|
+
if (!inString) {
|
|
45
|
+
if (c === "'" || c === '"') {
|
|
46
|
+
if (i !== start) {
|
|
47
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i)
|
|
48
|
+
}
|
|
49
|
+
inString = true
|
|
50
|
+
quoteChar = c
|
|
51
|
+
start = i + 1
|
|
52
|
+
} else if (c === '.') {
|
|
53
|
+
if (i === 0) {
|
|
54
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected . at position 0')
|
|
55
|
+
}
|
|
56
|
+
if (start !== i) {
|
|
57
|
+
v = str.substring(start, i)
|
|
58
|
+
if (/^\d+$/.test(v)) {
|
|
59
|
+
parts.push(parseInt(v))
|
|
60
|
+
} else {
|
|
61
|
+
parts.push(v)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (i === length - 1) {
|
|
65
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression')
|
|
66
|
+
}
|
|
67
|
+
// Next char is first char of an identifier: a-z 0-9 $ _
|
|
68
|
+
if (!/[a-z0-9\$\_]/i.test(str[i + 1])) {
|
|
69
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1))
|
|
70
|
+
}
|
|
71
|
+
start = i + 1
|
|
72
|
+
} else if (c === '[') {
|
|
73
|
+
if (i === 0) {
|
|
74
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i)
|
|
75
|
+
}
|
|
76
|
+
if (start !== i) {
|
|
77
|
+
parts.push(str.substring(start, i))
|
|
78
|
+
}
|
|
79
|
+
if (i === length - 1) {
|
|
80
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unterminated expression')
|
|
81
|
+
}
|
|
82
|
+
// Start of a new expression. If it starts with msg it is a nested expression
|
|
83
|
+
// Need to scan ahead to find the closing bracket
|
|
84
|
+
if (/^msg[.\[]/.test(str.substring(i + 1))) {
|
|
85
|
+
let depth = 1
|
|
86
|
+
let inLocalString = false
|
|
87
|
+
let localStringQuote
|
|
88
|
+
for (let j = i + 1; j < length; j++) {
|
|
89
|
+
if (/["']/.test(str[j])) {
|
|
90
|
+
if (inLocalString) {
|
|
91
|
+
if (str[j] === localStringQuote) {
|
|
92
|
+
inLocalString = false
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
inLocalString = true
|
|
96
|
+
localStringQuote = str[j]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (str[j] === '[') {
|
|
100
|
+
depth++
|
|
101
|
+
} else if (str[j] === ']') {
|
|
102
|
+
depth--
|
|
103
|
+
}
|
|
104
|
+
if (depth === 0) {
|
|
105
|
+
try {
|
|
106
|
+
if (msg) {
|
|
107
|
+
const crossRefProp = getMessageProperty(msg, str.substring(i + 1, j))
|
|
108
|
+
if (crossRefProp === undefined) {
|
|
109
|
+
throw createError('INVALID_EXPR', 'Invalid expression: undefined reference at position ' + (i + 1) + ' : ' + str.substring(i + 1, j))
|
|
110
|
+
}
|
|
111
|
+
parts.push(crossRefProp)
|
|
112
|
+
} else {
|
|
113
|
+
parts.push(normalisePropertyExpression(str.substring(i + 1, j), msg))
|
|
114
|
+
}
|
|
115
|
+
inBox = false
|
|
116
|
+
i = j
|
|
117
|
+
start = j + 1
|
|
118
|
+
break
|
|
119
|
+
} catch (err) {
|
|
120
|
+
throw createError('INVALID_EXPR', 'Invalid expression started at position ' + (i + 1))
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (depth > 0) {
|
|
125
|
+
throw createError('INVALID_EXPR', "Invalid property expression: unmatched '[' at position " + i)
|
|
126
|
+
}
|
|
127
|
+
continue
|
|
128
|
+
} else if (!/["'\d]/.test(str[i + 1])) {
|
|
129
|
+
// Next char is either a quote or a number
|
|
130
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' at position ' + (i + 1))
|
|
131
|
+
}
|
|
132
|
+
start = i + 1
|
|
133
|
+
inBox = true
|
|
134
|
+
} else if (c === ']') {
|
|
135
|
+
if (!inBox) {
|
|
136
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + c + ' at position ' + i)
|
|
137
|
+
}
|
|
138
|
+
if (start !== i) {
|
|
139
|
+
v = str.substring(start, i)
|
|
140
|
+
if (/^\d+$/.test(v)) {
|
|
141
|
+
parts.push(parseInt(v))
|
|
142
|
+
} else {
|
|
143
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
start = i + 1
|
|
147
|
+
inBox = false
|
|
148
|
+
} else if (c === ' ') {
|
|
149
|
+
throw createError('INVALID_EXPR', "Invalid property expression: unexpected ' ' at position " + i)
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
if (c === quoteChar) {
|
|
153
|
+
if (i - start === 0) {
|
|
154
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: zero-length string at position ' + start)
|
|
155
|
+
}
|
|
156
|
+
parts.push(str.substring(start, i))
|
|
157
|
+
// If inBox, next char must be a ]. Otherwise it may be [ or .
|
|
158
|
+
if (inBox && !/\]/.test(str[i + 1])) {
|
|
159
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected array expression at position ' + start)
|
|
160
|
+
} else if (!inBox && i + 1 !== length && !/[\[\.]/.test(str[i + 1])) {
|
|
161
|
+
throw createError('INVALID_EXPR', 'Invalid property expression: unexpected ' + str[i + 1] + ' expression at position ' + (i + 1))
|
|
162
|
+
}
|
|
163
|
+
start = i + 1
|
|
164
|
+
inString = false
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (inBox || inString) {
|
|
169
|
+
// eslint-disable-next-line new-cap
|
|
170
|
+
throw new createError('INVALID_EXPR', 'Invalid property expression: unterminated expression')
|
|
171
|
+
}
|
|
172
|
+
if (start < length) {
|
|
173
|
+
parts.push(str.substring(start))
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (toString) {
|
|
177
|
+
let result = parts.shift()
|
|
178
|
+
while (parts.length > 0) {
|
|
179
|
+
let p = parts.shift()
|
|
180
|
+
if (typeof p === 'string') {
|
|
181
|
+
if (/"/.test(p)) {
|
|
182
|
+
p = "'" + p + "'"
|
|
183
|
+
} else {
|
|
184
|
+
p = '"' + p + '"'
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
result = result + '[' + p + ']'
|
|
188
|
+
}
|
|
189
|
+
return result
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return parts
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Gets a property of a message object.
|
|
197
|
+
*
|
|
198
|
+
* Unlike {@link @node-red/util-util.getObjectProperty}, this function will strip `msg.` from the
|
|
199
|
+
* front of the property expression if present.
|
|
200
|
+
*
|
|
201
|
+
* @param {Object} msg - the message object
|
|
202
|
+
* @param {String} expr - the property expression
|
|
203
|
+
* @return {any} the message property, or undefined if it does not exist
|
|
204
|
+
* @throws Will throw an error if the *parent* of the property does not exist
|
|
205
|
+
* @memberof @node-red/util_util
|
|
206
|
+
*/
|
|
207
|
+
function getMessageProperty (msg, expr) {
|
|
208
|
+
if (expr.indexOf('msg.') === 0) {
|
|
209
|
+
expr = expr.substring(4)
|
|
210
|
+
}
|
|
211
|
+
return getObjectProperty(msg, expr)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Gets a property of an object.
|
|
216
|
+
*
|
|
217
|
+
* Given the object:
|
|
218
|
+
*
|
|
219
|
+
* {
|
|
220
|
+
* "pet": {
|
|
221
|
+
* "type": "cat"
|
|
222
|
+
* }
|
|
223
|
+
* }
|
|
224
|
+
*
|
|
225
|
+
* - `pet.type` will return `"cat"`.
|
|
226
|
+
* - `pet.name` will return `undefined`
|
|
227
|
+
* - `car` will return `undefined`
|
|
228
|
+
* - `car.type` will throw an Error (as `car` does not exist)
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} msg - the object
|
|
231
|
+
* @param {String} expr - the property expression
|
|
232
|
+
* @return {any} the object property, or undefined if it does not exist
|
|
233
|
+
* @throws Will throw an error if the *parent* of the property does not exist
|
|
234
|
+
* @memberof @node-red/util_util
|
|
235
|
+
*/
|
|
236
|
+
function getObjectProperty (msg, expr) {
|
|
237
|
+
let result = null
|
|
238
|
+
const msgPropParts = normalisePropertyExpression(expr, msg)
|
|
239
|
+
msgPropParts.reduce(function (obj, key) {
|
|
240
|
+
result = (typeof obj[key] !== 'undefined' ? obj[key] : undefined)
|
|
241
|
+
return result
|
|
242
|
+
}, msg)
|
|
243
|
+
return result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Sets a property of an object.
|
|
248
|
+
*
|
|
249
|
+
* @param {Object} msg - the object
|
|
250
|
+
* @param {String} prop - the property expression
|
|
251
|
+
* @param {any} value - the value to set
|
|
252
|
+
* @param {boolean} createMissing - whether to create missing parent properties
|
|
253
|
+
* @memberof @node-red/util_util
|
|
254
|
+
*/
|
|
255
|
+
function setObjectProperty (msg, prop, value, createMissing) {
|
|
256
|
+
if (typeof createMissing === 'undefined') {
|
|
257
|
+
createMissing = (typeof value !== 'undefined')
|
|
258
|
+
}
|
|
259
|
+
const msgPropParts = normalisePropertyExpression(prop, msg)
|
|
260
|
+
const length = msgPropParts.length
|
|
261
|
+
let obj = msg
|
|
262
|
+
let key
|
|
263
|
+
for (let i = 0; i < length - 1; i++) {
|
|
264
|
+
key = msgPropParts[i]
|
|
265
|
+
if (typeof key === 'string' || (typeof key === 'number' && !Array.isArray(obj))) {
|
|
266
|
+
if (hasOwnProperty.call(obj, key)) {
|
|
267
|
+
if (length > 1 && ((typeof obj[key] !== 'object' && typeof obj[key] !== 'function') || obj[key] === null)) {
|
|
268
|
+
// Break out early as we cannot create a property beneath
|
|
269
|
+
// this type of value
|
|
270
|
+
return false
|
|
271
|
+
}
|
|
272
|
+
obj = obj[key]
|
|
273
|
+
} else if (createMissing) {
|
|
274
|
+
if (typeof msgPropParts[i + 1] === 'string') {
|
|
275
|
+
obj[key] = {}
|
|
276
|
+
} else {
|
|
277
|
+
obj[key] = []
|
|
278
|
+
}
|
|
279
|
+
obj = obj[key]
|
|
280
|
+
} else {
|
|
281
|
+
return false
|
|
282
|
+
}
|
|
283
|
+
} else if (typeof key === 'number') {
|
|
284
|
+
// obj is an array
|
|
285
|
+
if (obj[key] === undefined) {
|
|
286
|
+
if (createMissing) {
|
|
287
|
+
if (typeof msgPropParts[i + 1] === 'string') {
|
|
288
|
+
obj[key] = {}
|
|
289
|
+
} else {
|
|
290
|
+
obj[key] = []
|
|
291
|
+
}
|
|
292
|
+
obj = obj[key]
|
|
293
|
+
} else {
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
} else {
|
|
297
|
+
obj = obj[key]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
key = msgPropParts[length - 1]
|
|
302
|
+
if (typeof value === 'undefined') {
|
|
303
|
+
if (typeof key === 'number' && Array.isArray(obj)) {
|
|
304
|
+
obj.splice(key, 1)
|
|
305
|
+
} else {
|
|
306
|
+
delete obj[key]
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
310
|
+
obj[key] = value
|
|
311
|
+
} else {
|
|
312
|
+
// Cannot set a property of a non-object/array
|
|
313
|
+
return false
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return true
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = {
|
|
320
|
+
getObjectProperty,
|
|
321
|
+
setObjectProperty
|
|
322
|
+
}
|
package/lib/mcp.js
CHANGED
|
@@ -260,7 +260,7 @@ class MCP {
|
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* Connect a fresh MCP client to the given (already-normalised) endpoint spec.
|
|
263
|
-
* Caller is responsible for
|
|
263
|
+
* Caller is responsible for calling {@link _disconnect} when finished.
|
|
264
264
|
* @param {McpEndpoint} endpoint
|
|
265
265
|
* @returns {Promise<McpClient>} a connected MCP Client instance
|
|
266
266
|
*/
|
|
@@ -274,6 +274,31 @@ class MCP {
|
|
|
274
274
|
return client
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
/**
|
|
278
|
+
* Tear down a connected client, terminating the server-side session first.
|
|
279
|
+
*
|
|
280
|
+
* The launcher opens a fresh session for every operation, so it must also
|
|
281
|
+
* close them: the SDK client's `close()` only aborts the local connection —
|
|
282
|
+
* it does NOT send the HTTP DELETE that terminates the session on the server.
|
|
283
|
+
* Without an explicit `terminateSession()`, server-side sessions accumulate
|
|
284
|
+
* (the MCP host has no idle reaper), and every subsequent connection re-runs
|
|
285
|
+
* its registration pass across all of them. So we terminate, then close.
|
|
286
|
+
* @param {McpClient} [client]
|
|
287
|
+
*/
|
|
288
|
+
async _disconnect (client) {
|
|
289
|
+
if (!client) {
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
/** @type {StreamableHTTPClientTransport} */
|
|
294
|
+
const transport = client.transport
|
|
295
|
+
await transport?.terminateSession?.()
|
|
296
|
+
} catch (_) { /* server may not support DELETE; fall through to close */ }
|
|
297
|
+
try {
|
|
298
|
+
await client.close()
|
|
299
|
+
} catch (_) { /* ignore */ }
|
|
300
|
+
}
|
|
301
|
+
|
|
277
302
|
async _getEndpointFeatures (/** @type {McpEndpoint} */ endpoint) {
|
|
278
303
|
const features = {
|
|
279
304
|
tools: [],
|
|
@@ -323,11 +348,7 @@ class MCP {
|
|
|
323
348
|
warn(`Failed to query MCP endpoint '${endpoint.key}': ${err.message}`)
|
|
324
349
|
features.error = err.message
|
|
325
350
|
} finally {
|
|
326
|
-
|
|
327
|
-
try {
|
|
328
|
-
await client.close()
|
|
329
|
-
} catch (_) { /* ignore */ }
|
|
330
|
-
}
|
|
351
|
+
await this._disconnect(client)
|
|
331
352
|
}
|
|
332
353
|
return features
|
|
333
354
|
}
|
|
@@ -416,11 +437,7 @@ class MCP {
|
|
|
416
437
|
const response = await client.callTool({ name, arguments: input || {} })
|
|
417
438
|
return response
|
|
418
439
|
} finally {
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
await client.close()
|
|
422
|
-
} catch (_) { /* ignore */ }
|
|
423
|
-
}
|
|
440
|
+
await this._disconnect(client)
|
|
424
441
|
}
|
|
425
442
|
}
|
|
426
443
|
|
|
@@ -449,11 +466,7 @@ class MCP {
|
|
|
449
466
|
const response = await client.readResource({ uri })
|
|
450
467
|
return response
|
|
451
468
|
} finally {
|
|
452
|
-
|
|
453
|
-
try {
|
|
454
|
-
await client.close()
|
|
455
|
-
} catch (_) { /* ignore */ }
|
|
456
|
-
}
|
|
469
|
+
await this._disconnect(client)
|
|
457
470
|
}
|
|
458
471
|
}
|
|
459
472
|
|