mcp-agents 0.5.8 → 0.6.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 +5 -1
- package/package.json +1 -1
- package/server.js +121 -9
package/README.md
CHANGED
|
@@ -196,7 +196,11 @@ After `npm link`, any edits to `server.js` take effect immediately — no reinst
|
|
|
196
196
|
4. Client calls `tools/call` with the tool name and a `prompt`
|
|
197
197
|
5. The server runs the CLI as a child process and returns tool text (Claude JSON `result`, or stdout/stderr for other providers)
|
|
198
198
|
|
|
199
|
-
The server
|
|
199
|
+
The server keeps a small keepalive timer so Node.js does not exit prematurely
|
|
200
|
+
when stdin reaches EOF before an async subprocess registers an active handle.
|
|
201
|
+
For Claude and Gemini provider mode, that keepalive is cleared during shutdown:
|
|
202
|
+
the server now exits when the MCP stdio connection closes and kills any tracked
|
|
203
|
+
detached provider child process groups that would otherwise linger.
|
|
200
204
|
|
|
201
205
|
## License
|
|
202
206
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -21,6 +21,9 @@ const VERSION = JSON.parse(
|
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
22
22
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
23
23
|
const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
|
|
24
|
+
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
25
|
+
const SHUTDOWN_TIMEOUT_MS = 3_000;
|
|
26
|
+
let fatalShutdown;
|
|
24
27
|
|
|
25
28
|
// ---------------------------------------------------------------------------
|
|
26
29
|
// CLI Backend Definitions
|
|
@@ -193,12 +196,19 @@ function parseArgs() {
|
|
|
193
196
|
* on timeout — prevents orphan child processes.
|
|
194
197
|
* @param {string} command
|
|
195
198
|
* @param {string[]} args
|
|
196
|
-
* @param {{
|
|
199
|
+
* @param {{
|
|
200
|
+
* timeoutMs?: number,
|
|
201
|
+
* stdinData?: string,
|
|
202
|
+
* onSpawn?: (childInfo: { pid?: number, killGroup: () => void }) => void,
|
|
203
|
+
* onSettled?: (pid?: number) => void,
|
|
204
|
+
* }} [opts]
|
|
197
205
|
* @returns {Promise<{ output: string, stdoutBytes: number, stderrBytes: number, durationMs: number }>}
|
|
198
206
|
*/
|
|
199
207
|
function runCli(command, args, opts = {}) {
|
|
200
208
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
201
209
|
const stdinData = opts.stdinData;
|
|
210
|
+
const onSpawn = opts.onSpawn;
|
|
211
|
+
const onSettled = opts.onSettled;
|
|
202
212
|
const startedAt = Date.now();
|
|
203
213
|
|
|
204
214
|
return new Promise((resolve, reject) => {
|
|
@@ -225,11 +235,13 @@ function runCli(command, args, opts = {}) {
|
|
|
225
235
|
const killGroup = () => {
|
|
226
236
|
try { process.kill(-child.pid, "SIGKILL"); } catch {}
|
|
227
237
|
};
|
|
238
|
+
onSpawn?.({ pid: child.pid, killGroup });
|
|
228
239
|
|
|
229
240
|
const done = (err) => {
|
|
230
241
|
clearTimeout(timer);
|
|
231
242
|
if (settled) return;
|
|
232
243
|
settled = true;
|
|
244
|
+
onSettled?.(child.pid);
|
|
233
245
|
err ? reject(err) : resolve({
|
|
234
246
|
output: (stdout || stderr || "").trimEnd(),
|
|
235
247
|
stdoutBytes: stdoutLen,
|
|
@@ -262,6 +274,7 @@ function runCli(command, args, opts = {}) {
|
|
|
262
274
|
const timer = setTimeout(() => {
|
|
263
275
|
killGroup();
|
|
264
276
|
}, timeoutMs);
|
|
277
|
+
timer.unref();
|
|
265
278
|
|
|
266
279
|
child.on("error", (err) => {
|
|
267
280
|
done(new Error(`Failed to start ${command}: ${err.message}`));
|
|
@@ -307,7 +320,6 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
307
320
|
logErr(`[codex] ${chunk.toString().trimEnd()}`);
|
|
308
321
|
});
|
|
309
322
|
|
|
310
|
-
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
311
323
|
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
312
324
|
process.once(sig, () => {
|
|
313
325
|
child.kill(sig);
|
|
@@ -358,6 +370,53 @@ async function main() {
|
|
|
358
370
|
{ name: "mcp-agents", version: VERSION },
|
|
359
371
|
{ capabilities: { tools: {} } },
|
|
360
372
|
);
|
|
373
|
+
let keepAlive;
|
|
374
|
+
let shutdownStarted = false;
|
|
375
|
+
let shutdownExitCode = 0;
|
|
376
|
+
let shutdownPromise;
|
|
377
|
+
let shutdownTimer;
|
|
378
|
+
let activeRequests = 0;
|
|
379
|
+
const activeChildren = new Map();
|
|
380
|
+
|
|
381
|
+
const maybeFinalizeShutdown = () => {
|
|
382
|
+
if (!shutdownStarted || activeRequests > 0 || shutdownPromise) return;
|
|
383
|
+
|
|
384
|
+
shutdownPromise = Promise.resolve()
|
|
385
|
+
.then(async () => {
|
|
386
|
+
if (keepAlive) clearInterval(keepAlive);
|
|
387
|
+
await server.close();
|
|
388
|
+
})
|
|
389
|
+
.catch((err) => {
|
|
390
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
391
|
+
logErr(`[mcp-agents] shutdown close failed: ${msg}`);
|
|
392
|
+
})
|
|
393
|
+
.finally(() => {
|
|
394
|
+
if (shutdownTimer) clearTimeout(shutdownTimer);
|
|
395
|
+
process.exit(shutdownExitCode);
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const beginShutdown = (reason, exitCode = 0) => {
|
|
400
|
+
if (shutdownStarted) return;
|
|
401
|
+
|
|
402
|
+
shutdownStarted = true;
|
|
403
|
+
shutdownExitCode = exitCode;
|
|
404
|
+
logErr(
|
|
405
|
+
`[mcp-agents] shutting down (provider=${providerName}, reason=${reason})`,
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
shutdownTimer = setTimeout(() => {
|
|
409
|
+
process.exit(shutdownExitCode);
|
|
410
|
+
}, SHUTDOWN_TIMEOUT_MS);
|
|
411
|
+
shutdownTimer.unref();
|
|
412
|
+
|
|
413
|
+
for (const killGroup of activeChildren.values()) {
|
|
414
|
+
killGroup();
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
maybeFinalizeShutdown();
|
|
418
|
+
};
|
|
419
|
+
fatalShutdown = beginShutdown;
|
|
361
420
|
|
|
362
421
|
const effectiveTimeout = defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
363
422
|
|
|
@@ -404,6 +463,13 @@ async function main() {
|
|
|
404
463
|
return { content: [{ type: "text", text: "pong" }] };
|
|
405
464
|
}
|
|
406
465
|
|
|
466
|
+
if (shutdownStarted) {
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: "text", text: "Server is shutting down" }],
|
|
469
|
+
isError: true,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
407
473
|
if (params.name !== backend.toolName) {
|
|
408
474
|
return {
|
|
409
475
|
content: [
|
|
@@ -416,6 +482,7 @@ async function main() {
|
|
|
416
482
|
};
|
|
417
483
|
}
|
|
418
484
|
|
|
485
|
+
activeRequests += 1;
|
|
419
486
|
const rawArgs =
|
|
420
487
|
params.arguments && typeof params.arguments === "object"
|
|
421
488
|
? params.arguments
|
|
@@ -441,6 +508,8 @@ async function main() {
|
|
|
441
508
|
: effectiveTimeout;
|
|
442
509
|
|
|
443
510
|
if (!prompt.trim()) {
|
|
511
|
+
activeRequests -= 1;
|
|
512
|
+
maybeFinalizeShutdown();
|
|
444
513
|
return {
|
|
445
514
|
content: [
|
|
446
515
|
{
|
|
@@ -461,13 +530,30 @@ async function main() {
|
|
|
461
530
|
? backend.buildArgs(extraOpts)
|
|
462
531
|
: backend.buildArgs(prompt, extraOpts);
|
|
463
532
|
const buildCliOpts = (attemptTimeoutMs) => (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
533
|
+
{
|
|
534
|
+
timeoutMs: attemptTimeoutMs,
|
|
535
|
+
...(backend.stdinPrompt ? { stdinData: prompt } : {}),
|
|
536
|
+
onSpawn: ({ pid, killGroup }) => {
|
|
537
|
+
if (!pid) return;
|
|
538
|
+
activeChildren.set(pid, killGroup);
|
|
539
|
+
},
|
|
540
|
+
onSettled: (pid) => {
|
|
541
|
+
if (!pid) return;
|
|
542
|
+
activeChildren.delete(pid);
|
|
543
|
+
maybeFinalizeShutdown();
|
|
544
|
+
},
|
|
545
|
+
}
|
|
467
546
|
);
|
|
468
547
|
|
|
469
548
|
logErr(`[mcp-agents] tools/call: running ${backend.command} …`);
|
|
470
549
|
try {
|
|
550
|
+
if (shutdownStarted) {
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: "Server is shutting down" }],
|
|
553
|
+
isError: true,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
471
557
|
const startedAt = Date.now();
|
|
472
558
|
const maxAttempts = providerName === "claude"
|
|
473
559
|
? CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS
|
|
@@ -552,6 +638,9 @@ async function main() {
|
|
|
552
638
|
content: [{ type: "text", text: msg }],
|
|
553
639
|
isError: true,
|
|
554
640
|
};
|
|
641
|
+
} finally {
|
|
642
|
+
activeRequests -= 1;
|
|
643
|
+
maybeFinalizeShutdown();
|
|
555
644
|
}
|
|
556
645
|
});
|
|
557
646
|
|
|
@@ -562,13 +651,28 @@ async function main() {
|
|
|
562
651
|
// request handlers (tools/call -> execFile) register active handles.
|
|
563
652
|
// The SDK transport doesn't listen for stdin 'end', so the event
|
|
564
653
|
// loop loses its only handle when the pipe closes.
|
|
565
|
-
|
|
654
|
+
keepAlive = setInterval(() => {}, 60_000);
|
|
566
655
|
const origOnClose = transport.onclose;
|
|
567
656
|
transport.onclose = () => {
|
|
568
657
|
clearInterval(keepAlive);
|
|
569
658
|
origOnClose?.();
|
|
570
659
|
};
|
|
571
660
|
|
|
661
|
+
process.stdin.once("end", () => {
|
|
662
|
+
beginShutdown("stdin-end");
|
|
663
|
+
});
|
|
664
|
+
process.stdin.once("close", () => {
|
|
665
|
+
beginShutdown("stdin-close");
|
|
666
|
+
});
|
|
667
|
+
process.stdout.on("error", (err) => {
|
|
668
|
+
if (err?.code === "EPIPE") beginShutdown("stdout-epipe");
|
|
669
|
+
});
|
|
670
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
671
|
+
process.once(sig, () => {
|
|
672
|
+
beginShutdown(sig, 128 + SIGNAL_CODES[sig]);
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
572
676
|
logErr(`[mcp-agents] ready (provider: ${providerName})`);
|
|
573
677
|
}
|
|
574
678
|
|
|
@@ -576,15 +680,23 @@ process.on("unhandledRejection", (reason) => {
|
|
|
576
680
|
logErr(
|
|
577
681
|
`UnhandledRejection: ${reason instanceof Error ? reason.stack : reason}`,
|
|
578
682
|
);
|
|
579
|
-
|
|
683
|
+
if (fatalShutdown) {
|
|
684
|
+
fatalShutdown("unhandledRejection", 1);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
process.exit(1);
|
|
580
688
|
});
|
|
581
689
|
|
|
582
690
|
process.on("uncaughtException", (err) => {
|
|
583
691
|
logErr(`UncaughtException: ${err.stack || err.message}`);
|
|
584
|
-
|
|
692
|
+
if (fatalShutdown) {
|
|
693
|
+
fatalShutdown("uncaughtException", 1);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
process.exit(1);
|
|
585
697
|
});
|
|
586
698
|
|
|
587
699
|
main().catch((err) => {
|
|
588
700
|
logErr(err.stack || err.message);
|
|
589
|
-
process.
|
|
701
|
+
process.exit(1);
|
|
590
702
|
});
|