@alvarosw/codex-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/package.json +30 -0
- package/src/codex.js +176 -0
- package/src/index.js +125 -0
- package/src/tools.js +178 -0
- package/test/smoke.mjs +150 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Codex MCP
|
|
2
|
+
|
|
3
|
+
A lightweight [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes the local [Codex CLI](https://github.com/openai/codex) (`codex`) to MCP-compatible coding agents.
|
|
4
|
+
|
|
5
|
+
The project intentionally keeps the architecture small: MCP tools validate inputs, a thin CLI adapter executes `codex exec` (and friends), and `codex_raw` provides an escape hatch for CLI options that are added in future Codex releases.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Run Codex as a local coding agent through MCP (`codex exec`).
|
|
10
|
+
- Run non-interactive code reviews (`codex exec review`).
|
|
11
|
+
- Resume or fork previous sessions (`codex exec resume` / `codex exec fork`).
|
|
12
|
+
- Control common CLI options such as model, sandbox policy, approval behavior, extra directories, images, and output handling.
|
|
13
|
+
- Inspect version, login status, and diagnostics (`codex doctor`).
|
|
14
|
+
- Pass arbitrary CLI arguments through `codex_raw` for forward compatibility.
|
|
15
|
+
- No shell execution: arguments are passed directly to the `codex` process (`shell: false`).
|
|
16
|
+
- Optional server-wide model override via an environment variable (see below).
|
|
17
|
+
|
|
18
|
+
## Requirements
|
|
19
|
+
|
|
20
|
+
- Node.js 18+
|
|
21
|
+
- Codex CLI installed and authenticated (`codex login`)
|
|
22
|
+
- `codex` available on `PATH`
|
|
23
|
+
- An MCP-compatible client
|
|
24
|
+
|
|
25
|
+
If `codex` is not on `PATH`, set `CODEX_MCP_CMD` to the executable path.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
CODEX_MCP_CMD=/custom/path/codex
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Windows PowerShell:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
$env:CODEX_MCP_CMD = "C:\path\to\codex.exe"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Forcing a specific model
|
|
38
|
+
|
|
39
|
+
Every tool that runs the agent (`codex_run`, `codex_review`, `codex_resume`, `codex_fork`, and `codex_raw` when its first argument is `exec`) accepts a `model` input. If you set the `CODEX_MCP_MODEL` environment variable on the MCP server process, it overrides the model for **every** such call, regardless of what the caller (or a raw argument list) requests. This is useful when you want to pin the server to a single model — for cost control, quota limits, or consistency — no matter what any individual tool call asks for.
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
CODEX_MCP_MODEL=your-model-id
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The override is applied last, after stripping any `-m`/`--model` flag or `-c model=...` config override already present in the constructed arguments, so it always wins. It is never applied to subcommands that don't accept a model (`--version`, `login status`, `help`, `doctor`, ...), so those keep working normally even when the override is set.
|
|
46
|
+
|
|
47
|
+
## Quick Start
|
|
48
|
+
|
|
49
|
+
The recommended setup is through npm. You do not need to clone this repository or install the MCP server manually.
|
|
50
|
+
|
|
51
|
+
### Claude Code
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
claude mcp add --scope user codex -- npx -y codex-mcp
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Verify the server:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
claude mcp list
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
If `codex` is not on `PATH`, or you want to pin the model:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
claude mcp add --scope user \
|
|
67
|
+
--env CODEX_MCP_CMD=/custom/path/codex \
|
|
68
|
+
--env CODEX_MCP_MODEL=your-model-id \
|
|
69
|
+
codex -- npx -y codex-mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Gemini CLI
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
gemini mcp add --scope user codex npx -y codex-mcp
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Verify:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
gemini mcp list
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Cursor
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"codex": {
|
|
90
|
+
"command": "npx",
|
|
91
|
+
"args": ["-y", "codex-mcp"]
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Windsurf
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"mcpServers": {
|
|
102
|
+
"codex": {
|
|
103
|
+
"command": "npx",
|
|
104
|
+
"args": ["-y", "codex-mcp"]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Cline / Roo Code / Other MCP Clients
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"command": "npx",
|
|
115
|
+
"args": ["-y", "codex-mcp"]
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
If the client supports environment variables, `CODEX_MCP_CMD` and `CODEX_MCP_MODEL` can be set there as well.
|
|
120
|
+
|
|
121
|
+
## Local Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
git clone https://github.com/alvarosw/codex-mcp.git
|
|
125
|
+
cd codex-mcp
|
|
126
|
+
npm install
|
|
127
|
+
npm start
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
No build step is required.
|
|
131
|
+
|
|
132
|
+
### Testing
|
|
133
|
+
|
|
134
|
+
`npm test` runs a static syntax check only — it makes no network calls and costs nothing.
|
|
135
|
+
|
|
136
|
+
`npm run test:live` drives the real MCP server end to end over stdio against a real, authenticated Codex CLI (in a throwaway temp git repo it creates and cleans up). It exercises every tool, including verifying that `CODEX_MCP_MODEL` (if set) wins over a deliberately wrong model passed in a tool call or smuggled into `codex_raw` arguments. This makes real model calls and is not run automatically — you need `codex login` completed first, and it will consume real quota/tokens against whichever model resolves for the call:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
CODEX_MCP_MODEL=your-model-id npm run test:live
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`CODEX_MCP_MODEL` is optional for this script; without it, the override-specific assertions are skipped and the rest of the suite still runs against your account's default configured model.
|
|
143
|
+
|
|
144
|
+
## Tools
|
|
145
|
+
|
|
146
|
+
### `codex_run`
|
|
147
|
+
|
|
148
|
+
Run Codex as an agent non-interactively (`codex exec`) with common CLI controls: prompt, model, sandbox policy, approval routing, extra directories, images, working directory, ephemeral/persisted sessions, and raw passthrough args.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"prompt": "Review the authentication implementation and identify security issues.",
|
|
155
|
+
"cwd": "/workspace/project",
|
|
156
|
+
"sandbox": "workspace-write",
|
|
157
|
+
"addDirs": ["/workspace/shared"]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `codex_review`
|
|
162
|
+
|
|
163
|
+
Runs `codex exec review` non-interactively against the current repository. Supports `uncommitted`, `base`, `commit`, and `title`, plus a custom review `prompt`.
|
|
164
|
+
|
|
165
|
+
### `codex_resume`
|
|
166
|
+
|
|
167
|
+
Resumes a previous session (`codex exec resume`) by `sessionId`, or the most recent one if omitted, optionally sending a new `prompt`.
|
|
168
|
+
|
|
169
|
+
### `codex_fork`
|
|
170
|
+
|
|
171
|
+
Forks a previous session (`codex exec fork`) by `sessionId` into a new session, optionally sending a `prompt`.
|
|
172
|
+
|
|
173
|
+
### `codex_version`
|
|
174
|
+
|
|
175
|
+
Returns the installed Codex CLI version.
|
|
176
|
+
|
|
177
|
+
### `codex_login_status`
|
|
178
|
+
|
|
179
|
+
Runs `codex login status` to check authentication state.
|
|
180
|
+
|
|
181
|
+
### `codex_doctor`
|
|
182
|
+
|
|
183
|
+
Runs `codex doctor --json` for install, auth, config, and connectivity diagnostics.
|
|
184
|
+
|
|
185
|
+
### `codex_help`
|
|
186
|
+
|
|
187
|
+
Shows CLI help. A command can be provided for command-specific help.
|
|
188
|
+
|
|
189
|
+
### `codex_raw`
|
|
190
|
+
|
|
191
|
+
Runs `codex` with an arbitrary argument array. This is the compatibility escape hatch for flags or commands not covered by the convenience tools. The model override, if set, still applies when the first argument is `exec`.
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"args": ["mcp", "list"]
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Architecture
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
src/
|
|
205
|
+
├── index.js # MCP server and tool registration
|
|
206
|
+
├── tools.js # Tool behavior and response formatting (JSONL event parsing)
|
|
207
|
+
└── codex.js # Thin process adapter for the codex CLI + model-override enforcement
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Dependency direction:
|
|
211
|
+
|
|
212
|
+
```text
|
|
213
|
+
MCP transport
|
|
214
|
+
↓
|
|
215
|
+
tool handlers
|
|
216
|
+
↓
|
|
217
|
+
codex CLI adapter
|
|
218
|
+
↓
|
|
219
|
+
local codex executable
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
There is intentionally no service container, repository layer, or framework abstraction. The project has one external process boundary and keeps that boundary explicit.
|
|
223
|
+
|
|
224
|
+
Tools that invoke `codex exec*` request `--json` internally so output can be parsed reliably; each tool's text response is the agent's final message(s), with the full parsed event stream, command executions, token usage, and thread id available in `structuredContent` for programmatic consumers.
|
|
225
|
+
|
|
226
|
+
## Security Notes
|
|
227
|
+
|
|
228
|
+
`codex_raw` can execute arbitrary Codex CLI arguments with the permissions of the user running the MCP server. The server itself does not invoke a shell, so tool arguments are not shell-interpreted, but `codex` still has whatever permissions its sandbox and approval settings grant it.
|
|
229
|
+
|
|
230
|
+
`dangerouslyBypassApprovalsAndSandbox` skips all confirmation prompts and sandboxing. Only use it when you explicitly trust the task and an already-isolated workspace.
|
|
231
|
+
|
|
232
|
+
Environment variables passed through the `env` field are inherited by the `codex` process. Avoid sending secrets through MCP tool arguments unless necessary.
|
|
233
|
+
|
|
234
|
+
## License
|
|
235
|
+
|
|
236
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alvarosw/codex-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A lightweight MCP server that exposes the local Codex CLI to MCP-compatible coding agents.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"codex-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "node --check src/index.js && node --check src/codex.js && node --check src/tools.js",
|
|
13
|
+
"test:live": "node test/smoke.mjs"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"codex",
|
|
18
|
+
"openai",
|
|
19
|
+
"claude-code",
|
|
20
|
+
"agent"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@modelcontextprotocol/sdk": "^1.17.5",
|
|
28
|
+
"zod": "^3.23.8"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/codex.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
function commandName() {
|
|
4
|
+
return process.env.CODEX_MCP_CMD || "codex";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function forcedModel() {
|
|
8
|
+
const value = process.env.CODEX_MCP_MODEL;
|
|
9
|
+
return value && value.trim() ? value.trim() : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Strips any user- or tool-supplied model selection (-m/--model, or a
|
|
14
|
+
* `-c model=...` / `--config model=...` config override) from an argument
|
|
15
|
+
* list, then appends a single `-m <override>` at the end. A trailing -m
|
|
16
|
+
* wins over an earlier -c model=... override, and clap resolves flags
|
|
17
|
+
* placed after positional arguments fine, so appending at the very end
|
|
18
|
+
* is sufficient to make the override authoritative for every call.
|
|
19
|
+
*/
|
|
20
|
+
export function applyModelOverride(args) {
|
|
21
|
+
const override = forcedModel();
|
|
22
|
+
if (!override) return args;
|
|
23
|
+
|
|
24
|
+
const out = [];
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const a = args[i];
|
|
27
|
+
|
|
28
|
+
if (a === "-m" || a === "--model") {
|
|
29
|
+
i++; // also skip its value
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (a.startsWith("--model=")) continue;
|
|
33
|
+
|
|
34
|
+
if (a === "-c" || a === "--config") {
|
|
35
|
+
const val = args[i + 1] ?? "";
|
|
36
|
+
if (/^model\s*=/.test(val)) {
|
|
37
|
+
i++; // also skip its value
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
out.push(a);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (a.startsWith("-c=") || a.startsWith("--config=")) {
|
|
44
|
+
const val = a.slice(a.indexOf("=") + 1);
|
|
45
|
+
if (/^model\s*=/.test(val)) continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
out.push(a);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
out.push("-m", override);
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runCodex(codexArgs, options = {}) {
|
|
56
|
+
const args = Array.isArray(codexArgs) ? codexArgs : [];
|
|
57
|
+
const cwd = options.cwd || process.cwd();
|
|
58
|
+
const timeoutMs = options.timeoutMs ?? 10 * 60 * 1000;
|
|
59
|
+
const env = { ...process.env, ...(options.env || {}) };
|
|
60
|
+
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const child = spawn(commandName(), args, {
|
|
63
|
+
cwd,
|
|
64
|
+
env,
|
|
65
|
+
shell: false,
|
|
66
|
+
windowsHide: true,
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
let stdout = "";
|
|
71
|
+
let stderr = "";
|
|
72
|
+
let settled = false;
|
|
73
|
+
|
|
74
|
+
const finish = (fn, value) => {
|
|
75
|
+
if (settled) return;
|
|
76
|
+
settled = true;
|
|
77
|
+
fn(value);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const timer = setTimeout(() => {
|
|
81
|
+
child.kill();
|
|
82
|
+
finish(reject, new Error(`codex timed out after ${timeoutMs} ms`));
|
|
83
|
+
}, timeoutMs);
|
|
84
|
+
|
|
85
|
+
child.stdout.on("data", (chunk) => {
|
|
86
|
+
stdout += chunk.toString();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
child.stderr.on("data", (chunk) => {
|
|
90
|
+
stderr += chunk.toString();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
child.on("error", (error) => {
|
|
94
|
+
clearTimeout(timer);
|
|
95
|
+
finish(reject, error);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
child.on("close", (code, signal) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
const result = {
|
|
101
|
+
command: commandName(),
|
|
102
|
+
args,
|
|
103
|
+
cwd,
|
|
104
|
+
exitCode: code,
|
|
105
|
+
signal,
|
|
106
|
+
stdout: stdout.trim(),
|
|
107
|
+
stderr: stderr.trim()
|
|
108
|
+
};
|
|
109
|
+
finish(resolve, result);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Flags shared by every `codex exec*` subcommand (run, review, resume, fork).
|
|
115
|
+
// NOTE: -C/--cd, --add-dir, and -i/--image are deliberately NOT here — per
|
|
116
|
+
// `codex exec <subcommand> --help`, -C and --add-dir only exist on bare
|
|
117
|
+
// `codex exec`, and -i doesn't exist on `codex exec review` at all. They are
|
|
118
|
+
// added individually only where the CLI actually accepts them.
|
|
119
|
+
function pushCommon(args, input) {
|
|
120
|
+
if (input.ephemeral !== false) args.push("--ephemeral");
|
|
121
|
+
if (input.skipGitRepoCheck !== false) args.push("--skip-git-repo-check");
|
|
122
|
+
if (input.ignoreRules === true) args.push("--ignore-rules");
|
|
123
|
+
if (input.model) args.push("-m", input.model);
|
|
124
|
+
if (input.outputSchema) args.push("--output-schema", input.outputSchema);
|
|
125
|
+
if (input.outputLastMessage) args.push("-o", input.outputLastMessage);
|
|
126
|
+
if (input.dangerouslyBypassApprovalsAndSandbox === true) {
|
|
127
|
+
args.push("--dangerously-bypass-approvals-and-sandbox");
|
|
128
|
+
}
|
|
129
|
+
if (Array.isArray(input.extraArgs)) args.push(...input.extraArgs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function buildRunArgs(input) {
|
|
133
|
+
const args = ["exec", "--json"];
|
|
134
|
+
if (input.sandbox) args.push("--sandbox", input.sandbox);
|
|
135
|
+
if (input.approveForMe === true) args.push("--approve-for-me");
|
|
136
|
+
if (input.cd) args.push("-C", input.cd);
|
|
137
|
+
if (Array.isArray(input.addDirs)) {
|
|
138
|
+
for (const dir of input.addDirs) args.push("--add-dir", dir);
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(input.images) && input.images.length) {
|
|
141
|
+
args.push("-i", ...input.images);
|
|
142
|
+
}
|
|
143
|
+
pushCommon(args, input);
|
|
144
|
+
if (input.prompt) args.push(input.prompt);
|
|
145
|
+
return applyModelOverride(args);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function buildReviewArgs(input) {
|
|
149
|
+
const args = ["exec", "review", "--json"];
|
|
150
|
+
if (input.uncommitted === true) args.push("--uncommitted");
|
|
151
|
+
if (input.base) args.push("--base", input.base);
|
|
152
|
+
if (input.commit) args.push("--commit", input.commit);
|
|
153
|
+
if (input.title) args.push("--title", input.title);
|
|
154
|
+
pushCommon(args, { ...input, skipGitRepoCheck: input.skipGitRepoCheck ?? false });
|
|
155
|
+
if (input.prompt) args.push(input.prompt);
|
|
156
|
+
return applyModelOverride(args);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function buildResumeArgs(input) {
|
|
160
|
+
const args = ["exec", "resume", "--json"];
|
|
161
|
+
if (input.all === true) args.push("--all");
|
|
162
|
+
pushCommon(args, input);
|
|
163
|
+
if (input.sessionId) args.push(input.sessionId);
|
|
164
|
+
else args.push("--last");
|
|
165
|
+
if (input.prompt) args.push(input.prompt);
|
|
166
|
+
return applyModelOverride(args);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function buildForkArgs(input) {
|
|
170
|
+
if (!input.sessionId) throw new Error("sessionId is required to fork a session");
|
|
171
|
+
const args = ["exec", "fork", "--json"];
|
|
172
|
+
pushCommon(args, input);
|
|
173
|
+
args.push(input.sessionId);
|
|
174
|
+
if (input.prompt) args.push(input.prompt);
|
|
175
|
+
return applyModelOverride(args);
|
|
176
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import {
|
|
7
|
+
codexDoctor,
|
|
8
|
+
codexFork,
|
|
9
|
+
codexHelp,
|
|
10
|
+
codexLoginStatus,
|
|
11
|
+
codexResume,
|
|
12
|
+
codexReview,
|
|
13
|
+
codexRun,
|
|
14
|
+
codexRaw,
|
|
15
|
+
codexVersion
|
|
16
|
+
} from "./tools.js";
|
|
17
|
+
|
|
18
|
+
const server = new McpServer({
|
|
19
|
+
name: "codex-mcp",
|
|
20
|
+
version: "1.0.0"
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const common = {
|
|
24
|
+
cwd: z.string().optional().describe("Working directory for the codex process."),
|
|
25
|
+
timeoutMs: z.number().int().positive().optional().describe("Process timeout in milliseconds."),
|
|
26
|
+
env: z.record(z.string()).optional().describe("Additional environment variables for codex.")
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Flags shared by every `codex exec*` subcommand (run, review, resume, fork).
|
|
30
|
+
// cd/addDirs/images are intentionally excluded here — the underlying CLI
|
|
31
|
+
// only accepts -C/--cd and --add-dir on bare `codex exec`, and doesn't
|
|
32
|
+
// accept -i/--image on `codex exec review` at all. Those are declared only
|
|
33
|
+
// on the tool schemas that actually support them.
|
|
34
|
+
const runControls = {
|
|
35
|
+
model: z.string().optional().describe("Model the agent should use. May be forced by the server's model-override environment variable."),
|
|
36
|
+
ephemeral: z.boolean().optional().describe("Skip persisting session files to disk. Defaults to true."),
|
|
37
|
+
skipGitRepoCheck: z.boolean().optional().describe("Allow running outside a Git repository. Defaults to true."),
|
|
38
|
+
ignoreRules: z.boolean().optional().describe("Do not load user or project execpolicy .rules files."),
|
|
39
|
+
outputSchema: z.string().optional().describe("Path to a JSON Schema file describing the expected final response shape."),
|
|
40
|
+
outputLastMessage: z.string().optional().describe("Path to a file where the final agent message should be written."),
|
|
41
|
+
dangerouslyBypassApprovalsAndSandbox: z.boolean().optional().describe("Skip all confirmation prompts and sandboxing. Only use in an already-isolated environment."),
|
|
42
|
+
extraArgs: z.array(z.string()).optional().describe("Additional raw codex CLI arguments appended to the command.")
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
server.registerTool("codex_run", {
|
|
46
|
+
description: "Run the Codex agent non-interactively (`codex exec`) with common CLI controls.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
prompt: z.string().describe("Task for the Codex agent."),
|
|
49
|
+
sandbox: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional().describe("Sandbox policy for model-generated shell commands."),
|
|
50
|
+
approveForMe: z.boolean().optional().describe("Route approval requests through automatic review using the workspace-write sandbox."),
|
|
51
|
+
cd: z.string().optional().describe("Directory codex should treat as its working root (--cd)."),
|
|
52
|
+
addDirs: z.array(z.string()).optional().describe("Additional directories to expose to the agent (--add-dir)."),
|
|
53
|
+
images: z.array(z.string()).optional().describe("Image file paths to attach to the prompt."),
|
|
54
|
+
...runControls,
|
|
55
|
+
...common
|
|
56
|
+
}
|
|
57
|
+
}, codexRun);
|
|
58
|
+
|
|
59
|
+
server.registerTool("codex_review", {
|
|
60
|
+
description: "Run a non-interactive Codex code review (`codex exec review`) against the current repository.",
|
|
61
|
+
inputSchema: {
|
|
62
|
+
prompt: z.string().optional().describe("Custom review instructions."),
|
|
63
|
+
uncommitted: z.boolean().optional().describe("Review staged, unstaged, and untracked changes."),
|
|
64
|
+
base: z.string().optional().describe("Review changes against the given base branch."),
|
|
65
|
+
commit: z.string().optional().describe("Review the changes introduced by a single commit."),
|
|
66
|
+
title: z.string().optional().describe("Optional commit title to display in the review summary."),
|
|
67
|
+
...runControls,
|
|
68
|
+
...common
|
|
69
|
+
}
|
|
70
|
+
}, codexReview);
|
|
71
|
+
|
|
72
|
+
server.registerTool("codex_resume", {
|
|
73
|
+
description: "Resume a previous Codex session non-interactively (`codex exec resume`) and optionally send a new prompt.",
|
|
74
|
+
inputSchema: {
|
|
75
|
+
sessionId: z.string().optional().describe("Conversation/session id (UUID) or thread name. Omit to resume the most recent session."),
|
|
76
|
+
all: z.boolean().optional().describe("Search across all directories, not just the current one."),
|
|
77
|
+
prompt: z.string().optional().describe("Prompt to send after resuming the session."),
|
|
78
|
+
...runControls,
|
|
79
|
+
...common
|
|
80
|
+
}
|
|
81
|
+
}, codexResume);
|
|
82
|
+
|
|
83
|
+
server.registerTool("codex_fork", {
|
|
84
|
+
description: "Fork a previous Codex session (`codex exec fork`) into a new session and optionally send a prompt.",
|
|
85
|
+
inputSchema: {
|
|
86
|
+
sessionId: z.string().describe("Conversation/session id (UUID) or thread name to fork."),
|
|
87
|
+
prompt: z.string().optional().describe("Optional prompt to send after forking."),
|
|
88
|
+
...runControls,
|
|
89
|
+
...common
|
|
90
|
+
}
|
|
91
|
+
}, codexFork);
|
|
92
|
+
|
|
93
|
+
server.registerTool("codex_version", {
|
|
94
|
+
description: "Return the installed Codex CLI version.",
|
|
95
|
+
inputSchema: common
|
|
96
|
+
}, codexVersion);
|
|
97
|
+
|
|
98
|
+
server.registerTool("codex_login_status", {
|
|
99
|
+
description: "Check Codex CLI authentication status (`codex login status`).",
|
|
100
|
+
inputSchema: common
|
|
101
|
+
}, codexLoginStatus);
|
|
102
|
+
|
|
103
|
+
server.registerTool("codex_doctor", {
|
|
104
|
+
description: "Run Codex's built-in diagnostics (`codex doctor --json`): install, auth, config, and connectivity health.",
|
|
105
|
+
inputSchema: common
|
|
106
|
+
}, codexDoctor);
|
|
107
|
+
|
|
108
|
+
server.registerTool("codex_help", {
|
|
109
|
+
description: "Show Codex CLI help, optionally for a specific subcommand.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
command: z.string().optional(),
|
|
112
|
+
...common
|
|
113
|
+
}
|
|
114
|
+
}, codexHelp);
|
|
115
|
+
|
|
116
|
+
server.registerTool("codex_raw", {
|
|
117
|
+
description: "Run the Codex CLI with an arbitrary argument array. Escape hatch for options not covered by the other tools. The server's model-override environment variable, if set, still applies.",
|
|
118
|
+
inputSchema: {
|
|
119
|
+
args: z.array(z.string()),
|
|
120
|
+
...common
|
|
121
|
+
}
|
|
122
|
+
}, codexRaw);
|
|
123
|
+
|
|
124
|
+
const transport = new StdioServerTransport();
|
|
125
|
+
await server.connect(transport);
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyModelOverride,
|
|
3
|
+
buildForkArgs,
|
|
4
|
+
buildResumeArgs,
|
|
5
|
+
buildReviewArgs,
|
|
6
|
+
buildRunArgs,
|
|
7
|
+
runCodex
|
|
8
|
+
} from "./codex.js";
|
|
9
|
+
|
|
10
|
+
function textResult(text, structuredContent) {
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: "text", text }],
|
|
13
|
+
...(structuredContent ? { structuredContent } : {})
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseJsonl(output) {
|
|
18
|
+
const events = [];
|
|
19
|
+
for (const line of output.split("\n")) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed) continue;
|
|
22
|
+
try {
|
|
23
|
+
events.push(JSON.parse(trimmed));
|
|
24
|
+
} catch {
|
|
25
|
+
events.push({ type: "raw", line: trimmed });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return events;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function summarizeEvents(events) {
|
|
32
|
+
const summary = {
|
|
33
|
+
threadId: null,
|
|
34
|
+
agentMessages: [],
|
|
35
|
+
commandExecutions: [],
|
|
36
|
+
errors: [],
|
|
37
|
+
usage: null,
|
|
38
|
+
turnStatus: "unknown"
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
for (const event of events) {
|
|
42
|
+
switch (event.type) {
|
|
43
|
+
case "thread.started":
|
|
44
|
+
summary.threadId = event.thread_id ?? summary.threadId;
|
|
45
|
+
break;
|
|
46
|
+
case "turn.completed":
|
|
47
|
+
summary.turnStatus = "completed";
|
|
48
|
+
summary.usage = event.usage ?? summary.usage;
|
|
49
|
+
break;
|
|
50
|
+
case "turn.failed":
|
|
51
|
+
summary.turnStatus = "failed";
|
|
52
|
+
if (event.error) summary.errors.push(event.error.message ?? String(event.error));
|
|
53
|
+
break;
|
|
54
|
+
case "error":
|
|
55
|
+
summary.errors.push(event.message ?? "unknown error");
|
|
56
|
+
break;
|
|
57
|
+
case "item.completed":
|
|
58
|
+
case "item.started": {
|
|
59
|
+
const item = event.item;
|
|
60
|
+
if (!item) break;
|
|
61
|
+
if (item.type === "agent_message" && item.text) {
|
|
62
|
+
summary.agentMessages.push(item.text);
|
|
63
|
+
} else if (item.type === "command_execution") {
|
|
64
|
+
summary.commandExecutions.push({
|
|
65
|
+
command: item.command,
|
|
66
|
+
exitCode: item.exit_code,
|
|
67
|
+
status: item.status,
|
|
68
|
+
output: item.aggregated_output
|
|
69
|
+
});
|
|
70
|
+
} else if (item.type === "error" && item.message) {
|
|
71
|
+
summary.errors.push(item.message);
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
default:
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return summary;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Formats the JSONL output of a `codex exec*` invocation into a readable
|
|
84
|
+
* text reply plus a structured payload for programmatic consumers. */
|
|
85
|
+
function formatExecResult(result) {
|
|
86
|
+
const events = parseJsonl(result.stdout);
|
|
87
|
+
const summary = summarizeEvents(events);
|
|
88
|
+
|
|
89
|
+
const parts = [];
|
|
90
|
+
if (summary.agentMessages.length) {
|
|
91
|
+
parts.push(summary.agentMessages.join("\n\n"));
|
|
92
|
+
}
|
|
93
|
+
if (summary.errors.length) {
|
|
94
|
+
parts.push(`Errors:\n${summary.errors.join("\n")}`);
|
|
95
|
+
}
|
|
96
|
+
if (!parts.length) {
|
|
97
|
+
parts.push(result.stdout || result.stderr || "(no output)");
|
|
98
|
+
}
|
|
99
|
+
if (result.exitCode !== 0) {
|
|
100
|
+
parts.push(`(codex exited with code ${result.exitCode})`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return textResult(parts.join("\n\n"), {
|
|
104
|
+
threadId: summary.threadId,
|
|
105
|
+
turnStatus: summary.turnStatus,
|
|
106
|
+
usage: summary.usage,
|
|
107
|
+
commandExecutions: summary.commandExecutions,
|
|
108
|
+
errors: summary.errors,
|
|
109
|
+
exitCode: result.exitCode,
|
|
110
|
+
events
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatPlainResult(result) {
|
|
115
|
+
const output = result.stdout || result.stderr || "";
|
|
116
|
+
return textResult(output, { exitCode: result.exitCode, stderr: result.stderr });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function processOptions(input) {
|
|
120
|
+
return { cwd: input.cwd, timeoutMs: input.timeoutMs, env: input.env };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function codexRun(input) {
|
|
124
|
+
if (!input.prompt?.trim()) throw new Error("prompt is required");
|
|
125
|
+
const args = buildRunArgs(input);
|
|
126
|
+
const result = await runCodex(args, processOptions(input));
|
|
127
|
+
return formatExecResult(result);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function codexReview(input = {}) {
|
|
131
|
+
const args = buildReviewArgs(input);
|
|
132
|
+
const result = await runCodex(args, processOptions(input));
|
|
133
|
+
return formatExecResult(result);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function codexResume(input = {}) {
|
|
137
|
+
const args = buildResumeArgs(input);
|
|
138
|
+
const result = await runCodex(args, processOptions(input));
|
|
139
|
+
return formatExecResult(result);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function codexFork(input) {
|
|
143
|
+
if (!input.sessionId?.trim()) throw new Error("sessionId is required");
|
|
144
|
+
const args = buildForkArgs(input);
|
|
145
|
+
const result = await runCodex(args, processOptions(input));
|
|
146
|
+
return formatExecResult(result);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function codexVersion(input = {}) {
|
|
150
|
+
const result = await runCodex(["--version"], processOptions(input));
|
|
151
|
+
return formatPlainResult(result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function codexLoginStatus(input = {}) {
|
|
155
|
+
const result = await runCodex(["login", "status"], processOptions(input));
|
|
156
|
+
return formatPlainResult(result);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function codexDoctor(input = {}) {
|
|
160
|
+
const result = await runCodex(["doctor", "--json"], processOptions(input));
|
|
161
|
+
return formatPlainResult(result);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function codexHelp(input = {}) {
|
|
165
|
+
const args = input.command ? ["help", input.command] : ["help"];
|
|
166
|
+
const result = await runCodex(args, processOptions(input));
|
|
167
|
+
return formatPlainResult(result);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function codexRaw(input) {
|
|
171
|
+
if (!Array.isArray(input.args)) throw new Error("args must be an array");
|
|
172
|
+
// Only exec-family invocations accept -m/--model; leave other subcommands
|
|
173
|
+
// (login, doctor, help, mcp, ...) untouched so the override never injects
|
|
174
|
+
// an unrecognized flag into a command that doesn't take one.
|
|
175
|
+
const args = input.args[0] === "exec" ? applyModelOverride(input.args) : input.args;
|
|
176
|
+
const result = await runCodex(args, processOptions(input));
|
|
177
|
+
return formatPlainResult(result);
|
|
178
|
+
}
|
package/test/smoke.mjs
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Manual, live end-to-end smoke test for codex-mcp.
|
|
3
|
+
//
|
|
4
|
+
// This spawns the real MCP server and drives it over stdio with the real
|
|
5
|
+
// @modelcontextprotocol/sdk Client, exercising every registered tool against
|
|
6
|
+
// a real, authenticated Codex CLI. It makes real model calls and is NOT run
|
|
7
|
+
// by `npm test` (which only does a static syntax check) because it costs
|
|
8
|
+
// real quota/tokens against whatever model resolves for the caller.
|
|
9
|
+
//
|
|
10
|
+
// Usage:
|
|
11
|
+
// CODEX_MCP_MODEL=<your-model> node test/smoke.mjs
|
|
12
|
+
//
|
|
13
|
+
// Setting CODEX_MCP_MODEL is optional but strongly recommended: it proves
|
|
14
|
+
// the override wins even when a tool call or raw arg list requests a
|
|
15
|
+
// different (here, intentionally bogus) model.
|
|
16
|
+
|
|
17
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { tmpdir } from "node:os";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
22
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
23
|
+
|
|
24
|
+
const overrideModel = process.env.CODEX_MCP_MODEL;
|
|
25
|
+
const badModel = "definitely-not-a-real-model"; // must never resolve to any real, billable provider
|
|
26
|
+
|
|
27
|
+
function assert(condition, message) {
|
|
28
|
+
if (!condition) throw new Error(`ASSERTION FAILED: ${message}`);
|
|
29
|
+
console.log(` ok - ${message}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const repoDir = mkdtempSync(join(tmpdir(), "codex-mcp-smoke-"));
|
|
33
|
+
execFileSync("git", ["init", "-q"], { cwd: repoDir });
|
|
34
|
+
execFileSync("git", ["config", "user.email", "test@example.com"], { cwd: repoDir });
|
|
35
|
+
execFileSync("git", ["config", "user.name", "test"], { cwd: repoDir });
|
|
36
|
+
writeFileSync(
|
|
37
|
+
join(repoDir, "math.js"),
|
|
38
|
+
"function add(a, b) {\n return a - b; // bug: should be a + b\n}\nmodule.exports = { add };\n"
|
|
39
|
+
);
|
|
40
|
+
execFileSync("git", ["add", "math.js"], { cwd: repoDir });
|
|
41
|
+
|
|
42
|
+
const transport = new StdioClientTransport({
|
|
43
|
+
command: "node",
|
|
44
|
+
args: [new URL("../src/index.js", import.meta.url).pathname],
|
|
45
|
+
env: { ...process.env }
|
|
46
|
+
});
|
|
47
|
+
const client = new Client({ name: "smoke-test", version: "1.0.0" });
|
|
48
|
+
await client.connect(transport);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
console.log("\n=== tools/list ===");
|
|
52
|
+
const tools = await client.listTools();
|
|
53
|
+
const names = tools.tools.map((t) => t.name);
|
|
54
|
+
console.log(names.join(", "));
|
|
55
|
+
for (const expected of ["codex_run", "codex_review", "codex_resume", "codex_fork", "codex_version", "codex_login_status", "codex_doctor", "codex_help", "codex_raw"]) {
|
|
56
|
+
assert(names.includes(expected), `${expected} is registered`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log("\n=== codex_version ===");
|
|
60
|
+
const version = await client.callTool({ name: "codex_version", arguments: {} });
|
|
61
|
+
assert(!version.isError, "codex_version did not error");
|
|
62
|
+
console.log(version.content[0].text);
|
|
63
|
+
|
|
64
|
+
console.log("\n=== codex_login_status ===");
|
|
65
|
+
const login = await client.callTool({ name: "codex_login_status", arguments: {} });
|
|
66
|
+
assert(!login.isError, "codex_login_status did not error");
|
|
67
|
+
console.log(login.content[0].text);
|
|
68
|
+
|
|
69
|
+
console.log(`\n=== codex_run: tool arg requests bogus model "${badModel}"${overrideModel ? "; env override must win" : "; no override set, expect failure" } ===`);
|
|
70
|
+
const run = await client.callTool({
|
|
71
|
+
name: "codex_run",
|
|
72
|
+
arguments: {
|
|
73
|
+
prompt: "Reply with exactly the single word: PONG",
|
|
74
|
+
model: badModel,
|
|
75
|
+
ephemeral: false,
|
|
76
|
+
cwd: repoDir,
|
|
77
|
+
timeoutMs: 60000
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
console.log("TEXT:", run.content[0].text);
|
|
81
|
+
if (overrideModel) {
|
|
82
|
+
assert(!run.isError && run.structuredContent?.turnStatus === "completed", "override forced a working model despite bogus tool-supplied model");
|
|
83
|
+
assert(run.content[0].text.includes("PONG"), "agent replied PONG using the overridden model");
|
|
84
|
+
} else {
|
|
85
|
+
console.log(" (no CODEX_MCP_MODEL set - skipping override assertion)");
|
|
86
|
+
}
|
|
87
|
+
const threadId = run.structuredContent?.threadId;
|
|
88
|
+
|
|
89
|
+
console.log("\n=== codex_raw: raw args also sneak in a bogus -m; override must still win when set ===");
|
|
90
|
+
const raw = await client.callTool({
|
|
91
|
+
name: "codex_raw",
|
|
92
|
+
arguments: {
|
|
93
|
+
args: ["exec", "--json", "--ephemeral", "--skip-git-repo-check", "-m", badModel, "Reply with exactly the single word: RAWPONG"],
|
|
94
|
+
cwd: repoDir,
|
|
95
|
+
timeoutMs: 60000
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
console.log("TEXT:", raw.content[0].text);
|
|
99
|
+
if (overrideModel) {
|
|
100
|
+
assert(!raw.isError && raw.content[0].text.includes("RAWPONG"), "override stripped the bogus -m from raw args and still succeeded");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log("\n=== codex_raw: override must NOT be injected into non-exec subcommands ===");
|
|
104
|
+
const rawHelp = await client.callTool({ name: "codex_raw", arguments: { args: ["--version"] } });
|
|
105
|
+
assert(!rawHelp.isError, "codex_raw --version unaffected by model override scoping");
|
|
106
|
+
console.log(rawHelp.content[0].text);
|
|
107
|
+
|
|
108
|
+
console.log("\n=== codex_help ===");
|
|
109
|
+
const help = await client.callTool({ name: "codex_help", arguments: {} });
|
|
110
|
+
assert(!help.isError, "codex_help did not error");
|
|
111
|
+
console.log(help.content[0].text.slice(0, 120));
|
|
112
|
+
|
|
113
|
+
console.log("\n=== codex_doctor ===");
|
|
114
|
+
const doctor = await client.callTool({ name: "codex_doctor", arguments: {} });
|
|
115
|
+
assert(!doctor.isError, "codex_doctor did not error");
|
|
116
|
+
console.log(doctor.content[0].text.slice(0, 120), "...");
|
|
117
|
+
|
|
118
|
+
console.log("\n=== codex_review: uncommitted change with a deliberate bug ===");
|
|
119
|
+
const review = await client.callTool({
|
|
120
|
+
name: "codex_review",
|
|
121
|
+
arguments: { uncommitted: true, cwd: repoDir, timeoutMs: 90000 }
|
|
122
|
+
});
|
|
123
|
+
assert(!review.isError, "codex_review did not error");
|
|
124
|
+
console.log("TEXT:", review.content[0].text.slice(0, 400));
|
|
125
|
+
|
|
126
|
+
console.log("\n=== codex_fork: missing required sessionId must surface as a tool error ===");
|
|
127
|
+
const fork = await client.callTool({ name: "codex_fork", arguments: {} });
|
|
128
|
+
assert(fork.isError === true, "codex_fork without sessionId reports isError");
|
|
129
|
+
console.log(fork.content[0].text);
|
|
130
|
+
|
|
131
|
+
if (threadId) {
|
|
132
|
+
console.log("\n=== codex_resume: recall context from the earlier codex_run ===");
|
|
133
|
+
const resume = await client.callTool({
|
|
134
|
+
name: "codex_resume",
|
|
135
|
+
arguments: {
|
|
136
|
+
sessionId: threadId,
|
|
137
|
+
prompt: "What single word did you just reply with in your previous message? Answer with just that one word.",
|
|
138
|
+
cwd: repoDir,
|
|
139
|
+
timeoutMs: 60000
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
console.log("TEXT:", resume.content[0].text);
|
|
143
|
+
if (overrideModel) assert(!resume.isError, "codex_resume did not error");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
console.log("\nALL CHECKS PASSED");
|
|
147
|
+
} finally {
|
|
148
|
+
await client.close();
|
|
149
|
+
rmSync(repoDir, { recursive: true, force: true });
|
|
150
|
+
}
|