@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 +41 -1
- package/dist/index.js +182 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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:
|
|
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_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.
|
|
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
|
|
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
|
|
628
|
-
import
|
|
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 =
|
|
632
|
-
if (!
|
|
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 =
|
|
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
|
|
659
|
-
import
|
|
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 =
|
|
662
|
-
if (!
|
|
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 =
|
|
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.
|
|
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 = (
|
|
3309
|
+
const addIssue = (path5, message) => {
|
|
3150
3310
|
ctx.addIssue({
|
|
3151
3311
|
code: z.ZodIssueCode.custom,
|
|
3152
|
-
path: [
|
|
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();
|