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.
Files changed (3) hide show
  1. package/README.md +15 -38
  2. package/package.json +7 -2
  3. 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), [Gemini CLI](https://github.com/google-gemini/gemini-cli), and [Codex CLI](https://github.com/openai/codex) — so any MCP client can call them as tools.
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
- | `gemini` | `npm install -g @anthropic-ai/gemini-cli` |
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` | `gemini [-s] -p <prompt>` |
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 `model_reasoning_effort`).
61
+ Any additional `tools/call` arguments are ignored (for example `model`, `effort`, or `config`).
65
62
 
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`).
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 Gemini CLI |
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 server-wide defaults for the native Codex MCP server. Per-call overrides still work through the native `codex` tool schema, for example:
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
- "config": {
100
- "model_reasoning_effort": "medium"
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 startup default can still be overridden for a single Codex tool call by passing `config.model_reasoning_effort` to the native `codex` tool.
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.6.6",
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.0.0"
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
- "Run Claude Code CLI with a prompt (via stdin). Supports prompt + optional timeout_ms only; other arguments are ignored.",
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: () => ["--no-session-persistence", "-p", "--output-format", "json"],
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: "gemini",
67
+ command: "agy",
54
68
  toolName: "gemini",
55
69
  description:
56
- "Run Gemini CLI with a prompt. Always runs in sandbox mode with --approval-mode=plan.",
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
- buildArgs: (prompt) => ["-s", "--approval-mode=plan", "-p", prompt],
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
- * Spawn codex mcp-server as a pass-through, piping stdio directly.
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
- stdio: ["inherit", "inherit", "pipe"],
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
  }