mcp-agents 0.11.0 → 0.12.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 +53 -4
- package/package.json +1 -1
- package/server.js +131 -17
package/README.md
CHANGED
|
@@ -86,10 +86,11 @@ or Gemini during bridge calls.
|
|
|
86
86
|
| `--model` | `gpt-5.5` | `model` |
|
|
87
87
|
| `--model_reasoning_effort` | `xhigh` | `model_reasoning_effort` |
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
`
|
|
89
|
+
Other startup defaults: `sandbox_mode=workspace-write`, `approval_policy=never`
|
|
90
|
+
(both configurable via `--sandbox_mode` / `--approval_policy`, and steerable per
|
|
91
|
+
call); `features.multi_agent=false` is fixed.
|
|
91
92
|
|
|
92
|
-
Startup flags (`--model`, `--model_reasoning_effort`) set the model and effort for the native Codex MCP server. Per-call `model` and `config`
|
|
93
|
+
Startup flags (`--model`, `--model_reasoning_effort`) set the model and effort for the native Codex MCP server. Per-call `model` and the model/effort keys inside a `config` override are stripped from `tools/call` before they reach Codex, so a client cannot override the pinned model/effort for a single call (`sandbox`, `cwd`, and `approval-policy` — top-level and the matching `config` keys — are intentionally left steerable per call). For example, this request:
|
|
93
94
|
|
|
94
95
|
```json
|
|
95
96
|
{
|
|
@@ -101,6 +102,54 @@ Startup flags (`--model`, `--model_reasoning_effort`) set the model and effort f
|
|
|
101
102
|
|
|
102
103
|
is forwarded to Codex as `{ "prompt": "Review this diff" }`. Change the model or effort at server startup instead.
|
|
103
104
|
|
|
105
|
+
**Goal injection.** You can give Codex a persistent objective. Set one at server
|
|
106
|
+
startup with `--goal "<text>"`, or per call with a `goal` argument in `tools/call`:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{ "prompt": "Refactor the parser", "goal": "Keep the public API unchanged" }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
For the initial `codex` call the objective is injected into Codex's native
|
|
113
|
+
`developer-instructions` field (a developer-role message), so this is forwarded
|
|
114
|
+
to Codex as:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"prompt": "Refactor the parser",
|
|
119
|
+
"developer-instructions": "Persistent objective for this Codex thread (a standing goal — keep pursuing it across turns unless explicitly superseded):\nKeep the public API unchanged"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
A developer message persists for the whole thread, so `codex-reply` follow-ups
|
|
124
|
+
inherit the objective automatically. Because `codex-reply` has no
|
|
125
|
+
`developer-instructions` field, a per-call `goal` on a reply is instead added as
|
|
126
|
+
a concise `Reminder — standing objective for this thread: …` preamble on the
|
|
127
|
+
prompt. Any caller-supplied `developer-instructions` are preserved, with the
|
|
128
|
+
objective merged ahead of them.
|
|
129
|
+
|
|
130
|
+
The wrapper-only `goal` argument is always stripped before it reaches Codex (its
|
|
131
|
+
schema has no `goal`). A per-call `goal` overrides the `--goal` default for that
|
|
132
|
+
call; a per-call empty `goal` (`""`) suppresses the default for that one call; a
|
|
133
|
+
non-string `goal` is ignored (the `--goal` default still applies).
|
|
134
|
+
|
|
135
|
+
**Precedence within a thread.** The objective set on the initial `codex` call is
|
|
136
|
+
a developer-role message and persists for the whole thread, so it takes
|
|
137
|
+
precedence: a *different* `goal` supplied later on a `codex-reply` is only a
|
|
138
|
+
prompt-level reminder and will not reliably override the standing objective
|
|
139
|
+
(verified live — a reply goal that conflicts with the initial one is ignored in
|
|
140
|
+
favor of the standing one). The reply reminder works when it is *not* opposed by
|
|
141
|
+
a conflicting standing objective. To genuinely change the objective mid-stream,
|
|
142
|
+
start a new `codex` call rather than changing it on a `codex-reply`.
|
|
143
|
+
|
|
144
|
+
> **Note — this is not Codex's native `/goal`.** Codex's `/goal` slash command
|
|
145
|
+
> (durable, thread-scoped goal state with lifecycle/budget/evidence-based
|
|
146
|
+
> completion) is a TUI-only feature — it is parsed in the Codex terminal UI and
|
|
147
|
+
> is *not* reachable through `codex mcp-server`. Prefixing an MCP prompt with
|
|
148
|
+
> `/goal …` does **not** activate it; the text is just passed through as a user
|
|
149
|
+
> message. This wrapper therefore steers Codex with `developer-instructions`
|
|
150
|
+
> (the MCP-native vehicle for a standing objective), which is prompt/role
|
|
151
|
+
> conditioning, not the native goal-lifecycle subsystem.
|
|
152
|
+
|
|
104
153
|
**Idle watchdog.** The codex pass-through is transparent, so a Codex session that
|
|
105
154
|
stalls after doing work (e.g. its final model turn hangs, or it waits on an
|
|
106
155
|
elicitation the client never answers) would otherwise hang the caller's
|
|
@@ -146,7 +195,7 @@ Override codex defaults at server startup:
|
|
|
146
195
|
}
|
|
147
196
|
```
|
|
148
197
|
|
|
149
|
-
The model and effort are fixed at server startup. Per-call `model` and `config`
|
|
198
|
+
The model and effort are fixed at server startup. Per-call `model` and the model/effort keys inside a `config` override sent to the native `codex` tool are stripped before reaching Codex, so they cannot override the startup model/effort (per-call `sandbox`/`cwd`/`approval-policy` are left intact). Add `"--goal", "<text>"` to `args` to inject a persistent objective into every Codex call (see [Goal injection](#codex-pass-through) above).
|
|
150
199
|
|
|
151
200
|
Because the bridge runs in an isolated Codex home, inherited MCP servers from your normal
|
|
152
201
|
`~/.codex/config.toml` are intentionally unavailable inside bridged Codex sessions.
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -192,6 +192,10 @@ Options:
|
|
|
192
192
|
danger-full-access [default: ${DEFAULT_CODEX_SANDBOX_MODE}]
|
|
193
193
|
--approval_policy <policy> Codex approval policy: untrusted, on-failure,
|
|
194
194
|
on-request, never [default: ${DEFAULT_CODEX_APPROVAL_POLICY}]
|
|
195
|
+
--goal <text> Persistent objective injected into every Codex
|
|
196
|
+
call (as developer-instructions, or a prompt
|
|
197
|
+
reminder on codex-reply); per-call \`goal\` arg
|
|
198
|
+
overrides it [default: none]
|
|
195
199
|
--codex_idle_timeout <secs> Codex pass-through idle watchdog; 0 disables
|
|
196
200
|
[default: ${DEFAULT_CODEX_IDLE_TIMEOUT_MS / 1000}]
|
|
197
201
|
--timeout <seconds> Default timeout per call [default: 300]
|
|
@@ -202,8 +206,9 @@ Options:
|
|
|
202
206
|
/**
|
|
203
207
|
* Parse CLI flags from process.argv.
|
|
204
208
|
* Handles --help, --version, --provider, --model, --model_reasoning_effort,
|
|
205
|
-
* --sandbox_mode, --approval_policy, --codex_idle_timeout, and unknown
|
|
206
|
-
*
|
|
209
|
+
* --sandbox_mode, --approval_policy, --goal, --codex_idle_timeout, and unknown
|
|
210
|
+
* flags.
|
|
211
|
+
* @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, goal?: string, codexIdleTimeoutMs?: number, defaultTimeoutMs?: number }}
|
|
207
212
|
*/
|
|
208
213
|
function parseArgs() {
|
|
209
214
|
const args = process.argv.slice(2);
|
|
@@ -212,6 +217,7 @@ function parseArgs() {
|
|
|
212
217
|
let modelReasoningEffort;
|
|
213
218
|
let sandboxMode;
|
|
214
219
|
let approvalPolicy;
|
|
220
|
+
let goal;
|
|
215
221
|
let codexIdleTimeoutMs;
|
|
216
222
|
let defaultTimeoutMs;
|
|
217
223
|
|
|
@@ -264,6 +270,13 @@ function parseArgs() {
|
|
|
264
270
|
}
|
|
265
271
|
approvalPolicy = args[++i];
|
|
266
272
|
break;
|
|
273
|
+
case "--goal":
|
|
274
|
+
if (i + 1 >= args.length) {
|
|
275
|
+
process.stderr.write("error: --goal requires a value\n");
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
goal = args[++i];
|
|
279
|
+
break;
|
|
267
280
|
case "--codex_idle_timeout": {
|
|
268
281
|
if (i + 1 >= args.length) {
|
|
269
282
|
process.stderr.write("error: --codex_idle_timeout requires a value\n");
|
|
@@ -304,6 +317,7 @@ function parseArgs() {
|
|
|
304
317
|
modelReasoningEffort,
|
|
305
318
|
sandboxMode,
|
|
306
319
|
approvalPolicy,
|
|
320
|
+
goal,
|
|
307
321
|
codexIdleTimeoutMs,
|
|
308
322
|
defaultTimeoutMs,
|
|
309
323
|
};
|
|
@@ -512,19 +526,69 @@ function createIsolatedCodexHome({
|
|
|
512
526
|
}
|
|
513
527
|
}
|
|
514
528
|
|
|
529
|
+
/**
|
|
530
|
+
* Build the text for codex's native `developer-instructions` field (a
|
|
531
|
+
* developer-role message) from a goal. This is the MCP-correct vehicle for a
|
|
532
|
+
* standing objective: it is higher-altitude than the user prompt and persists
|
|
533
|
+
* across the thread. It is NOT codex's `/goal` subsystem — that is a TUI-only
|
|
534
|
+
* slash command (parsed in codex-rs/tui, e.g. chatwidget/slash_dispatch.rs) and
|
|
535
|
+
* is not reachable through the MCP `codex`/`codex-reply` tool surface. Any
|
|
536
|
+
* caller-supplied developer instructions are preserved after the objective.
|
|
537
|
+
* @param {string} goal
|
|
538
|
+
* @param {string} [existing] caller-supplied developer-instructions, if any
|
|
539
|
+
* @returns {string}
|
|
540
|
+
*/
|
|
541
|
+
function buildGoalDeveloperInstructions(goal, existing) {
|
|
542
|
+
const directive =
|
|
543
|
+
"Persistent objective for this Codex thread (a standing goal — keep " +
|
|
544
|
+
"pursuing it across turns unless explicitly superseded):\n" +
|
|
545
|
+
goal.trim();
|
|
546
|
+
const prior = typeof existing === "string" ? existing.trim() : "";
|
|
547
|
+
return prior ? `${directive}\n\n---\n\n${prior}` : directive;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Prepend a concise goal reminder to a prompt. Used for `codex-reply` turns,
|
|
552
|
+
* which expose no `developer-instructions` field, so the prompt is the only
|
|
553
|
+
* vehicle left to restate the standing objective. A blank goal leaves the
|
|
554
|
+
* prompt untouched.
|
|
555
|
+
* @param {string} prompt
|
|
556
|
+
* @param {string} goal
|
|
557
|
+
* @returns {string}
|
|
558
|
+
*/
|
|
559
|
+
function applyGoalPreamble(prompt, goal) {
|
|
560
|
+
const trimmedGoal = (goal ?? "").trim();
|
|
561
|
+
const body = prompt ?? "";
|
|
562
|
+
if (!trimmedGoal) return body;
|
|
563
|
+
return `Reminder — standing objective for this thread: ${trimmedGoal}\n\n${body}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
515
566
|
/**
|
|
516
567
|
* Filter a single newline-delimited JSON-RPC message on its way to the codex
|
|
517
|
-
* pass-through.
|
|
518
|
-
*
|
|
519
|
-
*
|
|
520
|
-
* approval-policy (top-level and
|
|
521
|
-
* so callers can steer them
|
|
522
|
-
*
|
|
523
|
-
* is
|
|
568
|
+
* pass-through. Two transforms, both confined to `tools/call`:
|
|
569
|
+
* 1. Strip per-call model/effort overrides — the top-level `model` arg and the
|
|
570
|
+
* model-envelope keys inside a `config` override map — so the client cannot
|
|
571
|
+
* escape the pinned model/effort. sandbox/cwd/approval-policy (top-level and
|
|
572
|
+
* inside `config`) are intentionally left intact so callers can steer them
|
|
573
|
+
* per call.
|
|
574
|
+
* 2. Goal injection — codex's native `/goal` is a TUI-only slash command, not
|
|
575
|
+
* reachable via MCP, so a wrapper-only `goal` arg is always stripped and the
|
|
576
|
+
* objective is injected the MCP-correct way: into `developer-instructions`
|
|
577
|
+
* (a developer-role message) for the initial `codex` call, or as a concise
|
|
578
|
+
* prompt reminder for a `codex-reply` turn (which has no
|
|
579
|
+
* `developer-instructions` field). A per-call `goal` overrides the
|
|
580
|
+
* server-wide `--goal` default (`opts.serverGoal`); only a string per-call
|
|
581
|
+
* goal overrides (a blank one suppresses the default for that call), while a
|
|
582
|
+
* non-string `goal` is dropped without disturbing the default.
|
|
583
|
+
* Non-`tools/call`, unparseable, and nothing-to-change lines are returned
|
|
584
|
+
* byte-for-byte unchanged so the MCP framing is preserved; any actual mutation
|
|
585
|
+
* re-serializes the message (the intended, framing-safe path for a changed
|
|
586
|
+
* message).
|
|
524
587
|
* @param {string} line
|
|
588
|
+
* @param {{ serverGoal?: string }} [opts]
|
|
525
589
|
* @returns {string}
|
|
526
590
|
*/
|
|
527
|
-
function filterCodexToolCall(line) {
|
|
591
|
+
function filterCodexToolCall(line, opts = {}) {
|
|
528
592
|
const trimmed = line.trim();
|
|
529
593
|
if (!trimmed) return line;
|
|
530
594
|
|
|
@@ -567,11 +631,55 @@ function filterCodexToolCall(line) {
|
|
|
567
631
|
if (Object.keys(cfg).length === 0) delete args.config;
|
|
568
632
|
}
|
|
569
633
|
|
|
570
|
-
|
|
634
|
+
// ── Goal injection ────────────────────────────────────────────────────────
|
|
635
|
+
// A per-call `goal` (any value) is always stripped — codex's schema has no
|
|
636
|
+
// `goal`, so it must never be forwarded. Only a STRING per-call goal counts as
|
|
637
|
+
// an override: a string (including "") replaces the server default for this
|
|
638
|
+
// call, so "" suppresses it. A non-string `goal` is malformed and is dropped
|
|
639
|
+
// without disturbing the configured server default. A blank effective goal
|
|
640
|
+
// injects nothing.
|
|
641
|
+
let goalLog;
|
|
642
|
+
let goalSource = "server";
|
|
643
|
+
let effectiveGoal = opts.serverGoal;
|
|
644
|
+
if ("goal" in args) {
|
|
645
|
+
const perCallGoal = args.goal;
|
|
646
|
+
delete args.goal;
|
|
647
|
+
goalLog = "stripped per-call goal arg";
|
|
648
|
+
if (typeof perCallGoal === "string") {
|
|
649
|
+
effectiveGoal = perCallGoal;
|
|
650
|
+
goalSource = "per-call";
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
if (effectiveGoal && effectiveGoal.trim()) {
|
|
654
|
+
if (msg.params?.name === "codex") {
|
|
655
|
+
// Initial `codex` call: the native developer-instructions field is the
|
|
656
|
+
// correct, thread-persistent vehicle for a standing objective.
|
|
657
|
+
args["developer-instructions"] = buildGoalDeveloperInstructions(
|
|
658
|
+
effectiveGoal,
|
|
659
|
+
args["developer-instructions"],
|
|
660
|
+
);
|
|
661
|
+
goalLog = `injected ${goalSource} goal into developer-instructions`;
|
|
662
|
+
} else if (msg.params?.name === "codex-reply" && typeof args.prompt === "string") {
|
|
663
|
+
// codex-reply has no developer-instructions field, so restate the
|
|
664
|
+
// objective as a concise prompt reminder. Any other (unknown/future) tool
|
|
665
|
+
// is left untouched — only the wrapper-only `goal` arg stripped above is
|
|
666
|
+
// removed, never the prompt — so the byte-for-byte invariant holds for
|
|
667
|
+
// tools this wrapper does not explicitly support.
|
|
668
|
+
args.prompt = applyGoalPreamble(args.prompt, effectiveGoal);
|
|
669
|
+
goalLog = `injected ${goalSource} goal into codex-reply prompt`;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
571
672
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
)
|
|
673
|
+
if (removed.length === 0 && !goalLog) return line; // nothing changed — keep framing
|
|
674
|
+
|
|
675
|
+
if (removed.length > 0) {
|
|
676
|
+
logErr(
|
|
677
|
+
`[mcp-agents] codex passthrough: pinning model/effort, stripped: ${removed.join(", ")}`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (goalLog) {
|
|
681
|
+
logErr(`[mcp-agents] codex passthrough: ${goalLog}`);
|
|
682
|
+
}
|
|
575
683
|
return JSON.stringify(msg);
|
|
576
684
|
}
|
|
577
685
|
|
|
@@ -582,7 +690,7 @@ function filterCodexToolCall(line) {
|
|
|
582
690
|
* idle watchdog converts an unbounded codex stall (no stdout/stderr while a
|
|
583
691
|
* request is in flight) into a synthesized JSON-RPC error so the caller never
|
|
584
692
|
* hangs forever.
|
|
585
|
-
* @param {{ model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, idleTimeoutMs?: number }} opts
|
|
693
|
+
* @param {{ model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, idleTimeoutMs?: number, goal?: string }} opts
|
|
586
694
|
*/
|
|
587
695
|
function runCodexPassthrough({
|
|
588
696
|
model,
|
|
@@ -590,6 +698,7 @@ function runCodexPassthrough({
|
|
|
590
698
|
sandboxMode,
|
|
591
699
|
approvalPolicy,
|
|
592
700
|
idleTimeoutMs,
|
|
701
|
+
goal,
|
|
593
702
|
}) {
|
|
594
703
|
const resolvedModel = model || DEFAULT_CODEX_MODEL;
|
|
595
704
|
const resolvedModelReasoningEffort =
|
|
@@ -597,6 +706,8 @@ function runCodexPassthrough({
|
|
|
597
706
|
const resolvedSandboxMode = sandboxMode || DEFAULT_CODEX_SANDBOX_MODE;
|
|
598
707
|
const resolvedApprovalPolicy = approvalPolicy || DEFAULT_CODEX_APPROVAL_POLICY;
|
|
599
708
|
const resolvedIdleTimeoutMs = idleTimeoutMs ?? DEFAULT_CODEX_IDLE_TIMEOUT_MS;
|
|
709
|
+
// Server-wide default goal (string or undefined); per-call `goal` overrides it.
|
|
710
|
+
const resolvedGoal = goal;
|
|
600
711
|
let isolatedCodexHome;
|
|
601
712
|
|
|
602
713
|
try {
|
|
@@ -631,6 +742,7 @@ function runCodexPassthrough({
|
|
|
631
742
|
`[mcp-agents] passthrough: codex ${args.join(" ")} ` +
|
|
632
743
|
`(model=${resolvedModel}, reasoning_effort=${resolvedModelReasoningEffort}, ` +
|
|
633
744
|
`sandbox_mode=${resolvedSandboxMode}, approval_policy=${resolvedApprovalPolicy}, ` +
|
|
745
|
+
`goal=${resolvedGoal && resolvedGoal.trim() ? "set" : "none"}, ` +
|
|
634
746
|
`idle_timeout_ms=${resolvedIdleTimeoutMs}, isolated_home=true)`,
|
|
635
747
|
);
|
|
636
748
|
|
|
@@ -1043,7 +1155,7 @@ function runCodexPassthrough({
|
|
|
1043
1155
|
const line = stdinBuf.subarray(0, nl).toString("utf8");
|
|
1044
1156
|
stdinBuf = stdinBuf.subarray(nl + 1);
|
|
1045
1157
|
noteInbound(line);
|
|
1046
|
-
child.stdin.write(`${filterCodexToolCall(line)}\n`);
|
|
1158
|
+
child.stdin.write(`${filterCodexToolCall(line, { serverGoal: resolvedGoal })}\n`);
|
|
1047
1159
|
}
|
|
1048
1160
|
});
|
|
1049
1161
|
process.stdin.on("error", () => {});
|
|
@@ -1051,7 +1163,7 @@ function runCodexPassthrough({
|
|
|
1051
1163
|
if (stdinBuf.length > 0) {
|
|
1052
1164
|
const line = stdinBuf.toString("utf8");
|
|
1053
1165
|
noteInbound(line);
|
|
1054
|
-
child.stdin.write(filterCodexToolCall(line));
|
|
1166
|
+
child.stdin.write(filterCodexToolCall(line, { serverGoal: resolvedGoal }));
|
|
1055
1167
|
}
|
|
1056
1168
|
child.stdin.end();
|
|
1057
1169
|
});
|
|
@@ -1118,6 +1230,7 @@ async function main() {
|
|
|
1118
1230
|
modelReasoningEffort,
|
|
1119
1231
|
sandboxMode,
|
|
1120
1232
|
approvalPolicy,
|
|
1233
|
+
goal,
|
|
1121
1234
|
codexIdleTimeoutMs,
|
|
1122
1235
|
defaultTimeoutMs,
|
|
1123
1236
|
} = parseArgs();
|
|
@@ -1136,6 +1249,7 @@ async function main() {
|
|
|
1136
1249
|
modelReasoningEffort,
|
|
1137
1250
|
sandboxMode,
|
|
1138
1251
|
approvalPolicy,
|
|
1252
|
+
goal,
|
|
1139
1253
|
idleTimeoutMs: codexIdleTimeoutMs,
|
|
1140
1254
|
});
|
|
1141
1255
|
return;
|