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.
- package/README.md +35 -6
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
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"
|
|
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"
|
|
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
|
|
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
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(
|
|
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
|
|
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);
|