mcp-agents 0.8.0 → 0.10.1
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/package.json +1 -1
- package/server.js +176 -36
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -29,11 +29,36 @@ 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";
|
|
32
34
|
const DEFAULT_CLAUDE_MODEL = "claude-opus-4-8";
|
|
33
35
|
const DEFAULT_CLAUDE_EFFORT = "xhigh";
|
|
34
36
|
// tools/call argument keys stripped from the codex pass-through so callers
|
|
35
|
-
// cannot override the pinned model/effort
|
|
36
|
-
|
|
37
|
+
// cannot override the pinned model/effort. sandbox/cwd/approval-policy are
|
|
38
|
+
// intentionally left intact so callers can steer them per call.
|
|
39
|
+
// - top-level: only the dedicated `model` arg (there is no top-level
|
|
40
|
+
// model_reasoning_effort/profile arg in the codex tool schema)
|
|
41
|
+
// - inside the `config` override map: model/effort plus every other
|
|
42
|
+
// model-envelope vector — a `profile`/`profiles` can carry its own
|
|
43
|
+
// model/effort, provider/base-url keys re-point the same model name to a
|
|
44
|
+
// different backend, and the plan/review variants carry their own
|
|
45
|
+
// model/effort; all are stripped so the pin cannot be bypassed. Matched on
|
|
46
|
+
// each config key's HEAD segment so dotted overrides (codex accepts paths
|
|
47
|
+
// like `profiles.x.model`) are caught too, not just exact keys.
|
|
48
|
+
const CODEX_STRIPPED_TOP_LEVEL_ARGS = ["model"];
|
|
49
|
+
const CODEX_STRIPPED_CONFIG_KEYS = [
|
|
50
|
+
"model",
|
|
51
|
+
"model_reasoning_effort",
|
|
52
|
+
"profile",
|
|
53
|
+
"profiles",
|
|
54
|
+
"model_provider",
|
|
55
|
+
"model_providers",
|
|
56
|
+
"openai_base_url",
|
|
57
|
+
"chatgpt_base_url",
|
|
58
|
+
"model_catalog_json",
|
|
59
|
+
"plan_mode_reasoning_effort",
|
|
60
|
+
"review_model",
|
|
61
|
+
];
|
|
37
62
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
38
63
|
const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
|
|
39
64
|
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
@@ -103,6 +128,9 @@ function toStringArg(value) {
|
|
|
103
128
|
|
|
104
129
|
/**
|
|
105
130
|
* Normalize provider output and parse Claude's JSON print format when present.
|
|
131
|
+
* `--output-format json` emits either a single `{type:"result"}` object or
|
|
132
|
+
* (newer CLIs, e.g. 2.1.x) an array of stream events whose final
|
|
133
|
+
* `type:"result"` entry holds the answer; both are supported.
|
|
106
134
|
* @param {string} provider
|
|
107
135
|
* @param {string} output
|
|
108
136
|
* @returns {{ text: string, isError: boolean }}
|
|
@@ -115,10 +143,24 @@ function normalizeToolOutput(provider, output) {
|
|
|
115
143
|
|
|
116
144
|
try {
|
|
117
145
|
const parsed = JSON.parse(trimmed);
|
|
118
|
-
|
|
146
|
+
// Resolve the result event from either shape. Scanning from the end finds
|
|
147
|
+
// the terminal result without depending on Array.prototype.findLast
|
|
148
|
+
// (keeps the Node >=18 floor — see engines).
|
|
149
|
+
let result = parsed;
|
|
150
|
+
if (Array.isArray(parsed)) {
|
|
151
|
+
result = null;
|
|
152
|
+
for (let i = parsed.length - 1; i >= 0; i--) {
|
|
153
|
+
const event = parsed[i];
|
|
154
|
+
if (event && typeof event === "object" && event.type === "result") {
|
|
155
|
+
result = event;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (result && typeof result === "object" && result.type === "result") {
|
|
119
161
|
return {
|
|
120
|
-
text: toStringArg(
|
|
121
|
-
isError:
|
|
162
|
+
text: toStringArg(result.result),
|
|
163
|
+
isError: result.is_error === true,
|
|
122
164
|
};
|
|
123
165
|
}
|
|
124
166
|
} catch {
|
|
@@ -141,6 +183,10 @@ Options:
|
|
|
141
183
|
--provider <name> CLI backend to use (${providers}) [default: codex]
|
|
142
184
|
--model <model> Codex model [default: ${DEFAULT_CODEX_MODEL}]
|
|
143
185
|
--model_reasoning_effort <e> Codex reasoning effort [default: ${DEFAULT_CODEX_MODEL_REASONING_EFFORT}]
|
|
186
|
+
--sandbox_mode <mode> Codex sandbox mode: read-only, workspace-write,
|
|
187
|
+
danger-full-access [default: ${DEFAULT_CODEX_SANDBOX_MODE}]
|
|
188
|
+
--approval_policy <policy> Codex approval policy: untrusted, on-failure,
|
|
189
|
+
on-request, never [default: ${DEFAULT_CODEX_APPROVAL_POLICY}]
|
|
144
190
|
--timeout <seconds> Default timeout per call [default: 300]
|
|
145
191
|
--help, -h Show this help message
|
|
146
192
|
--version, -v Show version number`);
|
|
@@ -148,14 +194,17 @@ Options:
|
|
|
148
194
|
|
|
149
195
|
/**
|
|
150
196
|
* Parse CLI flags from process.argv.
|
|
151
|
-
* Handles --help, --version, --provider, --model, --model_reasoning_effort,
|
|
152
|
-
*
|
|
197
|
+
* Handles --help, --version, --provider, --model, --model_reasoning_effort,
|
|
198
|
+
* --sandbox_mode, --approval_policy, and unknown flags.
|
|
199
|
+
* @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, defaultTimeoutMs?: number }}
|
|
153
200
|
*/
|
|
154
201
|
function parseArgs() {
|
|
155
202
|
const args = process.argv.slice(2);
|
|
156
203
|
let provider = "codex";
|
|
157
204
|
let model;
|
|
158
205
|
let modelReasoningEffort;
|
|
206
|
+
let sandboxMode;
|
|
207
|
+
let approvalPolicy;
|
|
159
208
|
let defaultTimeoutMs;
|
|
160
209
|
|
|
161
210
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -193,6 +242,20 @@ function parseArgs() {
|
|
|
193
242
|
}
|
|
194
243
|
modelReasoningEffort = args[++i];
|
|
195
244
|
break;
|
|
245
|
+
case "--sandbox_mode":
|
|
246
|
+
if (i + 1 >= args.length) {
|
|
247
|
+
process.stderr.write("error: --sandbox_mode requires a value\n");
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
sandboxMode = args[++i];
|
|
251
|
+
break;
|
|
252
|
+
case "--approval_policy":
|
|
253
|
+
if (i + 1 >= args.length) {
|
|
254
|
+
process.stderr.write("error: --approval_policy requires a value\n");
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
approvalPolicy = args[++i];
|
|
258
|
+
break;
|
|
196
259
|
case "--timeout": {
|
|
197
260
|
if (i + 1 >= args.length) {
|
|
198
261
|
process.stderr.write("error: --timeout requires a value\n");
|
|
@@ -212,7 +275,14 @@ function parseArgs() {
|
|
|
212
275
|
}
|
|
213
276
|
}
|
|
214
277
|
|
|
215
|
-
return {
|
|
278
|
+
return {
|
|
279
|
+
provider,
|
|
280
|
+
model,
|
|
281
|
+
modelReasoningEffort,
|
|
282
|
+
sandboxMode,
|
|
283
|
+
approvalPolicy,
|
|
284
|
+
defaultTimeoutMs,
|
|
285
|
+
};
|
|
216
286
|
}
|
|
217
287
|
|
|
218
288
|
/**
|
|
@@ -356,15 +426,20 @@ function toTomlString(value) {
|
|
|
356
426
|
|
|
357
427
|
/**
|
|
358
428
|
* Build the minimal config for the isolated Codex bridge runtime.
|
|
359
|
-
* @param {{ model: string, modelReasoningEffort: string }} opts
|
|
429
|
+
* @param {{ model: string, modelReasoningEffort: string, sandboxMode: string, approvalPolicy: string }} opts
|
|
360
430
|
* @returns {string}
|
|
361
431
|
*/
|
|
362
|
-
function buildCodexBridgeConfig({
|
|
432
|
+
function buildCodexBridgeConfig({
|
|
433
|
+
model,
|
|
434
|
+
modelReasoningEffort,
|
|
435
|
+
sandboxMode,
|
|
436
|
+
approvalPolicy,
|
|
437
|
+
}) {
|
|
363
438
|
return [
|
|
364
439
|
`model = ${toTomlString(model)}`,
|
|
365
440
|
`model_reasoning_effort = ${toTomlString(modelReasoningEffort)}`,
|
|
366
|
-
|
|
367
|
-
|
|
441
|
+
`approval_policy = ${toTomlString(approvalPolicy)}`,
|
|
442
|
+
`sandbox_mode = ${toTomlString(sandboxMode)}`,
|
|
368
443
|
"",
|
|
369
444
|
"[features]",
|
|
370
445
|
"multi_agent = false",
|
|
@@ -374,10 +449,15 @@ function buildCodexBridgeConfig({ model, modelReasoningEffort }) {
|
|
|
374
449
|
|
|
375
450
|
/**
|
|
376
451
|
* Create an isolated Codex home that preserves auth but strips inherited MCP servers.
|
|
377
|
-
* @param {{ model: string, modelReasoningEffort: string }} opts
|
|
452
|
+
* @param {{ model: string, modelReasoningEffort: string, sandboxMode: string, approvalPolicy: string }} opts
|
|
378
453
|
* @returns {string}
|
|
379
454
|
*/
|
|
380
|
-
function createIsolatedCodexHome({
|
|
455
|
+
function createIsolatedCodexHome({
|
|
456
|
+
model,
|
|
457
|
+
modelReasoningEffort,
|
|
458
|
+
sandboxMode,
|
|
459
|
+
approvalPolicy,
|
|
460
|
+
}) {
|
|
381
461
|
const codexHome = mkdtempSync(join(tmpdir(), "mcp-agents-codex-"));
|
|
382
462
|
const sourceAuthPath = join(resolveCodexHome(), "auth.json");
|
|
383
463
|
const targetAuthPath = join(codexHome, "auth.json");
|
|
@@ -389,7 +469,12 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
|
|
|
389
469
|
|
|
390
470
|
writeFileSync(
|
|
391
471
|
configPath,
|
|
392
|
-
buildCodexBridgeConfig({
|
|
472
|
+
buildCodexBridgeConfig({
|
|
473
|
+
model,
|
|
474
|
+
modelReasoningEffort,
|
|
475
|
+
sandboxMode,
|
|
476
|
+
approvalPolicy,
|
|
477
|
+
}),
|
|
393
478
|
"utf8",
|
|
394
479
|
);
|
|
395
480
|
|
|
@@ -398,10 +483,13 @@ function createIsolatedCodexHome({ model, modelReasoningEffort }) {
|
|
|
398
483
|
|
|
399
484
|
/**
|
|
400
485
|
* Filter a single newline-delimited JSON-RPC message on its way to the codex
|
|
401
|
-
* pass-through. Strips per-call model/
|
|
402
|
-
* client cannot escape the pinned model/effort
|
|
403
|
-
*
|
|
404
|
-
*
|
|
486
|
+
* pass-through. Strips per-call model/effort overrides from `tools/call` so the
|
|
487
|
+
* client cannot escape the pinned model/effort — both the top-level `model` arg
|
|
488
|
+
* and the model-envelope keys inside a `config` override map. sandbox/cwd/
|
|
489
|
+
* approval-policy (top-level and inside `config`) are intentionally left intact
|
|
490
|
+
* so callers can steer them per call. Non-`tools/call`, unparseable, and
|
|
491
|
+
* nothing-to-strip lines are returned byte-for-byte unchanged so the MCP framing
|
|
492
|
+
* is preserved.
|
|
405
493
|
* @param {string} line
|
|
406
494
|
* @returns {string}
|
|
407
495
|
*/
|
|
@@ -422,12 +510,36 @@ function filterCodexToolCall(line) {
|
|
|
422
510
|
: null;
|
|
423
511
|
if (!args || typeof args !== "object") return line;
|
|
424
512
|
|
|
425
|
-
const
|
|
426
|
-
|
|
513
|
+
const removed = [];
|
|
514
|
+
|
|
515
|
+
for (const key of CODEX_STRIPPED_TOP_LEVEL_ARGS) {
|
|
516
|
+
if (key in args) {
|
|
517
|
+
delete args[key];
|
|
518
|
+
removed.push(key);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Per-call `config` overrides beat CODEX_HOME/config.toml, so the pinned
|
|
523
|
+
// model/effort must be stripped from here too; everything else (sandbox_mode,
|
|
524
|
+
// approval_policy, cwd, sandbox_workspace_write, …) is left untouched. codex
|
|
525
|
+
// config overrides also accept dotted paths (e.g. "profiles.x.model"), so
|
|
526
|
+
// match each key on its HEAD segment, not the exact key.
|
|
527
|
+
const cfg = args.config;
|
|
528
|
+
if (cfg && typeof cfg === "object" && !Array.isArray(cfg)) {
|
|
529
|
+
for (const key of Object.keys(cfg)) {
|
|
530
|
+
if (CODEX_STRIPPED_CONFIG_KEYS.includes(key.split(".")[0])) {
|
|
531
|
+
delete cfg[key];
|
|
532
|
+
removed.push(`config.${key}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Drop a now-empty override map so codex never receives a bare `config: {}`.
|
|
536
|
+
if (Object.keys(cfg).length === 0) delete args.config;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
if (removed.length === 0) return line; // nothing pinned to strip — keep framing
|
|
427
540
|
|
|
428
|
-
for (const key of stripped) delete args[key];
|
|
429
541
|
logErr(
|
|
430
|
-
`[mcp-agents] codex passthrough:
|
|
542
|
+
`[mcp-agents] codex passthrough: pinning model/effort, stripped: ${removed.join(", ")}`,
|
|
431
543
|
);
|
|
432
544
|
return JSON.stringify(msg);
|
|
433
545
|
}
|
|
@@ -436,18 +548,27 @@ function filterCodexToolCall(line) {
|
|
|
436
548
|
* Spawn codex mcp-server as a pass-through. stdout/stderr flow straight back to
|
|
437
549
|
* the client, but the client's stdin is intercepted line-by-line so per-call
|
|
438
550
|
* model/config overrides are stripped before reaching codex.
|
|
439
|
-
* @param {{ model?: string, modelReasoningEffort?: string }} opts
|
|
551
|
+
* @param {{ model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string }} opts
|
|
440
552
|
*/
|
|
441
|
-
function runCodexPassthrough({
|
|
553
|
+
function runCodexPassthrough({
|
|
554
|
+
model,
|
|
555
|
+
modelReasoningEffort,
|
|
556
|
+
sandboxMode,
|
|
557
|
+
approvalPolicy,
|
|
558
|
+
}) {
|
|
442
559
|
const resolvedModel = model || DEFAULT_CODEX_MODEL;
|
|
443
560
|
const resolvedModelReasoningEffort =
|
|
444
561
|
modelReasoningEffort || DEFAULT_CODEX_MODEL_REASONING_EFFORT;
|
|
562
|
+
const resolvedSandboxMode = sandboxMode || DEFAULT_CODEX_SANDBOX_MODE;
|
|
563
|
+
const resolvedApprovalPolicy = approvalPolicy || DEFAULT_CODEX_APPROVAL_POLICY;
|
|
445
564
|
let isolatedCodexHome;
|
|
446
565
|
|
|
447
566
|
try {
|
|
448
567
|
isolatedCodexHome = createIsolatedCodexHome({
|
|
449
568
|
model: resolvedModel,
|
|
450
569
|
modelReasoningEffort: resolvedModelReasoningEffort,
|
|
570
|
+
sandboxMode: resolvedSandboxMode,
|
|
571
|
+
approvalPolicy: resolvedApprovalPolicy,
|
|
451
572
|
});
|
|
452
573
|
} catch (err) {
|
|
453
574
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -472,7 +593,9 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
472
593
|
|
|
473
594
|
logErr(
|
|
474
595
|
`[mcp-agents] passthrough: codex ${args.join(" ")} ` +
|
|
475
|
-
`(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort},
|
|
596
|
+
`(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort}, ` +
|
|
597
|
+
`sandbox_mode=${resolvedSandboxMode}, approval_policy=${resolvedApprovalPolicy}, ` +
|
|
598
|
+
`isolated_home=true)`,
|
|
476
599
|
);
|
|
477
600
|
|
|
478
601
|
const child = spawn("codex", args, {
|
|
@@ -486,22 +609,27 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
486
609
|
logErr(`[codex] ${chunk.toString().trimEnd()}`);
|
|
487
610
|
});
|
|
488
611
|
|
|
489
|
-
// Pump client stdin -> codex stdin, splitting on
|
|
490
|
-
//
|
|
612
|
+
// Pump client stdin -> codex stdin, splitting on the newline BYTE (0x0a) that
|
|
613
|
+
// delimits MCP stdio JSON-RPC frames. Buffering raw bytes (not per-chunk
|
|
614
|
+
// strings) avoids corrupting a multibyte UTF-8 sequence that straddles two
|
|
615
|
+
// read chunks, which would otherwise break the byte-for-byte passthrough.
|
|
491
616
|
child.stdin.on("error", () => {}); // ignore EPIPE if codex exits early
|
|
492
|
-
|
|
617
|
+
const NEWLINE = 0x0a;
|
|
618
|
+
let stdinBuf = Buffer.alloc(0);
|
|
493
619
|
process.stdin.on("data", (chunk) => {
|
|
494
|
-
stdinBuf
|
|
620
|
+
stdinBuf = stdinBuf.length ? Buffer.concat([stdinBuf, chunk]) : chunk;
|
|
495
621
|
let nl;
|
|
496
|
-
while ((nl = stdinBuf.indexOf(
|
|
497
|
-
const line = stdinBuf.
|
|
498
|
-
stdinBuf = stdinBuf.
|
|
622
|
+
while ((nl = stdinBuf.indexOf(NEWLINE)) !== -1) {
|
|
623
|
+
const line = stdinBuf.subarray(0, nl).toString("utf8");
|
|
624
|
+
stdinBuf = stdinBuf.subarray(nl + 1);
|
|
499
625
|
child.stdin.write(`${filterCodexToolCall(line)}\n`);
|
|
500
626
|
}
|
|
501
627
|
});
|
|
502
628
|
process.stdin.on("error", () => {});
|
|
503
629
|
process.stdin.on("end", () => {
|
|
504
|
-
if (stdinBuf.length > 0)
|
|
630
|
+
if (stdinBuf.length > 0) {
|
|
631
|
+
child.stdin.write(filterCodexToolCall(stdinBuf.toString("utf8")));
|
|
632
|
+
}
|
|
505
633
|
child.stdin.end();
|
|
506
634
|
});
|
|
507
635
|
|
|
@@ -539,7 +667,14 @@ function runCodexPassthrough({ model, modelReasoningEffort }) {
|
|
|
539
667
|
// ---------------------------------------------------------------------------
|
|
540
668
|
|
|
541
669
|
async function main() {
|
|
542
|
-
const {
|
|
670
|
+
const {
|
|
671
|
+
provider: providerName,
|
|
672
|
+
model,
|
|
673
|
+
modelReasoningEffort,
|
|
674
|
+
sandboxMode,
|
|
675
|
+
approvalPolicy,
|
|
676
|
+
defaultTimeoutMs,
|
|
677
|
+
} = parseArgs();
|
|
543
678
|
const backend = CLI_BACKENDS[providerName];
|
|
544
679
|
|
|
545
680
|
if (!backend) {
|
|
@@ -550,7 +685,12 @@ async function main() {
|
|
|
550
685
|
}
|
|
551
686
|
|
|
552
687
|
if (backend.passthrough) {
|
|
553
|
-
runCodexPassthrough({
|
|
688
|
+
runCodexPassthrough({
|
|
689
|
+
model,
|
|
690
|
+
modelReasoningEffort,
|
|
691
|
+
sandboxMode,
|
|
692
|
+
approvalPolicy,
|
|
693
|
+
});
|
|
554
694
|
return;
|
|
555
695
|
}
|
|
556
696
|
|