mcp-agents 0.5.5 → 0.5.7
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 +6 -4
- package/package.json +1 -1
- package/server.js +119 -11
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Each `--provider` flag maps to a single exposed tool:
|
|
|
50
50
|
|
|
51
51
|
| Provider | Tool name | CLI command |
|
|
52
52
|
|----------|-----------|-------------|
|
|
53
|
-
| `claude` | `claude_code` | `claude -p
|
|
53
|
+
| `claude` | `claude_code` | `claude -p --output-format json` |
|
|
54
54
|
| `gemini` | `gemini` | `gemini [-s] -p <prompt>` |
|
|
55
55
|
| `codex` | *(pass-through)* | `codex mcp-server` |
|
|
56
56
|
|
|
@@ -63,6 +63,8 @@ Each `--provider` flag maps to a single exposed tool:
|
|
|
63
63
|
|
|
64
64
|
Any additional `tools/call` arguments are ignored (for example `model` or `model_reasoning_effort`).
|
|
65
65
|
|
|
66
|
+
Claude calls run with `--output-format json`; the server parses the JSON payload and returns the assistant `result` text (or an MCP error if `is_error=true`).
|
|
67
|
+
|
|
66
68
|
### `gemini` parameters
|
|
67
69
|
|
|
68
70
|
| Parameter | Type | Required | Description |
|
|
@@ -80,7 +82,7 @@ using `-c key=value` config overrides:
|
|
|
80
82
|
|
|
81
83
|
| CLI Flag | Default | Codex config key |
|
|
82
84
|
|----------|---------|-----------------|
|
|
83
|
-
| `--model` | `gpt-5.
|
|
85
|
+
| `--model` | `gpt-5.4` | `model` |
|
|
84
86
|
| `--model_reasoning_effort` | `high` | `model_reasoning_effort` |
|
|
85
87
|
|
|
86
88
|
Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never` (safe for MCP server mode).
|
|
@@ -124,7 +126,7 @@ Override codex defaults at server startup (not via `tools/call` arguments):
|
|
|
124
126
|
"mcpServers": {
|
|
125
127
|
"codex": {
|
|
126
128
|
"command": "mcp-agents",
|
|
127
|
-
"args": ["--provider", "codex", "--model", "gpt-5.
|
|
129
|
+
"args": ["--provider", "codex", "--model", "gpt-5.4", "--model_reasoning_effort", "medium"]
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
}
|
|
@@ -192,7 +194,7 @@ After `npm link`, any edits to `server.js` take effect immediately — no reinst
|
|
|
192
194
|
2. The server reads `--provider <name>` from its argv (defaults to `codex`)
|
|
193
195
|
3. It registers a single tool matching that provider's CLI
|
|
194
196
|
4. Client calls `tools/call` with the tool name and a `prompt`
|
|
195
|
-
5. The server runs the CLI as a child process and returns
|
|
197
|
+
5. The server runs the CLI as a child process and returns tool text (Claude JSON `result`, or stdout/stderr for other providers)
|
|
196
198
|
|
|
197
199
|
The server includes a keepalive timer to prevent Node.js from exiting prematurely when stdin reaches EOF before the async subprocess registers an active handle.
|
|
198
200
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -20,6 +20,7 @@ const VERSION = JSON.parse(
|
|
|
20
20
|
|
|
21
21
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
22
22
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
23
|
+
const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
|
|
23
24
|
|
|
24
25
|
// ---------------------------------------------------------------------------
|
|
25
26
|
// CLI Backend Definitions
|
|
@@ -32,7 +33,7 @@ const CLI_BACKENDS = {
|
|
|
32
33
|
description:
|
|
33
34
|
"Run Claude Code CLI with a prompt (via stdin). Supports prompt + optional timeout_ms only; other arguments are ignored.",
|
|
34
35
|
stdinPrompt: true,
|
|
35
|
-
buildArgs: () => ["--no-session-persistence", "-p"],
|
|
36
|
+
buildArgs: () => ["--no-session-persistence", "-p", "--output-format", "json"],
|
|
36
37
|
extraProperties: {},
|
|
37
38
|
},
|
|
38
39
|
gemini: {
|
|
@@ -83,6 +84,33 @@ function toStringArg(value) {
|
|
|
83
84
|
return String(value);
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Normalize provider output and parse Claude's JSON print format when present.
|
|
89
|
+
* @param {string} provider
|
|
90
|
+
* @param {string} output
|
|
91
|
+
* @returns {{ text: string, isError: boolean }}
|
|
92
|
+
*/
|
|
93
|
+
function normalizeToolOutput(provider, output) {
|
|
94
|
+
if (provider !== "claude") return { text: output, isError: false };
|
|
95
|
+
|
|
96
|
+
const trimmed = output.trim();
|
|
97
|
+
if (!trimmed) return { text: "", isError: false };
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(trimmed);
|
|
101
|
+
if (parsed && typeof parsed === "object" && parsed.type === "result") {
|
|
102
|
+
return {
|
|
103
|
+
text: toStringArg(parsed.result),
|
|
104
|
+
isError: parsed.is_error === true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// Fall back to raw text if output shape changes or isn't JSON.
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { text: output, isError: false };
|
|
112
|
+
}
|
|
113
|
+
|
|
86
114
|
/**
|
|
87
115
|
* Print usage information to stdout.
|
|
88
116
|
*/
|
|
@@ -94,7 +122,7 @@ Usage: mcp-agents [options]
|
|
|
94
122
|
|
|
95
123
|
Options:
|
|
96
124
|
--provider <name> CLI backend to use (${providers}) [default: codex]
|
|
97
|
-
--model <model> Codex model [default: gpt-5.
|
|
125
|
+
--model <model> Codex model [default: gpt-5.4]
|
|
98
126
|
--model_reasoning_effort <e> Codex reasoning effort [default: high]
|
|
99
127
|
--sandbox <bool> Gemini sandbox mode (true/false) [default: false]
|
|
100
128
|
--timeout <seconds> Default timeout per call [default: 300]
|
|
@@ -186,11 +214,12 @@ function parseArgs() {
|
|
|
186
214
|
* @param {string} command
|
|
187
215
|
* @param {string[]} args
|
|
188
216
|
* @param {{ timeoutMs?: number, stdinData?: string }} [opts]
|
|
189
|
-
* @returns {Promise<string>}
|
|
217
|
+
* @returns {Promise<{ output: string, stdoutBytes: number, stderrBytes: number, durationMs: number }>}
|
|
190
218
|
*/
|
|
191
219
|
function runCli(command, args, opts = {}) {
|
|
192
220
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
193
221
|
const stdinData = opts.stdinData;
|
|
222
|
+
const startedAt = Date.now();
|
|
194
223
|
|
|
195
224
|
return new Promise((resolve, reject) => {
|
|
196
225
|
let stdout = "";
|
|
@@ -221,7 +250,12 @@ function runCli(command, args, opts = {}) {
|
|
|
221
250
|
clearTimeout(timer);
|
|
222
251
|
if (settled) return;
|
|
223
252
|
settled = true;
|
|
224
|
-
err ? reject(err) : resolve(
|
|
253
|
+
err ? reject(err) : resolve({
|
|
254
|
+
output: (stdout || stderr || "").trimEnd(),
|
|
255
|
+
stdoutBytes: stdoutLen,
|
|
256
|
+
stderrBytes: stderrLen,
|
|
257
|
+
durationMs: Date.now() - startedAt,
|
|
258
|
+
});
|
|
225
259
|
};
|
|
226
260
|
|
|
227
261
|
child.stdout.on("data", (chunk) => {
|
|
@@ -277,7 +311,7 @@ function runCli(command, args, opts = {}) {
|
|
|
277
311
|
function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
278
312
|
const args = [
|
|
279
313
|
"mcp-server",
|
|
280
|
-
"-c", `model=${model || "gpt-5.
|
|
314
|
+
"-c", `model=${model || "gpt-5.4"}`,
|
|
281
315
|
"-c", "sandbox_mode=read-only",
|
|
282
316
|
"-c", "approval_policy=never",
|
|
283
317
|
"-c", `model_reasoning_effort=${modelReasoningEffort || "high"}`,
|
|
@@ -450,16 +484,90 @@ async function main() {
|
|
|
450
484
|
const cliArgs = backend.stdinPrompt
|
|
451
485
|
? backend.buildArgs(extraOpts)
|
|
452
486
|
: backend.buildArgs(prompt, extraOpts);
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
487
|
+
const buildCliOpts = (attemptTimeoutMs) => (
|
|
488
|
+
backend.stdinPrompt
|
|
489
|
+
? { timeoutMs: attemptTimeoutMs, stdinData: prompt }
|
|
490
|
+
: { timeoutMs: attemptTimeoutMs }
|
|
491
|
+
);
|
|
456
492
|
|
|
457
493
|
logErr(`[mcp-agents] tools/call: running ${backend.command} …`);
|
|
458
494
|
try {
|
|
459
|
-
const
|
|
460
|
-
|
|
495
|
+
const startedAt = Date.now();
|
|
496
|
+
const maxAttempts = providerName === "claude"
|
|
497
|
+
? CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS
|
|
498
|
+
: 1;
|
|
499
|
+
let lastResult;
|
|
500
|
+
let lastNormalized = { text: "", isError: false };
|
|
501
|
+
|
|
502
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
503
|
+
const elapsedMs = Date.now() - startedAt;
|
|
504
|
+
const remainingMs = timeoutMs - elapsedMs;
|
|
505
|
+
|
|
506
|
+
if (remainingMs <= 0) break;
|
|
507
|
+
|
|
508
|
+
const result = await runCli(
|
|
509
|
+
backend.command,
|
|
510
|
+
cliArgs,
|
|
511
|
+
buildCliOpts(remainingMs),
|
|
512
|
+
);
|
|
513
|
+
lastResult = result;
|
|
514
|
+
const normalized = normalizeToolOutput(providerName, result.output);
|
|
515
|
+
lastNormalized = normalized;
|
|
516
|
+
|
|
517
|
+
if (normalized.isError) {
|
|
518
|
+
const msg = normalized.text.trim() || `${backend.command} returned is_error=true`;
|
|
519
|
+
logErr(
|
|
520
|
+
`[mcp-agents] tools/call: provider returned error payload (provider=${providerName})`,
|
|
521
|
+
);
|
|
522
|
+
return {
|
|
523
|
+
content: [{ type: "text", text: msg }],
|
|
524
|
+
isError: true,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (normalized.text.trim()) {
|
|
529
|
+
logErr("[mcp-agents] tools/call: done");
|
|
530
|
+
return {
|
|
531
|
+
content: [{ type: "text", text: normalized.text }],
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (attempt < maxAttempts) {
|
|
536
|
+
logErr(
|
|
537
|
+
"[mcp-agents] tools/call: empty output; retrying " +
|
|
538
|
+
`(provider=${providerName}, attempt=${attempt}/${maxAttempts}, ` +
|
|
539
|
+
`duration_ms=${result.durationMs}, timeout_ms=${timeoutMs}, ` +
|
|
540
|
+
`stdout_bytes=${result.stdoutBytes}, stderr_bytes=${result.stderrBytes})`,
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (lastResult && !lastNormalized.text.trim()) {
|
|
546
|
+
const elapsedMs = Date.now() - startedAt;
|
|
547
|
+
const emptyMsg = providerName === "claude"
|
|
548
|
+
? "claude returned empty output twice (exit 0); treated as failure"
|
|
549
|
+
: `${backend.command} returned empty output (exit 0); treated as failure`;
|
|
550
|
+
|
|
551
|
+
logErr(
|
|
552
|
+
"[mcp-agents] tools/call: empty output after retries " +
|
|
553
|
+
`(provider=${providerName}, attempts=${maxAttempts}, ` +
|
|
554
|
+
`elapsed_ms=${elapsedMs}, timeout_ms=${timeoutMs}, ` +
|
|
555
|
+
`stdout_bytes=${lastResult.stdoutBytes}, stderr_bytes=${lastResult.stderrBytes})`,
|
|
556
|
+
);
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: "text", text: emptyMsg }],
|
|
559
|
+
isError: true,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const timeoutMsg = `${backend.command} failed: timeout budget exhausted before retry`;
|
|
564
|
+
logErr(
|
|
565
|
+
"[mcp-agents] tools/call: timeout budget exhausted " +
|
|
566
|
+
`(provider=${providerName}, timeout_ms=${timeoutMs})`,
|
|
567
|
+
);
|
|
461
568
|
return {
|
|
462
|
-
content: [{ type: "text", text:
|
|
569
|
+
content: [{ type: "text", text: timeoutMsg }],
|
|
570
|
+
isError: true,
|
|
463
571
|
};
|
|
464
572
|
} catch (err) {
|
|
465
573
|
const msg = err instanceof Error ? err.message : String(err);
|