mcp-agents 0.6.0 → 0.6.5

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 +24 -4
  2. package/package.json +1 -1
  3. package/server.js +110 -11
package/README.md CHANGED
@@ -78,14 +78,29 @@ Any additional `tools/call` arguments are ignored (for example `model` or `model
78
78
  ### `codex` (pass-through)
79
79
 
80
80
  The codex provider passes through to Codex's native MCP server (`codex mcp-server`)
81
- using `-c key=value` config overrides:
81
+ inside an isolated `CODEX_HOME`. The bridge copies `auth.json` into a temporary Codex
82
+ home, writes a minimal `config.toml`, and does not inherit your normal external MCP
83
+ server list. That keeps Codex from recursively starting other agent tools like Claude
84
+ or Gemini during bridge calls.
82
85
 
83
86
  | CLI Flag | Default | Codex config key |
84
87
  |----------|---------|-----------------|
85
88
  | `--model` | `gpt-5.4` | `model` |
86
- | `--model_reasoning_effort` | `high` | `model_reasoning_effort` |
89
+ | `--model_reasoning_effort` | `xhigh` | `model_reasoning_effort` |
87
90
 
88
- Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never` (safe for MCP server mode).
91
+ Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never`,
92
+ `features.multi_agent=false`.
93
+
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:
95
+
96
+ ```json
97
+ {
98
+ "prompt": "Review this diff",
99
+ "config": {
100
+ "model_reasoning_effort": "medium"
101
+ }
102
+ }
103
+ ```
89
104
 
90
105
  ## Integration with Claude Code
91
106
 
@@ -119,7 +134,7 @@ Optional Gemini sandbox mode in `.mcp.json`:
119
134
  }
120
135
  ```
121
136
 
122
- Override codex defaults at server startup (not via `tools/call` arguments):
137
+ Override codex defaults at server startup:
123
138
 
124
139
  ```json
125
140
  {
@@ -132,6 +147,11 @@ Override codex defaults at server startup (not via `tools/call` arguments):
132
147
  }
133
148
  ```
134
149
 
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.
151
+
152
+ Because the bridge runs in an isolated Codex home, inherited MCP servers from your normal
153
+ `~/.codex/config.toml` are intentionally unavailable inside bridged Codex sessions.
154
+
135
155
  <details>
136
156
  <summary>Alternative: using npx (slower, not recommended)</summary>
137
157
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.6.0",
3
+ "version": "0.6.5",
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
@@ -2,7 +2,15 @@
2
2
  /* eslint-disable no-console */
3
3
 
4
4
  import { spawn } from "node:child_process";
5
- import { readFileSync } from "node:fs";
5
+ import {
6
+ copyFileSync,
7
+ existsSync,
8
+ mkdtempSync,
9
+ readFileSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
6
14
  import { dirname, join } from "node:path";
7
15
  import { fileURLToPath } from "node:url";
8
16
 
@@ -19,6 +27,8 @@ const VERSION = JSON.parse(
19
27
  ).version;
20
28
 
21
29
  const DEFAULT_TIMEOUT_MS = 300_000;
30
+ const DEFAULT_CODEX_MODEL = "gpt-5.4";
31
+ const DEFAULT_CODEX_MODEL_REASONING_EFFORT = "xhigh";
22
32
  const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
23
33
  const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
24
34
  const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
@@ -114,8 +124,8 @@ Usage: mcp-agents [options]
114
124
 
115
125
  Options:
116
126
  --provider <name> CLI backend to use (${providers}) [default: codex]
117
- --model <model> Codex model [default: gpt-5.4]
118
- --model_reasoning_effort <e> Codex reasoning effort [default: high]
127
+ --model <model> Codex model [default: ${DEFAULT_CODEX_MODEL}]
128
+ --model_reasoning_effort <e> Codex reasoning effort [default: ${DEFAULT_CODEX_MODEL_REASONING_EFFORT}]
119
129
  --timeout <seconds> Default timeout per call [default: 300]
120
130
  --help, -h Show this help message
121
131
  --version, -v Show version number`);
@@ -297,22 +307,108 @@ function runCli(command, args, opts = {}) {
297
307
  });
298
308
  }
299
309
 
310
+ /**
311
+ * Resolve the source Codex home used by the parent process.
312
+ * @returns {string}
313
+ */
314
+ function resolveCodexHome() {
315
+ return process.env.CODEX_HOME || join(process.env.HOME || tmpdir(), ".codex");
316
+ }
317
+
318
+ /**
319
+ * Quote a string for TOML output.
320
+ * @param {string} value
321
+ * @returns {string}
322
+ */
323
+ function toTomlString(value) {
324
+ return JSON.stringify(value);
325
+ }
326
+
327
+ /**
328
+ * Build the minimal config for the isolated Codex bridge runtime.
329
+ * @param {{ model: string, modelReasoningEffort: string }} opts
330
+ * @returns {string}
331
+ */
332
+ function buildCodexBridgeConfig({ model, modelReasoningEffort }) {
333
+ return [
334
+ `model = ${toTomlString(model)}`,
335
+ `model_reasoning_effort = ${toTomlString(modelReasoningEffort)}`,
336
+ 'approval_policy = "never"',
337
+ 'sandbox_mode = "read-only"',
338
+ "",
339
+ "[features]",
340
+ "multi_agent = false",
341
+ "",
342
+ ].join("\n");
343
+ }
344
+
345
+ /**
346
+ * Create an isolated Codex home that preserves auth but strips inherited MCP servers.
347
+ * @param {{ model: string, modelReasoningEffort: string }} opts
348
+ * @returns {string}
349
+ */
350
+ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
351
+ const codexHome = mkdtempSync(join(tmpdir(), "mcp-agents-codex-"));
352
+ const sourceAuthPath = join(resolveCodexHome(), "auth.json");
353
+ const targetAuthPath = join(codexHome, "auth.json");
354
+ const configPath = join(codexHome, "config.toml");
355
+
356
+ if (existsSync(sourceAuthPath)) {
357
+ copyFileSync(sourceAuthPath, targetAuthPath);
358
+ }
359
+
360
+ writeFileSync(
361
+ configPath,
362
+ buildCodexBridgeConfig({ model, modelReasoningEffort }),
363
+ "utf8",
364
+ );
365
+
366
+ return codexHome;
367
+ }
368
+
300
369
  /**
301
370
  * Spawn codex mcp-server as a pass-through, piping stdio directly.
302
371
  * @param {{ model?: string, modelReasoningEffort?: string }} opts
303
372
  */
304
373
  function runCodexPassthrough({ model, modelReasoningEffort }) {
305
- const args = [
306
- "mcp-server",
307
- "-c", `model=${model || "gpt-5.4"}`,
308
- "-c", "sandbox_mode=read-only",
309
- "-c", "approval_policy=never",
310
- "-c", `model_reasoning_effort=${modelReasoningEffort || "high"}`,
311
- ];
374
+ const resolvedModel = model || DEFAULT_CODEX_MODEL;
375
+ const resolvedModelReasoningEffort =
376
+ modelReasoningEffort || DEFAULT_CODEX_MODEL_REASONING_EFFORT;
377
+ let isolatedCodexHome;
378
+
379
+ try {
380
+ isolatedCodexHome = createIsolatedCodexHome({
381
+ model: resolvedModel,
382
+ modelReasoningEffort: resolvedModelReasoningEffort,
383
+ });
384
+ } catch (err) {
385
+ const msg = err instanceof Error ? err.message : String(err);
386
+ logErr(`[mcp-agents] failed to prepare isolated codex home: ${msg}`);
387
+ process.exitCode = 1;
388
+ return;
389
+ }
390
+
391
+ const args = ["mcp-server"];
392
+ let cleanedUp = false;
393
+ const cleanupIsolatedCodexHome = () => {
394
+ if (cleanedUp || !isolatedCodexHome) return;
395
+ cleanedUp = true;
396
+
397
+ try {
398
+ rmSync(isolatedCodexHome, { recursive: true, force: true });
399
+ } catch (err) {
400
+ const msg = err instanceof Error ? err.message : String(err);
401
+ logErr(`[mcp-agents] failed to clean isolated codex home: ${msg}`);
402
+ }
403
+ };
312
404
 
313
- logErr(`[mcp-agents] passthrough: codex ${args.join(" ")}`);
405
+ logErr(
406
+ `[mcp-agents] passthrough: codex ${args.join(" ")} ` +
407
+ `(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort}, isolated_home=true)`,
408
+ );
314
409
 
315
410
  const child = spawn("codex", args, {
411
+ env: { ...process.env, CODEX_HOME: isolatedCodexHome },
316
412
  stdio: ["inherit", "inherit", "pipe"],
317
413
  });
318
414
 
@@ -325,17 +421,20 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
325
421
  child.kill(sig);
326
422
  setTimeout(() => {
327
423
  child.kill("SIGKILL");
424
+ cleanupIsolatedCodexHome();
328
425
  process.exit(128 + SIGNAL_CODES[sig]);
329
426
  }, 5000).unref();
330
427
  });
331
428
  }
332
429
 
333
430
  child.on("error", (err) => {
431
+ cleanupIsolatedCodexHome();
334
432
  logErr(`[mcp-agents] failed to start codex: ${err.message}`);
335
433
  process.exitCode = 1;
336
434
  });
337
435
 
338
436
  child.on("exit", (code, signal) => {
437
+ cleanupIsolatedCodexHome();
339
438
  if (signal) {
340
439
  logErr(`[mcp-agents] codex killed by ${signal}`);
341
440
  process.exitCode = 128 + (SIGNAL_CODES[signal] ?? 0);