mcp-agents 0.5.4 → 0.5.6

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 +35 -6
  2. package/package.json +1 -1
  3. package/server.js +117 -9
package/README.md CHANGED
@@ -37,7 +37,9 @@ mcp-agents
37
37
  # Specific provider
38
38
  mcp-agents --provider claude
39
39
  mcp-agents --provider gemini
40
- mcp-agents --provider gemini --sandbox false
40
+
41
+ # Optional: enable Gemini sandbox mode
42
+ mcp-agents --provider gemini --sandbox true
41
43
  ```
42
44
 
43
45
  The server speaks [JSON-RPC over stdio](https://modelcontextprotocol.io/docs/concepts/transports#stdio). It prints `[mcp-agents] ready (provider: <name>)` to stderr when it's listening.
@@ -48,7 +50,7 @@ Each `--provider` flag maps to a single exposed tool:
48
50
 
49
51
  | Provider | Tool name | CLI command |
50
52
  |----------|-----------|-------------|
51
- | `claude` | `claude_code` | `claude -p <prompt>` |
53
+ | `claude` | `claude_code` | `claude -p --output-format json` |
52
54
  | `gemini` | `gemini` | `gemini [-s] -p <prompt>` |
53
55
  | `codex` | *(pass-through)* | `codex mcp-server` |
54
56
 
@@ -61,6 +63,8 @@ Each `--provider` flag maps to a single exposed tool:
61
63
 
62
64
  Any additional `tools/call` arguments are ignored (for example `model` or `model_reasoning_effort`).
63
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
+
64
68
  ### `gemini` parameters
65
69
 
66
70
  | Parameter | Type | Required | Description |
@@ -96,7 +100,20 @@ Add entries to your project's `.mcp.json` (requires `npm i -g mcp-agents`):
96
100
  },
97
101
  "gemini": {
98
102
  "command": "mcp-agents",
99
- "args": ["--provider", "gemini", "--sandbox", "false"]
103
+ "args": ["--provider", "gemini"]
104
+ }
105
+ }
106
+ }
107
+ ```
108
+
109
+ Optional Gemini sandbox mode in `.mcp.json`:
110
+
111
+ ```json
112
+ {
113
+ "mcpServers": {
114
+ "gemini": {
115
+ "command": "mcp-agents",
116
+ "args": ["--provider", "gemini", "--sandbox", "true"]
100
117
  }
101
118
  }
102
119
  }
@@ -136,16 +153,28 @@ Override codex defaults at server startup (not via `tools/call` arguments):
136
153
 
137
154
  ## Integration with OpenAI Codex
138
155
 
139
- Add two entries to `~/.codex/config.toml` — one per provider you want available:
156
+ Add two entries to `~/.codex/config.toml` — one per provider you want available.
157
+ Set `tool_timeout_sec = 300` to avoid Codex MCP's default 60s per-tool timeout:
140
158
 
141
159
  ```toml
142
160
  [mcp_servers.claude-code]
143
161
  command = "mcp-agents"
144
162
  args = ["--provider", "claude"]
163
+ tool_timeout_sec = 300
145
164
 
146
165
  [mcp_servers.gemini]
147
166
  command = "mcp-agents"
148
- args = ["--provider", "gemini", "--sandbox", "false"]
167
+ args = ["--provider", "gemini"]
168
+ tool_timeout_sec = 300
169
+ ```
170
+
171
+ Optional Gemini sandbox mode in Codex config:
172
+
173
+ ```toml
174
+ [mcp_servers.gemini]
175
+ command = "mcp-agents"
176
+ args = ["--provider", "gemini", "--sandbox", "true"]
177
+ tool_timeout_sec = 300
149
178
  ```
150
179
 
151
180
  Then in a Codex session you can call the `claude_code` or `gemini` tools, which shell out to the respective CLIs.
@@ -165,7 +194,7 @@ After `npm link`, any edits to `server.js` take effect immediately — no reinst
165
194
  2. The server reads `--provider <name>` from its argv (defaults to `codex`)
166
195
  3. It registers a single tool matching that provider's CLI
167
196
  4. Client calls `tools/call` with the tool name and a `prompt`
168
- 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)
169
198
 
170
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.
171
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.5.4",
3
+ "version": "0.5.6",
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
  */
@@ -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) => {
@@ -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);