mcp-agents 0.7.0 → 0.9.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 +9 -8
  2. package/package.json +1 -1
  3. package/server.js +161 -19
package/README.md CHANGED
@@ -47,7 +47,7 @@ Each `--provider` flag maps to a single exposed tool:
47
47
 
48
48
  | Provider | Tool name | CLI command |
49
49
  |----------|-----------|-------------|
50
- | `claude` | `claude_code` | `claude -p --output-format json` |
50
+ | `claude` | `claude_code` | `claude --model claude-opus-4-8 --effort xhigh -p --output-format json` |
51
51
  | `gemini` | `gemini` | `agy --sandbox -p <prompt>` |
52
52
  | `codex` | *(pass-through)* | `codex mcp-server` |
53
53
 
@@ -58,9 +58,9 @@ Each `--provider` flag maps to a single exposed tool:
58
58
  | `prompt` | `string` | yes | The prompt to send to Claude Code |
59
59
  | `timeout_ms` | `integer` | no | Timeout in ms (default: 300 000 / 5 minutes) |
60
60
 
61
- 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`).
62
62
 
63
- 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`).
64
64
 
65
65
  ### `gemini` parameters
66
66
 
@@ -89,17 +89,18 @@ or Gemini during bridge calls.
89
89
  Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never`,
90
90
  `features.multi_agent=false`.
91
91
 
92
- 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:
93
93
 
94
94
  ```json
95
95
  {
96
96
  "prompt": "Review this diff",
97
- "config": {
98
- "model_reasoning_effort": "medium"
99
- }
97
+ "model": "gpt-5.5-codex",
98
+ "config": { "model_reasoning_effort": "medium" }
100
99
  }
101
100
  ```
102
101
 
102
+ is forwarded to Codex as `{ "prompt": "Review this diff" }`. Change the model or effort at server startup instead.
103
+
103
104
  ## Integration with Claude Code
104
105
 
105
106
  Add entries to your project's `.mcp.json` (requires `npm i -g mcp-agents`):
@@ -132,7 +133,7 @@ Override codex defaults at server startup:
132
133
  }
133
134
  ```
134
135
 
135
- 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.
136
137
 
137
138
  Because the bridge runs in an isolated Codex home, inherited MCP servers from your normal
138
139
  `~/.codex/config.toml` are intentionally unavailable inside bridged Codex sessions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.7.0",
3
+ "version": "0.9.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": {
package/server.js CHANGED
@@ -29,6 +29,14 @@ 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_CODEX_SANDBOX_MODE = "workspace-write";
33
+ const DEFAULT_CODEX_APPROVAL_POLICY = "never";
34
+ const DEFAULT_CLAUDE_MODEL = "claude-opus-4-8";
35
+ const DEFAULT_CLAUDE_EFFORT = "xhigh";
36
+ // tools/call argument keys stripped from the codex pass-through so callers
37
+ // cannot override the pinned model/effort (or the server's sandbox/approval
38
+ // config) for a single call.
39
+ const CODEX_STRIPPED_TOOL_ARGS = ["model", "config"];
32
40
  const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
33
41
  const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
34
42
  const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
@@ -44,9 +52,18 @@ const CLI_BACKENDS = {
44
52
  command: "claude",
45
53
  toolName: "claude_code",
46
54
  description:
47
- "Run Claude Code CLI with a prompt (via stdin). Supports prompt + optional timeout_ms only; other arguments are ignored.",
55
+ `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
56
  stdinPrompt: true,
49
- buildArgs: () => ["--no-session-persistence", "-p", "--output-format", "json"],
57
+ buildArgs: () => [
58
+ "--model",
59
+ DEFAULT_CLAUDE_MODEL,
60
+ "--effort",
61
+ DEFAULT_CLAUDE_EFFORT,
62
+ "--no-session-persistence",
63
+ "-p",
64
+ "--output-format",
65
+ "json",
66
+ ],
50
67
  extraProperties: {},
51
68
  },
52
69
  gemini: {
@@ -127,6 +144,10 @@ Options:
127
144
  --provider <name> CLI backend to use (${providers}) [default: codex]
128
145
  --model <model> Codex model [default: ${DEFAULT_CODEX_MODEL}]
129
146
  --model_reasoning_effort <e> Codex reasoning effort [default: ${DEFAULT_CODEX_MODEL_REASONING_EFFORT}]
147
+ --sandbox_mode <mode> Codex sandbox mode: read-only, workspace-write,
148
+ danger-full-access [default: ${DEFAULT_CODEX_SANDBOX_MODE}]
149
+ --approval_policy <policy> Codex approval policy: untrusted, on-failure,
150
+ on-request, never [default: ${DEFAULT_CODEX_APPROVAL_POLICY}]
130
151
  --timeout <seconds> Default timeout per call [default: 300]
131
152
  --help, -h Show this help message
132
153
  --version, -v Show version number`);
@@ -134,14 +155,17 @@ Options:
134
155
 
135
156
  /**
136
157
  * Parse CLI flags from process.argv.
137
- * Handles --help, --version, --provider, --model, --model_reasoning_effort, and unknown flags.
138
- * @returns {{ provider: string, model?: string, modelReasoningEffort?: string, defaultTimeoutMs?: number }}
158
+ * Handles --help, --version, --provider, --model, --model_reasoning_effort,
159
+ * --sandbox_mode, --approval_policy, and unknown flags.
160
+ * @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, defaultTimeoutMs?: number }}
139
161
  */
140
162
  function parseArgs() {
141
163
  const args = process.argv.slice(2);
142
164
  let provider = "codex";
143
165
  let model;
144
166
  let modelReasoningEffort;
167
+ let sandboxMode;
168
+ let approvalPolicy;
145
169
  let defaultTimeoutMs;
146
170
 
147
171
  for (let i = 0; i < args.length; i++) {
@@ -179,6 +203,20 @@ function parseArgs() {
179
203
  }
180
204
  modelReasoningEffort = args[++i];
181
205
  break;
206
+ case "--sandbox_mode":
207
+ if (i + 1 >= args.length) {
208
+ process.stderr.write("error: --sandbox_mode requires a value\n");
209
+ process.exit(1);
210
+ }
211
+ sandboxMode = args[++i];
212
+ break;
213
+ case "--approval_policy":
214
+ if (i + 1 >= args.length) {
215
+ process.stderr.write("error: --approval_policy requires a value\n");
216
+ process.exit(1);
217
+ }
218
+ approvalPolicy = args[++i];
219
+ break;
182
220
  case "--timeout": {
183
221
  if (i + 1 >= args.length) {
184
222
  process.stderr.write("error: --timeout requires a value\n");
@@ -198,7 +236,14 @@ function parseArgs() {
198
236
  }
199
237
  }
200
238
 
201
- return { provider, model, modelReasoningEffort, defaultTimeoutMs };
239
+ return {
240
+ provider,
241
+ model,
242
+ modelReasoningEffort,
243
+ sandboxMode,
244
+ approvalPolicy,
245
+ defaultTimeoutMs,
246
+ };
202
247
  }
203
248
 
204
249
  /**
@@ -342,15 +387,20 @@ function toTomlString(value) {
342
387
 
343
388
  /**
344
389
  * Build the minimal config for the isolated Codex bridge runtime.
345
- * @param {{ model: string, modelReasoningEffort: string }} opts
390
+ * @param {{ model: string, modelReasoningEffort: string, sandboxMode: string, approvalPolicy: string }} opts
346
391
  * @returns {string}
347
392
  */
348
- function buildCodexBridgeConfig({ model, modelReasoningEffort }) {
393
+ function buildCodexBridgeConfig({
394
+ model,
395
+ modelReasoningEffort,
396
+ sandboxMode,
397
+ approvalPolicy,
398
+ }) {
349
399
  return [
350
400
  `model = ${toTomlString(model)}`,
351
401
  `model_reasoning_effort = ${toTomlString(modelReasoningEffort)}`,
352
- 'approval_policy = "never"',
353
- 'sandbox_mode = "read-only"',
402
+ `approval_policy = ${toTomlString(approvalPolicy)}`,
403
+ `sandbox_mode = ${toTomlString(sandboxMode)}`,
354
404
  "",
355
405
  "[features]",
356
406
  "multi_agent = false",
@@ -360,10 +410,15 @@ function buildCodexBridgeConfig({ model, modelReasoningEffort }) {
360
410
 
361
411
  /**
362
412
  * Create an isolated Codex home that preserves auth but strips inherited MCP servers.
363
- * @param {{ model: string, modelReasoningEffort: string }} opts
413
+ * @param {{ model: string, modelReasoningEffort: string, sandboxMode: string, approvalPolicy: string }} opts
364
414
  * @returns {string}
365
415
  */
366
- function createIsolatedCodexHome({ model, modelReasoningEffort }) {
416
+ function createIsolatedCodexHome({
417
+ model,
418
+ modelReasoningEffort,
419
+ sandboxMode,
420
+ approvalPolicy,
421
+ }) {
367
422
  const codexHome = mkdtempSync(join(tmpdir(), "mcp-agents-codex-"));
368
423
  const sourceAuthPath = join(resolveCodexHome(), "auth.json");
369
424
  const targetAuthPath = join(codexHome, "auth.json");
@@ -375,7 +430,12 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
375
430
 
376
431
  writeFileSync(
377
432
  configPath,
378
- buildCodexBridgeConfig({ model, modelReasoningEffort }),
433
+ buildCodexBridgeConfig({
434
+ model,
435
+ modelReasoningEffort,
436
+ sandboxMode,
437
+ approvalPolicy,
438
+ }),
379
439
  "utf8",
380
440
  );
381
441
 
@@ -383,19 +443,66 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
383
443
  }
384
444
 
385
445
  /**
386
- * Spawn codex mcp-server as a pass-through, piping stdio directly.
387
- * @param {{ model?: string, modelReasoningEffort?: string }} opts
446
+ * Filter a single newline-delimited JSON-RPC message on its way to the codex
447
+ * pass-through. Strips per-call model/config overrides from `tools/call` so the
448
+ * client cannot escape the pinned model/effort (or the sandbox/approval config).
449
+ * Non-`tools/call` and unparseable lines are returned byte-for-byte unchanged so
450
+ * the MCP framing is preserved.
451
+ * @param {string} line
452
+ * @returns {string}
388
453
  */
389
- function runCodexPassthrough({ model, modelReasoningEffort }) {
454
+ function filterCodexToolCall(line) {
455
+ const trimmed = line.trim();
456
+ if (!trimmed) return line;
457
+
458
+ let msg;
459
+ try {
460
+ msg = JSON.parse(trimmed);
461
+ } catch {
462
+ return line; // not JSON (e.g. partial/keepalive) — pass through untouched
463
+ }
464
+
465
+ const args =
466
+ msg && typeof msg === "object" && msg.method === "tools/call"
467
+ ? msg.params?.arguments
468
+ : null;
469
+ if (!args || typeof args !== "object") return line;
470
+
471
+ const stripped = CODEX_STRIPPED_TOOL_ARGS.filter((key) => key in args);
472
+ if (stripped.length === 0) return line;
473
+
474
+ for (const key of stripped) delete args[key];
475
+ logErr(
476
+ `[mcp-agents] codex passthrough: ignoring per-call overrides: ${stripped.join(", ")}`,
477
+ );
478
+ return JSON.stringify(msg);
479
+ }
480
+
481
+ /**
482
+ * Spawn codex mcp-server as a pass-through. stdout/stderr flow straight back to
483
+ * the client, but the client's stdin is intercepted line-by-line so per-call
484
+ * model/config overrides are stripped before reaching codex.
485
+ * @param {{ model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string }} opts
486
+ */
487
+ function runCodexPassthrough({
488
+ model,
489
+ modelReasoningEffort,
490
+ sandboxMode,
491
+ approvalPolicy,
492
+ }) {
390
493
  const resolvedModel = model || DEFAULT_CODEX_MODEL;
391
494
  const resolvedModelReasoningEffort =
392
495
  modelReasoningEffort || DEFAULT_CODEX_MODEL_REASONING_EFFORT;
496
+ const resolvedSandboxMode = sandboxMode || DEFAULT_CODEX_SANDBOX_MODE;
497
+ const resolvedApprovalPolicy = approvalPolicy || DEFAULT_CODEX_APPROVAL_POLICY;
393
498
  let isolatedCodexHome;
394
499
 
395
500
  try {
396
501
  isolatedCodexHome = createIsolatedCodexHome({
397
502
  model: resolvedModel,
398
503
  modelReasoningEffort: resolvedModelReasoningEffort,
504
+ sandboxMode: resolvedSandboxMode,
505
+ approvalPolicy: resolvedApprovalPolicy,
399
506
  });
400
507
  } catch (err) {
401
508
  const msg = err instanceof Error ? err.message : String(err);
@@ -420,18 +527,41 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
420
527
 
421
528
  logErr(
422
529
  `[mcp-agents] passthrough: codex ${args.join(" ")} ` +
423
- `(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort}, isolated_home=true)`,
530
+ `(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort}, ` +
531
+ `sandbox_mode=${resolvedSandboxMode}, approval_policy=${resolvedApprovalPolicy}, ` +
532
+ `isolated_home=true)`,
424
533
  );
425
534
 
426
535
  const child = spawn("codex", args, {
427
536
  env: { ...process.env, CODEX_HOME: isolatedCodexHome },
428
- stdio: ["inherit", "inherit", "pipe"],
537
+ // stdin is piped (not inherited) so we can strip per-call overrides;
538
+ // stdout stays inherited so codex responses reach the client untouched.
539
+ stdio: ["pipe", "inherit", "pipe"],
429
540
  });
430
541
 
431
542
  child.stderr.on("data", (chunk) => {
432
543
  logErr(`[codex] ${chunk.toString().trimEnd()}`);
433
544
  });
434
545
 
546
+ // Pump client stdin -> codex stdin, splitting on newlines (MCP stdio framing)
547
+ // so each JSON-RPC message can be filtered before forwarding.
548
+ child.stdin.on("error", () => {}); // ignore EPIPE if codex exits early
549
+ let stdinBuf = "";
550
+ process.stdin.on("data", (chunk) => {
551
+ stdinBuf += chunk.toString("utf8");
552
+ let nl;
553
+ while ((nl = stdinBuf.indexOf("\n")) !== -1) {
554
+ const line = stdinBuf.slice(0, nl);
555
+ stdinBuf = stdinBuf.slice(nl + 1);
556
+ child.stdin.write(`${filterCodexToolCall(line)}\n`);
557
+ }
558
+ });
559
+ process.stdin.on("error", () => {});
560
+ process.stdin.on("end", () => {
561
+ if (stdinBuf.length > 0) child.stdin.write(filterCodexToolCall(stdinBuf));
562
+ child.stdin.end();
563
+ });
564
+
435
565
  for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
436
566
  process.once(sig, () => {
437
567
  child.kill(sig);
@@ -466,7 +596,14 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
466
596
  // ---------------------------------------------------------------------------
467
597
 
468
598
  async function main() {
469
- const { provider: providerName, model, modelReasoningEffort, defaultTimeoutMs } = parseArgs();
599
+ const {
600
+ provider: providerName,
601
+ model,
602
+ modelReasoningEffort,
603
+ sandboxMode,
604
+ approvalPolicy,
605
+ defaultTimeoutMs,
606
+ } = parseArgs();
470
607
  const backend = CLI_BACKENDS[providerName];
471
608
 
472
609
  if (!backend) {
@@ -477,7 +614,12 @@ async function main() {
477
614
  }
478
615
 
479
616
  if (backend.passthrough) {
480
- runCodexPassthrough({ model, modelReasoningEffort });
617
+ runCodexPassthrough({
618
+ model,
619
+ modelReasoningEffort,
620
+ sandboxMode,
621
+ approvalPolicy,
622
+ });
481
623
  return;
482
624
  }
483
625