@leo000001/codex-mcp 2.1.0 → 2.1.3
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 +92 -22
- package/dist/index.js +810 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -111,21 +111,38 @@ function resolveCodexInvocation(codexArgs, deps = {}) {
|
|
|
111
111
|
const readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
112
112
|
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
113
113
|
const delimiter = platform === "win32" ? ";" : ":";
|
|
114
|
+
const codexCommand = deps.codexCommand ?? "codex";
|
|
115
|
+
const codexIsPath = deps.codexIsPath ?? false;
|
|
116
|
+
if (codexIsPath) {
|
|
117
|
+
if (platform === "win32" && (codexCommand.toLowerCase().endsWith(".cmd") || codexCommand.toLowerCase().endsWith(".bat"))) {
|
|
118
|
+
const comspec2 = env.ComSpec || env.COMSPEC || "cmd.exe";
|
|
119
|
+
return {
|
|
120
|
+
cmd: comspec2,
|
|
121
|
+
args: ["/d", "/s", "/c", codexCommand, ...codexArgs],
|
|
122
|
+
spawnedViaCmd: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { cmd: codexCommand, args: codexArgs, spawnedViaCmd: false };
|
|
126
|
+
}
|
|
114
127
|
if (platform !== "win32") {
|
|
115
|
-
return { cmd:
|
|
128
|
+
return { cmd: codexCommand, args: codexArgs, spawnedViaCmd: false };
|
|
116
129
|
}
|
|
117
|
-
const shim = findOnPath(
|
|
130
|
+
const shim = findOnPath(codexCommand, env, exists, pathApi, delimiter, [".exe", ".cmd", ".bat"]);
|
|
118
131
|
if (shim && shim.toLowerCase().endsWith(".exe")) {
|
|
119
132
|
return { cmd: shim, args: codexArgs, spawnedViaCmd: false };
|
|
120
133
|
}
|
|
121
134
|
if (shim && (shim.toLowerCase().endsWith(".cmd") || shim.toLowerCase().endsWith(".bat"))) {
|
|
122
|
-
const script = tryResolveNodeScriptFromShim(shim, exists, readFile, pathApi);
|
|
135
|
+
const script = tryResolveNodeScriptFromShim(shim, codexCommand, exists, readFile, pathApi);
|
|
123
136
|
if (script) {
|
|
124
137
|
return { cmd: process.execPath, args: [script, ...codexArgs], spawnedViaCmd: false };
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
140
|
const comspec = env.ComSpec || env.COMSPEC || "cmd.exe";
|
|
128
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
cmd: comspec,
|
|
143
|
+
args: ["/d", "/s", "/c", codexCommand, ...codexArgs],
|
|
144
|
+
spawnedViaCmd: true
|
|
145
|
+
};
|
|
129
146
|
}
|
|
130
147
|
function findOnPath(base, env, exists, pathApi, delimiter, exts) {
|
|
131
148
|
const pathEnv = env.PATH || env.Path || env.path || "";
|
|
@@ -146,7 +163,7 @@ function stripSurroundingQuotes(value) {
|
|
|
146
163
|
}
|
|
147
164
|
return value;
|
|
148
165
|
}
|
|
149
|
-
function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
|
|
166
|
+
function tryResolveNodeScriptFromShim(shimPath, codexCommand, exists, readFile, pathApi) {
|
|
150
167
|
let contents;
|
|
151
168
|
try {
|
|
152
169
|
contents = readFile(shimPath);
|
|
@@ -161,7 +178,13 @@ function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
|
|
|
161
178
|
matches.push(m[1]);
|
|
162
179
|
}
|
|
163
180
|
if (matches.length === 0) return void 0;
|
|
164
|
-
const
|
|
181
|
+
const escapedCommand = codexCommand.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
182
|
+
const baseNameRe = new RegExp(escapedCommand, "i");
|
|
183
|
+
const pathRe = new RegExp(
|
|
184
|
+
`@openai\\\\${escapedCommand}|\\\\${escapedCommand}\\\\|\\/${escapedCommand}\\/`,
|
|
185
|
+
"i"
|
|
186
|
+
);
|
|
187
|
+
const preferred = matches.find((m) => baseNameRe.test(pathApi.basename(m))) ?? matches.find((m) => pathRe.test(m)) ?? matches[matches.length - 1];
|
|
165
188
|
const shimDir = pathApi.dirname(shimPath);
|
|
166
189
|
const dp0 = shimDir.endsWith(pathApi.sep) ? shimDir : shimDir + pathApi.sep;
|
|
167
190
|
let resolved = preferred.replace(/%~dp0/gi, dp0).replace(/%dp0%/gi, dp0);
|
|
@@ -171,6 +194,139 @@ function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
|
|
|
171
194
|
return abs;
|
|
172
195
|
}
|
|
173
196
|
|
|
197
|
+
// src/utils/codex-executable.ts
|
|
198
|
+
import { accessSync, constants, existsSync as existsSync2, statSync } from "fs";
|
|
199
|
+
import path2 from "path";
|
|
200
|
+
var CODEX_MCP_COMMAND = "CODEX_MCP_COMMAND";
|
|
201
|
+
var CODEX_MCP_PATH = "CODEX_MCP_PATH";
|
|
202
|
+
var AUTO_CODEX_COMMANDS = ["codex", "codex-internal"];
|
|
203
|
+
var _resolved;
|
|
204
|
+
var WINDOWS_SUPPORTED_EXTENSIONS = [".com", ".exe", ".bat", ".cmd"];
|
|
205
|
+
function stripSurroundingQuotes2(value) {
|
|
206
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
207
|
+
return value.slice(1, -1);
|
|
208
|
+
}
|
|
209
|
+
return value;
|
|
210
|
+
}
|
|
211
|
+
function normalizeMaybeQuotedToken(raw) {
|
|
212
|
+
return stripSurroundingQuotes2(raw.trim());
|
|
213
|
+
}
|
|
214
|
+
function normalizeWindowsExtension(value) {
|
|
215
|
+
const trimmed = stripSurroundingQuotes2(value.trim());
|
|
216
|
+
if (!trimmed) return void 0;
|
|
217
|
+
return trimmed.startsWith(".") ? trimmed.toLowerCase() : `.${trimmed.toLowerCase()}`;
|
|
218
|
+
}
|
|
219
|
+
function isExecutableFile(candidate) {
|
|
220
|
+
try {
|
|
221
|
+
const stat = statSync(candidate);
|
|
222
|
+
if (!stat.isFile()) return false;
|
|
223
|
+
if (process.platform === "win32") return true;
|
|
224
|
+
accessSync(candidate, constants.X_OK);
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function getPathEntries(env) {
|
|
231
|
+
const pathEnv = env.PATH || env.Path || env.path || "";
|
|
232
|
+
return pathEnv.split(process.platform === "win32" ? ";" : ":").map((entry) => stripSurroundingQuotes2(entry.trim())).filter(Boolean);
|
|
233
|
+
}
|
|
234
|
+
function getPathExtensions(env) {
|
|
235
|
+
if (process.platform !== "win32") return [""];
|
|
236
|
+
const configured = env.PATHEXT ?? env.Pathext ?? env.pathext ?? process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
237
|
+
const configuredExts = configured.split(";").map((entry) => normalizeWindowsExtension(entry)).filter((entry) => Boolean(entry));
|
|
238
|
+
const merged = configuredExts.filter(
|
|
239
|
+
(ext) => WINDOWS_SUPPORTED_EXTENSIONS.includes(ext)
|
|
240
|
+
);
|
|
241
|
+
for (const ext of WINDOWS_SUPPORTED_EXTENSIONS) {
|
|
242
|
+
if (!merged.includes(ext)) merged.push(ext);
|
|
243
|
+
}
|
|
244
|
+
return Array.from(new Set(merged));
|
|
245
|
+
}
|
|
246
|
+
function commandExistsOnPath(command, env) {
|
|
247
|
+
const dirs = getPathEntries(env);
|
|
248
|
+
const ext = process.platform === "win32" ? path2.extname(command) : "";
|
|
249
|
+
const names = process.platform === "win32" ? Array.from(
|
|
250
|
+
new Set(
|
|
251
|
+
ext ? [command] : [...getPathExtensions(env).map((suffix) => `${command}${suffix}`), command]
|
|
252
|
+
)
|
|
253
|
+
) : [command];
|
|
254
|
+
for (const dir of dirs) {
|
|
255
|
+
for (const name of names) {
|
|
256
|
+
const candidate = path2.join(dir, name);
|
|
257
|
+
if (isExecutableFile(candidate)) return true;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
function looksLikePath(value) {
|
|
263
|
+
return value.includes("/") || value.includes("\\");
|
|
264
|
+
}
|
|
265
|
+
function resolveDefaultCodexExecutable(env = process.env) {
|
|
266
|
+
const envPathRaw = env[CODEX_MCP_PATH]?.trim();
|
|
267
|
+
const envCommandRaw = env[CODEX_MCP_COMMAND]?.trim();
|
|
268
|
+
const envPath = envPathRaw ? normalizeMaybeQuotedToken(envPathRaw) : void 0;
|
|
269
|
+
const envCommand = envCommandRaw ? normalizeMaybeQuotedToken(envCommandRaw) : void 0;
|
|
270
|
+
if (envPath && envCommand) {
|
|
271
|
+
throw new Error(
|
|
272
|
+
`Cannot set both ${CODEX_MCP_PATH} and ${CODEX_MCP_COMMAND}. Use one or the other.`
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (envPath) {
|
|
276
|
+
const resolvedPath = path2.resolve(envPath);
|
|
277
|
+
if (!existsSync2(resolvedPath)) {
|
|
278
|
+
throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 file does not exist.`);
|
|
279
|
+
}
|
|
280
|
+
if (!isExecutableFile(resolvedPath)) {
|
|
281
|
+
throw new Error(`${CODEX_MCP_PATH}="${envPath}" \u2014 not an executable file.`);
|
|
282
|
+
}
|
|
283
|
+
return { command: resolvedPath, isPath: true, source: "env_path" };
|
|
284
|
+
}
|
|
285
|
+
if (envCommand) {
|
|
286
|
+
if (looksLikePath(envCommand)) {
|
|
287
|
+
throw new Error(
|
|
288
|
+
`${CODEX_MCP_COMMAND}="${envCommand}" looks like a path. Use ${CODEX_MCP_PATH} for filesystem paths.`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
if (!commandExistsOnPath(envCommand, env)) {
|
|
292
|
+
throw new Error(`${CODEX_MCP_COMMAND}="${envCommand}" was not found in PATH.`);
|
|
293
|
+
}
|
|
294
|
+
return { command: envCommand, isPath: false, source: "env_command" };
|
|
295
|
+
}
|
|
296
|
+
for (const candidate of AUTO_CODEX_COMMANDS) {
|
|
297
|
+
if (commandExistsOnPath(candidate, env)) {
|
|
298
|
+
return { command: candidate, isPath: false, source: "auto_detect" };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return { command: "codex", isPath: false, source: "default" };
|
|
302
|
+
}
|
|
303
|
+
function getDefaultCodexExecutable() {
|
|
304
|
+
if (!_resolved) {
|
|
305
|
+
_resolved = resolveDefaultCodexExecutable();
|
|
306
|
+
}
|
|
307
|
+
return _resolved;
|
|
308
|
+
}
|
|
309
|
+
function checkDefaultCodexExecutableAvailability() {
|
|
310
|
+
const info = getDefaultCodexExecutable();
|
|
311
|
+
const label = info.isPath ? "path" : "command";
|
|
312
|
+
switch (info.source) {
|
|
313
|
+
case "env_path":
|
|
314
|
+
console.error(`[codex-executable] Using ${CODEX_MCP_PATH}: ${info.command}`);
|
|
315
|
+
break;
|
|
316
|
+
case "env_command":
|
|
317
|
+
console.error(`[codex-executable] Using ${CODEX_MCP_COMMAND}: ${info.command}`);
|
|
318
|
+
break;
|
|
319
|
+
case "auto_detect":
|
|
320
|
+
console.error(`[codex-executable] Auto-detected ${label}: ${info.command}`);
|
|
321
|
+
break;
|
|
322
|
+
case "default":
|
|
323
|
+
console.error(
|
|
324
|
+
`[codex-executable] No codex found on PATH; falling back to "${info.command}". Set ${CODEX_MCP_COMMAND} or ${CODEX_MCP_PATH} to configure.`
|
|
325
|
+
);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
174
330
|
// src/types.ts
|
|
175
331
|
var APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
|
|
176
332
|
var SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
@@ -216,6 +372,7 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
216
372
|
ErrorCode2["THREAD_FORK_RESUME_FAILED"] = "THREAD_FORK_RESUME_FAILED";
|
|
217
373
|
ErrorCode2["PROTOCOL_PARSE_ERROR"] = "PROTOCOL_PARSE_ERROR";
|
|
218
374
|
ErrorCode2["WRITE_QUEUE_DROPPED"] = "WRITE_QUEUE_DROPPED";
|
|
375
|
+
ErrorCode2["EXEC_NOT_SUPPORTED"] = "EXEC_NOT_SUPPORTED";
|
|
219
376
|
ErrorCode2["INTERNAL"] = "INTERNAL";
|
|
220
377
|
return ErrorCode2;
|
|
221
378
|
})(ErrorCode || {});
|
|
@@ -235,7 +392,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
|
|
|
235
392
|
var CLEANUP_INTERVAL_MS = 6e4;
|
|
236
393
|
|
|
237
394
|
// src/app-server/client.ts
|
|
238
|
-
var CLIENT_VERSION = true ? "2.1.
|
|
395
|
+
var CLIENT_VERSION = true ? "2.1.3" : "0.0.0-dev";
|
|
239
396
|
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
240
397
|
var STARTUP_REQUEST_TIMEOUT = 9e4;
|
|
241
398
|
var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
|
|
@@ -257,6 +414,9 @@ var AppServerClient = class extends EventEmitter {
|
|
|
257
414
|
get destroyed() {
|
|
258
415
|
return this._destroyed;
|
|
259
416
|
}
|
|
417
|
+
get supportsTurnOverrides() {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
260
420
|
/**
|
|
261
421
|
* Spawn codex app-server and perform initialization handshake.
|
|
262
422
|
*/
|
|
@@ -264,7 +424,11 @@ var AppServerClient = class extends EventEmitter {
|
|
|
264
424
|
const args = buildAppServerArgs(opts);
|
|
265
425
|
const env = { ...process.env };
|
|
266
426
|
const stdio = ["pipe", "pipe", "pipe"];
|
|
267
|
-
const
|
|
427
|
+
const exe = getDefaultCodexExecutable();
|
|
428
|
+
const invocation = resolveCodexInvocation(args, {
|
|
429
|
+
codexCommand: exe.command,
|
|
430
|
+
codexIsPath: exe.isPath
|
|
431
|
+
});
|
|
268
432
|
this.spawnedViaCmd = invocation.spawnedViaCmd;
|
|
269
433
|
this.spawnedDetached = process.platform !== "win32";
|
|
270
434
|
const proc = spawn(invocation.cmd, invocation.args, {
|
|
@@ -624,16 +788,16 @@ var AppServerClient = class extends EventEmitter {
|
|
|
624
788
|
};
|
|
625
789
|
|
|
626
790
|
// src/utils/cwd.ts
|
|
627
|
-
import { existsSync as
|
|
628
|
-
import
|
|
791
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
792
|
+
import path3 from "path";
|
|
629
793
|
function resolveAndValidateCwd(inputCwd, baseCwd) {
|
|
630
794
|
const candidate = inputCwd ?? baseCwd;
|
|
631
|
-
const resolved =
|
|
632
|
-
if (!
|
|
795
|
+
const resolved = path3.isAbsolute(candidate) ? candidate : path3.resolve(baseCwd, candidate);
|
|
796
|
+
if (!existsSync3(resolved)) {
|
|
633
797
|
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd does not exist: ${resolved}`);
|
|
634
798
|
}
|
|
635
799
|
try {
|
|
636
|
-
const stat =
|
|
800
|
+
const stat = statSync2(resolved);
|
|
637
801
|
if (!stat.isDirectory()) {
|
|
638
802
|
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd is not a directory: ${resolved}`);
|
|
639
803
|
}
|
|
@@ -655,15 +819,15 @@ function redactPaths(message) {
|
|
|
655
819
|
}
|
|
656
820
|
|
|
657
821
|
// src/utils/files.ts
|
|
658
|
-
import { existsSync as
|
|
659
|
-
import
|
|
822
|
+
import { existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
823
|
+
import path4 from "path";
|
|
660
824
|
function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
|
|
661
|
-
const resolved =
|
|
662
|
-
if (!
|
|
825
|
+
const resolved = path4.isAbsolute(inputPath) ? inputPath : path4.resolve(baseDir, inputPath);
|
|
826
|
+
if (!existsSync4(resolved)) {
|
|
663
827
|
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} does not exist: ${resolved}`);
|
|
664
828
|
}
|
|
665
829
|
try {
|
|
666
|
-
const stat =
|
|
830
|
+
const stat = statSync3(resolved);
|
|
667
831
|
if (!stat.isFile()) {
|
|
668
832
|
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} is not a file: ${resolved}`);
|
|
669
833
|
}
|
|
@@ -841,12 +1005,13 @@ var SessionManager = class {
|
|
|
841
1005
|
const turnStartResult = await client.turnStart(turnParams);
|
|
842
1006
|
const startedTurnId = extractTurnId(turnStartResult);
|
|
843
1007
|
if (startedTurnId) session.activeTurnId = startedTurnId;
|
|
844
|
-
|
|
1008
|
+
const canOverride = client.supportsTurnOverrides;
|
|
1009
|
+
if (resolvedCwd && canOverride) session.cwd = resolvedCwd;
|
|
845
1010
|
if (overrides?.model) session.model = overrides.model;
|
|
846
1011
|
if (overrides?.approvalPolicy) {
|
|
847
1012
|
session.approvalPolicy = overrides.approvalPolicy;
|
|
848
1013
|
}
|
|
849
|
-
if (overrides?.sandbox) {
|
|
1014
|
+
if (overrides?.sandbox && canOverride) {
|
|
850
1015
|
session.sandbox = overrides.sandbox;
|
|
851
1016
|
}
|
|
852
1017
|
} catch (err) {
|
|
@@ -1398,10 +1563,14 @@ var SessionManager = class {
|
|
|
1398
1563
|
switch (method) {
|
|
1399
1564
|
case Methods.THREAD_STARTED: {
|
|
1400
1565
|
const thread = isRecord(p.thread) ? p.thread : void 0;
|
|
1566
|
+
const notifiedThreadId = normalizeOptionalString(p.threadId) ?? normalizeOptionalString(thread?.id);
|
|
1567
|
+
if (notifiedThreadId && notifiedThreadId !== session.threadId) {
|
|
1568
|
+
session.threadId = notifiedThreadId;
|
|
1569
|
+
}
|
|
1401
1570
|
pushEvent(session.eventBuffer, "progress", {
|
|
1402
1571
|
method,
|
|
1403
1572
|
...p,
|
|
1404
|
-
threadId:
|
|
1573
|
+
threadId: notifiedThreadId,
|
|
1405
1574
|
status: normalizeOptionalString(thread?.status)
|
|
1406
1575
|
});
|
|
1407
1576
|
break;
|
|
@@ -2648,6 +2817,7 @@ var ERROR_CODE_HINTS = {
|
|
|
2648
2817
|
["THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */]: "Forked thread could not resume in new process. Retry fork from current source session.",
|
|
2649
2818
|
["PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */]: "Non-JSON or malformed app-server line. Check shell/profile noise and transport health.",
|
|
2650
2819
|
["WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */]: "stdin backpressure overflow. Reduce burst size and re-run in smaller turns.",
|
|
2820
|
+
["EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */]: "Operation not supported in exec mode. Features like threadFork and threadResume require app-server mode.",
|
|
2651
2821
|
["INTERNAL" /* INTERNAL */]: "Unexpected server-side failure. Inspect logs and retry safely."
|
|
2652
2822
|
};
|
|
2653
2823
|
function asTextResource(uri, text, mimeType) {
|
|
@@ -2757,6 +2927,7 @@ function buildGotchasText() {
|
|
|
2757
2927
|
"- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
|
|
2758
2928
|
"- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
|
|
2759
2929
|
"- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
|
|
2930
|
+
"- **Poll frequency guidance**: Adapt poll interval to task complexity and previous poll results. For `running` sessions, start at 2 minutes and increase for long tasks. Only poll frequently (~1s) when `waiting_approval`. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.",
|
|
2760
2931
|
"",
|
|
2761
2932
|
"## Approval behavior",
|
|
2762
2933
|
"",
|
|
@@ -2790,6 +2961,13 @@ function buildGotchasText() {
|
|
|
2790
2961
|
"",
|
|
2791
2962
|
"- codex-mcp does not hard-code a strict concurrent-session cap.",
|
|
2792
2963
|
"- Practical limit depends on machine resources and child-process load.",
|
|
2964
|
+
"",
|
|
2965
|
+
"## Exec fallback mode",
|
|
2966
|
+
"",
|
|
2967
|
+
"- When the codex binary does not support `app-server`, codex-mcp falls back to `exec` mode (`codex exec --json`).",
|
|
2968
|
+
"- Check `codex-mcp:///server-info` `clientMode` field to detect which mode is active.",
|
|
2969
|
+
"- **Exec mode supports multi-turn**: first turn uses `codex exec`, subsequent turns use `codex exec resume <threadId>` for context continuity.",
|
|
2970
|
+
"- **Exec mode limitations**: no approval/user-input interactions, `threadFork`/`threadResume` throw `EXEC_NOT_SUPPORTED`. `sandbox`/`profile`/`cwd`/`outputSchema` overrides only apply on the first turn (exec resume does not support `-s`/`-p`/`-C`/`--output-schema`).",
|
|
2793
2971
|
""
|
|
2794
2972
|
].join("\n");
|
|
2795
2973
|
}
|
|
@@ -2970,6 +3148,7 @@ function registerResources(server, deps) {
|
|
|
2970
3148
|
name: "codex-mcp",
|
|
2971
3149
|
version: deps.version,
|
|
2972
3150
|
codexCliVersion: getCodexCliVersion(),
|
|
3151
|
+
clientMode: deps.clientMode ?? "app-server",
|
|
2973
3152
|
node: process.version,
|
|
2974
3153
|
platform: process.platform,
|
|
2975
3154
|
arch: process.arch,
|
|
@@ -3061,7 +3240,7 @@ function registerResources(server, deps) {
|
|
|
3061
3240
|
}
|
|
3062
3241
|
|
|
3063
3242
|
// src/server.ts
|
|
3064
|
-
var SERVER_VERSION = true ? "2.1.
|
|
3243
|
+
var SERVER_VERSION = true ? "2.1.3" : "0.0.0-dev";
|
|
3065
3244
|
function formatErrorMessage(err) {
|
|
3066
3245
|
const message = err instanceof Error ? err.message : String(err);
|
|
3067
3246
|
const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
|
|
@@ -3080,13 +3259,17 @@ function toStructuredContent(value) {
|
|
|
3080
3259
|
}
|
|
3081
3260
|
return { value };
|
|
3082
3261
|
}
|
|
3083
|
-
function createServer(serverCwd) {
|
|
3084
|
-
const sessionManager = new SessionManager();
|
|
3262
|
+
function createServer(serverCwd, options) {
|
|
3263
|
+
const sessionManager = new SessionManager(options);
|
|
3085
3264
|
const server = new McpServer({
|
|
3086
3265
|
name: "codex-mcp",
|
|
3087
3266
|
version: SERVER_VERSION
|
|
3088
3267
|
});
|
|
3089
|
-
registerResources(server, {
|
|
3268
|
+
registerResources(server, {
|
|
3269
|
+
version: SERVER_VERSION,
|
|
3270
|
+
sessionManager,
|
|
3271
|
+
clientMode: options?.clientMode
|
|
3272
|
+
});
|
|
3090
3273
|
const publicSessionInfoSchema = z.object({
|
|
3091
3274
|
sessionId: z.string(),
|
|
3092
3275
|
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]),
|
|
@@ -3146,10 +3329,10 @@ function createServer(serverCwd) {
|
|
|
3146
3329
|
})
|
|
3147
3330
|
).optional().describe("question-id -> answers map (id from actions[] user_input request).")
|
|
3148
3331
|
}).superRefine((value, ctx) => {
|
|
3149
|
-
const addIssue = (
|
|
3332
|
+
const addIssue = (path5, message) => {
|
|
3150
3333
|
ctx.addIssue({
|
|
3151
3334
|
code: z.ZodIssueCode.custom,
|
|
3152
|
-
path: [
|
|
3335
|
+
path: [path5],
|
|
3153
3336
|
message
|
|
3154
3337
|
});
|
|
3155
3338
|
};
|
|
@@ -3425,9 +3608,10 @@ function createServer(serverCwd) {
|
|
|
3425
3608
|
|
|
3426
3609
|
POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
|
|
3427
3610
|
- Treat pollInterval as a minimum hint, not a fixed schedule.
|
|
3428
|
-
- "running":
|
|
3611
|
+
- "running": sleep at least 2 minutes between polls; increase for complex tasks. Do NOT high-frequency poll \u2014 it wastes tokens and provides no benefit.
|
|
3429
3612
|
- "waiting_approval": poll about every 1000ms and respond quickly to actions[].
|
|
3430
3613
|
- When status is "idle"/"error"/"cancelled": stop polling, the session is done.
|
|
3614
|
+
- Adapt interval based on task complexity and whether the previous poll returned new events.
|
|
3431
3615
|
|
|
3432
3616
|
poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
|
|
3433
3617
|
|
|
@@ -3525,6 +3709,595 @@ cursor omitted => use session last cursor. cursorResetTo => reset and continue.`
|
|
|
3525
3709
|
return server;
|
|
3526
3710
|
}
|
|
3527
3711
|
|
|
3712
|
+
// src/app-server/detect.ts
|
|
3713
|
+
import { spawn as spawn2 } from "child_process";
|
|
3714
|
+
var DETECTION_TIMEOUT_MS = 5e3;
|
|
3715
|
+
async function detectClientMode(codexCommand, codexIsPath = false, env = process.env) {
|
|
3716
|
+
const override = env.CODEX_MCP_MODE;
|
|
3717
|
+
if (override === "app-server" || override === "exec") {
|
|
3718
|
+
return override;
|
|
3719
|
+
}
|
|
3720
|
+
try {
|
|
3721
|
+
const supported = await probeAppServer(codexCommand, codexIsPath, env);
|
|
3722
|
+
return supported ? "app-server" : "exec";
|
|
3723
|
+
} catch {
|
|
3724
|
+
return "exec";
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
function probeAppServer(codexCommand, codexIsPath, env) {
|
|
3728
|
+
return new Promise((resolve) => {
|
|
3729
|
+
const invocation = resolveCodexInvocation(["app-server", "--help"], {
|
|
3730
|
+
env,
|
|
3731
|
+
codexCommand,
|
|
3732
|
+
codexIsPath
|
|
3733
|
+
});
|
|
3734
|
+
let settled = false;
|
|
3735
|
+
const settle = (result) => {
|
|
3736
|
+
if (settled) return;
|
|
3737
|
+
settled = true;
|
|
3738
|
+
clearTimeout(timer);
|
|
3739
|
+
resolve(result);
|
|
3740
|
+
};
|
|
3741
|
+
let stdout = "";
|
|
3742
|
+
let stderr = "";
|
|
3743
|
+
const proc = spawn2(invocation.cmd, invocation.args, {
|
|
3744
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3745
|
+
env,
|
|
3746
|
+
windowsHide: true
|
|
3747
|
+
});
|
|
3748
|
+
proc.stdout?.on("data", (chunk) => {
|
|
3749
|
+
stdout += chunk.toString();
|
|
3750
|
+
});
|
|
3751
|
+
proc.stderr?.on("data", (chunk) => {
|
|
3752
|
+
stderr += chunk.toString();
|
|
3753
|
+
});
|
|
3754
|
+
proc.on("error", () => {
|
|
3755
|
+
settle(false);
|
|
3756
|
+
});
|
|
3757
|
+
proc.on("exit", (code) => {
|
|
3758
|
+
if (code === 0) {
|
|
3759
|
+
settle(true);
|
|
3760
|
+
} else {
|
|
3761
|
+
const combined = (stdout + stderr).toLowerCase();
|
|
3762
|
+
const isUnknown = combined.includes("unknown") || combined.includes("unrecognized") || combined.includes("not found") || combined.includes("no such subcommand");
|
|
3763
|
+
settle(!isUnknown && combined.includes("app-server"));
|
|
3764
|
+
}
|
|
3765
|
+
});
|
|
3766
|
+
const timer = setTimeout(() => {
|
|
3767
|
+
try {
|
|
3768
|
+
proc.kill("SIGTERM");
|
|
3769
|
+
} catch {
|
|
3770
|
+
}
|
|
3771
|
+
settle(false);
|
|
3772
|
+
}, DETECTION_TIMEOUT_MS);
|
|
3773
|
+
if (timer.unref) timer.unref();
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
|
|
3777
|
+
// src/app-server/exec-client.ts
|
|
3778
|
+
import { spawn as spawn3 } from "child_process";
|
|
3779
|
+
import { writeFileSync, mkdtempSync } from "fs";
|
|
3780
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
3781
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3782
|
+
import { tmpdir } from "os";
|
|
3783
|
+
import { join } from "path";
|
|
3784
|
+
import { StringDecoder as StringDecoder2 } from "string_decoder";
|
|
3785
|
+
var FORCE_KILL_TIMEOUT_MS = 5e3;
|
|
3786
|
+
function camelCaseItemType(snakeType) {
|
|
3787
|
+
return snakeType.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
3788
|
+
}
|
|
3789
|
+
function transformItem(item) {
|
|
3790
|
+
const result = { ...item };
|
|
3791
|
+
if (typeof result.type === "string") {
|
|
3792
|
+
result.type = camelCaseItemType(result.type);
|
|
3793
|
+
}
|
|
3794
|
+
return result;
|
|
3795
|
+
}
|
|
3796
|
+
function isRetryableError(event) {
|
|
3797
|
+
const msg = typeof event.message === "string" ? event.message : "";
|
|
3798
|
+
return /reconnect/i.test(msg) || /\d+\/\d+/.test(msg);
|
|
3799
|
+
}
|
|
3800
|
+
function sandboxPolicyToMode(policy) {
|
|
3801
|
+
switch (policy.type) {
|
|
3802
|
+
case "readOnly":
|
|
3803
|
+
return "read-only";
|
|
3804
|
+
case "workspaceWrite":
|
|
3805
|
+
return "workspace-write";
|
|
3806
|
+
case "dangerFullAccess":
|
|
3807
|
+
return "danger-full-access";
|
|
3808
|
+
case "externalSandbox":
|
|
3809
|
+
console.error(
|
|
3810
|
+
`[exec-client] SandboxPolicy type "externalSandbox" cannot be mapped to exec -s flag; using thread-level sandbox`
|
|
3811
|
+
);
|
|
3812
|
+
return void 0;
|
|
3813
|
+
default:
|
|
3814
|
+
return void 0;
|
|
3815
|
+
}
|
|
3816
|
+
}
|
|
3817
|
+
var EXEC_EVENT_TO_METHOD = {
|
|
3818
|
+
// Agent message deltas
|
|
3819
|
+
agent_message_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3820
|
+
agent_message_content_delta: Methods.AGENT_MESSAGE_DELTA,
|
|
3821
|
+
// Command execution
|
|
3822
|
+
exec_command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3823
|
+
command_output_delta: Methods.COMMAND_OUTPUT_DELTA,
|
|
3824
|
+
terminal_interaction: Methods.COMMAND_TERMINAL_INTERACTION,
|
|
3825
|
+
// File changes
|
|
3826
|
+
file_change_output_delta: Methods.FILE_CHANGE_OUTPUT_DELTA,
|
|
3827
|
+
// Reasoning
|
|
3828
|
+
reasoning_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3829
|
+
reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3830
|
+
agent_reasoning_delta: Methods.REASONING_TEXT_DELTA,
|
|
3831
|
+
agent_reasoning_raw_content_delta: Methods.REASONING_TEXT_DELTA,
|
|
3832
|
+
reasoning_summary_delta: Methods.REASONING_SUMMARY_DELTA,
|
|
3833
|
+
agent_reasoning_section_break: Methods.REASONING_SUMMARY_PART_ADDED,
|
|
3834
|
+
// Plan
|
|
3835
|
+
plan_delta: Methods.PLAN_DELTA,
|
|
3836
|
+
plan_update: Methods.TURN_PLAN_UPDATED,
|
|
3837
|
+
// Turn-level
|
|
3838
|
+
turn_diff: Methods.TURN_DIFF_UPDATED,
|
|
3839
|
+
diff_update: Methods.TURN_DIFF_UPDATED,
|
|
3840
|
+
// MCP
|
|
3841
|
+
mcp_tool_call_begin: Methods.MCP_TOOL_PROGRESS,
|
|
3842
|
+
mcp_tool_call_end: Methods.MCP_TOOL_PROGRESS,
|
|
3843
|
+
mcp_startup_update: Methods.MCP_TOOL_PROGRESS,
|
|
3844
|
+
mcp_startup_complete: Methods.MCP_TOOL_PROGRESS,
|
|
3845
|
+
// Model routing
|
|
3846
|
+
model_reroute: Methods.MODEL_REROUTED,
|
|
3847
|
+
// Thread/session events
|
|
3848
|
+
thread_name_updated: Methods.THREAD_NAME_UPDATED,
|
|
3849
|
+
token_count: Methods.THREAD_TOKEN_USAGE_UPDATED,
|
|
3850
|
+
session_configured: Methods.SESSION_CONFIGURED,
|
|
3851
|
+
// Item lifecycle (in case exec emits these outside the dot-notation variants)
|
|
3852
|
+
item_started: Methods.ITEM_STARTED,
|
|
3853
|
+
item_completed: Methods.ITEM_COMPLETED,
|
|
3854
|
+
raw_response_item: Methods.RAW_RESPONSE_ITEM_COMPLETED,
|
|
3855
|
+
// Stream errors — map to error method so retryable detection can handle it
|
|
3856
|
+
stream_error: Methods.ERROR,
|
|
3857
|
+
// Legacy turn lifecycle (v1 wire format used by older CLIs)
|
|
3858
|
+
// These are critical for exec fallback since it targets CLIs without app-server.
|
|
3859
|
+
task_started: Methods.TURN_STARTED,
|
|
3860
|
+
task_complete: Methods.TURN_COMPLETED,
|
|
3861
|
+
turn_aborted: Methods.TURN_COMPLETED
|
|
3862
|
+
};
|
|
3863
|
+
var ExecClient = class extends EventEmitter2 {
|
|
3864
|
+
_destroyed = false;
|
|
3865
|
+
process = null;
|
|
3866
|
+
spawnOpts = null;
|
|
3867
|
+
// Thread/turn state
|
|
3868
|
+
threadId = null;
|
|
3869
|
+
/** Real thread ID from CLI (received via thread.started event). Used for exec resume. */
|
|
3870
|
+
realThreadId = null;
|
|
3871
|
+
turnId = null;
|
|
3872
|
+
turnCount = 0;
|
|
3873
|
+
threadStartParams = null;
|
|
3874
|
+
lastAgentMessageText = "";
|
|
3875
|
+
turnCompleted = false;
|
|
3876
|
+
// Handlers
|
|
3877
|
+
notificationHandler = null;
|
|
3878
|
+
serverRequestHandler = null;
|
|
3879
|
+
// Stdout buffer for JSONL parsing
|
|
3880
|
+
buffer = "";
|
|
3881
|
+
decoder = new StringDecoder2("utf8");
|
|
3882
|
+
get destroyed() {
|
|
3883
|
+
return this._destroyed;
|
|
3884
|
+
}
|
|
3885
|
+
get supportsTurnOverrides() {
|
|
3886
|
+
return this.turnCount <= 1 || this.realThreadId == null;
|
|
3887
|
+
}
|
|
3888
|
+
async start(opts) {
|
|
3889
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3890
|
+
this.spawnOpts = opts;
|
|
3891
|
+
return { userAgent: "codex-exec" };
|
|
3892
|
+
}
|
|
3893
|
+
async threadStart(params) {
|
|
3894
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3895
|
+
this.threadStartParams = params;
|
|
3896
|
+
this.threadId = `exec_thread_${randomUUID2().slice(0, 12)}`;
|
|
3897
|
+
return { thread: { id: this.threadId } };
|
|
3898
|
+
}
|
|
3899
|
+
async threadFork(_params) {
|
|
3900
|
+
throw new Error(
|
|
3901
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadFork is not supported in exec mode`
|
|
3902
|
+
);
|
|
3903
|
+
}
|
|
3904
|
+
async threadResume(_params) {
|
|
3905
|
+
throw new Error(
|
|
3906
|
+
`Error [${"EXEC_NOT_SUPPORTED" /* EXEC_NOT_SUPPORTED */}]: threadResume is not supported in exec mode`
|
|
3907
|
+
);
|
|
3908
|
+
}
|
|
3909
|
+
async threadBackgroundTerminalsClean(_params) {
|
|
3910
|
+
return {};
|
|
3911
|
+
}
|
|
3912
|
+
async turnStart(params) {
|
|
3913
|
+
if (this._destroyed) throw new Error("Client destroyed");
|
|
3914
|
+
if (!this.threadId) throw new Error("No thread started");
|
|
3915
|
+
this.killProcess();
|
|
3916
|
+
this.turnCount++;
|
|
3917
|
+
this.turnId = `exec_turn_${randomUUID2().slice(0, 12)}`;
|
|
3918
|
+
this.lastAgentMessageText = "";
|
|
3919
|
+
this.turnCompleted = false;
|
|
3920
|
+
this.buffer = "";
|
|
3921
|
+
this.decoder = new StringDecoder2("utf8");
|
|
3922
|
+
const prompt = params.input.filter((i) => i.type === "text").map((i) => i.text).join("\n");
|
|
3923
|
+
const images = params.input.filter((i) => i.type === "localImage").map((i) => i.path);
|
|
3924
|
+
const isResume = this.turnCount > 1 && this.realThreadId != null;
|
|
3925
|
+
if (this.turnCount > 1 && !this.realThreadId) {
|
|
3926
|
+
console.error(
|
|
3927
|
+
"[exec-client] No realThreadId available for resume; falling back to fresh exec (context will be lost)"
|
|
3928
|
+
);
|
|
3929
|
+
this.emitNotification(Methods.ERROR, {
|
|
3930
|
+
threadId: this.threadId,
|
|
3931
|
+
turnId: this.turnId,
|
|
3932
|
+
error: "exec mode: multi-turn context unavailable (CLI did not provide thread ID). This turn runs without prior context.",
|
|
3933
|
+
willRetry: true
|
|
3934
|
+
// non-terminal: session continues, just without context
|
|
3935
|
+
});
|
|
3936
|
+
}
|
|
3937
|
+
const args = isResume ? this.buildResumeArgs(prompt, params, images) : this.buildExecArgs(prompt, params, images);
|
|
3938
|
+
const executable = getDefaultCodexExecutable();
|
|
3939
|
+
const invocation = resolveCodexInvocation(args, {
|
|
3940
|
+
codexCommand: executable.command,
|
|
3941
|
+
codexIsPath: executable.isPath
|
|
3942
|
+
});
|
|
3943
|
+
const proc = spawn3(invocation.cmd, invocation.args, {
|
|
3944
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
3945
|
+
env: { ...process.env },
|
|
3946
|
+
detached: process.platform !== "win32",
|
|
3947
|
+
windowsHide: process.platform === "win32"
|
|
3948
|
+
});
|
|
3949
|
+
this.process = proc;
|
|
3950
|
+
proc.stdin?.end();
|
|
3951
|
+
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
3952
|
+
proc.stderr.on("data", (chunk) => {
|
|
3953
|
+
console.error(`[exec-client stderr] ${chunk.toString().trimEnd()}`);
|
|
3954
|
+
});
|
|
3955
|
+
proc.on("error", (err) => {
|
|
3956
|
+
if (!this._destroyed) {
|
|
3957
|
+
this.emit("error", err);
|
|
3958
|
+
}
|
|
3959
|
+
});
|
|
3960
|
+
proc.on("exit", (code, signal) => {
|
|
3961
|
+
if (this.turnId && !this._destroyed && !this.turnCompleted) {
|
|
3962
|
+
if (code !== 0 && code !== null) {
|
|
3963
|
+
this.emitNotification(Methods.ERROR, {
|
|
3964
|
+
threadId: this.threadId,
|
|
3965
|
+
turnId: this.turnId,
|
|
3966
|
+
error: { message: `exec process exited with code ${code}` },
|
|
3967
|
+
willRetry: false
|
|
3968
|
+
});
|
|
3969
|
+
}
|
|
3970
|
+
}
|
|
3971
|
+
if (!this._destroyed) {
|
|
3972
|
+
this.emit("exit", code, signal);
|
|
3973
|
+
}
|
|
3974
|
+
this.process = null;
|
|
3975
|
+
});
|
|
3976
|
+
const turnId = this.turnId;
|
|
3977
|
+
return { turn: { id: turnId } };
|
|
3978
|
+
}
|
|
3979
|
+
async turnInterrupt(_params) {
|
|
3980
|
+
this.killProcess();
|
|
3981
|
+
}
|
|
3982
|
+
onNotification(handler) {
|
|
3983
|
+
this.notificationHandler = handler;
|
|
3984
|
+
}
|
|
3985
|
+
onServerRequest(handler) {
|
|
3986
|
+
this.serverRequestHandler = handler;
|
|
3987
|
+
}
|
|
3988
|
+
respondToServer(_id, _result) {
|
|
3989
|
+
}
|
|
3990
|
+
respondErrorToServer(_id, _code, _message) {
|
|
3991
|
+
}
|
|
3992
|
+
async destroy() {
|
|
3993
|
+
if (this._destroyed) return;
|
|
3994
|
+
this._destroyed = true;
|
|
3995
|
+
const proc = this.process;
|
|
3996
|
+
if (proc && !proc.killed) {
|
|
3997
|
+
const alreadyExited = proc.exitCode !== null;
|
|
3998
|
+
proc.stdin?.end();
|
|
3999
|
+
this.killProcess();
|
|
4000
|
+
const forceKill = setTimeout(() => {
|
|
4001
|
+
if (proc && !proc.killed && proc.exitCode === null) {
|
|
4002
|
+
if (process.platform === "win32" && proc.pid) {
|
|
4003
|
+
try {
|
|
4004
|
+
spawn3("taskkill", ["/PID", String(proc.pid), "/T", "/F"], {
|
|
4005
|
+
stdio: "ignore",
|
|
4006
|
+
windowsHide: true
|
|
4007
|
+
});
|
|
4008
|
+
} catch {
|
|
4009
|
+
}
|
|
4010
|
+
} else {
|
|
4011
|
+
try {
|
|
4012
|
+
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
4013
|
+
else proc.kill("SIGKILL");
|
|
4014
|
+
} catch {
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}, FORCE_KILL_TIMEOUT_MS);
|
|
4019
|
+
forceKill.unref();
|
|
4020
|
+
if (!alreadyExited) {
|
|
4021
|
+
await new Promise((resolve) => {
|
|
4022
|
+
proc.on("exit", () => {
|
|
4023
|
+
clearTimeout(forceKill);
|
|
4024
|
+
resolve();
|
|
4025
|
+
});
|
|
4026
|
+
const fallback = setTimeout(resolve, FORCE_KILL_TIMEOUT_MS + 1e3);
|
|
4027
|
+
fallback.unref();
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
this.process = null;
|
|
4032
|
+
this.removeAllListeners();
|
|
4033
|
+
}
|
|
4034
|
+
// ── Private helpers ─────────────────────────────────────────────
|
|
4035
|
+
/**
|
|
4036
|
+
* Build args for the first turn: `codex exec "<prompt>" --json --skip-git-repo-check [flags]`.
|
|
4037
|
+
* No --ephemeral so the session persists for subsequent resume turns.
|
|
4038
|
+
*/
|
|
4039
|
+
buildExecArgs(prompt, params, images) {
|
|
4040
|
+
const args = ["exec", prompt, "--json", "--skip-git-repo-check"];
|
|
4041
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4042
|
+
if (model) args.push("-m", model);
|
|
4043
|
+
let effectiveSandbox;
|
|
4044
|
+
if (params.sandboxPolicy) {
|
|
4045
|
+
effectiveSandbox = sandboxPolicyToMode(params.sandboxPolicy);
|
|
4046
|
+
}
|
|
4047
|
+
if (!effectiveSandbox) {
|
|
4048
|
+
effectiveSandbox = this.threadStartParams?.sandbox ?? this.spawnOpts?.sandbox;
|
|
4049
|
+
}
|
|
4050
|
+
if (effectiveSandbox) args.push("-s", effectiveSandbox);
|
|
4051
|
+
if (this.spawnOpts?.profile) args.push("-p", this.spawnOpts.profile);
|
|
4052
|
+
const cwd = params.cwd ?? this.threadStartParams?.cwd;
|
|
4053
|
+
if (cwd) args.push("-C", cwd);
|
|
4054
|
+
for (const img of images) args.push("-i", img);
|
|
4055
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4056
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4057
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4058
|
+
try {
|
|
4059
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "codex-mcp-schema-"));
|
|
4060
|
+
const schemaPath = join(tmpDir, "output-schema.json");
|
|
4061
|
+
writeFileSync(schemaPath, JSON.stringify(params.outputSchema));
|
|
4062
|
+
args.push("--output-schema", schemaPath);
|
|
4063
|
+
} catch (err) {
|
|
4064
|
+
console.error(
|
|
4065
|
+
`[exec-client] Failed to write output schema to temp file: ${err instanceof Error ? err.message : String(err)}`
|
|
4066
|
+
);
|
|
4067
|
+
}
|
|
4068
|
+
}
|
|
4069
|
+
const configs = {
|
|
4070
|
+
...this.spawnOpts?.config,
|
|
4071
|
+
...this.threadStartParams?.config
|
|
4072
|
+
};
|
|
4073
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4074
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4075
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4076
|
+
}
|
|
4077
|
+
return args;
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Build args for subsequent turns: `codex exec resume <threadId> "<prompt>" --json [flags]`.
|
|
4081
|
+
* Resumes the persisted session for multi-turn context continuity.
|
|
4082
|
+
* Note: exec resume only supports -m, -c, -i, --json, --skip-git-repo-check.
|
|
4083
|
+
* -s, -p, -C are NOT supported and inherit from the first turn's session.
|
|
4084
|
+
*/
|
|
4085
|
+
buildResumeArgs(prompt, params, images) {
|
|
4086
|
+
const args = [
|
|
4087
|
+
"exec",
|
|
4088
|
+
"resume",
|
|
4089
|
+
this.realThreadId,
|
|
4090
|
+
prompt,
|
|
4091
|
+
"--json",
|
|
4092
|
+
"--skip-git-repo-check"
|
|
4093
|
+
];
|
|
4094
|
+
if (params.sandboxPolicy) {
|
|
4095
|
+
console.error(
|
|
4096
|
+
"[exec-client] sandbox override ignored in resume mode (exec resume does not support -s)"
|
|
4097
|
+
);
|
|
4098
|
+
}
|
|
4099
|
+
if (params.cwd) {
|
|
4100
|
+
console.error(
|
|
4101
|
+
"[exec-client] cwd override ignored in resume mode (exec resume does not support -C)"
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
if (params.outputSchema && Object.keys(params.outputSchema).length > 0) {
|
|
4105
|
+
console.error(
|
|
4106
|
+
"[exec-client] outputSchema ignored in resume mode (exec resume does not support --output-schema)"
|
|
4107
|
+
);
|
|
4108
|
+
}
|
|
4109
|
+
const model = params.model ?? this.threadStartParams?.model ?? this.spawnOpts?.model;
|
|
4110
|
+
if (model) args.push("-m", model);
|
|
4111
|
+
for (const img of images) args.push("-i", img);
|
|
4112
|
+
const approvalPolicy = params.approvalPolicy ?? this.threadStartParams?.approvalPolicy ?? this.spawnOpts?.approvalPolicy;
|
|
4113
|
+
if (approvalPolicy) args.push("-c", `approval_policy=${approvalPolicy}`);
|
|
4114
|
+
const configs = {
|
|
4115
|
+
...this.spawnOpts?.config,
|
|
4116
|
+
...this.threadStartParams?.config
|
|
4117
|
+
};
|
|
4118
|
+
for (const [key, value] of Object.entries(configs)) {
|
|
4119
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
4120
|
+
args.push("-c", `${key}=${serialized}`);
|
|
4121
|
+
}
|
|
4122
|
+
return args;
|
|
4123
|
+
}
|
|
4124
|
+
onData(chunk) {
|
|
4125
|
+
this.buffer += this.decoder.write(chunk);
|
|
4126
|
+
const lines = this.buffer.split("\n");
|
|
4127
|
+
this.buffer = lines.pop() ?? "";
|
|
4128
|
+
for (const line of lines) {
|
|
4129
|
+
const trimmed = line.trim();
|
|
4130
|
+
if (!trimmed || trimmed[0] !== "{") continue;
|
|
4131
|
+
try {
|
|
4132
|
+
const event = JSON.parse(trimmed);
|
|
4133
|
+
this.handleExecEvent(event);
|
|
4134
|
+
} catch {
|
|
4135
|
+
console.error(`[exec-client] Failed to parse JSONL: ${trimmed.slice(0, 200)}`);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
/**
|
|
4140
|
+
* Transform exec JSONL event into app-server notification and dispatch.
|
|
4141
|
+
*/
|
|
4142
|
+
handleExecEvent(event) {
|
|
4143
|
+
const type = event.type;
|
|
4144
|
+
switch (type) {
|
|
4145
|
+
case "thread.started": {
|
|
4146
|
+
const cliThreadId = event.thread_id;
|
|
4147
|
+
if (cliThreadId) {
|
|
4148
|
+
this.threadId = cliThreadId;
|
|
4149
|
+
this.realThreadId = cliThreadId;
|
|
4150
|
+
}
|
|
4151
|
+
this.emitNotification(Methods.THREAD_STARTED, {
|
|
4152
|
+
thread: { id: this.threadId }
|
|
4153
|
+
});
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
case "turn.started":
|
|
4157
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4158
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4159
|
+
});
|
|
4160
|
+
return;
|
|
4161
|
+
case "item.started": {
|
|
4162
|
+
const item = event.item;
|
|
4163
|
+
if (item) {
|
|
4164
|
+
this.emitNotification(Methods.ITEM_STARTED, {
|
|
4165
|
+
threadId: this.threadId,
|
|
4166
|
+
turnId: this.turnId,
|
|
4167
|
+
item: transformItem(item)
|
|
4168
|
+
});
|
|
4169
|
+
}
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
case "item.completed": {
|
|
4173
|
+
const item = event.item;
|
|
4174
|
+
if (item) {
|
|
4175
|
+
const transformed = transformItem(item);
|
|
4176
|
+
if (transformed.type === "agentMessage" && typeof transformed.text === "string") {
|
|
4177
|
+
this.lastAgentMessageText = transformed.text;
|
|
4178
|
+
}
|
|
4179
|
+
this.emitNotification(Methods.ITEM_COMPLETED, {
|
|
4180
|
+
threadId: this.threadId,
|
|
4181
|
+
turnId: this.turnId,
|
|
4182
|
+
item: transformed
|
|
4183
|
+
});
|
|
4184
|
+
}
|
|
4185
|
+
return;
|
|
4186
|
+
}
|
|
4187
|
+
case "turn.completed": {
|
|
4188
|
+
const turnId = this.turnId ?? "";
|
|
4189
|
+
this.turnCompleted = true;
|
|
4190
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4191
|
+
threadId: this.threadId,
|
|
4192
|
+
turn: {
|
|
4193
|
+
id: turnId,
|
|
4194
|
+
status: "completed",
|
|
4195
|
+
output: this.lastAgentMessageText || void 0,
|
|
4196
|
+
usage: event.usage
|
|
4197
|
+
}
|
|
4198
|
+
});
|
|
4199
|
+
this.turnId = null;
|
|
4200
|
+
return;
|
|
4201
|
+
}
|
|
4202
|
+
case "turn.failed": {
|
|
4203
|
+
const turnId = this.turnId ?? "";
|
|
4204
|
+
const error = event.error;
|
|
4205
|
+
this.turnCompleted = true;
|
|
4206
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4207
|
+
threadId: this.threadId,
|
|
4208
|
+
turn: {
|
|
4209
|
+
id: turnId,
|
|
4210
|
+
status: "failed",
|
|
4211
|
+
error: error ?? { message: "Turn failed" }
|
|
4212
|
+
}
|
|
4213
|
+
});
|
|
4214
|
+
this.turnId = null;
|
|
4215
|
+
return;
|
|
4216
|
+
}
|
|
4217
|
+
case "error": {
|
|
4218
|
+
const willRetry = isRetryableError(event);
|
|
4219
|
+
this.emitNotification(Methods.ERROR, {
|
|
4220
|
+
threadId: this.threadId,
|
|
4221
|
+
turnId: this.turnId,
|
|
4222
|
+
error: event.message ?? event.error,
|
|
4223
|
+
willRetry
|
|
4224
|
+
});
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
default:
|
|
4228
|
+
break;
|
|
4229
|
+
}
|
|
4230
|
+
const mappedMethod = EXEC_EVENT_TO_METHOD[type];
|
|
4231
|
+
if (mappedMethod) {
|
|
4232
|
+
if (type === "task_started") {
|
|
4233
|
+
const turnId = event.turn_id ?? this.turnId;
|
|
4234
|
+
if (turnId) this.turnId = turnId;
|
|
4235
|
+
this.emitNotification(Methods.TURN_STARTED, {
|
|
4236
|
+
turn: { id: this.turnId, status: "inProgress" }
|
|
4237
|
+
});
|
|
4238
|
+
} else if (type === "task_complete") {
|
|
4239
|
+
const turnId = this.turnId ?? "";
|
|
4240
|
+
this.turnCompleted = true;
|
|
4241
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4242
|
+
threadId: this.threadId,
|
|
4243
|
+
turn: {
|
|
4244
|
+
id: turnId,
|
|
4245
|
+
status: "completed",
|
|
4246
|
+
output: this.lastAgentMessageText || void 0
|
|
4247
|
+
}
|
|
4248
|
+
});
|
|
4249
|
+
this.turnId = null;
|
|
4250
|
+
} else if (type === "turn_aborted") {
|
|
4251
|
+
const turnId = this.turnId ?? "";
|
|
4252
|
+
this.turnCompleted = true;
|
|
4253
|
+
this.emitNotification(Methods.TURN_COMPLETED, {
|
|
4254
|
+
threadId: this.threadId,
|
|
4255
|
+
turn: {
|
|
4256
|
+
id: turnId,
|
|
4257
|
+
status: "cancelled",
|
|
4258
|
+
error: event.reason ?? { message: "Turn aborted" }
|
|
4259
|
+
}
|
|
4260
|
+
});
|
|
4261
|
+
this.turnId = null;
|
|
4262
|
+
} else if (mappedMethod === Methods.ERROR) {
|
|
4263
|
+
this.emitNotification(Methods.ERROR, {
|
|
4264
|
+
threadId: this.threadId,
|
|
4265
|
+
turnId: this.turnId,
|
|
4266
|
+
error: event.message ?? event.error ?? type,
|
|
4267
|
+
willRetry: isRetryableError(event)
|
|
4268
|
+
});
|
|
4269
|
+
} else {
|
|
4270
|
+
this.emitNotification(mappedMethod, {
|
|
4271
|
+
threadId: this.threadId,
|
|
4272
|
+
turnId: this.turnId,
|
|
4273
|
+
...event
|
|
4274
|
+
});
|
|
4275
|
+
}
|
|
4276
|
+
return;
|
|
4277
|
+
}
|
|
4278
|
+
console.error(`[exec-client] Unmapped exec event type: ${type}`);
|
|
4279
|
+
}
|
|
4280
|
+
emitNotification(method, params) {
|
|
4281
|
+
if (this.notificationHandler) {
|
|
4282
|
+
this.notificationHandler(method, params);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
killProcess() {
|
|
4286
|
+
if (!this.process || this.process.killed) return;
|
|
4287
|
+
if (process.platform !== "win32" && this.process.pid) {
|
|
4288
|
+
try {
|
|
4289
|
+
process.kill(-this.process.pid, "SIGTERM");
|
|
4290
|
+
return;
|
|
4291
|
+
} catch {
|
|
4292
|
+
}
|
|
4293
|
+
}
|
|
4294
|
+
try {
|
|
4295
|
+
this.process.kill("SIGTERM");
|
|
4296
|
+
} catch {
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
};
|
|
4300
|
+
|
|
3528
4301
|
// src/index.ts
|
|
3529
4302
|
async function main() {
|
|
3530
4303
|
const preflight = runStdioPreflight();
|
|
@@ -3545,8 +4318,16 @@ async function main() {
|
|
|
3545
4318
|
"STDIO preflight failed in strict mode due to blocking stdout contamination risk"
|
|
3546
4319
|
);
|
|
3547
4320
|
}
|
|
4321
|
+
checkDefaultCodexExecutableAvailability();
|
|
4322
|
+
const executable = getDefaultCodexExecutable();
|
|
4323
|
+
const clientMode = await detectClientMode(executable.command, executable.isPath);
|
|
4324
|
+
console.error(`[codex-mcp] client mode: ${clientMode} (binary: ${executable.command})`);
|
|
4325
|
+
const createClient = () => clientMode === "exec" ? new ExecClient() : new AppServerClient();
|
|
3548
4326
|
const serverCwd = process.cwd();
|
|
3549
|
-
const server = createServer(serverCwd
|
|
4327
|
+
const server = createServer(serverCwd, {
|
|
4328
|
+
createClient,
|
|
4329
|
+
clientMode
|
|
4330
|
+
});
|
|
3550
4331
|
const transport = new StdioServerTransport();
|
|
3551
4332
|
let closing = false;
|
|
3552
4333
|
const shutdown = async () => {
|