mcp-agents 0.6.6 → 0.8.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 +15 -38
- package/package.json +7 -2
- package/server.js +124 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# mcp-agents
|
|
2
2
|
|
|
3
|
-
MCP server that wraps AI CLI tools — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [
|
|
3
|
+
MCP server that wraps AI CLI tools — [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Antigravity CLI](https://antigravity.google/) (`agy`), and [Codex CLI](https://github.com/openai/codex) — so any MCP client can call them as tools.
|
|
4
4
|
|
|
5
5
|
## Prerequisites
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ MCP server that wraps AI CLI tools — [Claude Code](https://docs.anthropic.com/
|
|
|
10
10
|
| CLI | Install |
|
|
11
11
|
|-----|---------|
|
|
12
12
|
| `claude` | [Claude Code docs](https://docs.anthropic.com/en/docs/claude-code) |
|
|
13
|
-
| `
|
|
13
|
+
| `agy` | [Google Antigravity](https://antigravity.google/) |
|
|
14
14
|
| `codex` | `npm install -g @openai/codex` |
|
|
15
15
|
|
|
16
16
|
Only the CLI you select with `--provider` needs to be present.
|
|
@@ -37,9 +37,6 @@ mcp-agents
|
|
|
37
37
|
# Specific provider
|
|
38
38
|
mcp-agents --provider claude
|
|
39
39
|
mcp-agents --provider gemini
|
|
40
|
-
|
|
41
|
-
# Optional: enable Gemini sandbox mode
|
|
42
|
-
mcp-agents --provider gemini --sandbox true
|
|
43
40
|
```
|
|
44
41
|
|
|
45
42
|
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.
|
|
@@ -50,8 +47,8 @@ Each `--provider` flag maps to a single exposed tool:
|
|
|
50
47
|
|
|
51
48
|
| Provider | Tool name | CLI command |
|
|
52
49
|
|----------|-----------|-------------|
|
|
53
|
-
| `claude` | `claude_code` | `claude -p --output-format json` |
|
|
54
|
-
| `gemini` | `gemini` | `
|
|
50
|
+
| `claude` | `claude_code` | `claude --model claude-opus-4-8 --effort xhigh -p --output-format json` |
|
|
51
|
+
| `gemini` | `gemini` | `agy --sandbox -p <prompt>` |
|
|
55
52
|
| `codex` | *(pass-through)* | `codex mcp-server` |
|
|
56
53
|
|
|
57
54
|
### `claude_code` parameters
|
|
@@ -61,20 +58,21 @@ Each `--provider` flag maps to a single exposed tool:
|
|
|
61
58
|
| `prompt` | `string` | yes | The prompt to send to Claude Code |
|
|
62
59
|
| `timeout_ms` | `integer` | no | Timeout in ms (default: 300 000 / 5 minutes) |
|
|
63
60
|
|
|
64
|
-
Any additional `tools/call` arguments are ignored (for example `model` or `
|
|
61
|
+
Any additional `tools/call` arguments are ignored (for example `model`, `effort`, or `config`).
|
|
65
62
|
|
|
66
|
-
Claude
|
|
63
|
+
Claude is pinned to `claude-opus-4-8` at effort `xhigh`; callers cannot change the model or effort per call. 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
65
|
### `gemini` parameters
|
|
69
66
|
|
|
70
67
|
| Parameter | Type | Required | Description |
|
|
71
68
|
|-----------|------|----------|-------------|
|
|
72
|
-
| `prompt` | `string` | yes | The prompt to send to
|
|
73
|
-
| `sandbox` | `boolean` | no | Run in sandbox mode (`-s` flag, default: false) |
|
|
69
|
+
| `prompt` | `string` | yes | The prompt to send to the Antigravity CLI (`agy`) |
|
|
74
70
|
| `timeout_ms` | `integer` | no | Timeout in ms (default: 300 000 / 5 minutes) |
|
|
75
71
|
|
|
76
72
|
Any additional `tools/call` arguments are ignored (for example `model` or `model_reasoning_effort`).
|
|
77
73
|
|
|
74
|
+
`agy` always runs with `--sandbox` (terminal restrictions enabled); there is no per-call sandbox toggle.
|
|
75
|
+
|
|
78
76
|
### `codex` (pass-through)
|
|
79
77
|
|
|
80
78
|
The codex provider passes through to Codex's native MCP server (`codex mcp-server`)
|
|
@@ -91,17 +89,18 @@ or Gemini during bridge calls.
|
|
|
91
89
|
Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never`,
|
|
92
90
|
`features.multi_agent=false`.
|
|
93
91
|
|
|
94
|
-
Startup flags set
|
|
92
|
+
Startup flags (`--model`, `--model_reasoning_effort`) set the model and effort for the native Codex MCP server. Per-call `model` and `config` arguments are stripped from `tools/call` before they reach Codex, so a client cannot override the pinned model/effort (or the read-only/never sandbox config) for a single call. For example, this request:
|
|
95
93
|
|
|
96
94
|
```json
|
|
97
95
|
{
|
|
98
96
|
"prompt": "Review this diff",
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
}
|
|
97
|
+
"model": "gpt-5.5-codex",
|
|
98
|
+
"config": { "model_reasoning_effort": "medium" }
|
|
102
99
|
}
|
|
103
100
|
```
|
|
104
101
|
|
|
102
|
+
is forwarded to Codex as `{ "prompt": "Review this diff" }`. Change the model or effort at server startup instead.
|
|
103
|
+
|
|
105
104
|
## Integration with Claude Code
|
|
106
105
|
|
|
107
106
|
Add entries to your project's `.mcp.json` (requires `npm i -g mcp-agents`):
|
|
@@ -121,19 +120,6 @@ Add entries to your project's `.mcp.json` (requires `npm i -g mcp-agents`):
|
|
|
121
120
|
}
|
|
122
121
|
```
|
|
123
122
|
|
|
124
|
-
Optional Gemini sandbox mode in `.mcp.json`:
|
|
125
|
-
|
|
126
|
-
```json
|
|
127
|
-
{
|
|
128
|
-
"mcpServers": {
|
|
129
|
-
"gemini": {
|
|
130
|
-
"command": "mcp-agents",
|
|
131
|
-
"args": ["--provider", "gemini", "--sandbox", "true"]
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
```
|
|
136
|
-
|
|
137
123
|
Override codex defaults at server startup:
|
|
138
124
|
|
|
139
125
|
```json
|
|
@@ -147,7 +133,7 @@ Override codex defaults at server startup:
|
|
|
147
133
|
}
|
|
148
134
|
```
|
|
149
135
|
|
|
150
|
-
The
|
|
136
|
+
The model and effort are fixed at server startup. Per-call `model` and `config` arguments sent to the native `codex` tool are stripped before reaching Codex, so they cannot override the startup defaults.
|
|
151
137
|
|
|
152
138
|
Because the bridge runs in an isolated Codex home, inherited MCP servers from your normal
|
|
153
139
|
`~/.codex/config.toml` are intentionally unavailable inside bridged Codex sessions.
|
|
@@ -188,15 +174,6 @@ args = ["--provider", "gemini"]
|
|
|
188
174
|
tool_timeout_sec = 300
|
|
189
175
|
```
|
|
190
176
|
|
|
191
|
-
Optional Gemini sandbox mode in Codex config:
|
|
192
|
-
|
|
193
|
-
```toml
|
|
194
|
-
[mcp_servers.gemini]
|
|
195
|
-
command = "mcp-agents"
|
|
196
|
-
args = ["--provider", "gemini", "--sandbox", "true"]
|
|
197
|
-
tool_timeout_sec = 300
|
|
198
|
-
```
|
|
199
|
-
|
|
200
177
|
Then in a Codex session you can call the `claude_code` or `gemini` tools, which shell out to the respective CLIs.
|
|
201
178
|
|
|
202
179
|
## Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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": {
|
|
@@ -27,7 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"author": "Thomas Witt",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
31
|
+
},
|
|
32
|
+
"overrides": {
|
|
33
|
+
"fast-uri": "^3.1.2",
|
|
34
|
+
"hono": "^4.12.22",
|
|
35
|
+
"ip-address": "^10.2.0"
|
|
31
36
|
},
|
|
32
37
|
"engines": {
|
|
33
38
|
"node": ">=18"
|
package/server.js
CHANGED
|
@@ -29,6 +29,11 @@ const VERSION = JSON.parse(
|
|
|
29
29
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
30
30
|
const DEFAULT_CODEX_MODEL = "gpt-5.5";
|
|
31
31
|
const DEFAULT_CODEX_MODEL_REASONING_EFFORT = "xhigh";
|
|
32
|
+
const DEFAULT_CLAUDE_MODEL = "claude-opus-4-8";
|
|
33
|
+
const DEFAULT_CLAUDE_EFFORT = "xhigh";
|
|
34
|
+
// tools/call argument keys stripped from the codex pass-through so callers
|
|
35
|
+
// cannot override the pinned model/effort (or the read-only/never config).
|
|
36
|
+
const CODEX_STRIPPED_TOOL_ARGS = ["model", "config"];
|
|
32
37
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
33
38
|
const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
|
|
34
39
|
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
@@ -44,18 +49,28 @@ const CLI_BACKENDS = {
|
|
|
44
49
|
command: "claude",
|
|
45
50
|
toolName: "claude_code",
|
|
46
51
|
description:
|
|
47
|
-
|
|
52
|
+
`Run Claude Code CLI with a prompt (via stdin), pinned to ${DEFAULT_CLAUDE_MODEL} at effort ${DEFAULT_CLAUDE_EFFORT}. Supports prompt + optional timeout_ms only; other arguments (model/effort/config) are ignored.`,
|
|
48
53
|
stdinPrompt: true,
|
|
49
|
-
buildArgs: () => [
|
|
54
|
+
buildArgs: () => [
|
|
55
|
+
"--model",
|
|
56
|
+
DEFAULT_CLAUDE_MODEL,
|
|
57
|
+
"--effort",
|
|
58
|
+
DEFAULT_CLAUDE_EFFORT,
|
|
59
|
+
"--no-session-persistence",
|
|
60
|
+
"-p",
|
|
61
|
+
"--output-format",
|
|
62
|
+
"json",
|
|
63
|
+
],
|
|
50
64
|
extraProperties: {},
|
|
51
65
|
},
|
|
52
66
|
gemini: {
|
|
53
|
-
command: "
|
|
67
|
+
command: "agy",
|
|
54
68
|
toolName: "gemini",
|
|
55
69
|
description:
|
|
56
|
-
"Run
|
|
70
|
+
"Run the Antigravity CLI (`agy`, Google's Gemini-backed agent) with a prompt. Always runs in --sandbox mode (terminal restrictions enabled).",
|
|
57
71
|
stdinPrompt: false,
|
|
58
|
-
|
|
72
|
+
isolateCwd: true,
|
|
73
|
+
buildArgs: (prompt) => ["--sandbox", "-p", prompt],
|
|
59
74
|
extraProperties: {},
|
|
60
75
|
},
|
|
61
76
|
codex: {
|
|
@@ -209,6 +224,7 @@ function parseArgs() {
|
|
|
209
224
|
* @param {{
|
|
210
225
|
* timeoutMs?: number,
|
|
211
226
|
* stdinData?: string,
|
|
227
|
+
* cwd?: string,
|
|
212
228
|
* onSpawn?: (childInfo: { pid?: number, killGroup: () => void }) => void,
|
|
213
229
|
* onSettled?: (pid?: number) => void,
|
|
214
230
|
* }} [opts]
|
|
@@ -217,6 +233,7 @@ function parseArgs() {
|
|
|
217
233
|
function runCli(command, args, opts = {}) {
|
|
218
234
|
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
219
235
|
const stdinData = opts.stdinData;
|
|
236
|
+
const cwd = opts.cwd;
|
|
220
237
|
const onSpawn = opts.onSpawn;
|
|
221
238
|
const onSettled = opts.onSettled;
|
|
222
239
|
const startedAt = Date.now();
|
|
@@ -229,6 +246,7 @@ function runCli(command, args, opts = {}) {
|
|
|
229
246
|
let settled = false;
|
|
230
247
|
|
|
231
248
|
const child = spawn(command, args, {
|
|
249
|
+
cwd,
|
|
232
250
|
detached: true,
|
|
233
251
|
stdio: ["pipe", "pipe", "pipe"],
|
|
234
252
|
env: { ...process.env, NO_COLOR: "1" },
|
|
@@ -307,6 +325,18 @@ function runCli(command, args, opts = {}) {
|
|
|
307
325
|
});
|
|
308
326
|
}
|
|
309
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Create a fresh, empty working directory under the OS temp dir for an
|
|
330
|
+
* agentic CLI. Agentic CLIs (e.g. agy/Antigravity) treat their cwd as a
|
|
331
|
+
* workspace and write project files into it; running them here keeps them
|
|
332
|
+
* from mutating whatever directory the MCP server was started in.
|
|
333
|
+
* @param {string} provider
|
|
334
|
+
* @returns {string}
|
|
335
|
+
*/
|
|
336
|
+
function createIsolatedWorkdir(provider) {
|
|
337
|
+
return mkdtempSync(join(tmpdir(), `mcp-agents-${provider}-`));
|
|
338
|
+
}
|
|
339
|
+
|
|
310
340
|
/**
|
|
311
341
|
* Resolve the source Codex home used by the parent process.
|
|
312
342
|
* @returns {string}
|
|
@@ -367,7 +397,45 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
|
|
|
367
397
|
}
|
|
368
398
|
|
|
369
399
|
/**
|
|
370
|
-
*
|
|
400
|
+
* Filter a single newline-delimited JSON-RPC message on its way to the codex
|
|
401
|
+
* pass-through. Strips per-call model/config overrides from `tools/call` so the
|
|
402
|
+
* client cannot escape the pinned model/effort (or the read-only/never config).
|
|
403
|
+
* Non-`tools/call` and unparseable lines are returned byte-for-byte unchanged so
|
|
404
|
+
* the MCP framing is preserved.
|
|
405
|
+
* @param {string} line
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
function filterCodexToolCall(line) {
|
|
409
|
+
const trimmed = line.trim();
|
|
410
|
+
if (!trimmed) return line;
|
|
411
|
+
|
|
412
|
+
let msg;
|
|
413
|
+
try {
|
|
414
|
+
msg = JSON.parse(trimmed);
|
|
415
|
+
} catch {
|
|
416
|
+
return line; // not JSON (e.g. partial/keepalive) — pass through untouched
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const args =
|
|
420
|
+
msg && typeof msg === "object" && msg.method === "tools/call"
|
|
421
|
+
? msg.params?.arguments
|
|
422
|
+
: null;
|
|
423
|
+
if (!args || typeof args !== "object") return line;
|
|
424
|
+
|
|
425
|
+
const stripped = CODEX_STRIPPED_TOOL_ARGS.filter((key) => key in args);
|
|
426
|
+
if (stripped.length === 0) return line;
|
|
427
|
+
|
|
428
|
+
for (const key of stripped) delete args[key];
|
|
429
|
+
logErr(
|
|
430
|
+
`[mcp-agents] codex passthrough: ignoring per-call overrides: ${stripped.join(", ")}`,
|
|
431
|
+
);
|
|
432
|
+
return JSON.stringify(msg);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Spawn codex mcp-server as a pass-through. stdout/stderr flow straight back to
|
|
437
|
+
* the client, but the client's stdin is intercepted line-by-line so per-call
|
|
438
|
+
* model/config overrides are stripped before reaching codex.
|
|
371
439
|
* @param {{ model?: string, modelReasoningEffort?: string }} opts
|
|
372
440
|
*/
|
|
373
441
|
function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
@@ -409,13 +477,34 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
409
477
|
|
|
410
478
|
const child = spawn("codex", args, {
|
|
411
479
|
env: { ...process.env, CODEX_HOME: isolatedCodexHome },
|
|
412
|
-
|
|
480
|
+
// stdin is piped (not inherited) so we can strip per-call overrides;
|
|
481
|
+
// stdout stays inherited so codex responses reach the client untouched.
|
|
482
|
+
stdio: ["pipe", "inherit", "pipe"],
|
|
413
483
|
});
|
|
414
484
|
|
|
415
485
|
child.stderr.on("data", (chunk) => {
|
|
416
486
|
logErr(`[codex] ${chunk.toString().trimEnd()}`);
|
|
417
487
|
});
|
|
418
488
|
|
|
489
|
+
// Pump client stdin -> codex stdin, splitting on newlines (MCP stdio framing)
|
|
490
|
+
// so each JSON-RPC message can be filtered before forwarding.
|
|
491
|
+
child.stdin.on("error", () => {}); // ignore EPIPE if codex exits early
|
|
492
|
+
let stdinBuf = "";
|
|
493
|
+
process.stdin.on("data", (chunk) => {
|
|
494
|
+
stdinBuf += chunk.toString("utf8");
|
|
495
|
+
let nl;
|
|
496
|
+
while ((nl = stdinBuf.indexOf("\n")) !== -1) {
|
|
497
|
+
const line = stdinBuf.slice(0, nl);
|
|
498
|
+
stdinBuf = stdinBuf.slice(nl + 1);
|
|
499
|
+
child.stdin.write(`${filterCodexToolCall(line)}\n`);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
process.stdin.on("error", () => {});
|
|
503
|
+
process.stdin.on("end", () => {
|
|
504
|
+
if (stdinBuf.length > 0) child.stdin.write(filterCodexToolCall(stdinBuf));
|
|
505
|
+
child.stdin.end();
|
|
506
|
+
});
|
|
507
|
+
|
|
419
508
|
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
|
|
420
509
|
process.once(sig, () => {
|
|
421
510
|
child.kill(sig);
|
|
@@ -628,10 +717,12 @@ async function main() {
|
|
|
628
717
|
const cliArgs = backend.stdinPrompt
|
|
629
718
|
? backend.buildArgs(extraOpts)
|
|
630
719
|
: backend.buildArgs(prompt, extraOpts);
|
|
720
|
+
let isolatedWorkdir;
|
|
631
721
|
const buildCliOpts = (attemptTimeoutMs) => (
|
|
632
722
|
{
|
|
633
723
|
timeoutMs: attemptTimeoutMs,
|
|
634
724
|
...(backend.stdinPrompt ? { stdinData: prompt } : {}),
|
|
725
|
+
...(isolatedWorkdir ? { cwd: isolatedWorkdir } : {}),
|
|
635
726
|
onSpawn: ({ pid, killGroup }) => {
|
|
636
727
|
if (!pid) return;
|
|
637
728
|
activeChildren.set(pid, killGroup);
|
|
@@ -653,6 +744,24 @@ async function main() {
|
|
|
653
744
|
};
|
|
654
745
|
}
|
|
655
746
|
|
|
747
|
+
if (backend.isolateCwd) {
|
|
748
|
+
try {
|
|
749
|
+
isolatedWorkdir = createIsolatedWorkdir(providerName);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
752
|
+
logErr(`[mcp-agents] failed to create isolated workdir: ${msg}`);
|
|
753
|
+
return {
|
|
754
|
+
content: [
|
|
755
|
+
{
|
|
756
|
+
type: "text",
|
|
757
|
+
text: `Failed to prepare isolated working directory: ${msg}`,
|
|
758
|
+
},
|
|
759
|
+
],
|
|
760
|
+
isError: true,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
656
765
|
const startedAt = Date.now();
|
|
657
766
|
const maxAttempts = providerName === "claude"
|
|
658
767
|
? CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS
|
|
@@ -738,6 +847,14 @@ async function main() {
|
|
|
738
847
|
isError: true,
|
|
739
848
|
};
|
|
740
849
|
} finally {
|
|
850
|
+
if (isolatedWorkdir) {
|
|
851
|
+
try {
|
|
852
|
+
rmSync(isolatedWorkdir, { recursive: true, force: true });
|
|
853
|
+
} catch (err) {
|
|
854
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
855
|
+
logErr(`[mcp-agents] failed to clean isolated workdir: ${msg}`);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
741
858
|
activeRequests -= 1;
|
|
742
859
|
maybeFinalizeShutdown();
|
|
743
860
|
}
|