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.
Files changed (3) hide show
  1. package/README.md +53 -4
  2. package/package.json +1 -1
  3. 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
- Hardcoded defaults: `sandbox_mode=read-only`, `approval_policy=never`,
90
- `features.multi_agent=false`.
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` 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
+ 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` arguments sent to the native `codex` tool are stripped before reaching Codex, so they cannot override the startup defaults.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-agents",
3
- "version": "0.11.0",
3
+ "version": "0.12.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
@@ -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 flags.
206
- * @returns {{ provider: string, model?: string, modelReasoningEffort?: string, sandboxMode?: string, approvalPolicy?: string, codexIdleTimeoutMs?: number, defaultTimeoutMs?: number }}
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. Strips per-call model/effort overrides from `tools/call` so the
518
- * client cannot escape the pinned model/effort — both the top-level `model` arg
519
- * and the model-envelope keys inside a `config` override map. sandbox/cwd/
520
- * approval-policy (top-level and inside `config`) are intentionally left intact
521
- * so callers can steer them per call. Non-`tools/call`, unparseable, and
522
- * nothing-to-strip lines are returned byte-for-byte unchanged so the MCP framing
523
- * is preserved.
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
- if (removed.length === 0) return line; // nothing pinned to strip — keep framing
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
- logErr(
573
- `[mcp-agents] codex passthrough: pinning model/effort, stripped: ${removed.join(", ")}`,
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;