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.
Files changed (3) hide show
  1. package/README.md +6 -4
  2. package/package.json +1 -1
  3. 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 <prompt>` |
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.3-codex` | `model` |
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.3-codex", "--model_reasoning_effort", "medium"]
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 stdout (or stderr) as the tool result
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "MCP server that wraps AI CLI tools (Claude Code, Gemini CLI, Codex CLI) for use by any MCP client",
5
5
  "type": "module",
6
6
  "bin": {
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.3-codex]
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((stdout || stderr || "").trimEnd());
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.3-codex"}`,
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 cliOpts = backend.stdinPrompt
454
- ? { timeoutMs, stdinData: prompt }
455
- : { timeoutMs };
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 output = await runCli(backend.command, cliArgs, cliOpts);
460
- logErr("[mcp-agents] tools/call: done");
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: output || "" }],
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);