@leo000001/codex-mcp 2.1.0 → 2.1.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/README.md CHANGED
@@ -19,7 +19,7 @@ MCP server that wraps [OpenAI Codex](https://github.com/openai/codex) `app-serve
19
19
  ## Prerequisites
20
20
 
21
21
  - [Node.js](https://nodejs.org) >= 18
22
- - [OpenAI Codex CLI](https://github.com/openai/codex) installed and configured (`codex` in PATH)
22
+ - [OpenAI Codex CLI](https://github.com/openai/codex) installed and configured (`codex` or `codex-internal` in PATH)
23
23
 
24
24
  ## Quick Start
25
25
 
@@ -76,6 +76,46 @@ Or add to `~/.claude/settings.json`:
76
76
  }
77
77
  ```
78
78
 
79
+ ## Codex Executable Configuration
80
+
81
+ By default, codex-mcp auto-detects the Codex CLI by searching PATH for `codex`, then `codex-internal`. You can override this with environment variables:
82
+
83
+ | Variable | Description | Example |
84
+ | --- | --- | --- |
85
+ | `CODEX_MCP_DEFAULT_CODEX_COMMAND` | Bare command name (resolved from PATH) | `codex-internal` |
86
+ | `CODEX_MCP_DEFAULT_CODEX_PATH` | Absolute or relative filesystem path to the executable | `/usr/local/bin/codex-internal` |
87
+
88
+ - The two variables are **mutually exclusive** — setting both causes a startup error.
89
+ - When neither is set, codex-mcp tries `codex` then `codex-internal` on PATH automatically.
90
+
91
+ Examples:
92
+
93
+ ```bash
94
+ # Use codex-internal instead of codex
95
+ CODEX_MCP_DEFAULT_CODEX_COMMAND=codex-internal npx -y @leo000001/codex-mcp
96
+ ```
97
+
98
+ ```bash
99
+ # Use an explicit path
100
+ CODEX_MCP_DEFAULT_CODEX_PATH=/opt/codex/bin/codex npx -y @leo000001/codex-mcp
101
+ ```
102
+
103
+ MCP client config with env override:
104
+
105
+ ```json
106
+ {
107
+ "mcpServers": {
108
+ "codex": {
109
+ "command": "npx",
110
+ "args": ["-y", "@leo000001/codex-mcp"],
111
+ "env": {
112
+ "CODEX_MCP_DEFAULT_CODEX_COMMAND": "codex-internal"
113
+ }
114
+ }
115
+ }
116
+ }
117
+ ```
118
+
79
119
  ## STDIO Guard Modes
80
120
 
81
121
  `codex-mcp` includes a startup preflight guard for stdout contamination risk.
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: "codex", args: codexArgs, spawnedViaCmd: false };
128
+ return { cmd: codexCommand, args: codexArgs, spawnedViaCmd: false };
116
129
  }
117
- const shim = findOnPath("codex", env, exists, pathApi, delimiter, [".exe", ".cmd", ".bat"]);
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 { cmd: comspec, args: ["/d", "/s", "/c", "codex", ...codexArgs], spawnedViaCmd: true };
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 preferred = matches.find((m) => /codex/i.test(pathApi.basename(m))) ?? matches.find((m) => /@openai\\codex|\\codex\\|\/codex\//i.test(m)) ?? matches[matches.length - 1];
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_DEFAULT_CODEX_COMMAND = "CODEX_MCP_DEFAULT_CODEX_COMMAND";
201
+ var CODEX_MCP_DEFAULT_CODEX_PATH = "CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_PATH]?.trim();
267
+ const envCommandRaw = env[CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_PATH} and ${CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_PATH}="${envPath}" \u2014 file does not exist.`);
279
+ }
280
+ if (!isExecutableFile(resolvedPath)) {
281
+ throw new Error(`${CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_COMMAND}="${envCommand}" looks like a path. Use ${CODEX_MCP_DEFAULT_CODEX_PATH} for filesystem paths.`
289
+ );
290
+ }
291
+ if (!commandExistsOnPath(envCommand, env)) {
292
+ throw new Error(`${CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_PATH}: ${info.command}`);
315
+ break;
316
+ case "env_command":
317
+ console.error(`[codex-executable] Using ${CODEX_MCP_DEFAULT_CODEX_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_DEFAULT_CODEX_COMMAND} or ${CODEX_MCP_DEFAULT_CODEX_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"];
@@ -235,7 +391,7 @@ var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
235
391
  var CLEANUP_INTERVAL_MS = 6e4;
236
392
 
237
393
  // src/app-server/client.ts
238
- var CLIENT_VERSION = true ? "2.1.0" : "0.0.0-dev";
394
+ var CLIENT_VERSION = true ? "2.1.1" : "0.0.0-dev";
239
395
  var DEFAULT_REQUEST_TIMEOUT = 3e4;
240
396
  var STARTUP_REQUEST_TIMEOUT = 9e4;
241
397
  var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
@@ -264,7 +420,11 @@ var AppServerClient = class extends EventEmitter {
264
420
  const args = buildAppServerArgs(opts);
265
421
  const env = { ...process.env };
266
422
  const stdio = ["pipe", "pipe", "pipe"];
267
- const invocation = resolveCodexInvocation(args);
423
+ const exe = getDefaultCodexExecutable();
424
+ const invocation = resolveCodexInvocation(args, {
425
+ codexCommand: exe.command,
426
+ codexIsPath: exe.isPath
427
+ });
268
428
  this.spawnedViaCmd = invocation.spawnedViaCmd;
269
429
  this.spawnedDetached = process.platform !== "win32";
270
430
  const proc = spawn(invocation.cmd, invocation.args, {
@@ -624,16 +784,16 @@ var AppServerClient = class extends EventEmitter {
624
784
  };
625
785
 
626
786
  // src/utils/cwd.ts
627
- import { existsSync as existsSync2, statSync } from "fs";
628
- import path2 from "path";
787
+ import { existsSync as existsSync3, statSync as statSync2 } from "fs";
788
+ import path3 from "path";
629
789
  function resolveAndValidateCwd(inputCwd, baseCwd) {
630
790
  const candidate = inputCwd ?? baseCwd;
631
- const resolved = path2.isAbsolute(candidate) ? candidate : path2.resolve(baseCwd, candidate);
632
- if (!existsSync2(resolved)) {
791
+ const resolved = path3.isAbsolute(candidate) ? candidate : path3.resolve(baseCwd, candidate);
792
+ if (!existsSync3(resolved)) {
633
793
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd does not exist: ${resolved}`);
634
794
  }
635
795
  try {
636
- const stat = statSync(resolved);
796
+ const stat = statSync2(resolved);
637
797
  if (!stat.isDirectory()) {
638
798
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd is not a directory: ${resolved}`);
639
799
  }
@@ -655,15 +815,15 @@ function redactPaths(message) {
655
815
  }
656
816
 
657
817
  // src/utils/files.ts
658
- import { existsSync as existsSync3, statSync as statSync2 } from "fs";
659
- import path3 from "path";
818
+ import { existsSync as existsSync4, statSync as statSync3 } from "fs";
819
+ import path4 from "path";
660
820
  function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
661
- const resolved = path3.isAbsolute(inputPath) ? inputPath : path3.resolve(baseDir, inputPath);
662
- if (!existsSync3(resolved)) {
821
+ const resolved = path4.isAbsolute(inputPath) ? inputPath : path4.resolve(baseDir, inputPath);
822
+ if (!existsSync4(resolved)) {
663
823
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} does not exist: ${resolved}`);
664
824
  }
665
825
  try {
666
- const stat = statSync2(resolved);
826
+ const stat = statSync3(resolved);
667
827
  if (!stat.isFile()) {
668
828
  throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} is not a file: ${resolved}`);
669
829
  }
@@ -3061,7 +3221,7 @@ function registerResources(server, deps) {
3061
3221
  }
3062
3222
 
3063
3223
  // src/server.ts
3064
- var SERVER_VERSION = true ? "2.1.0" : "0.0.0-dev";
3224
+ var SERVER_VERSION = true ? "2.1.1" : "0.0.0-dev";
3065
3225
  function formatErrorMessage(err) {
3066
3226
  const message = err instanceof Error ? err.message : String(err);
3067
3227
  const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
@@ -3146,10 +3306,10 @@ function createServer(serverCwd) {
3146
3306
  })
3147
3307
  ).optional().describe("question-id -> answers map (id from actions[] user_input request).")
3148
3308
  }).superRefine((value, ctx) => {
3149
- const addIssue = (path4, message) => {
3309
+ const addIssue = (path5, message) => {
3150
3310
  ctx.addIssue({
3151
3311
  code: z.ZodIssueCode.custom,
3152
- path: [path4],
3312
+ path: [path5],
3153
3313
  message
3154
3314
  });
3155
3315
  };
@@ -3545,6 +3705,7 @@ async function main() {
3545
3705
  "STDIO preflight failed in strict mode due to blocking stdout contamination risk"
3546
3706
  );
3547
3707
  }
3708
+ checkDefaultCodexExecutableAvailability();
3548
3709
  const serverCwd = process.cwd();
3549
3710
  const server = createServer(serverCwd);
3550
3711
  const transport = new StdioServerTransport();