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.
- package/README.md +9 -8
- package/package.json +1 -1
- 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 `
|
|
61
|
+
Any additional `tools/call` arguments are ignored (for example `model`, `effort`, or `config`).
|
|
62
62
|
|
|
63
|
-
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`).
|
|
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
|
|
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
|
-
"
|
|
98
|
-
|
|
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
|
|
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
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
|
-
|
|
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: () => [
|
|
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,
|
|
138
|
-
*
|
|
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 {
|
|
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({
|
|
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
|
-
|
|
353
|
-
|
|
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({
|
|
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({
|
|
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
|
-
*
|
|
387
|
-
*
|
|
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
|
|
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},
|
|
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
|
-
|
|
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 {
|
|
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({
|
|
617
|
+
runCodexPassthrough({
|
|
618
|
+
model,
|
|
619
|
+
modelReasoningEffort,
|
|
620
|
+
sandboxMode,
|
|
621
|
+
approvalPolicy,
|
|
622
|
+
});
|
|
481
623
|
return;
|
|
482
624
|
}
|
|
483
625
|
|