mcp-agents 0.9.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 +93 -22
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -34,9 +34,31 @@ const DEFAULT_CODEX_APPROVAL_POLICY = "never";
|
|
|
34
34
|
const DEFAULT_CLAUDE_MODEL = "claude-opus-4-8";
|
|
35
35
|
const DEFAULT_CLAUDE_EFFORT = "xhigh";
|
|
36
36
|
// tools/call argument keys stripped from the codex pass-through so callers
|
|
37
|
-
// cannot override the pinned model/effort
|
|
38
|
-
//
|
|
39
|
-
|
|
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
|
+
];
|
|
40
62
|
const MAX_BUFFER_BYTES = 10 * 1024 * 1024;
|
|
41
63
|
const CLAUDE_EMPTY_OUTPUT_MAX_ATTEMPTS = 2;
|
|
42
64
|
const SIGNAL_CODES = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
|
|
@@ -106,6 +128,9 @@ function toStringArg(value) {
|
|
|
106
128
|
|
|
107
129
|
/**
|
|
108
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.
|
|
109
134
|
* @param {string} provider
|
|
110
135
|
* @param {string} output
|
|
111
136
|
* @returns {{ text: string, isError: boolean }}
|
|
@@ -118,10 +143,24 @@ function normalizeToolOutput(provider, output) {
|
|
|
118
143
|
|
|
119
144
|
try {
|
|
120
145
|
const parsed = JSON.parse(trimmed);
|
|
121
|
-
|
|
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") {
|
|
122
161
|
return {
|
|
123
|
-
text: toStringArg(
|
|
124
|
-
isError:
|
|
162
|
+
text: toStringArg(result.result),
|
|
163
|
+
isError: result.is_error === true,
|
|
125
164
|
};
|
|
126
165
|
}
|
|
127
166
|
} catch {
|
|
@@ -444,10 +483,13 @@ function createIsolatedCodexHome({
|
|
|
444
483
|
|
|
445
484
|
/**
|
|
446
485
|
* Filter a single newline-delimited JSON-RPC message on its way to the codex
|
|
447
|
-
* pass-through. Strips per-call model/
|
|
448
|
-
* client cannot escape the pinned model/effort
|
|
449
|
-
*
|
|
450
|
-
*
|
|
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.
|
|
451
493
|
* @param {string} line
|
|
452
494
|
* @returns {string}
|
|
453
495
|
*/
|
|
@@ -468,12 +510,36 @@ function filterCodexToolCall(line) {
|
|
|
468
510
|
: null;
|
|
469
511
|
if (!args || typeof args !== "object") return line;
|
|
470
512
|
|
|
471
|
-
const
|
|
472
|
-
|
|
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
|
|
473
540
|
|
|
474
|
-
for (const key of stripped) delete args[key];
|
|
475
541
|
logErr(
|
|
476
|
-
`[mcp-agents] codex passthrough:
|
|
542
|
+
`[mcp-agents] codex passthrough: pinning model/effort, stripped: ${removed.join(", ")}`,
|
|
477
543
|
);
|
|
478
544
|
return JSON.stringify(msg);
|
|
479
545
|
}
|
|
@@ -543,22 +609,27 @@ function runCodexPassthrough({
|
|
|
543
609
|
logErr(`[codex] ${chunk.toString().trimEnd()}`);
|
|
544
610
|
});
|
|
545
611
|
|
|
546
|
-
// Pump client stdin -> codex stdin, splitting on
|
|
547
|
-
//
|
|
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.
|
|
548
616
|
child.stdin.on("error", () => {}); // ignore EPIPE if codex exits early
|
|
549
|
-
|
|
617
|
+
const NEWLINE = 0x0a;
|
|
618
|
+
let stdinBuf = Buffer.alloc(0);
|
|
550
619
|
process.stdin.on("data", (chunk) => {
|
|
551
|
-
stdinBuf
|
|
620
|
+
stdinBuf = stdinBuf.length ? Buffer.concat([stdinBuf, chunk]) : chunk;
|
|
552
621
|
let nl;
|
|
553
|
-
while ((nl = stdinBuf.indexOf(
|
|
554
|
-
const line = stdinBuf.
|
|
555
|
-
stdinBuf = stdinBuf.
|
|
622
|
+
while ((nl = stdinBuf.indexOf(NEWLINE)) !== -1) {
|
|
623
|
+
const line = stdinBuf.subarray(0, nl).toString("utf8");
|
|
624
|
+
stdinBuf = stdinBuf.subarray(nl + 1);
|
|
556
625
|
child.stdin.write(`${filterCodexToolCall(line)}\n`);
|
|
557
626
|
}
|
|
558
627
|
});
|
|
559
628
|
process.stdin.on("error", () => {});
|
|
560
629
|
process.stdin.on("end", () => {
|
|
561
|
-
if (stdinBuf.length > 0)
|
|
630
|
+
if (stdinBuf.length > 0) {
|
|
631
|
+
child.stdin.write(filterCodexToolCall(stdinBuf.toString("utf8")));
|
|
632
|
+
}
|
|
562
633
|
child.stdin.end();
|
|
563
634
|
});
|
|
564
635
|
|