mcp-agents 0.5.7 → 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 +128 -40
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
|
|
@@ -40,21 +43,10 @@ const CLI_BACKENDS = {
|
|
|
40
43
|
command: "gemini",
|
|
41
44
|
toolName: "gemini",
|
|
42
45
|
description:
|
|
43
|
-
"Run Gemini CLI
|
|
46
|
+
"Run Gemini CLI with a prompt. Always runs in sandbox mode with --approval-mode=plan.",
|
|
44
47
|
stdinPrompt: false,
|
|
45
|
-
buildArgs: (prompt
|
|
46
|
-
|
|
47
|
-
if (opts.sandbox === true) args.push("-s");
|
|
48
|
-
args.push("-p", prompt);
|
|
49
|
-
return args;
|
|
50
|
-
},
|
|
51
|
-
extraProperties: {
|
|
52
|
-
sandbox: {
|
|
53
|
-
type: "boolean",
|
|
54
|
-
default: false,
|
|
55
|
-
description: "Run in sandbox mode (-s flag). Defaults to false.",
|
|
56
|
-
},
|
|
57
|
-
},
|
|
48
|
+
buildArgs: (prompt) => ["-s", "--approval-mode=plan", "-p", prompt],
|
|
49
|
+
extraProperties: {},
|
|
58
50
|
},
|
|
59
51
|
codex: {
|
|
60
52
|
passthrough: true,
|
|
@@ -124,7 +116,6 @@ Options:
|
|
|
124
116
|
--provider <name> CLI backend to use (${providers}) [default: codex]
|
|
125
117
|
--model <model> Codex model [default: gpt-5.4]
|
|
126
118
|
--model_reasoning_effort <e> Codex reasoning effort [default: high]
|
|
127
|
-
--sandbox <bool> Gemini sandbox mode (true/false) [default: false]
|
|
128
119
|
--timeout <seconds> Default timeout per call [default: 300]
|
|
129
120
|
--help, -h Show this help message
|
|
130
121
|
--version, -v Show version number`);
|
|
@@ -132,15 +123,14 @@ Options:
|
|
|
132
123
|
|
|
133
124
|
/**
|
|
134
125
|
* Parse CLI flags from process.argv.
|
|
135
|
-
* Handles --help, --version, --provider, --model, --model_reasoning_effort,
|
|
136
|
-
* @returns {{ provider: string, model?: string, modelReasoningEffort?: string,
|
|
126
|
+
* Handles --help, --version, --provider, --model, --model_reasoning_effort, and unknown flags.
|
|
127
|
+
* @returns {{ provider: string, model?: string, modelReasoningEffort?: string, defaultTimeoutMs?: number }}
|
|
137
128
|
*/
|
|
138
129
|
function parseArgs() {
|
|
139
130
|
const args = process.argv.slice(2);
|
|
140
131
|
let provider = "codex";
|
|
141
132
|
let model;
|
|
142
133
|
let modelReasoningEffort;
|
|
143
|
-
let sandbox = false;
|
|
144
134
|
let defaultTimeoutMs;
|
|
145
135
|
|
|
146
136
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -178,13 +168,6 @@ function parseArgs() {
|
|
|
178
168
|
}
|
|
179
169
|
modelReasoningEffort = args[++i];
|
|
180
170
|
break;
|
|
181
|
-
case "--sandbox":
|
|
182
|
-
if (i + 1 >= args.length) {
|
|
183
|
-
process.stderr.write("error: --sandbox requires a value\n");
|
|
184
|
-
process.exit(1);
|
|
185
|
-
}
|
|
186
|
-
sandbox = args[++i] === "true";
|
|
187
|
-
break;
|
|
188
171
|
case "--timeout": {
|
|
189
172
|
if (i + 1 >= args.length) {
|
|
190
173
|
process.stderr.write("error: --timeout requires a value\n");
|
|
@@ -204,7 +187,7 @@ function parseArgs() {
|
|
|
204
187
|
}
|
|
205
188
|
}
|
|
206
189
|
|
|
207
|
-
return { provider, model, modelReasoningEffort,
|
|
190
|
+
return { provider, model, modelReasoningEffort, defaultTimeoutMs };
|
|
208
191
|
}
|
|
209
192
|
|
|
210
193
|
/**
|
|
@@ -213,12 +196,19 @@ function parseArgs() {
|
|
|
213
196
|
* on timeout — prevents orphan child processes.
|
|
214
197
|
* @param {string} command
|
|
215
198
|
* @param {string[]} args
|
|
216
|
-
* @param {{
|
|
199
|
+
* @param {{
|
|
200
|
+
* timeoutMs?: number,
|
|
201
|
+
* stdinData?: string,
|
|
202
|
+
* onSpawn?: (childInfo: { pid?: number, killGroup: () => void }) => void,
|
|
203
|
+
* onSettled?: (pid?: number) => void,
|
|
204
|
+
* }} [opts]
|
|
217
205
|
* @returns {Promise<{ output: string, stdoutBytes: number, stderrBytes: number, durationMs: number }>}
|
|
218
206
|
*/
|
|
219
207
|
function runCli(command, args, opts = {}) {
|
|
220
208
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
221
209
|
const stdinData = opts.stdinData;
|
|
210
|
+
const onSpawn = opts.onSpawn;
|
|
211
|
+
const onSettled = opts.onSettled;
|
|
222
212
|
const startedAt = Date.now();
|
|
223
213
|
|
|
224
214
|
return new Promise((resolve, reject) => {
|
|
@@ -245,11 +235,13 @@ function runCli(command, args, opts = {}) {
|
|
|
245
235
|
const killGroup = () => {
|
|
246
236
|
try { process.kill(-child.pid, "SIGKILL"); } catch {}
|
|
247
237
|
};
|
|
238
|
+
onSpawn?.({ pid: child.pid, killGroup });
|
|
248
239
|
|
|
249
240
|
const done = (err) => {
|
|
250
241
|
clearTimeout(timer);
|
|
251
242
|
if (settled) return;
|
|
252
243
|
settled = true;
|
|
244
|
+
onSettled?.(child.pid);
|
|
253
245
|
err ? reject(err) : resolve({
|
|
254
246
|
output: (stdout || stderr || "").trimEnd(),
|
|
255
247
|
stdoutBytes: stdoutLen,
|
|
@@ -282,6 +274,7 @@ function runCli(command, args, opts = {}) {
|
|
|
282
274
|
const timer = setTimeout(() => {
|
|
283
275
|
killGroup();
|
|
284
276
|
}, timeoutMs);
|
|
277
|
+
timer.unref();
|
|
285
278
|
|
|
286
279
|
child.on("error", (err) => {
|
|
287
280
|
done(new Error(`Failed to start ${command}: ${err.message}`));
|
|
@@ -327,7 +320,6 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
327
320
|
logErr(`[codex] ${chunk.toString().trimEnd()}`);
|
|
328
321
|
});
|
|
329
322
|
|
|
330
|
-
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
331
323
|
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
332
324
|
process.once(sig, () => {
|
|
333
325
|
child.kill(sig);
|
|
@@ -359,7 +351,7 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
359
351
|
// ---------------------------------------------------------------------------
|
|
360
352
|
|
|
361
353
|
async function main() {
|
|
362
|
-
const { provider: providerName, model, modelReasoningEffort,
|
|
354
|
+
const { provider: providerName, model, modelReasoningEffort, defaultTimeoutMs } = parseArgs();
|
|
363
355
|
const backend = CLI_BACKENDS[providerName];
|
|
364
356
|
|
|
365
357
|
if (!backend) {
|
|
@@ -374,14 +366,57 @@ async function main() {
|
|
|
374
366
|
return;
|
|
375
367
|
}
|
|
376
368
|
|
|
377
|
-
if (backend.extraProperties.sandbox) {
|
|
378
|
-
backend.extraProperties.sandbox.default = sandbox;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
369
|
const server = new Server(
|
|
382
370
|
{ name: "mcp-agents", version: VERSION },
|
|
383
371
|
{ capabilities: { tools: {} } },
|
|
384
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;
|
|
385
420
|
|
|
386
421
|
const effectiveTimeout = defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
387
422
|
|
|
@@ -428,6 +463,13 @@ async function main() {
|
|
|
428
463
|
return { content: [{ type: "text", text: "pong" }] };
|
|
429
464
|
}
|
|
430
465
|
|
|
466
|
+
if (shutdownStarted) {
|
|
467
|
+
return {
|
|
468
|
+
content: [{ type: "text", text: "Server is shutting down" }],
|
|
469
|
+
isError: true,
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
431
473
|
if (params.name !== backend.toolName) {
|
|
432
474
|
return {
|
|
433
475
|
content: [
|
|
@@ -440,6 +482,7 @@ async function main() {
|
|
|
440
482
|
};
|
|
441
483
|
}
|
|
442
484
|
|
|
485
|
+
activeRequests += 1;
|
|
443
486
|
const rawArgs =
|
|
444
487
|
params.arguments && typeof params.arguments === "object"
|
|
445
488
|
? params.arguments
|
|
@@ -465,6 +508,8 @@ async function main() {
|
|
|
465
508
|
: effectiveTimeout;
|
|
466
509
|
|
|
467
510
|
if (!prompt.trim()) {
|
|
511
|
+
activeRequests -= 1;
|
|
512
|
+
maybeFinalizeShutdown();
|
|
468
513
|
return {
|
|
469
514
|
content: [
|
|
470
515
|
{
|
|
@@ -485,13 +530,30 @@ async function main() {
|
|
|
485
530
|
? backend.buildArgs(extraOpts)
|
|
486
531
|
: backend.buildArgs(prompt, extraOpts);
|
|
487
532
|
const buildCliOpts = (attemptTimeoutMs) => (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
546
|
);
|
|
492
547
|
|
|
493
548
|
logErr(`[mcp-agents] tools/call: running ${backend.command} …`);
|
|
494
549
|
try {
|
|
550
|
+
if (shutdownStarted) {
|
|
551
|
+
return {
|
|
552
|
+
content: [{ type: "text", text: "Server is shutting down" }],
|
|
553
|
+
isError: true,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
495
557
|
const startedAt = Date.now();
|
|
496
558
|
const maxAttempts = providerName === "claude"
|
|
497
559
|
? CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS
|
|
@@ -576,6 +638,9 @@ async function main() {
|
|
|
576
638
|
content: [{ type: "text", text: msg }],
|
|
577
639
|
isError: true,
|
|
578
640
|
};
|
|
641
|
+
} finally {
|
|
642
|
+
activeRequests -= 1;
|
|
643
|
+
maybeFinalizeShutdown();
|
|
579
644
|
}
|
|
580
645
|
});
|
|
581
646
|
|
|
@@ -586,13 +651,28 @@ async function main() {
|
|
|
586
651
|
// request handlers (tools/call -> execFile) register active handles.
|
|
587
652
|
// The SDK transport doesn't listen for stdin 'end', so the event
|
|
588
653
|
// loop loses its only handle when the pipe closes.
|
|
589
|
-
|
|
654
|
+
keepAlive = setInterval(() => {}, 60_000);
|
|
590
655
|
const origOnClose = transport.onclose;
|
|
591
656
|
transport.onclose = () => {
|
|
592
657
|
clearInterval(keepAlive);
|
|
593
658
|
origOnClose?.();
|
|
594
659
|
};
|
|
595
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
|
+
|
|
596
676
|
logErr(`[mcp-agents] ready (provider: ${providerName})`);
|
|
597
677
|
}
|
|
598
678
|
|
|
@@ -600,15 +680,23 @@ process.on("unhandledRejection", (reason) => {
|
|
|
600
680
|
logErr(
|
|
601
681
|
`UnhandledRejection: ${reason instanceof Error ? reason.stack : reason}`,
|
|
602
682
|
);
|
|
603
|
-
|
|
683
|
+
if (fatalShutdown) {
|
|
684
|
+
fatalShutdown("unhandledRejection", 1);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
process.exit(1);
|
|
604
688
|
});
|
|
605
689
|
|
|
606
690
|
process.on("uncaughtException", (err) => {
|
|
607
691
|
logErr(`UncaughtException: ${err.stack || err.message}`);
|
|
608
|
-
|
|
692
|
+
if (fatalShutdown) {
|
|
693
|
+
fatalShutdown("uncaughtException", 1);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
process.exit(1);
|
|
609
697
|
});
|
|
610
698
|
|
|
611
699
|
main().catch((err) => {
|
|
612
700
|
logErr(err.stack || err.message);
|
|
613
|
-
process.
|
|
701
|
+
process.exit(1);
|
|
614
702
|
});
|