@easynet-run/node 0.27.14 → 0.36.9
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/README.md +12 -0
- package/native/dendrite-bridge-manifest.json +5 -4
- package/native/dendrite-bridge.json +1 -1
- package/native/include/axon_dendrite_bridge.h +18 -4
- package/native/libaxon_dendrite_bridge.so +0 -0
- package/package.json +7 -5
- package/runtime/easynet-runtime-rs-0.36.9-x86_64-unknown-linux-gnu.tar.gz +0 -0
- package/runtime/runtime-bridge-manifest.json +4 -4
- package/runtime/runtime-bridge.json +3 -3
- package/src/ability_lifecycle.d.ts +1 -0
- package/src/ability_lifecycle.js +12 -6
- package/src/capability_request.js +3 -1
- package/src/dendrite_bridge/bridge.d.ts +9 -2
- package/src/dendrite_bridge/bridge.js +54 -5
- package/src/dendrite_bridge/ffi.d.ts +3 -0
- package/src/dendrite_bridge/ffi.js +2 -0
- package/src/dendrite_bridge/types.d.ts +4 -0
- package/src/errors.js +9 -3
- package/src/index.d.ts +2 -2
- package/src/mcp/server.d.ts +24 -2
- package/src/mcp/server.js +218 -18
- package/src/mcp/server.test.js +62 -0
- package/src/presets/remote_control/config.d.ts +3 -0
- package/src/presets/remote_control/config.js +17 -3
- package/src/presets/remote_control/descriptor.js +28 -11
- package/src/presets/remote_control/handlers.d.ts +2 -0
- package/src/presets/remote_control/handlers.js +22 -17
- package/src/presets/remote_control/kit.d.ts +4 -2
- package/src/presets/remote_control/kit.js +70 -1
- package/src/presets/remote_control/kit.test.js +449 -0
- package/src/presets/remote_control/orchestrator.d.ts +5 -0
- package/src/presets/remote_control/orchestrator.js +9 -1
- package/src/presets/remote_control/specs.js +80 -61
- package/src/receipt.js +6 -3
- package/runtime/easynet-runtime-rs-0.27.14-x86_64-unknown-linux-gnu.tar.gz +0 -0
|
@@ -3,12 +3,20 @@ import assert from "node:assert/strict";
|
|
|
3
3
|
|
|
4
4
|
import { RemoteControlCaseKit } from "./kit.js";
|
|
5
5
|
import { remoteControlToolSpecs } from "./specs.js";
|
|
6
|
+
import { consumeStream } from "../../mcp/server.js";
|
|
7
|
+
import { DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS } from "./config.js";
|
|
6
8
|
|
|
7
9
|
function makeOrchestrator() {
|
|
8
10
|
return {
|
|
9
11
|
listNodes: () => [],
|
|
10
12
|
listMcpTools: () => [],
|
|
11
13
|
callMcpTool: () => ({ ok: true, state: 5, is_error: false }),
|
|
14
|
+
callMcpToolStream: () => ({
|
|
15
|
+
close: () => {},
|
|
16
|
+
async *[Symbol.asyncIterator]() {
|
|
17
|
+
yield Buffer.from("chunk");
|
|
18
|
+
},
|
|
19
|
+
}),
|
|
12
20
|
disconnectDevice: (nodeId, reason) => ({ node_id: nodeId, reason }),
|
|
13
21
|
uninstallAbility: (nodeId, installId, reason) => ({
|
|
14
22
|
node_id: nodeId,
|
|
@@ -26,6 +34,7 @@ describe("RemoteControlCaseKit", () => {
|
|
|
26
34
|
const names = remoteControlToolSpecs().map((spec) => String(spec.name));
|
|
27
35
|
assert.ok(names.includes("disconnect_device"));
|
|
28
36
|
assert.ok(names.includes("uninstall_ability"));
|
|
37
|
+
assert.ok(names.includes("call_remote_tool_stream"));
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
it("dispatches disconnect_device", () => {
|
|
@@ -84,4 +93,444 @@ describe("RemoteControlCaseKit", () => {
|
|
|
84
93
|
},
|
|
85
94
|
});
|
|
86
95
|
});
|
|
96
|
+
|
|
97
|
+
it("closes the orchestrator when a streaming handle is closed", () => {
|
|
98
|
+
let streamClosed = 0;
|
|
99
|
+
let orchestratorClosed = 0;
|
|
100
|
+
const stream = {
|
|
101
|
+
close: () => {
|
|
102
|
+
streamClosed += 1;
|
|
103
|
+
},
|
|
104
|
+
async *[Symbol.asyncIterator]() {
|
|
105
|
+
yield Buffer.from("chunk");
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const kit = new RemoteControlCaseKit(
|
|
109
|
+
{
|
|
110
|
+
endpoint: "http://127.0.0.1:50051",
|
|
111
|
+
tenant: "tenant-a",
|
|
112
|
+
connectTimeoutMs: 5000,
|
|
113
|
+
signatureBase64: "sig",
|
|
114
|
+
},
|
|
115
|
+
() => ({
|
|
116
|
+
...makeOrchestrator(),
|
|
117
|
+
callMcpToolStream: () => stream,
|
|
118
|
+
close: () => {
|
|
119
|
+
orchestratorClosed += 1;
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
125
|
+
node_id: "node-a",
|
|
126
|
+
tool_name: "tool-a",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
assert.ok(handle);
|
|
130
|
+
handle.close();
|
|
131
|
+
handle.close();
|
|
132
|
+
assert.equal(streamClosed, 1);
|
|
133
|
+
assert.equal(orchestratorClosed, 1);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("closes the orchestrator after streaming iteration completes", async () => {
|
|
137
|
+
let streamClosed = 0;
|
|
138
|
+
let orchestratorClosed = 0;
|
|
139
|
+
const stream = {
|
|
140
|
+
close: () => {
|
|
141
|
+
streamClosed += 1;
|
|
142
|
+
},
|
|
143
|
+
async *[Symbol.asyncIterator]() {
|
|
144
|
+
yield Buffer.from("chunk-1");
|
|
145
|
+
yield Buffer.from("chunk-2");
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
const kit = new RemoteControlCaseKit(
|
|
149
|
+
{
|
|
150
|
+
endpoint: "http://127.0.0.1:50051",
|
|
151
|
+
tenant: "tenant-a",
|
|
152
|
+
connectTimeoutMs: 5000,
|
|
153
|
+
signatureBase64: "sig",
|
|
154
|
+
},
|
|
155
|
+
() => ({
|
|
156
|
+
...makeOrchestrator(),
|
|
157
|
+
callMcpToolStream: () => stream,
|
|
158
|
+
close: () => {
|
|
159
|
+
orchestratorClosed += 1;
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
165
|
+
node_id: "node-a",
|
|
166
|
+
tool_name: "tool-a",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
assert.ok(handle);
|
|
170
|
+
const chunks = [];
|
|
171
|
+
for await (const chunk of handle.stream) {
|
|
172
|
+
chunks.push(Buffer.from(chunk).toString("utf8"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
assert.deepEqual(chunks, ["chunk-1", "chunk-2"]);
|
|
176
|
+
assert.equal(streamClosed, 1);
|
|
177
|
+
assert.equal(orchestratorClosed, 1);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("closes the orchestrator after an empty stream completes", async () => {
|
|
181
|
+
let streamClosed = 0;
|
|
182
|
+
let orchestratorClosed = 0;
|
|
183
|
+
const stream = {
|
|
184
|
+
close: () => {
|
|
185
|
+
streamClosed += 1;
|
|
186
|
+
},
|
|
187
|
+
async *[Symbol.asyncIterator]() {
|
|
188
|
+
// empty stream
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const kit = new RemoteControlCaseKit(
|
|
192
|
+
{
|
|
193
|
+
endpoint: "http://127.0.0.1:50051",
|
|
194
|
+
tenant: "tenant-a",
|
|
195
|
+
connectTimeoutMs: 5000,
|
|
196
|
+
signatureBase64: "sig",
|
|
197
|
+
},
|
|
198
|
+
() => ({
|
|
199
|
+
...makeOrchestrator(),
|
|
200
|
+
callMcpToolStream: () => stream,
|
|
201
|
+
close: () => {
|
|
202
|
+
orchestratorClosed += 1;
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
208
|
+
node_id: "node-a",
|
|
209
|
+
tool_name: "tool-a",
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
assert.ok(handle);
|
|
213
|
+
const chunks = [];
|
|
214
|
+
for await (const chunk of handle.stream) {
|
|
215
|
+
chunks.push(Buffer.from(chunk).toString("utf8"));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
assert.deepEqual(chunks, []);
|
|
219
|
+
assert.equal(streamClosed, 1);
|
|
220
|
+
assert.equal(orchestratorClosed, 1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("closes the orchestrator when streaming iteration fails", async () => {
|
|
224
|
+
let streamClosed = 0;
|
|
225
|
+
let orchestratorClosed = 0;
|
|
226
|
+
const stream = {
|
|
227
|
+
close: () => {
|
|
228
|
+
streamClosed += 1;
|
|
229
|
+
},
|
|
230
|
+
async *[Symbol.asyncIterator]() {
|
|
231
|
+
yield Buffer.from("chunk-1");
|
|
232
|
+
throw new Error("stream chunk timeout");
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
const kit = new RemoteControlCaseKit(
|
|
236
|
+
{
|
|
237
|
+
endpoint: "http://127.0.0.1:50051",
|
|
238
|
+
tenant: "tenant-a",
|
|
239
|
+
connectTimeoutMs: 5000,
|
|
240
|
+
signatureBase64: "sig",
|
|
241
|
+
},
|
|
242
|
+
() => ({
|
|
243
|
+
...makeOrchestrator(),
|
|
244
|
+
callMcpToolStream: () => stream,
|
|
245
|
+
close: () => {
|
|
246
|
+
orchestratorClosed += 1;
|
|
247
|
+
},
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
252
|
+
node_id: "node-a",
|
|
253
|
+
tool_name: "tool-a",
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
assert.ok(handle);
|
|
257
|
+
const chunks = [];
|
|
258
|
+
await assert.rejects(async () => {
|
|
259
|
+
for await (const chunk of handle.stream) {
|
|
260
|
+
chunks.push(Buffer.from(chunk).toString("utf8"));
|
|
261
|
+
}
|
|
262
|
+
}, /stream chunk timeout/);
|
|
263
|
+
|
|
264
|
+
assert.deepEqual(chunks, ["chunk-1"]);
|
|
265
|
+
assert.equal(streamClosed, 1);
|
|
266
|
+
assert.equal(orchestratorClosed, 1);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("passes timeout_ms through to the orchestrator", () => {
|
|
270
|
+
let capturedOptions = null;
|
|
271
|
+
const kit = new RemoteControlCaseKit(
|
|
272
|
+
{
|
|
273
|
+
endpoint: "http://127.0.0.1:50051",
|
|
274
|
+
tenant: "tenant-a",
|
|
275
|
+
connectTimeoutMs: 5000,
|
|
276
|
+
signatureBase64: "sig",
|
|
277
|
+
},
|
|
278
|
+
() => ({
|
|
279
|
+
...makeOrchestrator(),
|
|
280
|
+
callMcpToolStream: (_toolName, _nodeId, _args, opts) => {
|
|
281
|
+
capturedOptions = opts;
|
|
282
|
+
return {
|
|
283
|
+
close: () => {},
|
|
284
|
+
async *[Symbol.asyncIterator]() {},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
close: () => {},
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
292
|
+
node_id: "node-a",
|
|
293
|
+
tool_name: "tool-a",
|
|
294
|
+
timeout_ms: 42000,
|
|
295
|
+
});
|
|
296
|
+
assert.ok(handle);
|
|
297
|
+
handle.close();
|
|
298
|
+
assert.ok(capturedOptions);
|
|
299
|
+
assert.equal(capturedOptions.timeoutMs, 42000);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("returns null for unknown tool in handleToolCallStream", () => {
|
|
303
|
+
const kit = new RemoteControlCaseKit(
|
|
304
|
+
{
|
|
305
|
+
endpoint: "http://127.0.0.1:50051",
|
|
306
|
+
tenant: "tenant-a",
|
|
307
|
+
connectTimeoutMs: 5000,
|
|
308
|
+
signatureBase64: "sig",
|
|
309
|
+
},
|
|
310
|
+
() => makeOrchestrator(),
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
const handle = kit.handleToolCallStream("nonexistent_tool", {
|
|
314
|
+
node_id: "node-a",
|
|
315
|
+
tool_name: "tool-a",
|
|
316
|
+
});
|
|
317
|
+
assert.equal(handle, null);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it("buffers call_remote_tool_stream via unary handleToolCall", async () => {
|
|
321
|
+
const kit = new RemoteControlCaseKit(
|
|
322
|
+
{
|
|
323
|
+
endpoint: "http://127.0.0.1:50051",
|
|
324
|
+
tenant: "tenant-a",
|
|
325
|
+
connectTimeoutMs: 5000,
|
|
326
|
+
signatureBase64: "sig",
|
|
327
|
+
},
|
|
328
|
+
() => makeOrchestrator(),
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const result = await kit.handleToolCall("call_remote_tool_stream", {
|
|
332
|
+
node_id: "node-a",
|
|
333
|
+
tool_name: "tool-a",
|
|
334
|
+
});
|
|
335
|
+
assert.equal(result.isError, false);
|
|
336
|
+
assert.equal(result.payload.ok, true);
|
|
337
|
+
assert.equal(result.payload.chunk_count, 1);
|
|
338
|
+
assert.deepEqual(result.payload.chunks, ["chunk"]);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("keeps unary and buffered-stream payload shapes distinct", async () => {
|
|
342
|
+
const kit = new RemoteControlCaseKit(
|
|
343
|
+
{
|
|
344
|
+
endpoint: "http://127.0.0.1:50051",
|
|
345
|
+
tenant: "tenant-a",
|
|
346
|
+
connectTimeoutMs: 5000,
|
|
347
|
+
signatureBase64: "sig",
|
|
348
|
+
},
|
|
349
|
+
() => makeOrchestrator(),
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const unaryResult = await kit.handleToolCall("call_remote_tool", {
|
|
353
|
+
node_id: "node-a",
|
|
354
|
+
tool_name: "tool-a",
|
|
355
|
+
});
|
|
356
|
+
const bufferedStreamResult = await kit.handleToolCall("call_remote_tool_stream", {
|
|
357
|
+
node_id: "node-a",
|
|
358
|
+
tool_name: "tool-a",
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
assert.equal(unaryResult.isError, false);
|
|
362
|
+
assert.equal(unaryResult.payload.ok, true);
|
|
363
|
+
assert.ok("call" in unaryResult.payload);
|
|
364
|
+
assert.equal("chunks" in unaryResult.payload, false);
|
|
365
|
+
|
|
366
|
+
assert.equal(bufferedStreamResult.isError, false);
|
|
367
|
+
assert.equal(bufferedStreamResult.payload.ok, true);
|
|
368
|
+
assert.equal(bufferedStreamResult.payload.chunk_count, 1);
|
|
369
|
+
assert.deepEqual(bufferedStreamResult.payload.chunks, ["chunk"]);
|
|
370
|
+
assert.equal("call" in bufferedStreamResult.payload, false);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("returns an error payload when unary stream buffering cannot open the stream", async () => {
|
|
374
|
+
const kit = new RemoteControlCaseKit(
|
|
375
|
+
{
|
|
376
|
+
endpoint: "http://127.0.0.1:50051",
|
|
377
|
+
tenant: "tenant-a",
|
|
378
|
+
connectTimeoutMs: 5000,
|
|
379
|
+
signatureBase64: "sig",
|
|
380
|
+
},
|
|
381
|
+
() => ({
|
|
382
|
+
...makeOrchestrator(),
|
|
383
|
+
callMcpToolStream: () => {
|
|
384
|
+
throw new Error("stream open failed");
|
|
385
|
+
},
|
|
386
|
+
close: () => {},
|
|
387
|
+
}),
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
const result = await kit.handleToolCall("call_remote_tool_stream", {
|
|
391
|
+
node_id: "node-a",
|
|
392
|
+
tool_name: "tool-a",
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
assert.equal(result.isError, true);
|
|
396
|
+
assert.equal(result.payload.ok, false);
|
|
397
|
+
assert.match(String(result.payload.error), /stream open failed/);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("consumeStream enforces 64 MiB buffer limit", async () => {
|
|
401
|
+
// Each chunk is 1 MiB; yield 65 chunks to exceed the 64 MiB limit.
|
|
402
|
+
const oneMiB = Buffer.alloc(1024 * 1024, 0x41); // 'A'
|
|
403
|
+
let closeCalled = 0;
|
|
404
|
+
const handle = {
|
|
405
|
+
close: () => {
|
|
406
|
+
closeCalled += 1;
|
|
407
|
+
},
|
|
408
|
+
stream: {
|
|
409
|
+
async *[Symbol.asyncIterator]() {
|
|
410
|
+
for (let i = 0; i < 65; i++) {
|
|
411
|
+
yield oneMiB;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const result = await consumeStream(handle);
|
|
418
|
+
|
|
419
|
+
assert.equal(result.isError, true);
|
|
420
|
+
assert.equal(result.payload.ok, false);
|
|
421
|
+
assert.match(String(result.payload.error), /64 MiB/);
|
|
422
|
+
assert.equal(closeCalled, 1);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("consumeStream detects lossy UTF-8 in binary chunks", async () => {
|
|
426
|
+
let closeCalled = 0;
|
|
427
|
+
const handle = {
|
|
428
|
+
close: () => {
|
|
429
|
+
closeCalled += 1;
|
|
430
|
+
},
|
|
431
|
+
stream: {
|
|
432
|
+
async *[Symbol.asyncIterator]() {
|
|
433
|
+
yield Buffer.from([0xff, 0xfe, 0x41]);
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const result = await consumeStream(handle);
|
|
439
|
+
|
|
440
|
+
assert.equal(result.isError, false);
|
|
441
|
+
assert.equal(result.payload.ok, true);
|
|
442
|
+
assert.equal(result.payload.contains_invalid_utf8, true);
|
|
443
|
+
assert.equal(closeCalled, 1);
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
it("StdioMcpServer tries stream handler before unary", async () => {
|
|
447
|
+
const { StdioMcpServer } = await import("../../mcp/server.js");
|
|
448
|
+
|
|
449
|
+
let streamCalled = false;
|
|
450
|
+
let unaryCalled = false;
|
|
451
|
+
|
|
452
|
+
const provider = {
|
|
453
|
+
toolSpecs() {
|
|
454
|
+
return [
|
|
455
|
+
{
|
|
456
|
+
name: "my_tool",
|
|
457
|
+
description: "A test tool",
|
|
458
|
+
inputSchema: { type: "object", properties: {} },
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
},
|
|
462
|
+
handleToolCall(_name, _args) {
|
|
463
|
+
unaryCalled = true;
|
|
464
|
+
return { payload: { ok: true, source: "unary" }, isError: false };
|
|
465
|
+
},
|
|
466
|
+
handleToolCallStream(_name, _args) {
|
|
467
|
+
streamCalled = true;
|
|
468
|
+
return {
|
|
469
|
+
stream: {
|
|
470
|
+
async *[Symbol.asyncIterator]() {
|
|
471
|
+
yield Buffer.from("stream-chunk-1");
|
|
472
|
+
yield Buffer.from("stream-chunk-2");
|
|
473
|
+
},
|
|
474
|
+
},
|
|
475
|
+
close() {},
|
|
476
|
+
};
|
|
477
|
+
},
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const server = new StdioMcpServer(provider);
|
|
481
|
+
const request = JSON.stringify({
|
|
482
|
+
jsonrpc: "2.0",
|
|
483
|
+
id: 1,
|
|
484
|
+
method: "tools/call",
|
|
485
|
+
params: { name: "my_tool", arguments: { foo: "bar" } },
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const response = await server.handleRawLine(request);
|
|
489
|
+
|
|
490
|
+
assert.equal(streamCalled, true, "handleToolCallStream should have been called");
|
|
491
|
+
assert.equal(unaryCalled, false, "handleToolCall should NOT have been called");
|
|
492
|
+
|
|
493
|
+
// The server buffers the stream into a JSON-RPC success response
|
|
494
|
+
assert.equal(response.jsonrpc, "2.0");
|
|
495
|
+
assert.equal(response.id, 1);
|
|
496
|
+
assert.ok(response.result);
|
|
497
|
+
assert.ok(response.result.content);
|
|
498
|
+
const text = response.result.content[0].text;
|
|
499
|
+
const payload = JSON.parse(text);
|
|
500
|
+
assert.equal(payload.ok, true);
|
|
501
|
+
assert.equal(payload.chunk_count, 2);
|
|
502
|
+
assert.deepEqual(payload.chunks, ["stream-chunk-1", "stream-chunk-2"]);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it("uses default timeout when timeout_ms is missing", () => {
|
|
506
|
+
let capturedOptions = null;
|
|
507
|
+
const kit = new RemoteControlCaseKit(
|
|
508
|
+
{
|
|
509
|
+
endpoint: "http://127.0.0.1:50051",
|
|
510
|
+
tenant: "tenant-a",
|
|
511
|
+
connectTimeoutMs: 5000,
|
|
512
|
+
signatureBase64: "sig",
|
|
513
|
+
},
|
|
514
|
+
() => ({
|
|
515
|
+
...makeOrchestrator(),
|
|
516
|
+
callMcpToolStream: (_toolName, _nodeId, _args, opts) => {
|
|
517
|
+
capturedOptions = opts;
|
|
518
|
+
return {
|
|
519
|
+
close: () => {},
|
|
520
|
+
async *[Symbol.asyncIterator]() {},
|
|
521
|
+
};
|
|
522
|
+
},
|
|
523
|
+
close: () => {},
|
|
524
|
+
}),
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
const handle = kit.handleToolCallStream("call_remote_tool_stream", {
|
|
528
|
+
node_id: "node-a",
|
|
529
|
+
tool_name: "tool-a",
|
|
530
|
+
});
|
|
531
|
+
assert.ok(handle);
|
|
532
|
+
handle.close();
|
|
533
|
+
assert.ok(capturedOptions);
|
|
534
|
+
assert.equal(capturedOptions.timeoutMs, DEFAULT_MCP_TOOL_STREAM_TIMEOUT_MS);
|
|
535
|
+
});
|
|
87
536
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DendriteServerStream } from "../../dendrite_bridge.js";
|
|
1
2
|
import type { JsonRecord, AbilityPackageDescriptor } from "./descriptor.js";
|
|
2
3
|
export type { JsonRecord };
|
|
3
4
|
export type OrchestratorFactory = (config: {
|
|
@@ -10,6 +11,10 @@ export interface RemoteOrchestrator {
|
|
|
10
11
|
listNodes(ownerId?: string): JsonRecord[];
|
|
11
12
|
listMcpTools(namePattern?: string, tags?: string[], nodeId?: string): JsonRecord[];
|
|
12
13
|
callMcpTool(toolName: string, targetNodeId: string, argumentsJson: JsonRecord): JsonRecord;
|
|
14
|
+
/** Call an MCP tool on a remote node and return a stream of incremental response chunks. */
|
|
15
|
+
callMcpToolStream(toolName: string, targetNodeId: string, argumentsJson: JsonRecord, options?: {
|
|
16
|
+
timeoutMs?: number;
|
|
17
|
+
}): DendriteServerStream;
|
|
13
18
|
disconnectDevice(nodeId: string, reason: string): JsonRecord;
|
|
14
19
|
uninstallAbility(nodeId: string, installId: string, reason: string): JsonRecord;
|
|
15
20
|
deployAbilityPackage(descriptor: AbilityPackageDescriptor, nodeId: string, cleanupOnActivateFailure: boolean): JsonRecord;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DendriteBridge } from "../../dendrite_bridge.js";
|
|
1
|
+
import { DendriteBridge, } from "../../dendrite_bridge.js";
|
|
2
2
|
import { DEFAULT_EXECUTION_MODE, DEFAULT_INSTALL_TIMEOUT_SECONDS } from "./config.js";
|
|
3
3
|
export function buildOrchestrator(config, tenant) {
|
|
4
4
|
return new OrchestratorAdapter(config, tenant);
|
|
@@ -26,6 +26,14 @@ class OrchestratorAdapter {
|
|
|
26
26
|
const options = { targetNodeId, argumentsJson };
|
|
27
27
|
return this.bridge.callMcpTool(this.tenant, toolName, options);
|
|
28
28
|
}
|
|
29
|
+
callMcpToolStream(toolName, targetNodeId, argumentsJson = {}, options = {}) {
|
|
30
|
+
const streamOptions = {
|
|
31
|
+
targetNodeId,
|
|
32
|
+
argumentsJson,
|
|
33
|
+
timeoutMs: options.timeoutMs,
|
|
34
|
+
};
|
|
35
|
+
return this.bridge.callMcpToolStream(this.tenant, toolName, streamOptions);
|
|
36
|
+
}
|
|
29
37
|
disconnectDevice(nodeId, reason) {
|
|
30
38
|
return this.bridge.deregisterNode(this.tenant, nodeId, reason);
|
|
31
39
|
}
|