@animalabs/agent-framework 0.30.0 → 0.31.0
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 +71 -8
- package/dist/src/agent.d.ts +29 -9
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +82 -17
- package/dist/src/agent.js.map +1 -1
- package/dist/src/api/mcp-server-core.d.ts.map +1 -1
- package/dist/src/api/mcp-server-core.js +10 -3
- package/dist/src/api/mcp-server-core.js.map +1 -1
- package/dist/src/code-execution/isolate/render.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/render.js +7 -6
- package/dist/src/code-execution/isolate/render.js.map +1 -1
- package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
- package/dist/src/code-execution/isolate/scripts.js +5 -4
- package/dist/src/code-execution/isolate/scripts.js.map +1 -1
- package/dist/src/framework.d.ts +129 -92
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +1115 -704
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +24 -17
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +198 -144
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/gate/recipe-reconcile.d.ts.map +1 -1
- package/dist/src/gate/recipe-reconcile.js +6 -5
- package/dist/src/gate/recipe-reconcile.js.map +1 -1
- package/dist/src/host-presentation.d.ts.map +1 -1
- package/dist/src/host-presentation.js +5 -7
- package/dist/src/host-presentation.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/inference-bounds.d.ts +37 -0
- package/dist/src/inference-bounds.d.ts.map +1 -0
- package/dist/src/inference-bounds.js +84 -0
- package/dist/src/inference-bounds.js.map +1 -0
- package/dist/src/line-reader.d.ts +7 -1
- package/dist/src/line-reader.d.ts.map +1 -1
- package/dist/src/line-reader.js +17 -6
- package/dist/src/line-reader.js.map +1 -1
- package/dist/src/mcpl/errors.d.ts +1 -0
- package/dist/src/mcpl/errors.d.ts.map +1 -1
- package/dist/src/mcpl/errors.js +3 -0
- package/dist/src/mcpl/errors.js.map +1 -1
- package/dist/src/mcpl/inbound-source.d.ts +2 -2
- package/dist/src/mcpl/inbound-source.js +2 -2
- package/dist/src/mcpl/inference-router.d.ts +11 -1
- package/dist/src/mcpl/inference-router.d.ts.map +1 -1
- package/dist/src/mcpl/inference-router.js +65 -9
- package/dist/src/mcpl/inference-router.js.map +1 -1
- package/dist/src/mcpl/modern-connection.d.ts +10 -7
- package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
- package/dist/src/mcpl/modern-connection.js +85 -74
- package/dist/src/mcpl/modern-connection.js.map +1 -1
- package/dist/src/mcpl/protocol-family.d.ts +1 -1
- package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
- package/dist/src/mcpl/protocol-family.js +45 -3
- package/dist/src/mcpl/protocol-family.js.map +1 -1
- package/dist/src/mcpl/push-coalescer.d.ts +2 -7
- package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
- package/dist/src/mcpl/push-coalescer.js.map +1 -1
- package/dist/src/mcpl/references.d.ts +1 -2
- package/dist/src/mcpl/references.d.ts.map +1 -1
- package/dist/src/mcpl/references.js +3 -3
- package/dist/src/mcpl/references.js.map +1 -1
- package/dist/src/mcpl/server-connection.d.ts +14 -0
- package/dist/src/mcpl/server-connection.d.ts.map +1 -1
- package/dist/src/mcpl/server-connection.js +26 -22
- package/dist/src/mcpl/server-connection.js.map +1 -1
- package/dist/src/mcpl/tool-signature.d.ts +1 -0
- package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
- package/dist/src/mcpl/tool-signature.js +16 -11
- package/dist/src/mcpl/tool-signature.js.map +1 -1
- package/dist/src/mcpl/transport.d.ts +44 -21
- package/dist/src/mcpl/transport.d.ts.map +1 -1
- package/dist/src/mcpl/transport.js +283 -64
- package/dist/src/mcpl/transport.js.map +1 -1
- package/dist/src/mcpl/types.d.ts +17 -1
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/module-registry.d.ts +32 -4
- package/dist/src/module-registry.d.ts.map +1 -1
- package/dist/src/module-registry.js +189 -25
- package/dist/src/module-registry.js.map +1 -1
- package/dist/src/modules/discord/discord-js-client.d.ts.map +1 -1
- package/dist/src/modules/discord/discord-js-client.js +24 -16
- package/dist/src/modules/discord/discord-js-client.js.map +1 -1
- package/dist/src/modules/sender-name.d.ts +5 -4
- package/dist/src/modules/sender-name.d.ts.map +1 -1
- package/dist/src/modules/sender-name.js +7 -5
- package/dist/src/modules/sender-name.js.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.d.ts +16 -8
- package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
- package/dist/src/modules/workspace/disk-agreement.js +33 -12
- package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +6 -3
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +61 -15
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/observe.d.ts +9 -1
- package/dist/src/modules/workspace/observe.d.ts.map +1 -1
- package/dist/src/modules/workspace/observe.js +50 -10
- package/dist/src/modules/workspace/observe.js.map +1 -1
- package/dist/src/modules/workspace/reconcile.d.ts +11 -1
- package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
- package/dist/src/modules/workspace/reconcile.js +19 -8
- package/dist/src/modules/workspace/reconcile.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +8 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/modules/workspace/watcher.d.ts +7 -0
- package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
- package/dist/src/modules/workspace/watcher.js +7 -5
- package/dist/src/modules/workspace/watcher.js.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
- package/dist/src/recovery/discord-awareness-outbox.js +1 -9
- package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
- package/dist/src/supplied-text.d.ts +2 -0
- package/dist/src/supplied-text.d.ts.map +1 -1
- package/dist/src/supplied-text.js +16 -7
- package/dist/src/supplied-text.js.map +1 -1
- package/dist/src/tool-result-guard.js +1 -1
- package/dist/src/tool-result-guard.js.map +1 -1
- package/dist/src/tune-out/coordinator.d.ts +2 -20
- package/dist/src/tune-out/coordinator.d.ts.map +1 -1
- package/dist/src/tune-out/coordinator.js +175 -70
- package/dist/src/tune-out/coordinator.js.map +1 -1
- package/dist/src/tune-out/tools.d.ts +14 -2
- package/dist/src/tune-out/tools.d.ts.map +1 -1
- package/dist/src/tune-out/tools.js.map +1 -1
- package/dist/src/types/agent.d.ts +2 -1
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +54 -15
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/types/module.d.ts +42 -3
- package/dist/src/types/module.d.ts.map +1 -1
- package/dist/src/types/trace.d.ts +4 -3
- package/dist/src/types/trace.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/agent.ts +90 -25
- package/src/api/mcp-server-core.ts +10 -3
- package/src/code-execution/isolate/render.ts +7 -6
- package/src/code-execution/isolate/scripts.ts +5 -4
- package/src/framework.ts +1145 -723
- package/src/gate/event-gate.ts +220 -145
- package/src/gate/recipe-reconcile.ts +7 -5
- package/src/host-presentation.ts +4 -7
- package/src/index.ts +1 -0
- package/src/inference-bounds.ts +111 -0
- package/src/line-reader.ts +22 -5
- package/src/mcpl/errors.ts +4 -0
- package/src/mcpl/inbound-source.ts +2 -2
- package/src/mcpl/inference-router.ts +67 -10
- package/src/mcpl/modern-connection.ts +83 -68
- package/src/mcpl/protocol-family.ts +37 -4
- package/src/mcpl/push-coalescer.ts +2 -7
- package/src/mcpl/references.ts +3 -4
- package/src/mcpl/server-connection.ts +30 -21
- package/src/mcpl/tool-signature.ts +18 -11
- package/src/mcpl/transport.ts +261 -60
- package/src/mcpl/types.ts +18 -1
- package/src/module-registry.ts +178 -25
- package/src/modules/discord/discord-js-client.ts +33 -29
- package/src/modules/sender-name.ts +8 -5
- package/src/modules/workspace/disk-agreement.ts +39 -15
- package/src/modules/workspace/index.ts +65 -14
- package/src/modules/workspace/observe.ts +51 -10
- package/src/modules/workspace/reconcile.ts +22 -8
- package/src/modules/workspace/types.ts +5 -0
- package/src/modules/workspace/watcher.ts +8 -6
- package/src/recovery/discord-awareness-outbox.ts +1 -11
- package/src/supplied-text.ts +17 -6
- package/src/tool-result-guard.ts +1 -1
- package/src/tune-out/coordinator.ts +187 -85
- package/src/tune-out/tools.ts +14 -2
- package/src/types/agent.ts +2 -1
- package/src/types/framework.ts +55 -15
- package/src/types/module.ts +42 -3
- package/src/types/trace.ts +4 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/mcpl/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/mcpl/transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
import { spawn } from 'node:child_process';
|
|
21
21
|
import { EventEmitter } from 'node:events';
|
|
22
22
|
import { StringDecoder } from 'node:string_decoder';
|
|
23
|
-
import { escapeLineSeparators, readLines } from '../line-reader.js';
|
|
23
|
+
import { MAX_LINE_CHARS, escapeLineSeparators, readLines } from '../line-reader.js';
|
|
24
24
|
import WebSocket from 'ws';
|
|
25
|
-
import { WITHHELD_TEXT, describeCredentialError, headerCarries, maskedError, noteDialSent, redactServerText, redactUrl, resolveServerBinding, serverTextRedactor, tokenTransportProblem, urlAsDialed } from './protocol-family.js';
|
|
25
|
+
import { WITHHELD_TEXT, describeCredentialError, headerCarries, maskedError, noteDialSent, redactServerText, redactUrl, resolveServerBinding, serverConfigProblems, serverTextRedactor, tokenTransportProblem, urlAsDialed } from './protocol-family.js';
|
|
26
26
|
import { assertBooleanSetting } from '../config-checks.js';
|
|
27
27
|
import { logDetail } from './log-value.js';
|
|
28
28
|
/**
|
|
@@ -33,19 +33,15 @@ import { logDetail } from './log-value.js';
|
|
|
33
33
|
* - `'stderr'` `(line: string)` diagnostic line (stdio child stderr; never WS)
|
|
34
34
|
* - `'error'` `(err: Error)` transport-level error
|
|
35
35
|
* - `'close'` `(info: TransportCloseInfo)` transport closed (once)
|
|
36
|
-
* - `'unreaped'` `(failure: Error)` a stdio child
|
|
37
|
-
* outlived SIGKILL too: the transport stays open, owning the
|
|
38
|
-
* close() retries the reap (never WS)
|
|
39
|
-
*
|
|
40
|
-
* Inbound `'line'`/`'stderr'` events are buffered until the first matching
|
|
41
|
-
* listener attaches (and re-buffered whenever there are none), so no message is
|
|
42
|
-
* lost across the handshake→routing handoff regardless of attach timing.
|
|
36
|
+
* - `'unreaped'` `(failure: Error)` a stdio child this transport was
|
|
37
|
+
* ending outlived SIGKILL too: the transport stays open, owning the
|
|
38
|
+
* child, and close() retries the reap (never WS)
|
|
43
39
|
*/
|
|
44
40
|
export class McplTransport extends EventEmitter {
|
|
45
41
|
serverId;
|
|
46
42
|
_closed = false;
|
|
47
43
|
lineBuffer = [];
|
|
48
|
-
|
|
44
|
+
heldStderr = new HeldStderr();
|
|
49
45
|
/** The server's id from its config, so a line the transport logs names it.
|
|
50
46
|
* The stdio and WebSocket transports always give it; a test's stand-in
|
|
51
47
|
* transport may leave it out. */
|
|
@@ -60,9 +56,8 @@ export class McplTransport extends EventEmitter {
|
|
|
60
56
|
queueMicrotask(() => { for (const l of pending)
|
|
61
57
|
this.emit('line', l); });
|
|
62
58
|
}
|
|
63
|
-
else if (event === 'stderr' && this.
|
|
64
|
-
const pending = this.
|
|
65
|
-
this.stderrBuffer = [];
|
|
59
|
+
else if (event === 'stderr' && !this.heldStderr.empty) {
|
|
60
|
+
const pending = this.heldStderr.take();
|
|
66
61
|
queueMicrotask(() => { for (const l of pending)
|
|
67
62
|
this.emit('stderr', l); });
|
|
68
63
|
}
|
|
@@ -94,14 +89,7 @@ export class McplTransport extends EventEmitter {
|
|
|
94
89
|
stderrTailRedacted(_redact) {
|
|
95
90
|
return '';
|
|
96
91
|
}
|
|
97
|
-
|
|
98
|
-
* The error a stdio child's stdin or stdout failed with, once one has. The
|
|
99
|
-
* transport is ending the child then, and stays open until it exits, but
|
|
100
|
-
* writes nothing more to it: writeLine drops each line, and both engines
|
|
101
|
-
* refuse a request as not sent. Undefined while writes go out, and always
|
|
102
|
-
* for a transport that has no pipes.
|
|
103
|
-
*/
|
|
104
|
-
get pipeFailure() {
|
|
92
|
+
get stdioStopCause() {
|
|
105
93
|
return undefined;
|
|
106
94
|
}
|
|
107
95
|
/**
|
|
@@ -124,7 +112,7 @@ export class McplTransport extends EventEmitter {
|
|
|
124
112
|
if (this.listenerCount('stderr') > 0)
|
|
125
113
|
this.emit('stderr', line);
|
|
126
114
|
else
|
|
127
|
-
this.
|
|
115
|
+
this.heldStderr.hold(line);
|
|
128
116
|
}
|
|
129
117
|
/**
|
|
130
118
|
* Report a transport error: to the 'error' listeners if there are any, else
|
|
@@ -247,8 +235,8 @@ export const STDERR_END_WAIT_MS = 1_000;
|
|
|
247
235
|
export class StdioTransport extends McplTransport {
|
|
248
236
|
child;
|
|
249
237
|
kind = 'stdio';
|
|
250
|
-
|
|
251
|
-
|
|
238
|
+
stopCause;
|
|
239
|
+
endedChild = false;
|
|
252
240
|
/** The stream's last STDERR_WINDOW_CHARS, raw. */
|
|
253
241
|
stderrWindow = '';
|
|
254
242
|
/** True once the child's stderr has ended or closed. */
|
|
@@ -272,8 +260,8 @@ export class StdioTransport extends McplTransport {
|
|
|
272
260
|
kept = kept.slice(1);
|
|
273
261
|
return (cut ? '…' : '') + kept;
|
|
274
262
|
}
|
|
275
|
-
get
|
|
276
|
-
return this.
|
|
263
|
+
get stdioStopCause() {
|
|
264
|
+
return this.stopCause;
|
|
277
265
|
}
|
|
278
266
|
stderrEnded(ms) {
|
|
279
267
|
const stderr = this.child.stderr;
|
|
@@ -301,20 +289,44 @@ export class StdioTransport extends McplTransport {
|
|
|
301
289
|
serverId) {
|
|
302
290
|
super(serverId);
|
|
303
291
|
this.child = child;
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
292
|
+
const lineBound = testLineBound ?? MAX_LINE_CHARS;
|
|
293
|
+
this.stopLines = readLines(stdout, {
|
|
294
|
+
line: (line) => this.pushLine(line),
|
|
295
|
+
tooLong: (maxChars) => this.stopStdio(new Error(`the server wrote a line on stdout longer than ${maxChars.toLocaleString('en-US')} UTF-16 code units`), false),
|
|
296
|
+
}, lineBound);
|
|
308
297
|
let carry = '';
|
|
298
|
+
let dropping = false;
|
|
299
|
+
const dropped = `[a stderr line longer than ${lineBound.toLocaleString('en-US')} UTF-16 code units was dropped]`;
|
|
309
300
|
const decoder = new StringDecoder('utf8');
|
|
310
301
|
const received = (text) => {
|
|
311
302
|
const window = this.stderrWindow + text;
|
|
312
303
|
this.stderrWindow = window.slice(-STDERR_WINDOW_CHARS);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (
|
|
317
|
-
|
|
304
|
+
let rest = text;
|
|
305
|
+
if (dropping) {
|
|
306
|
+
const end = rest.indexOf('\n');
|
|
307
|
+
if (end === -1)
|
|
308
|
+
return;
|
|
309
|
+
dropping = false;
|
|
310
|
+
rest = rest.slice(end + 1);
|
|
311
|
+
}
|
|
312
|
+
if (rest.includes('\n')) {
|
|
313
|
+
const lines = (carry + rest).split('\n');
|
|
314
|
+
carry = lines.pop() ?? '';
|
|
315
|
+
for (const l of lines) {
|
|
316
|
+
if (l.length > lineBound)
|
|
317
|
+
this.pushStderr(dropped);
|
|
318
|
+
else if (l.length > 0)
|
|
319
|
+
this.pushStderr(l);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
carry += rest;
|
|
324
|
+
}
|
|
325
|
+
if (carry.length > lineBound) {
|
|
326
|
+
carry = '';
|
|
327
|
+
dropping = true;
|
|
328
|
+
this.pushStderr(dropped);
|
|
329
|
+
}
|
|
318
330
|
};
|
|
319
331
|
this.child.stderr?.on('data', (chunk) => received(decoder.write(chunk)));
|
|
320
332
|
// A last line with no newline is complete once the stream ends.
|
|
@@ -322,6 +334,7 @@ export class StdioTransport extends McplTransport {
|
|
|
322
334
|
received(decoder.end());
|
|
323
335
|
if (carry.length > 0)
|
|
324
336
|
this.pushStderr(carry);
|
|
337
|
+
carry = '';
|
|
325
338
|
this.stderrDone = true;
|
|
326
339
|
});
|
|
327
340
|
// A stream destroyed by an error closes without ending: nothing more comes.
|
|
@@ -329,13 +342,17 @@ export class StdioTransport extends McplTransport {
|
|
|
329
342
|
this.child.on('error', (err) => this.reportError(err));
|
|
330
343
|
// The child's stdin and stdout carry the protocol, so after an error on
|
|
331
344
|
// either, a write's EPIPE or a read error, this transport can't reach the
|
|
332
|
-
// child even if it's still running.
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
//
|
|
345
|
+
// child even if it's still running. Nothing more is written (stdioStopCause),
|
|
346
|
+
// and the handler first gives the child its exit grace to exit on its
|
|
347
|
+
// own: a child already dead when the write failed, as one a stop's signal
|
|
348
|
+
// reached first, exits within it, and its close carries its own code and
|
|
349
|
+
// signal. One still running then is ended as close() ends it, SIGTERM and
|
|
350
|
+
// then SIGKILL, in the one attempt close() shares, and only that close is
|
|
351
|
+
// pipeFailed, since its signal is this transport's. Either way the exit's
|
|
352
|
+
// close names the error, so the connection handles it as any other exit.
|
|
353
|
+
// The close is the report: no 'error' is emitted and nothing is thrown.
|
|
354
|
+
// The first error starts the wait, and a later one, from either pipe,
|
|
355
|
+
// adds nothing to it. A child that outlives SIGKILL too stays this
|
|
339
356
|
// transport's: 'unreaped' says so, unless the host is closing it, whose
|
|
340
357
|
// close() reports the failure itself. A child that stops reading without
|
|
341
358
|
// closing its stdin raises no error: the writes are buffered, and only
|
|
@@ -343,17 +360,8 @@ export class StdioTransport extends McplTransport {
|
|
|
343
360
|
// Bun a child that closes its stdin isn't reported either: Bun emits no
|
|
344
361
|
// 'error' for the writes to it (checked on 1.3.10, 1.4.3 and 1.4.4's
|
|
345
362
|
// canary), so request timeouts are its only bound too.
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return;
|
|
349
|
-
this.pipeError = err;
|
|
350
|
-
void this.terminate().catch((failure) => {
|
|
351
|
-
if (!this.closed)
|
|
352
|
-
this.emit('unreaped', failure instanceof Error ? failure : new Error(String(failure)));
|
|
353
|
-
});
|
|
354
|
-
};
|
|
355
|
-
this.child.stdin?.on('error', endOnPipeError);
|
|
356
|
-
this.child.stdout?.on('error', endOnPipeError);
|
|
363
|
+
this.child.stdin?.on('error', (err) => this.stopStdio(err, true));
|
|
364
|
+
this.child.stdout?.on('error', (err) => this.stopStdio(err, true));
|
|
357
365
|
// stderr carries diagnostics only: a read error on it is reported, to the
|
|
358
366
|
// listeners or else in the log, and never thrown. It ends nothing once
|
|
359
367
|
// the connection is wired; during a handshake the listener is the
|
|
@@ -362,9 +370,25 @@ export class StdioTransport extends McplTransport {
|
|
|
362
370
|
this.child.on('exit', (code, signal) => this.markClosed({
|
|
363
371
|
code,
|
|
364
372
|
signal,
|
|
365
|
-
reason: this.
|
|
373
|
+
reason: this.stopCause ? `child process exited, and this host had stopped using the server's stdio (${this.stopCause.message})` : 'child process exited',
|
|
374
|
+
...(this.endedChild ? { pipeFailed: true } : {}),
|
|
366
375
|
}));
|
|
367
376
|
}
|
|
377
|
+
stopStdio(cause, exitGraceFirst) {
|
|
378
|
+
if (this.stopCause)
|
|
379
|
+
return;
|
|
380
|
+
this.stopCause = cause;
|
|
381
|
+
const exitedFirst = exitGraceFirst ? this.exitWithin(this.exitGrace()) : Promise.resolve(this.exited());
|
|
382
|
+
void exitedFirst.then((gone) => {
|
|
383
|
+
if (gone || this.terminating)
|
|
384
|
+
return;
|
|
385
|
+
this.endedChild = true;
|
|
386
|
+
return this.terminate();
|
|
387
|
+
}).catch((failure) => {
|
|
388
|
+
if (!this.closed)
|
|
389
|
+
this.emit('unreaped', failure instanceof Error ? failure : new Error(String(failure)));
|
|
390
|
+
});
|
|
391
|
+
}
|
|
368
392
|
/**
|
|
369
393
|
* Spawn `config.command` and resolve with a ready stdio transport once the
|
|
370
394
|
* child has spawned. Rejects if the command is missing (a config bug) or the
|
|
@@ -419,9 +443,7 @@ export class StdioTransport extends McplTransport {
|
|
|
419
443
|
return new StdioTransport(child, child.stdout, config.id);
|
|
420
444
|
}
|
|
421
445
|
writeLine(json) {
|
|
422
|
-
|
|
423
|
-
// transport is ending it.
|
|
424
|
-
if (this.pipeError)
|
|
446
|
+
if (this.stopCause)
|
|
425
447
|
return;
|
|
426
448
|
// stdin may be gone if the child died between the closed-check and here.
|
|
427
449
|
this.child.stdin?.write(escapeLineSeparators(json) + '\n');
|
|
@@ -455,6 +477,21 @@ export class StdioTransport extends McplTransport {
|
|
|
455
477
|
this.stopLines();
|
|
456
478
|
await this.terminate();
|
|
457
479
|
}
|
|
480
|
+
/** How long the child gets to exit after a pipe error, after SIGTERM and
|
|
481
|
+
* after SIGKILL: {@link STDIO_EXIT_GRACE_MS}, or a test's grace for it. */
|
|
482
|
+
exitGrace() {
|
|
483
|
+
return testExitGraces.get(this.child) ?? STDIO_EXIT_GRACE_MS;
|
|
484
|
+
}
|
|
485
|
+
/** Whether the child exits within `ms`: true at once if it has. */
|
|
486
|
+
exitWithin(ms) {
|
|
487
|
+
if (this.exited())
|
|
488
|
+
return Promise.resolve(true);
|
|
489
|
+
return new Promise((resolve) => {
|
|
490
|
+
const onExit = () => { clearTimeout(timer); resolve(true); };
|
|
491
|
+
const timer = setTimeout(() => { this.child.off('exit', onExit); resolve(false); }, ms);
|
|
492
|
+
this.child.once('exit', onExit);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
458
495
|
/** Whether the child has exited, or never started (a spawn error). */
|
|
459
496
|
exited() {
|
|
460
497
|
return this.child.pid === undefined || this.child.exitCode !== null || this.child.signalCode !== null;
|
|
@@ -479,24 +516,71 @@ export class StdioTransport extends McplTransport {
|
|
|
479
516
|
const child = this.child;
|
|
480
517
|
if (this.exited())
|
|
481
518
|
return;
|
|
519
|
+
const grace = this.exitGrace();
|
|
482
520
|
const exited = new Promise((resolve) => child.once('exit', () => resolve()));
|
|
483
521
|
const exitWithin = (ms) => new Promise((resolve) => {
|
|
484
522
|
const timer = setTimeout(() => resolve(false), ms);
|
|
485
523
|
void exited.then(() => { clearTimeout(timer); resolve(true); });
|
|
486
524
|
});
|
|
487
525
|
child.kill('SIGTERM');
|
|
488
|
-
if (await exitWithin(
|
|
526
|
+
if (await exitWithin(grace))
|
|
489
527
|
return;
|
|
490
528
|
const named = `stdio child ${child.pid} of MCP server "${this.serverId}"`;
|
|
491
|
-
console.error(`[mcpl] ${named} ignored SIGTERM for ${
|
|
529
|
+
console.error(`[mcpl] ${named} ignored SIGTERM for ${grace}ms — sending SIGKILL`);
|
|
492
530
|
child.kill('SIGKILL');
|
|
493
|
-
if (await exitWithin(
|
|
531
|
+
if (await exitWithin(grace))
|
|
494
532
|
return;
|
|
495
|
-
throw new Error(`${named} did not exit within ${
|
|
533
|
+
throw new Error(`${named} did not exit within ${grace}ms of SIGKILL; it could not be reaped`);
|
|
496
534
|
}
|
|
497
535
|
}
|
|
498
|
-
/** How long a stdio child gets to exit after
|
|
536
|
+
/** How long a stdio child gets to exit on its own after a pipe error, after
|
|
537
|
+
* SIGTERM, and again after SIGKILL. */
|
|
499
538
|
const STDIO_EXIT_GRACE_MS = 2_000;
|
|
539
|
+
const testExitGraces = new WeakMap();
|
|
540
|
+
/**
|
|
541
|
+
* For tests only: `child` gets `ms` in place of {@link STDIO_EXIT_GRACE_MS}.
|
|
542
|
+
* Set it only on a child that can exit by no means, such as one whose
|
|
543
|
+
* `kill()` is a no-op and that nothing else ends while it's closed: a short
|
|
544
|
+
* grace there races no exit, and only brings "could not be reaped" sooner.
|
|
545
|
+
*/
|
|
546
|
+
export function setStdioExitGraceForTests(child, ms) {
|
|
547
|
+
testExitGraces.set(child, ms);
|
|
548
|
+
}
|
|
549
|
+
let testLineBound;
|
|
550
|
+
const MAX_HELD_STDERR_LINES = 200;
|
|
551
|
+
export class HeldStderr {
|
|
552
|
+
lines = [];
|
|
553
|
+
chars = 0;
|
|
554
|
+
dropped = 0;
|
|
555
|
+
get empty() {
|
|
556
|
+
return this.lines.length === 0 && this.dropped === 0;
|
|
557
|
+
}
|
|
558
|
+
hold(line) {
|
|
559
|
+
// V8 can make a line split from a longer string a slice that keeps the
|
|
560
|
+
// whole string alive, so a held line is copied to own only its characters.
|
|
561
|
+
this.lines.push(Buffer.from(line, 'utf16le').toString('utf16le'));
|
|
562
|
+
this.chars += line.length;
|
|
563
|
+
const ceiling = testLineBound ?? MAX_LINE_CHARS;
|
|
564
|
+
while (this.lines.length > MAX_HELD_STDERR_LINES || this.chars > ceiling) {
|
|
565
|
+
this.chars -= this.lines.shift().length;
|
|
566
|
+
this.dropped++;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
take() {
|
|
570
|
+
const held = this.lines;
|
|
571
|
+
const taken = this.dropped > 0
|
|
572
|
+
? [`[${this.dropped.toLocaleString('en-US')} earlier stderr line${this.dropped === 1 ? ' was' : 's were'} dropped: unread stderr is held to ${MAX_HELD_STDERR_LINES} lines and ${(testLineBound ?? MAX_LINE_CHARS).toLocaleString('en-US')} UTF-16 code units]`, ...held]
|
|
573
|
+
: held;
|
|
574
|
+
this.lines = [];
|
|
575
|
+
this.chars = 0;
|
|
576
|
+
this.dropped = 0;
|
|
577
|
+
return taken;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/** @internal */
|
|
581
|
+
export function setStdioLineBoundForTests(maxChars) {
|
|
582
|
+
testLineBound = maxChars;
|
|
583
|
+
}
|
|
500
584
|
// ---------------------------------------------------------------------------
|
|
501
585
|
// WebSocket
|
|
502
586
|
// ---------------------------------------------------------------------------
|
|
@@ -550,6 +634,9 @@ export class WebSocketTransport extends McplTransport {
|
|
|
550
634
|
* at the open timeout.
|
|
551
635
|
*/
|
|
552
636
|
static async open(config, signal) {
|
|
637
|
+
const credentialRequired = checkedCredentialRequired(config);
|
|
638
|
+
if (credentialRequired === true)
|
|
639
|
+
return WebSocketTransport.openStrict(config, signal);
|
|
553
640
|
// Resolve the per-dial credential first: the provider runs on every
|
|
554
641
|
// connect AND reconnect, so rotation happens at the transport layer and
|
|
555
642
|
// nothing above it ever holds a credential.
|
|
@@ -650,6 +737,127 @@ export class WebSocketTransport extends McplTransport {
|
|
|
650
737
|
});
|
|
651
738
|
});
|
|
652
739
|
}
|
|
740
|
+
/** Separate opt-in path: the legacy dial above keeps its objects, reads,
|
|
741
|
+
* receivers and error behavior. Capture policy before calling host code;
|
|
742
|
+
* only noteDialSent uses the original config after awaiting (as a WeakMap
|
|
743
|
+
* key, without reading it). No provider result is stored on either config. */
|
|
744
|
+
static async openStrict(config, signal) {
|
|
745
|
+
const captured = {
|
|
746
|
+
id: config.id, url: config.url, tokenTransport: config.tokenTransport,
|
|
747
|
+
credentialRequired: true, token: config.token, accessProvider: config.accessProvider,
|
|
748
|
+
command: config.command, transport: config.transport, protocol: config.protocol,
|
|
749
|
+
};
|
|
750
|
+
const id = captured.id;
|
|
751
|
+
const fail = (reason) => new Error(`MCPL server "${id}": ${reason}`);
|
|
752
|
+
let problems;
|
|
753
|
+
try {
|
|
754
|
+
problems = serverConfigProblems(captured);
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
throw fail('connection-failed');
|
|
758
|
+
}
|
|
759
|
+
if (problems.length)
|
|
760
|
+
throw fail('connection-failed');
|
|
761
|
+
const provider = captured.accessProvider;
|
|
762
|
+
if (typeof provider !== 'function')
|
|
763
|
+
throw fail('provider-failed');
|
|
764
|
+
const followRedirects = false;
|
|
765
|
+
let url;
|
|
766
|
+
try {
|
|
767
|
+
url = buildWebSocketUrl(captured, null);
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
throw fail('connection-failed');
|
|
771
|
+
}
|
|
772
|
+
if (signal?.aborted)
|
|
773
|
+
throw fail('connection-failed');
|
|
774
|
+
let token;
|
|
775
|
+
try {
|
|
776
|
+
// Preserve the provider's receiver, without re-reading any config input.
|
|
777
|
+
const provided = Promise.resolve(provider.call(config));
|
|
778
|
+
provided.catch(() => { });
|
|
779
|
+
token = signal ? await raceAbort(provided, signal) : await provided;
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
if (signal?.aborted)
|
|
783
|
+
throw fail('connection-failed');
|
|
784
|
+
throw fail('provider-failed');
|
|
785
|
+
}
|
|
786
|
+
if (signal?.aborted)
|
|
787
|
+
throw fail('connection-failed');
|
|
788
|
+
if (typeof token !== 'string' || !headerCarries(token))
|
|
789
|
+
throw fail('provider-failed');
|
|
790
|
+
noteDialSent(config, { url, token });
|
|
791
|
+
let ws;
|
|
792
|
+
try {
|
|
793
|
+
ws = new WebSocket(url, { headers: { Authorization: `Bearer ${token}` }, followRedirects });
|
|
794
|
+
}
|
|
795
|
+
catch {
|
|
796
|
+
throw fail('connection-failed');
|
|
797
|
+
}
|
|
798
|
+
const swallowLateErrors = () => { };
|
|
799
|
+
return new Promise((resolve, reject) => {
|
|
800
|
+
let settled = false;
|
|
801
|
+
const abandon = (reason = 'connection-failed', request, response) => {
|
|
802
|
+
if (settled)
|
|
803
|
+
return;
|
|
804
|
+
settled = true;
|
|
805
|
+
clearTimeout(timer);
|
|
806
|
+
signal?.removeEventListener('abort', onAbort);
|
|
807
|
+
ws.removeAllListeners();
|
|
808
|
+
ws.on('error', swallowLateErrors);
|
|
809
|
+
// The status listener suppresses ws's default abort. Discard the body
|
|
810
|
+
// without inspecting it, and use the request API this engine offers.
|
|
811
|
+
try {
|
|
812
|
+
response?.destroy();
|
|
813
|
+
}
|
|
814
|
+
catch { /* already gone */ }
|
|
815
|
+
try {
|
|
816
|
+
if (typeof request?.destroy === 'function')
|
|
817
|
+
request.destroy();
|
|
818
|
+
else
|
|
819
|
+
request?.abort?.(); // Some clients expose abort(), not destroy().
|
|
820
|
+
}
|
|
821
|
+
catch { /* already gone */ }
|
|
822
|
+
try {
|
|
823
|
+
ws.terminate();
|
|
824
|
+
}
|
|
825
|
+
catch { /* already gone */ }
|
|
826
|
+
reject(fail(reason));
|
|
827
|
+
};
|
|
828
|
+
const timer = setTimeout(() => abandon(), WS_OPEN_TIMEOUT_MS);
|
|
829
|
+
const onAbort = () => abandon();
|
|
830
|
+
if (signal?.aborted) {
|
|
831
|
+
onAbort();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
else
|
|
835
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
836
|
+
// Installed only on this explicit strict path. No peer text is a status.
|
|
837
|
+
ws.once('unexpected-response', (request, response) => {
|
|
838
|
+
const status = response?.statusCode;
|
|
839
|
+
// A refused upgrade without structured status cannot classify auth.
|
|
840
|
+
// Generic socket errors still mean connection-failed, never parsed text.
|
|
841
|
+
abandon(typeof status !== 'number' || !Number.isFinite(status) ? 'engine-unsupported'
|
|
842
|
+
: status === 401 || status === 403 ? 'credential-rejected' : 'connection-failed', request, response);
|
|
843
|
+
});
|
|
844
|
+
ws.once('open', () => {
|
|
845
|
+
if (settled)
|
|
846
|
+
return;
|
|
847
|
+
settled = true;
|
|
848
|
+
clearTimeout(timer);
|
|
849
|
+
signal?.removeEventListener('abort', onAbort);
|
|
850
|
+
ws.removeAllListeners('unexpected-response');
|
|
851
|
+
const transport = new WebSocketTransport(ws, captured, url);
|
|
852
|
+
// Strict transport-created errors carry only id and a fixed code;
|
|
853
|
+
// don't project a native error or its cause into them.
|
|
854
|
+
ws.removeAllListeners('error');
|
|
855
|
+
ws.on('error', () => transport.reportError(fail('connection-failed')));
|
|
856
|
+
resolve(transport);
|
|
857
|
+
});
|
|
858
|
+
ws.once('error', () => abandon());
|
|
859
|
+
});
|
|
860
|
+
}
|
|
653
861
|
writeLine(json) {
|
|
654
862
|
if (this.ws.readyState === WebSocket.OPEN) {
|
|
655
863
|
this.ws.send(escapeLineSeparators(json));
|
|
@@ -778,14 +986,23 @@ function dialsNetwork(config) {
|
|
|
778
986
|
* boolean setting added to McplServerConfig doesn't compile until it has
|
|
779
987
|
* an entry here, so the check below covers it too. */
|
|
780
988
|
const SERVER_CONFIG_BOOLEANS = {
|
|
989
|
+
credentialRequired: 'true requires a provider credential on every WebSocket dial without fallback; false or leaving it out keeps legacy behavior',
|
|
781
990
|
inheritEnv: 'true passes the server the host\'s whole environment, and false or leaving it out passes only the allowlisted and LC_* host variables',
|
|
782
991
|
reconnect: 'true retries in the background when the server can\'t be reached or its connection drops, and false or leaving it out doesn\'t',
|
|
783
992
|
allowHostCommands: 'true lets the server run the host\'s admin commands, such as undo, hide, unstick and quiesce, and false or leaving it out refuses them',
|
|
784
993
|
toolHints: 'true advertises MCPL tool hints (RFC-014) to an MCPL server, so its dynamic tools are announced in the conversation and called through toolsets--act; leaving it out does the same wherever the name toolsets is free; false lists every tool as before and advertises none',
|
|
785
994
|
};
|
|
995
|
+
/** Direct transport callers bypass config validation, but not this check. */
|
|
996
|
+
function checkedCredentialRequired(config) {
|
|
997
|
+
const value = config.credentialRequired;
|
|
998
|
+
if (value !== undefined && value !== null && typeof value !== 'boolean') {
|
|
999
|
+
assertBooleanSetting(`MCPL server "${config.id}" credentialRequired`, value, SERVER_CONFIG_BOOLEANS.credentialRequired);
|
|
1000
|
+
}
|
|
1001
|
+
return value;
|
|
1002
|
+
}
|
|
786
1003
|
/**
|
|
787
|
-
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands
|
|
788
|
-
* or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
1004
|
+
* Refuse a server config whose `inheritEnv`, `reconnect`, `allowHostCommands`,
|
|
1005
|
+
* `credentialRequired` or `toolHints` holds anything but true or false. A config read from JSON can carry a quoted
|
|
789
1006
|
* "false" where the type says boolean, and every reader of these settings takes
|
|
790
1007
|
* only `true` as on, so the quotes would hide what the value does. null counts
|
|
791
1008
|
* as not set, so a generated config that writes an unset setting as null
|
|
@@ -817,6 +1034,8 @@ export function assertServerConfigBooleans(config) {
|
|
|
817
1034
|
* performed the MCPL handshake.
|
|
818
1035
|
*/
|
|
819
1036
|
export async function openTransport(config, signal) {
|
|
1037
|
+
if (checkedCredentialRequired(config) === true)
|
|
1038
|
+
return WebSocketTransport.open(config, signal);
|
|
820
1039
|
const binding = resolveServerBinding(config);
|
|
821
1040
|
if (binding.family !== 'legacy') {
|
|
822
1041
|
throw new Error(`MCP server "${config.id}" is configured for modern MCP; the legacy transports can't open it`);
|