mcp-agents 0.7.0 → 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 +9 -8
  2. package/package.json +1 -1
  3. package/server.js +77 -4
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.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": {
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,9 +49,18 @@ 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: {
@@ -383,7 +397,45 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
383
397
  }
384
398
 
385
399
  /**
386
- * 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.
387
439
  * @param {{ model?: string, modelReasoningEffort?: string }} opts
388
440
  */
389
441
  function runCodexPassthrough({ model, modelReasoningEffort }) {
@@ -425,13 +477,34 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
425
477
 
426
478
  const child = spawn("codex", args, {
427
479
  env: { ...process.env, CODEX_HOME: isolatedCodexHome },
428
- 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"],
429
483
  });
430
484
 
431
485
  child.stderr.on("data", (chunk) => {
432
486
  logErr(`[codex] ${chunk.toString().trimEnd()}`);
433
487
  });
434
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
+
435
508
  for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
436
509
  process.once(sig, () => {
437
510
  child.kill(sig);