@leo000001/codex-mcp 0.2.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/CHANGELOG.md +40 -0
- package/CODE_OF_CONDUCT.md +26 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +354 -0
- package/SECURITY.md +28 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3168 -0
- package/dist/index.js.map +1 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3168 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
// src/session/manager.ts
|
|
11
|
+
import { randomUUID } from "crypto";
|
|
12
|
+
|
|
13
|
+
// src/app-server/client.ts
|
|
14
|
+
import { spawn } from "child_process";
|
|
15
|
+
import { EventEmitter } from "events";
|
|
16
|
+
import { StringDecoder } from "string_decoder";
|
|
17
|
+
|
|
18
|
+
// src/app-server/protocol.ts
|
|
19
|
+
function toSandboxPolicy(mode) {
|
|
20
|
+
switch (mode) {
|
|
21
|
+
case "read-only":
|
|
22
|
+
return { type: "readOnly" };
|
|
23
|
+
case "workspace-write":
|
|
24
|
+
return { type: "workspaceWrite" };
|
|
25
|
+
case "danger-full-access":
|
|
26
|
+
return { type: "dangerFullAccess" };
|
|
27
|
+
default:
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
var Methods = {
|
|
32
|
+
// Client → Server
|
|
33
|
+
INITIALIZE: "initialize",
|
|
34
|
+
THREAD_START: "thread/start",
|
|
35
|
+
THREAD_RESUME: "thread/resume",
|
|
36
|
+
THREAD_FORK: "thread/fork",
|
|
37
|
+
TURN_START: "turn/start",
|
|
38
|
+
TURN_INTERRUPT: "turn/interrupt",
|
|
39
|
+
TURN_STEER: "turn/steer",
|
|
40
|
+
// Server → Client requests
|
|
41
|
+
COMMAND_APPROVAL: "item/commandExecution/requestApproval",
|
|
42
|
+
FILE_CHANGE_APPROVAL: "item/fileChange/requestApproval",
|
|
43
|
+
USER_INPUT_REQUEST: "item/tool/requestUserInput",
|
|
44
|
+
DYNAMIC_TOOL_CALL: "item/tool/call",
|
|
45
|
+
AUTH_TOKEN_REFRESH: "account/chatgptAuthTokens/refresh",
|
|
46
|
+
LEGACY_PATCH_APPROVAL: "applyPatchApproval",
|
|
47
|
+
LEGACY_EXEC_APPROVAL: "execCommandApproval",
|
|
48
|
+
// Server → Client notifications
|
|
49
|
+
ERROR: "error",
|
|
50
|
+
THREAD_STARTED: "thread/started",
|
|
51
|
+
TURN_STARTED: "turn/started",
|
|
52
|
+
TURN_COMPLETED: "turn/completed",
|
|
53
|
+
TURN_DIFF_UPDATED: "turn/diff/updated",
|
|
54
|
+
TURN_PLAN_UPDATED: "turn/plan/updated",
|
|
55
|
+
ITEM_STARTED: "item/started",
|
|
56
|
+
ITEM_COMPLETED: "item/completed",
|
|
57
|
+
AGENT_MESSAGE_DELTA: "item/agentMessage/delta",
|
|
58
|
+
COMMAND_OUTPUT_DELTA: "item/commandExecution/outputDelta",
|
|
59
|
+
FILE_CHANGE_OUTPUT_DELTA: "item/fileChange/outputDelta",
|
|
60
|
+
REASONING_TEXT_DELTA: "item/reasoning/textDelta",
|
|
61
|
+
REASONING_SUMMARY_DELTA: "item/reasoning/summaryTextDelta",
|
|
62
|
+
PLAN_DELTA: "item/plan/delta",
|
|
63
|
+
MCP_TOOL_PROGRESS: "item/mcpToolCall/progress",
|
|
64
|
+
SESSION_CONFIGURED: "sessionConfigured"
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// src/app-server/lifecycle.ts
|
|
68
|
+
function buildAppServerArgs(opts) {
|
|
69
|
+
const args = ["app-server"];
|
|
70
|
+
if (opts.profile) {
|
|
71
|
+
args.push("-p", opts.profile);
|
|
72
|
+
}
|
|
73
|
+
if (opts.model) {
|
|
74
|
+
args.push("-c", `model=${opts.model}`);
|
|
75
|
+
}
|
|
76
|
+
if (opts.approvalPolicy) {
|
|
77
|
+
args.push("-c", `approval_policy=${opts.approvalPolicy}`);
|
|
78
|
+
}
|
|
79
|
+
if (opts.sandbox) {
|
|
80
|
+
args.push("-c", `sandbox_mode=${opts.sandbox}`);
|
|
81
|
+
}
|
|
82
|
+
if (opts.config) {
|
|
83
|
+
for (const [key, value] of Object.entries(opts.config)) {
|
|
84
|
+
const serialized = typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
|
|
85
|
+
args.push("-c", `${key}=${serialized}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return args;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/app-server/codex-bin.ts
|
|
92
|
+
import { existsSync, readFileSync } from "fs";
|
|
93
|
+
import path from "path";
|
|
94
|
+
function resolveCodexInvocation(codexArgs, deps = {}) {
|
|
95
|
+
const platform = deps.platform ?? process.platform;
|
|
96
|
+
const env = deps.env ?? process.env;
|
|
97
|
+
const exists = deps.exists ?? existsSync;
|
|
98
|
+
const readFile = deps.readFile ?? ((p) => readFileSync(p, "utf8"));
|
|
99
|
+
const pathApi = platform === "win32" ? path.win32 : path.posix;
|
|
100
|
+
const delimiter = platform === "win32" ? ";" : ":";
|
|
101
|
+
if (platform !== "win32") {
|
|
102
|
+
return { cmd: "codex", args: codexArgs, spawnedViaCmd: false };
|
|
103
|
+
}
|
|
104
|
+
const shim = findOnPath("codex", env, exists, pathApi, delimiter, [".exe", ".cmd", ".bat"]);
|
|
105
|
+
if (shim && shim.toLowerCase().endsWith(".exe")) {
|
|
106
|
+
return { cmd: shim, args: codexArgs, spawnedViaCmd: false };
|
|
107
|
+
}
|
|
108
|
+
if (shim && (shim.toLowerCase().endsWith(".cmd") || shim.toLowerCase().endsWith(".bat"))) {
|
|
109
|
+
const script = tryResolveNodeScriptFromShim(shim, exists, readFile, pathApi);
|
|
110
|
+
if (script) {
|
|
111
|
+
return { cmd: process.execPath, args: [script, ...codexArgs], spawnedViaCmd: false };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const comspec = env.ComSpec || env.COMSPEC || "cmd.exe";
|
|
115
|
+
return { cmd: comspec, args: ["/d", "/s", "/c", "codex", ...codexArgs], spawnedViaCmd: true };
|
|
116
|
+
}
|
|
117
|
+
function findOnPath(base, env, exists, pathApi, delimiter, exts) {
|
|
118
|
+
const pathEnv = env.PATH || env.Path || env.path || "";
|
|
119
|
+
const dirs = pathEnv.split(delimiter).map((d) => stripSurroundingQuotes(d.trim())).filter(Boolean);
|
|
120
|
+
for (const dir of dirs) {
|
|
121
|
+
for (const ext of exts) {
|
|
122
|
+
const candidate = pathApi.join(dir, `${base}${ext}`);
|
|
123
|
+
if (exists(candidate)) return candidate;
|
|
124
|
+
}
|
|
125
|
+
const raw = pathApi.join(dir, base);
|
|
126
|
+
if (exists(raw)) return raw;
|
|
127
|
+
}
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
function stripSurroundingQuotes(value) {
|
|
131
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
132
|
+
return value.slice(1, -1);
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
136
|
+
function tryResolveNodeScriptFromShim(shimPath, exists, readFile, pathApi) {
|
|
137
|
+
let contents;
|
|
138
|
+
try {
|
|
139
|
+
contents = readFile(shimPath);
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const matches = [];
|
|
144
|
+
const re = /"([^"]+\.(?:m?js|cjs))"/gi;
|
|
145
|
+
for (; ; ) {
|
|
146
|
+
const m = re.exec(contents);
|
|
147
|
+
if (!m) break;
|
|
148
|
+
matches.push(m[1]);
|
|
149
|
+
}
|
|
150
|
+
if (matches.length === 0) return void 0;
|
|
151
|
+
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];
|
|
152
|
+
const shimDir = pathApi.dirname(shimPath);
|
|
153
|
+
const dp0 = shimDir.endsWith(pathApi.sep) ? shimDir : shimDir + pathApi.sep;
|
|
154
|
+
let resolved = preferred.replace(/%~dp0/gi, dp0).replace(/%dp0%/gi, dp0);
|
|
155
|
+
resolved = resolved.replace(/\//g, "\\");
|
|
156
|
+
const abs = pathApi.isAbsolute(resolved) ? pathApi.normalize(resolved) : pathApi.resolve(shimDir, resolved);
|
|
157
|
+
if (!exists(abs)) return void 0;
|
|
158
|
+
return abs;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/types.ts
|
|
162
|
+
var APPROVAL_POLICIES = ["untrusted", "on-failure", "on-request", "never"];
|
|
163
|
+
var SANDBOX_MODES = ["read-only", "workspace-write", "danger-full-access"];
|
|
164
|
+
var PERSONALITIES = ["none", "friendly", "pragmatic"];
|
|
165
|
+
var EFFORT_LEVELS = ["none", "minimal", "low", "medium", "high", "xhigh"];
|
|
166
|
+
var SUMMARY_MODES = ["auto", "concise", "detailed", "none"];
|
|
167
|
+
var SESSION_ACTIONS = ["list", "get", "cancel", "interrupt", "fork"];
|
|
168
|
+
var CHECK_ACTIONS = [
|
|
169
|
+
"poll",
|
|
170
|
+
"respond_permission",
|
|
171
|
+
"respond_approval",
|
|
172
|
+
"respond_user_input"
|
|
173
|
+
];
|
|
174
|
+
var RESPONSE_MODES = ["minimal", "delta_compact", "full"];
|
|
175
|
+
var COMMAND_DECISIONS = [
|
|
176
|
+
"accept",
|
|
177
|
+
"acceptForSession",
|
|
178
|
+
"acceptWithExecpolicyAmendment",
|
|
179
|
+
"decline",
|
|
180
|
+
"cancel"
|
|
181
|
+
];
|
|
182
|
+
var FILE_CHANGE_DECISIONS = ["accept", "acceptForSession", "decline", "cancel"];
|
|
183
|
+
var ALL_DECISIONS = [
|
|
184
|
+
"accept",
|
|
185
|
+
"acceptForSession",
|
|
186
|
+
"acceptWithExecpolicyAmendment",
|
|
187
|
+
"decline",
|
|
188
|
+
"cancel"
|
|
189
|
+
];
|
|
190
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
191
|
+
ErrorCode2["INVALID_ARGUMENT"] = "INVALID_ARGUMENT";
|
|
192
|
+
ErrorCode2["SESSION_NOT_FOUND"] = "SESSION_NOT_FOUND";
|
|
193
|
+
ErrorCode2["SESSION_BUSY"] = "SESSION_BUSY";
|
|
194
|
+
ErrorCode2["SESSION_NOT_RUNNING"] = "SESSION_NOT_RUNNING";
|
|
195
|
+
ErrorCode2["REQUEST_NOT_FOUND"] = "REQUEST_NOT_FOUND";
|
|
196
|
+
ErrorCode2["TIMEOUT"] = "TIMEOUT";
|
|
197
|
+
ErrorCode2["CANCELLED"] = "CANCELLED";
|
|
198
|
+
ErrorCode2["APP_SERVER_START_FAILED"] = "APP_SERVER_START_FAILED";
|
|
199
|
+
ErrorCode2["THREAD_FORK_RESUME_FAILED"] = "THREAD_FORK_RESUME_FAILED";
|
|
200
|
+
ErrorCode2["PROTOCOL_PARSE_ERROR"] = "PROTOCOL_PARSE_ERROR";
|
|
201
|
+
ErrorCode2["WRITE_QUEUE_DROPPED"] = "WRITE_QUEUE_DROPPED";
|
|
202
|
+
ErrorCode2["INTERNAL"] = "INTERNAL";
|
|
203
|
+
return ErrorCode2;
|
|
204
|
+
})(ErrorCode || {});
|
|
205
|
+
var DEFAULT_EFFORT_LEVEL = "low";
|
|
206
|
+
var DEFAULT_POLL_INTERVAL = 12e4;
|
|
207
|
+
var WAITING_APPROVAL_POLL_INTERVAL = 1e3;
|
|
208
|
+
var POLL_DEFAULT_MAX_EVENTS = 1;
|
|
209
|
+
var POLL_MIN_MAX_EVENTS = 1;
|
|
210
|
+
var RESPOND_DEFAULT_MAX_EVENTS = 0;
|
|
211
|
+
var DEFAULT_MAX_EVENTS = 200;
|
|
212
|
+
var DEFAULT_EVENT_BUFFER_SIZE = 1e3;
|
|
213
|
+
var DEFAULT_EVENT_BUFFER_HARD_SIZE = 2e3;
|
|
214
|
+
var DEFAULT_APPROVAL_TIMEOUT_MS = 6e4;
|
|
215
|
+
var DEFAULT_IDLE_CLEANUP_MS = 30 * 60 * 1e3;
|
|
216
|
+
var DEFAULT_RUNNING_CLEANUP_MS = 4 * 60 * 60 * 1e3;
|
|
217
|
+
var DEFAULT_TERMINAL_CLEANUP_MS = 5 * 60 * 1e3;
|
|
218
|
+
var CLEANUP_INTERVAL_MS = 6e4;
|
|
219
|
+
|
|
220
|
+
// src/app-server/client.ts
|
|
221
|
+
var CLIENT_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
222
|
+
var DEFAULT_REQUEST_TIMEOUT = 3e4;
|
|
223
|
+
var STARTUP_REQUEST_TIMEOUT = 9e4;
|
|
224
|
+
var MAX_WRITE_QUEUE_BYTES = 5 * 1024 * 1024;
|
|
225
|
+
var AppServerClient = class extends EventEmitter {
|
|
226
|
+
process = null;
|
|
227
|
+
nextId = 1;
|
|
228
|
+
pending = /* @__PURE__ */ new Map();
|
|
229
|
+
buffer = "";
|
|
230
|
+
decoder = new StringDecoder("utf8");
|
|
231
|
+
_destroyed = false;
|
|
232
|
+
lastFailure = null;
|
|
233
|
+
backpressure = false;
|
|
234
|
+
writeQueue = [];
|
|
235
|
+
queuedBytes = 0;
|
|
236
|
+
spawnedViaCmd = false;
|
|
237
|
+
spawnedDetached = false;
|
|
238
|
+
notificationHandler = null;
|
|
239
|
+
serverRequestHandler = null;
|
|
240
|
+
get destroyed() {
|
|
241
|
+
return this._destroyed;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Spawn codex app-server and perform initialization handshake.
|
|
245
|
+
*/
|
|
246
|
+
async start(opts) {
|
|
247
|
+
const args = buildAppServerArgs(opts);
|
|
248
|
+
const env = { ...process.env };
|
|
249
|
+
const stdio = ["pipe", "pipe", "pipe"];
|
|
250
|
+
const invocation = resolveCodexInvocation(args);
|
|
251
|
+
this.spawnedViaCmd = invocation.spawnedViaCmd;
|
|
252
|
+
this.spawnedDetached = process.platform !== "win32";
|
|
253
|
+
const proc = spawn(invocation.cmd, invocation.args, {
|
|
254
|
+
stdio,
|
|
255
|
+
env,
|
|
256
|
+
detached: this.spawnedDetached,
|
|
257
|
+
windowsHide: process.platform === "win32"
|
|
258
|
+
});
|
|
259
|
+
this.process = proc;
|
|
260
|
+
proc.stdout.on("data", (chunk) => this.onData(chunk));
|
|
261
|
+
proc.stderr.on("data", (chunk) => {
|
|
262
|
+
console.error(`[app-server stderr] ${chunk.toString().trimEnd()}`);
|
|
263
|
+
});
|
|
264
|
+
proc.stdin?.on("drain", () => this.flushWriteQueue());
|
|
265
|
+
proc.stdin?.on("error", (err) => {
|
|
266
|
+
this.lastFailure = err instanceof Error ? err : new Error(String(err));
|
|
267
|
+
this.failAllPending(this.lastFailure);
|
|
268
|
+
});
|
|
269
|
+
proc.stdin?.on("close", () => {
|
|
270
|
+
this.lastFailure ??= new Error("app-server stdin closed");
|
|
271
|
+
this.failAllPending(this.lastFailure);
|
|
272
|
+
});
|
|
273
|
+
proc.on("exit", (code, signal) => {
|
|
274
|
+
this.lastFailure ??= new Error(
|
|
275
|
+
`app-server exited (code: ${code}, signal: ${signal ?? "null"})`
|
|
276
|
+
);
|
|
277
|
+
this.failAllPending(this.lastFailure);
|
|
278
|
+
if (!this._destroyed) {
|
|
279
|
+
this.emit("exit", code, signal);
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
proc.on("error", (err) => {
|
|
283
|
+
this.lastFailure = err instanceof Error ? err : new Error(String(err));
|
|
284
|
+
this.failAllPending(this.lastFailure);
|
|
285
|
+
this.emit("error", err);
|
|
286
|
+
});
|
|
287
|
+
const result = await this.request(Methods.INITIALIZE, {
|
|
288
|
+
clientInfo: { name: "codex-mcp", version: CLIENT_VERSION }
|
|
289
|
+
});
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Register handler for server notifications.
|
|
294
|
+
*/
|
|
295
|
+
onNotification(handler) {
|
|
296
|
+
this.notificationHandler = handler;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Register handler for server-initiated requests (approvals, user input, etc.).
|
|
300
|
+
*/
|
|
301
|
+
onServerRequest(handler) {
|
|
302
|
+
this.serverRequestHandler = handler;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Send a JSON-RPC response to a server-initiated request.
|
|
306
|
+
*/
|
|
307
|
+
respondToServer(id, result) {
|
|
308
|
+
try {
|
|
309
|
+
this.send({ jsonrpc: "2.0", id, result });
|
|
310
|
+
} catch (err) {
|
|
311
|
+
console.error(
|
|
312
|
+
`[app-server] Failed to send JSON-RPC response for server request id=${String(id)}: ${err instanceof Error ? err.message : String(err)}`
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Send a JSON-RPC error response to a server-initiated request.
|
|
318
|
+
*/
|
|
319
|
+
respondErrorToServer(id, code, message) {
|
|
320
|
+
try {
|
|
321
|
+
this.send({ jsonrpc: "2.0", id, error: { code, message } });
|
|
322
|
+
} catch (err) {
|
|
323
|
+
console.error(
|
|
324
|
+
`[app-server] Failed to send JSON-RPC error response for server request id=${String(id)}: ${err instanceof Error ? err.message : String(err)}`
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// ── High-level protocol methods ────────────────────────────────
|
|
329
|
+
async threadStart(params, timeout = STARTUP_REQUEST_TIMEOUT) {
|
|
330
|
+
return this.request(Methods.THREAD_START, params, timeout);
|
|
331
|
+
}
|
|
332
|
+
async threadFork(params) {
|
|
333
|
+
return this.request(Methods.THREAD_FORK, params);
|
|
334
|
+
}
|
|
335
|
+
async threadResume(params) {
|
|
336
|
+
return this.request(Methods.THREAD_RESUME, params);
|
|
337
|
+
}
|
|
338
|
+
async turnStart(params, timeout = STARTUP_REQUEST_TIMEOUT) {
|
|
339
|
+
return this.request(Methods.TURN_START, params, timeout);
|
|
340
|
+
}
|
|
341
|
+
async turnInterrupt(params) {
|
|
342
|
+
await this.request(Methods.TURN_INTERRUPT, params);
|
|
343
|
+
}
|
|
344
|
+
// ── Low-level JSON-RPC ─────────────────────────────────────────
|
|
345
|
+
request(method, params, timeout = DEFAULT_REQUEST_TIMEOUT) {
|
|
346
|
+
return new Promise((resolve, reject) => {
|
|
347
|
+
if (this._destroyed) {
|
|
348
|
+
reject(new Error("Client destroyed"));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!this.process?.stdin?.writable) {
|
|
352
|
+
reject(this.lastFailure ?? new Error("app-server is not running (stdin not writable)"));
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const id = this.nextId++;
|
|
356
|
+
const timer = setTimeout(() => {
|
|
357
|
+
this.pending.delete(id);
|
|
358
|
+
reject(new Error(`Request ${method} timed out after ${timeout}ms`));
|
|
359
|
+
}, timeout);
|
|
360
|
+
if (timer.unref) timer.unref();
|
|
361
|
+
this.pending.set(id, {
|
|
362
|
+
resolve,
|
|
363
|
+
reject,
|
|
364
|
+
timer
|
|
365
|
+
});
|
|
366
|
+
try {
|
|
367
|
+
this.send({ jsonrpc: "2.0", id, method, params });
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const pending = this.pending.get(id);
|
|
370
|
+
if (pending) {
|
|
371
|
+
this.pending.delete(id);
|
|
372
|
+
clearTimeout(pending.timer);
|
|
373
|
+
}
|
|
374
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
send(msg) {
|
|
379
|
+
if (!this.process?.stdin) throw new Error("app-server process not started");
|
|
380
|
+
if (!this.process.stdin.writable) throw new Error("app-server stdin not writable");
|
|
381
|
+
const payload = JSON.stringify(msg) + "\n";
|
|
382
|
+
this.enqueueWrite(payload);
|
|
383
|
+
}
|
|
384
|
+
onData(chunk) {
|
|
385
|
+
this.buffer += this.decoder.write(chunk);
|
|
386
|
+
const lines = this.buffer.split("\n");
|
|
387
|
+
this.buffer = lines.pop() ?? "";
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
const trimmed = line.trim();
|
|
390
|
+
if (!trimmed) continue;
|
|
391
|
+
if (trimmed[0] !== "{" && trimmed[0] !== "[") {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const parsed = JSON.parse(trimmed);
|
|
396
|
+
if (Array.isArray(parsed)) {
|
|
397
|
+
for (const item of parsed) {
|
|
398
|
+
if (item && typeof item === "object") {
|
|
399
|
+
this.handleMessage(item);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
} else if (parsed && typeof parsed === "object") {
|
|
403
|
+
this.handleMessage(parsed);
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
const error = new Error(
|
|
407
|
+
`Error [${"PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */}]: app-server protocol error: failed to parse JSON line: ${trimmed.slice(0, 200)}`
|
|
408
|
+
);
|
|
409
|
+
console.error(`[app-server] ${error.message}`);
|
|
410
|
+
this.lastFailure ??= error;
|
|
411
|
+
this.failAllPending(error);
|
|
412
|
+
try {
|
|
413
|
+
this.terminate("SIGTERM");
|
|
414
|
+
} catch (terminateErr) {
|
|
415
|
+
console.error(
|
|
416
|
+
`[app-server] Failed to terminate app-server after protocol parse error: ${terminateErr instanceof Error ? terminateErr.message : String(terminateErr)}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
enqueueWrite(payload) {
|
|
423
|
+
if (!this.process?.stdin?.writable) throw new Error("app-server stdin not writable");
|
|
424
|
+
if (this.backpressure || this.writeQueue.length > 0) {
|
|
425
|
+
if (this.queuedBytes + payload.length > MAX_WRITE_QUEUE_BYTES) {
|
|
426
|
+
const error = new Error(
|
|
427
|
+
`Error [${"WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */}]: app-server stdin backpressure: write queue exceeded limit`
|
|
428
|
+
);
|
|
429
|
+
this.lastFailure = error;
|
|
430
|
+
this.failAllPending(error);
|
|
431
|
+
this.writeQueue = [];
|
|
432
|
+
this.queuedBytes = 0;
|
|
433
|
+
try {
|
|
434
|
+
this.terminate("SIGTERM");
|
|
435
|
+
} catch (terminateErr) {
|
|
436
|
+
console.error(
|
|
437
|
+
`[app-server] Failed to terminate app-server after write queue overflow: ${terminateErr instanceof Error ? terminateErr.message : String(terminateErr)}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
this.writeQueue.push(payload);
|
|
443
|
+
this.queuedBytes += payload.length;
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
try {
|
|
447
|
+
const ok = this.process.stdin.write(payload);
|
|
448
|
+
if (!ok) this.backpressure = true;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
451
|
+
this.lastFailure = error;
|
|
452
|
+
this.failAllPending(error);
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
flushWriteQueue() {
|
|
457
|
+
if (!this.process?.stdin?.writable) {
|
|
458
|
+
const dropped = this.dropQueuedWrites("stdin is not writable while flushing");
|
|
459
|
+
if (dropped) {
|
|
460
|
+
try {
|
|
461
|
+
this.terminate("SIGTERM");
|
|
462
|
+
} catch (terminateErr) {
|
|
463
|
+
console.error(
|
|
464
|
+
`[app-server] Failed to terminate app-server after dropping queued writes: ${terminateErr instanceof Error ? terminateErr.message : String(terminateErr)}`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
this.backpressure = false;
|
|
471
|
+
while (this.writeQueue.length > 0 && !this.backpressure) {
|
|
472
|
+
const next = this.writeQueue.shift();
|
|
473
|
+
this.queuedBytes -= next.length;
|
|
474
|
+
try {
|
|
475
|
+
const ok = this.process.stdin.write(next);
|
|
476
|
+
if (!ok) this.backpressure = true;
|
|
477
|
+
} catch (err) {
|
|
478
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
479
|
+
this.lastFailure = error;
|
|
480
|
+
this.failAllPending(error);
|
|
481
|
+
this.writeQueue = [];
|
|
482
|
+
this.queuedBytes = 0;
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
dropQueuedWrites(reason) {
|
|
488
|
+
if (this.writeQueue.length === 0) return false;
|
|
489
|
+
const error = new Error(`Error [${"WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */}]: ${reason}`);
|
|
490
|
+
console.error(
|
|
491
|
+
`[app-server] Dropping ${this.writeQueue.length} queued writes (${this.queuedBytes} bytes): ${reason}`
|
|
492
|
+
);
|
|
493
|
+
this.lastFailure = error;
|
|
494
|
+
this.failAllPending(error);
|
|
495
|
+
this.writeQueue = [];
|
|
496
|
+
this.queuedBytes = 0;
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
handleMessage(msg) {
|
|
500
|
+
if ("id" in msg && ("result" in msg || "error" in msg)) {
|
|
501
|
+
const resp = msg;
|
|
502
|
+
const pending = this.pending.get(resp.id);
|
|
503
|
+
if (pending) {
|
|
504
|
+
this.pending.delete(resp.id);
|
|
505
|
+
clearTimeout(pending.timer);
|
|
506
|
+
if (resp.error) {
|
|
507
|
+
pending.reject(new Error(`RPC error ${resp.error.code}: ${resp.error.message}`));
|
|
508
|
+
} else {
|
|
509
|
+
pending.resolve(resp.result);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if ("id" in msg && "method" in msg) {
|
|
515
|
+
const req = msg;
|
|
516
|
+
if (this.serverRequestHandler) {
|
|
517
|
+
this.serverRequestHandler(req.id, req.method, req.params);
|
|
518
|
+
} else {
|
|
519
|
+
this.respondErrorToServer(req.id, -32601, `Method not handled: ${req.method}`);
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if ("method" in msg && !("id" in msg)) {
|
|
524
|
+
const notif = msg;
|
|
525
|
+
if (this.notificationHandler) {
|
|
526
|
+
this.notificationHandler(notif.method, notif.params);
|
|
527
|
+
}
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
failAllPending(error) {
|
|
532
|
+
if (this.pending.size === 0) return;
|
|
533
|
+
const entries = Array.from(this.pending.entries());
|
|
534
|
+
this.pending.clear();
|
|
535
|
+
for (const [, pending] of entries) {
|
|
536
|
+
clearTimeout(pending.timer);
|
|
537
|
+
pending.reject(error);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Gracefully destroy the client and kill the subprocess.
|
|
542
|
+
*/
|
|
543
|
+
async destroy() {
|
|
544
|
+
if (this._destroyed) return;
|
|
545
|
+
this._destroyed = true;
|
|
546
|
+
this.failAllPending(new Error("Client destroyed"));
|
|
547
|
+
if (this.process && !this.process.killed) {
|
|
548
|
+
const alreadyExited = this.process.exitCode !== null;
|
|
549
|
+
this.process.stdin?.end();
|
|
550
|
+
this.terminate("SIGTERM");
|
|
551
|
+
const forceKill = setTimeout(() => {
|
|
552
|
+
if (this.process && !this.process.killed) {
|
|
553
|
+
if (process.platform === "win32" && this.process.pid) {
|
|
554
|
+
try {
|
|
555
|
+
spawn("taskkill", ["/PID", String(this.process.pid), "/T", "/F"], {
|
|
556
|
+
stdio: "ignore",
|
|
557
|
+
windowsHide: true
|
|
558
|
+
});
|
|
559
|
+
} catch (err) {
|
|
560
|
+
console.error(
|
|
561
|
+
`[app-server] Failed to force-kill app-server via taskkill: ${err instanceof Error ? err.message : String(err)}`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
} else {
|
|
565
|
+
this.terminate("SIGKILL");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}, 5e3);
|
|
569
|
+
forceKill.unref();
|
|
570
|
+
if (!alreadyExited) {
|
|
571
|
+
await new Promise((resolve) => {
|
|
572
|
+
this.process.on("exit", () => {
|
|
573
|
+
clearTimeout(forceKill);
|
|
574
|
+
resolve();
|
|
575
|
+
});
|
|
576
|
+
const fallback = setTimeout(resolve, 6e3);
|
|
577
|
+
fallback.unref();
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
this.process = null;
|
|
582
|
+
this.removeAllListeners();
|
|
583
|
+
}
|
|
584
|
+
terminate(signal) {
|
|
585
|
+
if (!this.process) return;
|
|
586
|
+
if (process.platform !== "win32" && this.spawnedDetached && this.process.pid) {
|
|
587
|
+
try {
|
|
588
|
+
process.kill(-this.process.pid, signal);
|
|
589
|
+
return;
|
|
590
|
+
} catch (err) {
|
|
591
|
+
console.error(
|
|
592
|
+
`[app-server] Failed to kill detached process group with ${signal}, falling back to direct kill: ${err instanceof Error ? err.message : String(err)}`
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
this.process.kill(signal);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error(
|
|
600
|
+
`[app-server] Failed to send ${signal} to app-server process: ${err instanceof Error ? err.message : String(err)}`
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/utils/cwd.ts
|
|
607
|
+
import { existsSync as existsSync2, statSync } from "fs";
|
|
608
|
+
import path2 from "path";
|
|
609
|
+
function resolveAndValidateCwd(inputCwd, baseCwd) {
|
|
610
|
+
const candidate = inputCwd ?? baseCwd;
|
|
611
|
+
const resolved = path2.isAbsolute(candidate) ? candidate : path2.resolve(baseCwd, candidate);
|
|
612
|
+
if (!existsSync2(resolved)) {
|
|
613
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd does not exist: ${resolved}`);
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
const stat = statSync(resolved);
|
|
617
|
+
if (!stat.isDirectory()) {
|
|
618
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cwd is not a directory: ${resolved}`);
|
|
619
|
+
}
|
|
620
|
+
} catch (err) {
|
|
621
|
+
if (err instanceof Error && err.message.includes(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]`)) {
|
|
622
|
+
throw err;
|
|
623
|
+
}
|
|
624
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cannot access cwd: ${resolved}`);
|
|
625
|
+
}
|
|
626
|
+
return resolved;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// src/utils/redact.ts
|
|
630
|
+
function redactPaths(message) {
|
|
631
|
+
const uncPath = /(^|[\s'"(])\\\\[^\s\\/:]+\\[^\s:]+(?:\\[^\s:]+)*/g;
|
|
632
|
+
const windowsPath = /\b[A-Za-z]:\\[^\s:]+/g;
|
|
633
|
+
const posixPath = /(^|[\s'"(])\/[^\s:'")]+/g;
|
|
634
|
+
return message.replace(uncPath, (_m, prefix) => `${prefix}<path>`).replace(windowsPath, "<path>").replace(posixPath, (_m, prefix) => `${prefix}<path>`);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// src/utils/files.ts
|
|
638
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
639
|
+
import path3 from "path";
|
|
640
|
+
function resolveAndValidateFilePath(inputPath, baseDir, label = "path") {
|
|
641
|
+
const resolved = path3.isAbsolute(inputPath) ? inputPath : path3.resolve(baseDir, inputPath);
|
|
642
|
+
if (!existsSync3(resolved)) {
|
|
643
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} does not exist: ${resolved}`);
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
const stat = statSync2(resolved);
|
|
647
|
+
if (!stat.isFile()) {
|
|
648
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: ${label} is not a file: ${resolved}`);
|
|
649
|
+
}
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (err instanceof Error && err.message.includes(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]`)) {
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
throw new Error(`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: cannot access ${label}: ${resolved}`);
|
|
655
|
+
}
|
|
656
|
+
return resolved;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/session/manager.ts
|
|
660
|
+
var COALESCED_PROGRESS_DELTA_METHODS = /* @__PURE__ */ new Set([
|
|
661
|
+
Methods.COMMAND_OUTPUT_DELTA,
|
|
662
|
+
Methods.FILE_CHANGE_OUTPUT_DELTA,
|
|
663
|
+
Methods.REASONING_TEXT_DELTA,
|
|
664
|
+
Methods.REASONING_SUMMARY_DELTA
|
|
665
|
+
]);
|
|
666
|
+
var MAX_COALESCED_DELTA_CHARS = 16384;
|
|
667
|
+
var NOISE_FILTER_ENABLED = process.env.CODEX_MCP_DISABLE_NOISE_FILTER !== "1";
|
|
668
|
+
var WINDOWS_TERMINAL_INTEGRATION_PREFIX = `${String.fromCharCode(27)}]633;`;
|
|
669
|
+
var SHELL_NOISE_LINE_PATTERNS = [
|
|
670
|
+
// oh-my-posh migration / update prompts
|
|
671
|
+
/oh-my-posh/i,
|
|
672
|
+
// PSReadLine configuration errors
|
|
673
|
+
/PSReadLine/i,
|
|
674
|
+
/Set-PSReadLineOption/i,
|
|
675
|
+
// PowerShell module auto-import warnings
|
|
676
|
+
/^WARNING:\s/,
|
|
677
|
+
// PowerShell profile loading messages
|
|
678
|
+
/Loading personal and system profiles/i,
|
|
679
|
+
// conda/mamba init noise that leaks through profiles
|
|
680
|
+
/^(\(base\)|\(conda\))/,
|
|
681
|
+
// Common "new version available" nag lines from profile tools
|
|
682
|
+
/A new version of .+ is available/i
|
|
683
|
+
];
|
|
684
|
+
function stripShellNoise(delta) {
|
|
685
|
+
if (!NOISE_FILTER_ENABLED) return delta;
|
|
686
|
+
const lines = delta.split("\n");
|
|
687
|
+
const cleaned = lines.filter(
|
|
688
|
+
(line) => !line.includes(WINDOWS_TERMINAL_INTEGRATION_PREFIX) && !SHELL_NOISE_LINE_PATTERNS.some((re) => re.test(line))
|
|
689
|
+
);
|
|
690
|
+
if (cleaned.length === 0) return "";
|
|
691
|
+
return cleaned.join("\n");
|
|
692
|
+
}
|
|
693
|
+
var SessionManager = class {
|
|
694
|
+
sessions = /* @__PURE__ */ new Map();
|
|
695
|
+
clients = /* @__PURE__ */ new Map();
|
|
696
|
+
cancellationInFlight = /* @__PURE__ */ new Map();
|
|
697
|
+
cleanupTimer = null;
|
|
698
|
+
createClient;
|
|
699
|
+
constructor(options = {}) {
|
|
700
|
+
this.createClient = options.createClient ?? (() => new AppServerClient());
|
|
701
|
+
if (!options.disableCleanup) {
|
|
702
|
+
this.cleanupTimer = setInterval(() => this.cleanupSessions(), CLEANUP_INTERVAL_MS);
|
|
703
|
+
if (this.cleanupTimer.unref) this.cleanupTimer.unref();
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// ── Session Creation ─────────────────────────────────────────────
|
|
707
|
+
async createSession(prompt, cwd, spawnOpts, effort, advanced) {
|
|
708
|
+
const sessionId = `sess_${randomUUID().slice(0, 12)}`;
|
|
709
|
+
const client = this.createClient();
|
|
710
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
711
|
+
const approvalTimeoutMs = advanced?.approvalTimeoutMs ?? DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
712
|
+
const resolvedImages = advanced?.images ? advanced.images.map((p) => resolveAndValidateFilePath(p, cwd, "image")) : void 0;
|
|
713
|
+
const session = {
|
|
714
|
+
sessionId,
|
|
715
|
+
status: "running",
|
|
716
|
+
lastEventCursor: 0,
|
|
717
|
+
createdAt: now,
|
|
718
|
+
lastActiveAt: now,
|
|
719
|
+
approvalTimeoutMs,
|
|
720
|
+
cwd,
|
|
721
|
+
model: spawnOpts.model,
|
|
722
|
+
profile: spawnOpts.profile,
|
|
723
|
+
approvalPolicy: spawnOpts.approvalPolicy,
|
|
724
|
+
sandbox: spawnOpts.sandbox,
|
|
725
|
+
config: spawnOpts.config,
|
|
726
|
+
eventBuffer: createEventBuffer(),
|
|
727
|
+
pendingRequests: /* @__PURE__ */ new Map()
|
|
728
|
+
};
|
|
729
|
+
this.sessions.set(sessionId, session);
|
|
730
|
+
this.clients.set(sessionId, client);
|
|
731
|
+
try {
|
|
732
|
+
this.registerHandlers(sessionId, client, approvalTimeoutMs);
|
|
733
|
+
await client.start(spawnOpts);
|
|
734
|
+
const threadStartResult = await client.threadStart({
|
|
735
|
+
cwd,
|
|
736
|
+
model: spawnOpts.model,
|
|
737
|
+
approvalPolicy: spawnOpts.approvalPolicy,
|
|
738
|
+
sandbox: spawnOpts.sandbox,
|
|
739
|
+
personality: advanced?.personality,
|
|
740
|
+
ephemeral: advanced?.ephemeral,
|
|
741
|
+
baseInstructions: advanced?.baseInstructions,
|
|
742
|
+
developerInstructions: advanced?.developerInstructions,
|
|
743
|
+
config: advanced?.config
|
|
744
|
+
});
|
|
745
|
+
const threadId = extractThreadId(threadStartResult);
|
|
746
|
+
session.threadId = threadId;
|
|
747
|
+
const input = [{ type: "text", text: prompt }];
|
|
748
|
+
if (resolvedImages) {
|
|
749
|
+
for (const imagePath of resolvedImages) {
|
|
750
|
+
input.push({ type: "localImage", path: imagePath });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
const turnStartResult = await client.turnStart({
|
|
754
|
+
threadId,
|
|
755
|
+
input,
|
|
756
|
+
effort,
|
|
757
|
+
summary: advanced?.summary,
|
|
758
|
+
outputSchema: advanced?.outputSchema
|
|
759
|
+
});
|
|
760
|
+
const startedTurnId = extractTurnId(turnStartResult);
|
|
761
|
+
if (startedTurnId) session.activeTurnId = startedTurnId;
|
|
762
|
+
return {
|
|
763
|
+
sessionId,
|
|
764
|
+
threadId,
|
|
765
|
+
status: "running",
|
|
766
|
+
pollInterval: DEFAULT_POLL_INTERVAL
|
|
767
|
+
};
|
|
768
|
+
} catch (err) {
|
|
769
|
+
session.status = "error";
|
|
770
|
+
pushEvent(session.eventBuffer, "error", {
|
|
771
|
+
message: redactPaths(err instanceof Error ? err.message : String(err))
|
|
772
|
+
});
|
|
773
|
+
await client.destroy();
|
|
774
|
+
this.clients.delete(sessionId);
|
|
775
|
+
this.sessions.delete(sessionId);
|
|
776
|
+
throw err;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// ── Session Reply ────────────────────────────────────────────────
|
|
780
|
+
async replyToSession(sessionId, prompt, overrides) {
|
|
781
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
782
|
+
const client = this.getClientOrThrow(sessionId);
|
|
783
|
+
if (session.status === "cancelled") {
|
|
784
|
+
throw new Error(
|
|
785
|
+
`Error [${"CANCELLED" /* CANCELLED */}]: Session '${sessionId}' has been cancelled and cannot be resumed`
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
if (session.status !== "idle" && session.status !== "error") {
|
|
789
|
+
throw new Error(
|
|
790
|
+
`Error [${"SESSION_BUSY" /* SESSION_BUSY */}]: Session '${sessionId}' is ${session.status}, expected idle or error`
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (!session.threadId) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`Error [${"INTERNAL" /* INTERNAL */}]: Session '${sessionId}' has no threadId, cannot reply`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
clearTerminalEvents(session.eventBuffer);
|
|
799
|
+
session.status = "running";
|
|
800
|
+
session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
801
|
+
const input = [{ type: "text", text: prompt }];
|
|
802
|
+
const resolvedCwd = overrides?.cwd ? resolveAndValidateCwd(overrides.cwd, session.cwd) : void 0;
|
|
803
|
+
const turnParams = {
|
|
804
|
+
threadId: session.threadId,
|
|
805
|
+
input,
|
|
806
|
+
model: overrides?.model,
|
|
807
|
+
approvalPolicy: overrides?.approvalPolicy,
|
|
808
|
+
effort: overrides?.effort,
|
|
809
|
+
summary: overrides?.summary,
|
|
810
|
+
personality: overrides?.personality,
|
|
811
|
+
cwd: resolvedCwd,
|
|
812
|
+
outputSchema: overrides?.outputSchema
|
|
813
|
+
};
|
|
814
|
+
if (overrides?.sandbox) {
|
|
815
|
+
turnParams.sandboxPolicy = toSandboxPolicy(overrides.sandbox);
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
const turnStartResult = await client.turnStart(turnParams);
|
|
819
|
+
const startedTurnId = extractTurnId(turnStartResult);
|
|
820
|
+
if (startedTurnId) session.activeTurnId = startedTurnId;
|
|
821
|
+
if (resolvedCwd) session.cwd = resolvedCwd;
|
|
822
|
+
if (overrides?.model) session.model = overrides.model;
|
|
823
|
+
if (overrides?.approvalPolicy) {
|
|
824
|
+
session.approvalPolicy = overrides.approvalPolicy;
|
|
825
|
+
}
|
|
826
|
+
if (overrides?.sandbox) {
|
|
827
|
+
session.sandbox = overrides.sandbox;
|
|
828
|
+
}
|
|
829
|
+
} catch (err) {
|
|
830
|
+
session.status = "error";
|
|
831
|
+
pushEvent(session.eventBuffer, "error", {
|
|
832
|
+
message: redactPaths(
|
|
833
|
+
`Failed to start turn: ${err instanceof Error ? err.message : String(err)}`
|
|
834
|
+
)
|
|
835
|
+
});
|
|
836
|
+
throw err;
|
|
837
|
+
}
|
|
838
|
+
return {
|
|
839
|
+
sessionId,
|
|
840
|
+
threadId: session.threadId,
|
|
841
|
+
status: "running",
|
|
842
|
+
pollInterval: DEFAULT_POLL_INTERVAL
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
// ── Session Management ───────────────────────────────────────────
|
|
846
|
+
listSessions() {
|
|
847
|
+
return Array.from(this.sessions.values()).map(toPublicInfo);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Count currently active sessions for lightweight runtime observability.
|
|
851
|
+
* "Active" here means the session can still be interacted with.
|
|
852
|
+
*/
|
|
853
|
+
getActiveSessionCount() {
|
|
854
|
+
let count = 0;
|
|
855
|
+
for (const session of this.sessions.values()) {
|
|
856
|
+
if (session.status === "running" || session.status === "waiting_approval" || session.status === "idle") {
|
|
857
|
+
count++;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return count;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Best-effort effective default model observed from recent sessions.
|
|
864
|
+
* Returns null when no model can be inferred from in-memory state.
|
|
865
|
+
*/
|
|
866
|
+
getObservedDefaultModel() {
|
|
867
|
+
let latestModel = null;
|
|
868
|
+
let latestTs = Number.NEGATIVE_INFINITY;
|
|
869
|
+
for (const session of this.sessions.values()) {
|
|
870
|
+
if (session.status === "cancelled") continue;
|
|
871
|
+
if (typeof session.model !== "string" || session.model.length === 0) continue;
|
|
872
|
+
const ts = Date.parse(session.lastActiveAt);
|
|
873
|
+
const comparableTs = Number.isFinite(ts) ? ts : Number.NEGATIVE_INFINITY;
|
|
874
|
+
if (comparableTs >= latestTs) {
|
|
875
|
+
latestTs = comparableTs;
|
|
876
|
+
latestModel = session.model;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
return latestModel;
|
|
880
|
+
}
|
|
881
|
+
getSession(sessionId, includeSensitive = false) {
|
|
882
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
883
|
+
return includeSensitive ? toSensitiveInfo(session) : toPublicInfo(session);
|
|
884
|
+
}
|
|
885
|
+
async cancelSession(sessionId, reason) {
|
|
886
|
+
const existing = this.cancellationInFlight.get(sessionId);
|
|
887
|
+
if (existing) {
|
|
888
|
+
await existing;
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const cancellation = this.performCancelSession(sessionId, reason);
|
|
892
|
+
this.cancellationInFlight.set(sessionId, cancellation);
|
|
893
|
+
try {
|
|
894
|
+
await cancellation;
|
|
895
|
+
} finally {
|
|
896
|
+
this.cancellationInFlight.delete(sessionId);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async performCancelSession(sessionId, reason) {
|
|
900
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
901
|
+
if (session.status === "cancelled") return;
|
|
902
|
+
const client = this.clients.get(sessionId);
|
|
903
|
+
session.status = "cancelled";
|
|
904
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
905
|
+
session.cancelledAt = now;
|
|
906
|
+
session.lastActiveAt = now;
|
|
907
|
+
session.cancelledReason = reason ?? "Cancelled by user";
|
|
908
|
+
for (const [reqId, req] of session.pendingRequests) {
|
|
909
|
+
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
910
|
+
if (!req.resolved && req.respond) {
|
|
911
|
+
req.resolved = true;
|
|
912
|
+
try {
|
|
913
|
+
if (req.kind === "command") req.respond({ decision: "cancel" });
|
|
914
|
+
else if (req.kind === "fileChange") req.respond({ decision: "cancel" });
|
|
915
|
+
else if (req.kind === "user_input") req.respond({ answers: {} });
|
|
916
|
+
} catch (err) {
|
|
917
|
+
console.error(
|
|
918
|
+
`[codex-mcp] Failed to respond pending request during cancel: session=${sessionId} request=${reqId} kind=${req.kind} error=${err instanceof Error ? err.message : String(err)}`
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
session.pendingRequests.delete(reqId);
|
|
923
|
+
}
|
|
924
|
+
pushEvent(
|
|
925
|
+
session.eventBuffer,
|
|
926
|
+
"progress",
|
|
927
|
+
{ message: "Session cancelled", cancelledReason: session.cancelledReason },
|
|
928
|
+
true
|
|
929
|
+
);
|
|
930
|
+
const cancelledTurnId = session.activeTurnId ?? "";
|
|
931
|
+
session.activeTurnId = void 0;
|
|
932
|
+
session.lastResult = {
|
|
933
|
+
turnId: cancelledTurnId,
|
|
934
|
+
status: "cancelled",
|
|
935
|
+
error: session.cancelledReason,
|
|
936
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
937
|
+
};
|
|
938
|
+
pushEvent(
|
|
939
|
+
session.eventBuffer,
|
|
940
|
+
"result",
|
|
941
|
+
{ status: "cancelled", reason: session.cancelledReason, turnId: cancelledTurnId },
|
|
942
|
+
true
|
|
943
|
+
);
|
|
944
|
+
if (client) {
|
|
945
|
+
await client.destroy();
|
|
946
|
+
this.clients.delete(sessionId);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async interruptSession(sessionId) {
|
|
950
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
951
|
+
const client = this.getClientOrThrow(sessionId);
|
|
952
|
+
if (session.status !== "running" && session.status !== "waiting_approval") {
|
|
953
|
+
throw new Error(
|
|
954
|
+
`Error [${"SESSION_NOT_RUNNING" /* SESSION_NOT_RUNNING */}]: Cannot interrupt session in ${session.status} state`
|
|
955
|
+
);
|
|
956
|
+
}
|
|
957
|
+
if (!session.threadId || !session.activeTurnId) {
|
|
958
|
+
throw new Error(
|
|
959
|
+
`Error [${"INTERNAL" /* INTERNAL */}]: Missing threadId or activeTurnId for interrupt`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
await client.turnInterrupt({
|
|
963
|
+
threadId: session.threadId,
|
|
964
|
+
turnId: session.activeTurnId
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
async forkSession(sessionId) {
|
|
968
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
969
|
+
const originalClient = this.getClientOrThrow(sessionId);
|
|
970
|
+
if (!session.threadId) {
|
|
971
|
+
throw new Error(`Error [${"INTERNAL" /* INTERNAL */}]: No threadId to fork`);
|
|
972
|
+
}
|
|
973
|
+
const forkResult = await originalClient.threadFork({ threadId: session.threadId });
|
|
974
|
+
const forkedThreadId = extractThreadId(forkResult);
|
|
975
|
+
const newSessionId = `sess_${randomUUID().slice(0, 12)}`;
|
|
976
|
+
const newClient = this.createClient();
|
|
977
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
978
|
+
const newSession = {
|
|
979
|
+
sessionId: newSessionId,
|
|
980
|
+
status: "idle",
|
|
981
|
+
lastEventCursor: 0,
|
|
982
|
+
createdAt: now,
|
|
983
|
+
lastActiveAt: now,
|
|
984
|
+
approvalTimeoutMs: session.approvalTimeoutMs,
|
|
985
|
+
cwd: session.cwd,
|
|
986
|
+
model: session.model,
|
|
987
|
+
profile: session.profile,
|
|
988
|
+
approvalPolicy: session.approvalPolicy,
|
|
989
|
+
sandbox: session.sandbox,
|
|
990
|
+
config: session.config,
|
|
991
|
+
eventBuffer: createEventBuffer(),
|
|
992
|
+
pendingRequests: /* @__PURE__ */ new Map()
|
|
993
|
+
};
|
|
994
|
+
this.sessions.set(newSessionId, newSession);
|
|
995
|
+
this.clients.set(newSessionId, newClient);
|
|
996
|
+
try {
|
|
997
|
+
this.registerHandlers(newSessionId, newClient, newSession.approvalTimeoutMs);
|
|
998
|
+
await newClient.start({
|
|
999
|
+
profile: session.profile,
|
|
1000
|
+
model: session.model,
|
|
1001
|
+
approvalPolicy: session.approvalPolicy,
|
|
1002
|
+
sandbox: session.sandbox,
|
|
1003
|
+
config: session.config
|
|
1004
|
+
});
|
|
1005
|
+
await newClient.threadResume({ threadId: forkedThreadId });
|
|
1006
|
+
newSession.threadId = forkedThreadId;
|
|
1007
|
+
return {
|
|
1008
|
+
sessionId: newSessionId,
|
|
1009
|
+
threadId: forkedThreadId,
|
|
1010
|
+
status: "idle",
|
|
1011
|
+
pollInterval: DEFAULT_POLL_INTERVAL
|
|
1012
|
+
};
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
const errorMessage = redactPaths(err instanceof Error ? err.message : String(err));
|
|
1015
|
+
console.error(
|
|
1016
|
+
`[codex-mcp] forkSession failed after thread/fork created thread=${forkedThreadId}. The app-server protocol does not currently expose a guaranteed thread-delete RPC, so manual cleanup may be required.`
|
|
1017
|
+
);
|
|
1018
|
+
newSession.status = "error";
|
|
1019
|
+
try {
|
|
1020
|
+
await newClient.destroy();
|
|
1021
|
+
} catch (destroyErr) {
|
|
1022
|
+
console.error(
|
|
1023
|
+
`[codex-mcp] Failed to destroy forked app-server client after resume failure: session=${newSessionId} error=${destroyErr instanceof Error ? destroyErr.message : String(destroyErr)}`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
this.clients.delete(newSessionId);
|
|
1027
|
+
this.sessions.delete(newSessionId);
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
`Error [${"THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */}]: Failed to resume forked thread '${forkedThreadId}' in new app-server process: ${errorMessage}`
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// ── Event Polling ────────────────────────────────────────────────
|
|
1034
|
+
pollEvents(sessionId, cursor, maxEvents = DEFAULT_MAX_EVENTS, options = {}) {
|
|
1035
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
1036
|
+
const buf = session.eventBuffer;
|
|
1037
|
+
const responseMode = options.responseMode ?? "full";
|
|
1038
|
+
const pollOptions = options.pollOptions;
|
|
1039
|
+
const includeEvents = pollOptions?.includeEvents ?? true;
|
|
1040
|
+
const includeActions = pollOptions?.includeActions ?? true;
|
|
1041
|
+
const includeResult = pollOptions?.includeResult ?? true;
|
|
1042
|
+
const maxBytes = pollOptions?.maxBytes;
|
|
1043
|
+
const effectiveCursor = cursor ?? session.lastEventCursor;
|
|
1044
|
+
let events = includeEvents ? buf.events.filter((e) => e.id >= effectiveCursor) : [];
|
|
1045
|
+
let cursorResetTo;
|
|
1046
|
+
if (includeEvents && buf.events.length > 0) {
|
|
1047
|
+
const earliest = buf.events[0].id;
|
|
1048
|
+
if (earliest > effectiveCursor) {
|
|
1049
|
+
cursorResetTo = earliest;
|
|
1050
|
+
events = buf.events;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const cursorFloor = cursorResetTo ?? effectiveCursor;
|
|
1054
|
+
if (events.length > maxEvents) {
|
|
1055
|
+
events = events.slice(0, maxEvents);
|
|
1056
|
+
}
|
|
1057
|
+
let nextCursor = clampCursorToLatest(
|
|
1058
|
+
events.length > 0 ? events[events.length - 1].id + 1 : cursorFloor,
|
|
1059
|
+
buf.nextId
|
|
1060
|
+
);
|
|
1061
|
+
const actions = [];
|
|
1062
|
+
if (includeActions) {
|
|
1063
|
+
for (const [, req] of session.pendingRequests) {
|
|
1064
|
+
if (!req.resolved) {
|
|
1065
|
+
actions.push({
|
|
1066
|
+
type: req.kind === "user_input" ? "user_input" : "approval",
|
|
1067
|
+
requestId: req.requestId,
|
|
1068
|
+
kind: req.kind,
|
|
1069
|
+
params: req.params,
|
|
1070
|
+
itemId: req.itemId,
|
|
1071
|
+
reason: req.reason,
|
|
1072
|
+
createdAt: req.createdAt
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const result = {
|
|
1078
|
+
sessionId,
|
|
1079
|
+
status: session.status,
|
|
1080
|
+
pollInterval: pollIntervalForStatus(session.status),
|
|
1081
|
+
events: events.map((event) => serializeEventForMode(event, responseMode)),
|
|
1082
|
+
nextCursor,
|
|
1083
|
+
cursorResetTo,
|
|
1084
|
+
actions: actions.length > 0 ? actions : void 0,
|
|
1085
|
+
result: includeResult && (session.status === "idle" || session.status === "error" || session.status === "cancelled") ? session.lastResult : void 0
|
|
1086
|
+
};
|
|
1087
|
+
if (pollOptions?.includeTools === true) {
|
|
1088
|
+
addCompatWarningWithinBudget(
|
|
1089
|
+
result,
|
|
1090
|
+
"pollOptions.includeTools is not yet supported by codex-mcp; returning no tool metadata.",
|
|
1091
|
+
maxBytes
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof maxBytes === "number") {
|
|
1095
|
+
const normalizedMaxBytes = Math.max(1, Math.floor(maxBytes));
|
|
1096
|
+
const hasAnyPayload = result.events.length > 0 || typeof result.actions !== "undefined" || typeof result.result !== "undefined";
|
|
1097
|
+
if (hasAnyPayload && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1098
|
+
const truncatedFields = [];
|
|
1099
|
+
if (result.events.length > 0) {
|
|
1100
|
+
while (result.events.length > 0 && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1101
|
+
result.events.pop();
|
|
1102
|
+
}
|
|
1103
|
+
nextCursor = clampCursorToLatest(
|
|
1104
|
+
result.events.length > 0 ? result.events[result.events.length - 1].id + 1 : cursorFloor,
|
|
1105
|
+
buf.nextId
|
|
1106
|
+
);
|
|
1107
|
+
result.nextCursor = nextCursor;
|
|
1108
|
+
truncatedFields.push("events");
|
|
1109
|
+
}
|
|
1110
|
+
if (typeof result.result !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1111
|
+
result.result = void 0;
|
|
1112
|
+
truncatedFields.push("result");
|
|
1113
|
+
}
|
|
1114
|
+
if (typeof result.actions !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1115
|
+
if (session.status === "waiting_approval") {
|
|
1116
|
+
result.actions = compactActionsForBudget(result.actions);
|
|
1117
|
+
while (result.actions.length > 1 && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1118
|
+
result.actions.pop();
|
|
1119
|
+
}
|
|
1120
|
+
truncatedFields.push("actions");
|
|
1121
|
+
}
|
|
1122
|
+
if (typeof result.actions !== "undefined" && payloadByteSize(result) > normalizedMaxBytes) {
|
|
1123
|
+
result.actions = void 0;
|
|
1124
|
+
truncatedFields.push("actions");
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if (truncatedFields.length > 0) {
|
|
1128
|
+
result.truncated = true;
|
|
1129
|
+
result.truncatedFields = Array.from(new Set(truncatedFields));
|
|
1130
|
+
addCompatWarningWithinBudget(
|
|
1131
|
+
result,
|
|
1132
|
+
`Response truncated to respect pollOptions.maxBytes=${normalizedMaxBytes}.`,
|
|
1133
|
+
maxBytes
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
if (includeEvents) {
|
|
1139
|
+
session.lastEventCursor = persistMonotonicCursor(
|
|
1140
|
+
session.lastEventCursor,
|
|
1141
|
+
result.nextCursor,
|
|
1142
|
+
buf.nextId
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
return result;
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Monotonic polling helper for respond_* flows.
|
|
1149
|
+
* Uses max(providedCursor, session.lastEventCursor) to avoid replaying
|
|
1150
|
+
* already-consumed history when clients send stale/default cursors.
|
|
1151
|
+
*/
|
|
1152
|
+
pollEventsMonotonic(sessionId, cursor, maxEvents = DEFAULT_MAX_EVENTS, options = {}) {
|
|
1153
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
1154
|
+
const sessionCursor = session.lastEventCursor;
|
|
1155
|
+
const staleCursor = typeof cursor === "number" && cursor < sessionCursor;
|
|
1156
|
+
const effectiveCursor = typeof cursor === "number" ? Math.max(cursor, sessionCursor) : void 0;
|
|
1157
|
+
const result = this.pollEvents(sessionId, effectiveCursor, maxEvents, options);
|
|
1158
|
+
if (staleCursor) {
|
|
1159
|
+
addCompatWarningWithinBudget(
|
|
1160
|
+
result,
|
|
1161
|
+
`Provided cursor ${cursor} is stale; used session cursor ${sessionCursor}.`,
|
|
1162
|
+
options.pollOptions?.maxBytes
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return result;
|
|
1166
|
+
}
|
|
1167
|
+
// ── Approval Response ────────────────────────────────────────────
|
|
1168
|
+
resolveApproval(sessionId, requestId, decision, extra) {
|
|
1169
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
1170
|
+
const req = session.pendingRequests.get(requestId);
|
|
1171
|
+
if (!req || req.resolved) {
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`Error [${"REQUEST_NOT_FOUND" /* REQUEST_NOT_FOUND */}]: Request '${requestId}' not found or already resolved`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (req.kind === "command") {
|
|
1177
|
+
if (!COMMAND_DECISIONS.includes(decision)) {
|
|
1178
|
+
throw new Error(
|
|
1179
|
+
`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: Invalid command decision '${decision}'`
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (decision === "acceptWithExecpolicyAmendment" && (!extra?.execpolicyAmendment || extra.execpolicyAmendment.length === 0)) {
|
|
1183
|
+
throw new Error(
|
|
1184
|
+
`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: execpolicyAmendment required for acceptWithExecpolicyAmendment`
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1187
|
+
} else if (req.kind === "fileChange") {
|
|
1188
|
+
if (!FILE_CHANGE_DECISIONS.includes(decision)) {
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: Invalid fileChange decision '${decision}'`
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
throw new Error(
|
|
1195
|
+
`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: Request '${requestId}' is not an approval request`
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
let response;
|
|
1199
|
+
if (req.kind === "command") {
|
|
1200
|
+
response = buildCommandApprovalResponse(decision, extra?.execpolicyAmendment);
|
|
1201
|
+
} else if (req.kind === "fileChange") {
|
|
1202
|
+
response = { decision };
|
|
1203
|
+
}
|
|
1204
|
+
if (!response) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`Error [${"INTERNAL" /* INTERNAL */}]: Failed to build approval response for request '${requestId}'`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
sendPendingRequestResponseOrThrow(req, response, sessionId, requestId);
|
|
1210
|
+
req.resolved = true;
|
|
1211
|
+
req.decision = decision;
|
|
1212
|
+
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1213
|
+
pushEvent(
|
|
1214
|
+
session.eventBuffer,
|
|
1215
|
+
"approval_result",
|
|
1216
|
+
{
|
|
1217
|
+
requestId,
|
|
1218
|
+
kind: req.kind,
|
|
1219
|
+
decision,
|
|
1220
|
+
denyMessage: extra?.denyMessage
|
|
1221
|
+
},
|
|
1222
|
+
true
|
|
1223
|
+
);
|
|
1224
|
+
session.pendingRequests.delete(requestId);
|
|
1225
|
+
if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
|
|
1226
|
+
session.status = "running";
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
// ── User Input Response ──────────────────────────────────────────
|
|
1230
|
+
resolveUserInput(sessionId, requestId, answers) {
|
|
1231
|
+
const session = this.getSessionOrThrow(sessionId);
|
|
1232
|
+
const req = session.pendingRequests.get(requestId);
|
|
1233
|
+
if (!req || req.resolved || req.kind !== "user_input") {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`Error [${"REQUEST_NOT_FOUND" /* REQUEST_NOT_FOUND */}]: User input request '${requestId}' not found`
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
sendPendingRequestResponseOrThrow(
|
|
1239
|
+
req,
|
|
1240
|
+
{ answers },
|
|
1241
|
+
sessionId,
|
|
1242
|
+
requestId
|
|
1243
|
+
);
|
|
1244
|
+
req.resolved = true;
|
|
1245
|
+
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1246
|
+
pushEvent(
|
|
1247
|
+
session.eventBuffer,
|
|
1248
|
+
"approval_result",
|
|
1249
|
+
{
|
|
1250
|
+
requestId,
|
|
1251
|
+
kind: "user_input",
|
|
1252
|
+
answers
|
|
1253
|
+
},
|
|
1254
|
+
true
|
|
1255
|
+
);
|
|
1256
|
+
session.pendingRequests.delete(requestId);
|
|
1257
|
+
if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
|
|
1258
|
+
session.status = "running";
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
// ── Cleanup ──────────────────────────────────────────────────────
|
|
1262
|
+
destroy() {
|
|
1263
|
+
if (this.cleanupTimer) {
|
|
1264
|
+
clearInterval(this.cleanupTimer);
|
|
1265
|
+
this.cleanupTimer = null;
|
|
1266
|
+
}
|
|
1267
|
+
this.cancellationInFlight.clear();
|
|
1268
|
+
for (const [, session] of this.sessions) {
|
|
1269
|
+
clearSessionPendingRequests(session);
|
|
1270
|
+
}
|
|
1271
|
+
for (const [id, client] of this.clients) {
|
|
1272
|
+
client.destroy().catch((err) => {
|
|
1273
|
+
console.error(
|
|
1274
|
+
`[codex-mcp] Failed to destroy app-server client during manager.destroy(): session=${id} error=${err instanceof Error ? err.message : String(err)}`
|
|
1275
|
+
);
|
|
1276
|
+
});
|
|
1277
|
+
this.clients.delete(id);
|
|
1278
|
+
}
|
|
1279
|
+
this.sessions.clear();
|
|
1280
|
+
}
|
|
1281
|
+
// ── Private ──────────────────────────────────────────────────────
|
|
1282
|
+
getSessionOrThrow(sessionId) {
|
|
1283
|
+
const session = this.sessions.get(sessionId);
|
|
1284
|
+
if (!session) {
|
|
1285
|
+
throw new Error(`Error [${"SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */}]: Session '${sessionId}' not found`);
|
|
1286
|
+
}
|
|
1287
|
+
return session;
|
|
1288
|
+
}
|
|
1289
|
+
getClientOrThrow(sessionId) {
|
|
1290
|
+
const client = this.clients.get(sessionId);
|
|
1291
|
+
if (!client) {
|
|
1292
|
+
throw new Error(
|
|
1293
|
+
`Error [${"SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */}]: No client for session '${sessionId}'`
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
return client;
|
|
1297
|
+
}
|
|
1298
|
+
registerHandlers(sessionId, client, approvalTimeoutMs = DEFAULT_APPROVAL_TIMEOUT_MS) {
|
|
1299
|
+
const session = this.sessions.get(sessionId);
|
|
1300
|
+
client.onNotification((method, params) => {
|
|
1301
|
+
session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1302
|
+
const p = params;
|
|
1303
|
+
switch (method) {
|
|
1304
|
+
case Methods.TURN_STARTED:
|
|
1305
|
+
if (session.status === "cancelled") break;
|
|
1306
|
+
session.activeTurnId = p.turn?.id ?? (typeof p.turnId === "string" ? p.turnId : void 0);
|
|
1307
|
+
pushEvent(session.eventBuffer, "progress", { method, ...p });
|
|
1308
|
+
break;
|
|
1309
|
+
case Methods.TURN_COMPLETED: {
|
|
1310
|
+
if (session.status === "cancelled") break;
|
|
1311
|
+
const turnObj = p.turn;
|
|
1312
|
+
const completedTurnId = (typeof p.turnId === "string" ? p.turnId : void 0) ?? turnObj?.id ?? session.activeTurnId ?? "";
|
|
1313
|
+
session.status = "idle";
|
|
1314
|
+
session.activeTurnId = void 0;
|
|
1315
|
+
session.lastResult = {
|
|
1316
|
+
turnId: completedTurnId,
|
|
1317
|
+
output: turnObj?.output,
|
|
1318
|
+
structuredOutput: turnObj?.structuredOutput,
|
|
1319
|
+
turn: p.turn,
|
|
1320
|
+
status: turnObj?.status,
|
|
1321
|
+
turnError: turnObj?.error,
|
|
1322
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1323
|
+
};
|
|
1324
|
+
pushEvent(session.eventBuffer, "result", { method, ...p }, true);
|
|
1325
|
+
break;
|
|
1326
|
+
}
|
|
1327
|
+
case Methods.ERROR: {
|
|
1328
|
+
if (session.status === "cancelled") break;
|
|
1329
|
+
const willRetry = p.willRetry;
|
|
1330
|
+
if (!willRetry) {
|
|
1331
|
+
session.status = "error";
|
|
1332
|
+
}
|
|
1333
|
+
{
|
|
1334
|
+
const data = { method, ...p };
|
|
1335
|
+
if (typeof data.message === "string") data.message = redactPaths(data.message);
|
|
1336
|
+
if (typeof data.error === "string") data.error = redactPaths(data.error);
|
|
1337
|
+
if (willRetry) {
|
|
1338
|
+
pushEvent(
|
|
1339
|
+
session.eventBuffer,
|
|
1340
|
+
"progress",
|
|
1341
|
+
{
|
|
1342
|
+
...data,
|
|
1343
|
+
method: "codex-mcp/reconnect",
|
|
1344
|
+
sourceMethod: method,
|
|
1345
|
+
phase: "retrying"
|
|
1346
|
+
},
|
|
1347
|
+
true
|
|
1348
|
+
);
|
|
1349
|
+
} else {
|
|
1350
|
+
pushEvent(session.eventBuffer, "error", data, true);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
break;
|
|
1354
|
+
}
|
|
1355
|
+
case Methods.AGENT_MESSAGE_DELTA:
|
|
1356
|
+
pushEvent(session.eventBuffer, "output", { method, delta: p.delta, itemId: p.itemId });
|
|
1357
|
+
break;
|
|
1358
|
+
case Methods.ITEM_COMPLETED:
|
|
1359
|
+
{
|
|
1360
|
+
const item = p.item;
|
|
1361
|
+
const itemType = item && typeof item.type === "string" ? item.type : void 0;
|
|
1362
|
+
const eventType = itemType === "agentMessage" || itemType === "userMessage" ? "output" : "progress";
|
|
1363
|
+
pushEvent(session.eventBuffer, eventType, { method, item: p.item });
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
case Methods.COMMAND_OUTPUT_DELTA: {
|
|
1367
|
+
if (typeof p.delta === "string") {
|
|
1368
|
+
const cleaned = stripShellNoise(p.delta);
|
|
1369
|
+
if (cleaned.length === 0) break;
|
|
1370
|
+
pushEvent(session.eventBuffer, "progress", { method, ...p, delta: cleaned });
|
|
1371
|
+
} else {
|
|
1372
|
+
pushEvent(session.eventBuffer, "progress", { method, ...p });
|
|
1373
|
+
}
|
|
1374
|
+
break;
|
|
1375
|
+
}
|
|
1376
|
+
case Methods.FILE_CHANGE_OUTPUT_DELTA:
|
|
1377
|
+
case Methods.REASONING_TEXT_DELTA:
|
|
1378
|
+
case Methods.REASONING_SUMMARY_DELTA:
|
|
1379
|
+
case Methods.PLAN_DELTA:
|
|
1380
|
+
case Methods.MCP_TOOL_PROGRESS:
|
|
1381
|
+
case Methods.ITEM_STARTED:
|
|
1382
|
+
case Methods.TURN_DIFF_UPDATED:
|
|
1383
|
+
case Methods.TURN_PLAN_UPDATED:
|
|
1384
|
+
pushEvent(session.eventBuffer, "progress", { method, ...p });
|
|
1385
|
+
break;
|
|
1386
|
+
default:
|
|
1387
|
+
break;
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
client.onServerRequest((id, method, params) => {
|
|
1391
|
+
if (session.status === "cancelled" || session.status === "error") {
|
|
1392
|
+
respondToTerminalSessionRequest(client, id, method);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
session.lastActiveAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1396
|
+
const p = params;
|
|
1397
|
+
switch (method) {
|
|
1398
|
+
case Methods.COMMAND_APPROVAL: {
|
|
1399
|
+
const requestId = `req_${randomUUID().slice(0, 8)}`;
|
|
1400
|
+
const reason = normalizeOptionalString(p.reason);
|
|
1401
|
+
const pending = {
|
|
1402
|
+
requestId,
|
|
1403
|
+
kind: "command",
|
|
1404
|
+
params,
|
|
1405
|
+
itemId: p.itemId,
|
|
1406
|
+
threadId: p.threadId,
|
|
1407
|
+
turnId: p.turnId,
|
|
1408
|
+
reason,
|
|
1409
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1410
|
+
resolved: false,
|
|
1411
|
+
respond: (result) => client.respondToServer(id, result)
|
|
1412
|
+
};
|
|
1413
|
+
pending.timeoutHandle = createUnrefTimeout(() => {
|
|
1414
|
+
if (!pending.resolved) {
|
|
1415
|
+
pending.resolved = true;
|
|
1416
|
+
pending.decision = "decline";
|
|
1417
|
+
try {
|
|
1418
|
+
client.respondToServer(id, { decision: "decline" });
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
console.error(
|
|
1421
|
+
`[codex-mcp] Failed to auto-decline command approval timeout: session=${sessionId} request=${requestId} error=${err instanceof Error ? err.message : String(err)}`
|
|
1422
|
+
);
|
|
1423
|
+
}
|
|
1424
|
+
pushEvent(
|
|
1425
|
+
session.eventBuffer,
|
|
1426
|
+
"approval_result",
|
|
1427
|
+
{
|
|
1428
|
+
requestId,
|
|
1429
|
+
kind: "command",
|
|
1430
|
+
decision: "decline",
|
|
1431
|
+
timeout: true
|
|
1432
|
+
},
|
|
1433
|
+
true
|
|
1434
|
+
);
|
|
1435
|
+
session.pendingRequests.delete(requestId);
|
|
1436
|
+
if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
|
|
1437
|
+
session.status = "running";
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
}, approvalTimeoutMs);
|
|
1441
|
+
session.pendingRequests.set(requestId, pending);
|
|
1442
|
+
session.status = "waiting_approval";
|
|
1443
|
+
pushEvent(
|
|
1444
|
+
session.eventBuffer,
|
|
1445
|
+
"approval_request",
|
|
1446
|
+
{
|
|
1447
|
+
requestId,
|
|
1448
|
+
kind: "command",
|
|
1449
|
+
command: p.command,
|
|
1450
|
+
cwd: p.cwd,
|
|
1451
|
+
reason
|
|
1452
|
+
},
|
|
1453
|
+
true
|
|
1454
|
+
);
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
case Methods.FILE_CHANGE_APPROVAL: {
|
|
1458
|
+
const requestId = `req_${randomUUID().slice(0, 8)}`;
|
|
1459
|
+
const reason = normalizeOptionalString(p.reason);
|
|
1460
|
+
const pending = {
|
|
1461
|
+
requestId,
|
|
1462
|
+
kind: "fileChange",
|
|
1463
|
+
params,
|
|
1464
|
+
itemId: p.itemId,
|
|
1465
|
+
threadId: p.threadId,
|
|
1466
|
+
turnId: p.turnId,
|
|
1467
|
+
reason,
|
|
1468
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1469
|
+
resolved: false,
|
|
1470
|
+
respond: (result) => client.respondToServer(id, result)
|
|
1471
|
+
};
|
|
1472
|
+
pending.timeoutHandle = createUnrefTimeout(() => {
|
|
1473
|
+
if (!pending.resolved) {
|
|
1474
|
+
pending.resolved = true;
|
|
1475
|
+
pending.decision = "decline";
|
|
1476
|
+
try {
|
|
1477
|
+
client.respondToServer(id, { decision: "decline" });
|
|
1478
|
+
} catch (err) {
|
|
1479
|
+
console.error(
|
|
1480
|
+
`[codex-mcp] Failed to auto-decline file-change approval timeout: session=${sessionId} request=${requestId} error=${err instanceof Error ? err.message : String(err)}`
|
|
1481
|
+
);
|
|
1482
|
+
}
|
|
1483
|
+
pushEvent(
|
|
1484
|
+
session.eventBuffer,
|
|
1485
|
+
"approval_result",
|
|
1486
|
+
{
|
|
1487
|
+
requestId,
|
|
1488
|
+
kind: "fileChange",
|
|
1489
|
+
decision: "decline",
|
|
1490
|
+
timeout: true
|
|
1491
|
+
},
|
|
1492
|
+
true
|
|
1493
|
+
);
|
|
1494
|
+
session.pendingRequests.delete(requestId);
|
|
1495
|
+
if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
|
|
1496
|
+
session.status = "running";
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
}, approvalTimeoutMs);
|
|
1500
|
+
session.pendingRequests.set(requestId, pending);
|
|
1501
|
+
session.status = "waiting_approval";
|
|
1502
|
+
pushEvent(
|
|
1503
|
+
session.eventBuffer,
|
|
1504
|
+
"approval_request",
|
|
1505
|
+
{
|
|
1506
|
+
requestId,
|
|
1507
|
+
kind: "fileChange",
|
|
1508
|
+
itemId: p.itemId,
|
|
1509
|
+
reason
|
|
1510
|
+
},
|
|
1511
|
+
true
|
|
1512
|
+
);
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
case Methods.USER_INPUT_REQUEST: {
|
|
1516
|
+
const requestId = `req_${randomUUID().slice(0, 8)}`;
|
|
1517
|
+
const pending = {
|
|
1518
|
+
requestId,
|
|
1519
|
+
kind: "user_input",
|
|
1520
|
+
params,
|
|
1521
|
+
itemId: p.itemId,
|
|
1522
|
+
threadId: p.threadId,
|
|
1523
|
+
turnId: p.turnId,
|
|
1524
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1525
|
+
resolved: false,
|
|
1526
|
+
respond: (result) => client.respondToServer(id, result)
|
|
1527
|
+
};
|
|
1528
|
+
pending.timeoutHandle = createUnrefTimeout(() => {
|
|
1529
|
+
if (!pending.resolved) {
|
|
1530
|
+
pending.resolved = true;
|
|
1531
|
+
try {
|
|
1532
|
+
client.respondToServer(id, { answers: {} });
|
|
1533
|
+
} catch (err) {
|
|
1534
|
+
console.error(
|
|
1535
|
+
`[codex-mcp] Failed to auto-answer user-input timeout: session=${sessionId} request=${requestId} error=${err instanceof Error ? err.message : String(err)}`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
pushEvent(
|
|
1539
|
+
session.eventBuffer,
|
|
1540
|
+
"approval_result",
|
|
1541
|
+
{
|
|
1542
|
+
requestId,
|
|
1543
|
+
kind: "user_input",
|
|
1544
|
+
timeout: true
|
|
1545
|
+
},
|
|
1546
|
+
true
|
|
1547
|
+
);
|
|
1548
|
+
session.pendingRequests.delete(requestId);
|
|
1549
|
+
if (session.pendingRequests.size === 0 && session.status === "waiting_approval") {
|
|
1550
|
+
session.status = "running";
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}, approvalTimeoutMs);
|
|
1554
|
+
session.pendingRequests.set(requestId, pending);
|
|
1555
|
+
session.status = "waiting_approval";
|
|
1556
|
+
pushEvent(
|
|
1557
|
+
session.eventBuffer,
|
|
1558
|
+
"approval_request",
|
|
1559
|
+
{
|
|
1560
|
+
requestId,
|
|
1561
|
+
kind: "user_input",
|
|
1562
|
+
questions: p.questions
|
|
1563
|
+
},
|
|
1564
|
+
true
|
|
1565
|
+
);
|
|
1566
|
+
break;
|
|
1567
|
+
}
|
|
1568
|
+
case Methods.DYNAMIC_TOOL_CALL:
|
|
1569
|
+
client.respondToServer(id, {
|
|
1570
|
+
success: false,
|
|
1571
|
+
contentItems: [{ type: "inputText", text: "Not supported by codex-mcp" }]
|
|
1572
|
+
});
|
|
1573
|
+
break;
|
|
1574
|
+
case Methods.AUTH_TOKEN_REFRESH:
|
|
1575
|
+
client.respondErrorToServer(id, -32601, "Auth token refresh not supported by codex-mcp");
|
|
1576
|
+
break;
|
|
1577
|
+
case Methods.LEGACY_PATCH_APPROVAL:
|
|
1578
|
+
case Methods.LEGACY_EXEC_APPROVAL:
|
|
1579
|
+
client.respondToServer(id, { decision: "denied" });
|
|
1580
|
+
console.error(`[codex-mcp] Legacy approval request received: ${method}`);
|
|
1581
|
+
break;
|
|
1582
|
+
default:
|
|
1583
|
+
client.respondErrorToServer(id, -32601, `Unhandled server request: ${method}`);
|
|
1584
|
+
break;
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
client.on("exit", (code) => {
|
|
1588
|
+
clearSessionPendingRequests(session);
|
|
1589
|
+
if (session.status === "running" || session.status === "waiting_approval") {
|
|
1590
|
+
session.status = "error";
|
|
1591
|
+
const message = `app-server exited unexpectedly (code: ${code})`;
|
|
1592
|
+
setTerminalErrorResult(session, message);
|
|
1593
|
+
pushEvent(
|
|
1594
|
+
session.eventBuffer,
|
|
1595
|
+
"error",
|
|
1596
|
+
{
|
|
1597
|
+
message
|
|
1598
|
+
},
|
|
1599
|
+
true
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
client.on("error", (err) => {
|
|
1604
|
+
clearSessionPendingRequests(session);
|
|
1605
|
+
if (session.status === "running" || session.status === "waiting_approval") {
|
|
1606
|
+
session.status = "error";
|
|
1607
|
+
const message = redactPaths(`app-server error: ${err.message}`);
|
|
1608
|
+
setTerminalErrorResult(session, message);
|
|
1609
|
+
pushEvent(
|
|
1610
|
+
session.eventBuffer,
|
|
1611
|
+
"error",
|
|
1612
|
+
{
|
|
1613
|
+
message
|
|
1614
|
+
},
|
|
1615
|
+
true
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
cleanupSessions() {
|
|
1621
|
+
const now = Date.now();
|
|
1622
|
+
for (const [id, session] of this.sessions) {
|
|
1623
|
+
const lastActive = new Date(session.lastActiveAt).getTime();
|
|
1624
|
+
if (Number.isNaN(lastActive)) {
|
|
1625
|
+
this.requestCancellation(id, "Invalid timestamp");
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
const age = now - lastActive;
|
|
1629
|
+
if (session.status === "idle" && age > DEFAULT_IDLE_CLEANUP_MS) {
|
|
1630
|
+
this.requestCancellation(id, "Idle timeout");
|
|
1631
|
+
} else if (session.status === "waiting_approval" && age > DEFAULT_RUNNING_CLEANUP_MS) {
|
|
1632
|
+
this.requestCancellation(id, "Approval timeout");
|
|
1633
|
+
} else if (session.status === "running" && age > DEFAULT_RUNNING_CLEANUP_MS) {
|
|
1634
|
+
this.requestCancellation(id, "Running timeout");
|
|
1635
|
+
} else if ((session.status === "cancelled" || session.status === "error") && age > DEFAULT_TERMINAL_CLEANUP_MS) {
|
|
1636
|
+
this.clients.get(id)?.destroy().catch((err) => {
|
|
1637
|
+
console.error(
|
|
1638
|
+
`[codex-mcp] Failed to destroy app-server client during cleanup: session=${id} error=${err instanceof Error ? err.message : String(err)}`
|
|
1639
|
+
);
|
|
1640
|
+
});
|
|
1641
|
+
this.clients.delete(id);
|
|
1642
|
+
this.sessions.delete(id);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
requestCancellation(sessionId, reason) {
|
|
1647
|
+
if (this.cancellationInFlight.has(sessionId)) return;
|
|
1648
|
+
this.cancelSession(sessionId, reason).catch((err) => {
|
|
1649
|
+
console.error(
|
|
1650
|
+
`[codex-mcp] Failed to cancel session during cleanup: session=${sessionId} reason=${reason} error=${err instanceof Error ? err.message : String(err)}`
|
|
1651
|
+
);
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
};
|
|
1655
|
+
function pollIntervalForStatus(status) {
|
|
1656
|
+
if (status === "waiting_approval") return WAITING_APPROVAL_POLL_INTERVAL;
|
|
1657
|
+
if (status === "running") return DEFAULT_POLL_INTERVAL;
|
|
1658
|
+
return void 0;
|
|
1659
|
+
}
|
|
1660
|
+
function createEventBuffer() {
|
|
1661
|
+
return {
|
|
1662
|
+
events: [],
|
|
1663
|
+
maxSize: DEFAULT_EVENT_BUFFER_SIZE,
|
|
1664
|
+
hardMaxSize: DEFAULT_EVENT_BUFFER_HARD_SIZE,
|
|
1665
|
+
nextId: 0
|
|
1666
|
+
};
|
|
1667
|
+
}
|
|
1668
|
+
function clearTerminalEvents(buf) {
|
|
1669
|
+
buf.events = buf.events.filter((e) => e.type !== "result" && e.type !== "error");
|
|
1670
|
+
}
|
|
1671
|
+
function clearSessionPendingRequests(session) {
|
|
1672
|
+
const entries = Array.from(session.pendingRequests.entries());
|
|
1673
|
+
session.pendingRequests.clear();
|
|
1674
|
+
for (const [, req] of entries) {
|
|
1675
|
+
if (req.timeoutHandle) clearTimeout(req.timeoutHandle);
|
|
1676
|
+
req.resolved = true;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
function setTerminalErrorResult(session, message) {
|
|
1680
|
+
const completedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1681
|
+
const failedTurnId = session.activeTurnId ?? "";
|
|
1682
|
+
session.activeTurnId = void 0;
|
|
1683
|
+
session.lastResult = {
|
|
1684
|
+
turnId: failedTurnId,
|
|
1685
|
+
status: "error",
|
|
1686
|
+
error: message,
|
|
1687
|
+
completedAt
|
|
1688
|
+
};
|
|
1689
|
+
pushEvent(
|
|
1690
|
+
session.eventBuffer,
|
|
1691
|
+
"result",
|
|
1692
|
+
{
|
|
1693
|
+
status: "error",
|
|
1694
|
+
turnId: failedTurnId,
|
|
1695
|
+
error: message,
|
|
1696
|
+
completedAt
|
|
1697
|
+
},
|
|
1698
|
+
true
|
|
1699
|
+
);
|
|
1700
|
+
}
|
|
1701
|
+
function createUnrefTimeout(handler, timeoutMs) {
|
|
1702
|
+
const timer = setTimeout(handler, timeoutMs);
|
|
1703
|
+
if (typeof timer.unref === "function") {
|
|
1704
|
+
timer.unref();
|
|
1705
|
+
}
|
|
1706
|
+
return timer;
|
|
1707
|
+
}
|
|
1708
|
+
function respondToTerminalSessionRequest(client, id, method) {
|
|
1709
|
+
switch (method) {
|
|
1710
|
+
case Methods.COMMAND_APPROVAL:
|
|
1711
|
+
case Methods.FILE_CHANGE_APPROVAL:
|
|
1712
|
+
client.respondToServer(id, { decision: "cancel" });
|
|
1713
|
+
break;
|
|
1714
|
+
case Methods.USER_INPUT_REQUEST:
|
|
1715
|
+
client.respondToServer(id, { answers: {} });
|
|
1716
|
+
break;
|
|
1717
|
+
case Methods.DYNAMIC_TOOL_CALL:
|
|
1718
|
+
client.respondToServer(id, {
|
|
1719
|
+
success: false,
|
|
1720
|
+
contentItems: [{ type: "inputText", text: "Session is terminal" }]
|
|
1721
|
+
});
|
|
1722
|
+
break;
|
|
1723
|
+
case Methods.AUTH_TOKEN_REFRESH:
|
|
1724
|
+
client.respondErrorToServer(id, -32601, "Session is terminal");
|
|
1725
|
+
break;
|
|
1726
|
+
case Methods.LEGACY_PATCH_APPROVAL:
|
|
1727
|
+
case Methods.LEGACY_EXEC_APPROVAL:
|
|
1728
|
+
client.respondToServer(id, { decision: "denied" });
|
|
1729
|
+
break;
|
|
1730
|
+
default:
|
|
1731
|
+
client.respondErrorToServer(id, -32601, `Unhandled server request: ${method}`);
|
|
1732
|
+
break;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
function normalizeOptionalString(value) {
|
|
1736
|
+
return typeof value === "string" ? value : void 0;
|
|
1737
|
+
}
|
|
1738
|
+
function sendPendingRequestResponseOrThrow(req, response, sessionId, requestId) {
|
|
1739
|
+
if (!req.respond) {
|
|
1740
|
+
throw new Error(
|
|
1741
|
+
`Error [${"INTERNAL" /* INTERNAL */}]: Missing response handler for request '${requestId}'`
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
try {
|
|
1745
|
+
req.respond(response);
|
|
1746
|
+
} catch (err) {
|
|
1747
|
+
throw new Error(
|
|
1748
|
+
`Error [${"INTERNAL" /* INTERNAL */}]: Failed to send response: session=${sessionId} request=${requestId} kind=${req.kind} error=${err instanceof Error ? err.message : String(err)}`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function compactActionsForBudget(actions) {
|
|
1753
|
+
return actions.map((action) => ({
|
|
1754
|
+
type: action.type,
|
|
1755
|
+
requestId: action.requestId,
|
|
1756
|
+
kind: action.kind,
|
|
1757
|
+
params: compactActionParamsForBudget(action),
|
|
1758
|
+
itemId: action.itemId,
|
|
1759
|
+
createdAt: action.createdAt
|
|
1760
|
+
}));
|
|
1761
|
+
}
|
|
1762
|
+
function compactActionParamsForBudget(action) {
|
|
1763
|
+
if (action.kind !== "user_input" || !isRecord(action.params)) {
|
|
1764
|
+
return void 0;
|
|
1765
|
+
}
|
|
1766
|
+
const rawQuestions = action.params.questions;
|
|
1767
|
+
if (!Array.isArray(rawQuestions)) {
|
|
1768
|
+
return void 0;
|
|
1769
|
+
}
|
|
1770
|
+
const compactQuestions = [];
|
|
1771
|
+
for (const entry of rawQuestions) {
|
|
1772
|
+
if (isRecord(entry) && typeof entry.questionId === "string") {
|
|
1773
|
+
compactQuestions.push({ questionId: entry.questionId });
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
return compactQuestions.length > 0 ? { questions: compactQuestions } : void 0;
|
|
1777
|
+
}
|
|
1778
|
+
function clampCursorToLatest(cursor, latestCursor) {
|
|
1779
|
+
return Math.max(0, Math.min(cursor, latestCursor));
|
|
1780
|
+
}
|
|
1781
|
+
function persistMonotonicCursor(previousCursor, nextCursor, latestCursor) {
|
|
1782
|
+
const boundedCursor = clampCursorToLatest(nextCursor, latestCursor);
|
|
1783
|
+
return Math.max(previousCursor, boundedCursor);
|
|
1784
|
+
}
|
|
1785
|
+
function pushEvent(buf, type, data, pinned = false) {
|
|
1786
|
+
if (tryCoalesceProgressDelta(buf, type, data, pinned)) return;
|
|
1787
|
+
buf.events.push({
|
|
1788
|
+
id: buf.nextId++,
|
|
1789
|
+
type,
|
|
1790
|
+
data,
|
|
1791
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1792
|
+
pinned
|
|
1793
|
+
});
|
|
1794
|
+
evictEvents(buf);
|
|
1795
|
+
}
|
|
1796
|
+
function serializeEventForMode(event, mode) {
|
|
1797
|
+
if (mode === "full") {
|
|
1798
|
+
return { id: event.id, type: event.type, data: event.data, timestamp: event.timestamp };
|
|
1799
|
+
}
|
|
1800
|
+
const minimal = mode === "minimal";
|
|
1801
|
+
return {
|
|
1802
|
+
id: event.id,
|
|
1803
|
+
type: event.type,
|
|
1804
|
+
data: compactEventData(event.data, minimal),
|
|
1805
|
+
timestamp: event.timestamp
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
function compactEventData(data, minimal) {
|
|
1809
|
+
if (!isRecord(data)) return data;
|
|
1810
|
+
const compact = {};
|
|
1811
|
+
if (typeof data.method === "string") {
|
|
1812
|
+
compact.method = data.method;
|
|
1813
|
+
}
|
|
1814
|
+
const preferredKeys = minimal ? [
|
|
1815
|
+
"delta",
|
|
1816
|
+
"message",
|
|
1817
|
+
"error",
|
|
1818
|
+
"status",
|
|
1819
|
+
"phase",
|
|
1820
|
+
"itemId",
|
|
1821
|
+
"turnId",
|
|
1822
|
+
"requestId",
|
|
1823
|
+
"kind",
|
|
1824
|
+
"decision",
|
|
1825
|
+
"timeout",
|
|
1826
|
+
"willRetry",
|
|
1827
|
+
"retryCount",
|
|
1828
|
+
"maxRetries"
|
|
1829
|
+
] : [
|
|
1830
|
+
"delta",
|
|
1831
|
+
"message",
|
|
1832
|
+
"error",
|
|
1833
|
+
"status",
|
|
1834
|
+
"phase",
|
|
1835
|
+
"itemId",
|
|
1836
|
+
"turnId",
|
|
1837
|
+
"requestId",
|
|
1838
|
+
"kind",
|
|
1839
|
+
"decision",
|
|
1840
|
+
"timeout",
|
|
1841
|
+
"willRetry",
|
|
1842
|
+
"retryCount",
|
|
1843
|
+
"maxRetries",
|
|
1844
|
+
"reason",
|
|
1845
|
+
"command",
|
|
1846
|
+
"cwd",
|
|
1847
|
+
"sourceMethod"
|
|
1848
|
+
];
|
|
1849
|
+
for (const key of preferredKeys) {
|
|
1850
|
+
if (key in data) {
|
|
1851
|
+
compact[key] = data[key];
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (typeof compact.delta === "string") {
|
|
1855
|
+
const limit = minimal ? 256 : 2048;
|
|
1856
|
+
if (compact.delta.length > limit) {
|
|
1857
|
+
compact.delta = compact.delta.slice(0, limit);
|
|
1858
|
+
compact.deltaTruncated = true;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (Object.keys(compact).length === 0) {
|
|
1862
|
+
return minimal ? { summary: "omitted for minimal response mode" } : { ...data };
|
|
1863
|
+
}
|
|
1864
|
+
return compact;
|
|
1865
|
+
}
|
|
1866
|
+
function payloadByteSize(value) {
|
|
1867
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
1868
|
+
}
|
|
1869
|
+
function addCompatWarning(result, warning) {
|
|
1870
|
+
if (!result.compatWarnings) {
|
|
1871
|
+
result.compatWarnings = [];
|
|
1872
|
+
}
|
|
1873
|
+
result.compatWarnings.push(warning);
|
|
1874
|
+
}
|
|
1875
|
+
function addCompatWarningWithinBudget(result, warning, maxBytes) {
|
|
1876
|
+
const previousWarnings = result.compatWarnings ? [...result.compatWarnings] : void 0;
|
|
1877
|
+
addCompatWarning(result, warning);
|
|
1878
|
+
if (typeof maxBytes !== "number") {
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const normalizedMaxBytes = Math.max(1, Math.floor(maxBytes));
|
|
1882
|
+
if (payloadByteSize(result) <= normalizedMaxBytes) {
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (!previousWarnings || previousWarnings.length === 0) {
|
|
1886
|
+
result.compatWarnings = void 0;
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1889
|
+
result.compatWarnings = previousWarnings;
|
|
1890
|
+
}
|
|
1891
|
+
function tryCoalesceProgressDelta(buf, type, data, pinned) {
|
|
1892
|
+
if (type !== "progress" || pinned || buf.events.length === 0) return false;
|
|
1893
|
+
if (!isRecord(data)) return false;
|
|
1894
|
+
const method = data.method;
|
|
1895
|
+
const delta = data.delta;
|
|
1896
|
+
const itemId = data.itemId;
|
|
1897
|
+
const turnId = data.turnId;
|
|
1898
|
+
const itemKey = typeof itemId === "string" ? itemId : "";
|
|
1899
|
+
const turnKey = typeof turnId === "string" ? turnId : "";
|
|
1900
|
+
if (typeof method !== "string" || !COALESCED_PROGRESS_DELTA_METHODS.has(method) || typeof delta !== "string") {
|
|
1901
|
+
return false;
|
|
1902
|
+
}
|
|
1903
|
+
if (itemKey.length === 0 && turnKey.length === 0) return false;
|
|
1904
|
+
const last = buf.events[buf.events.length - 1];
|
|
1905
|
+
if (last.type !== "progress" || last.pinned || !isRecord(last.data)) return false;
|
|
1906
|
+
const lastMethod = last.data.method;
|
|
1907
|
+
const lastItemId = last.data.itemId;
|
|
1908
|
+
const lastTurnId = last.data.turnId;
|
|
1909
|
+
const lastDelta = last.data.delta;
|
|
1910
|
+
const lastItemKey = typeof lastItemId === "string" ? lastItemId : "";
|
|
1911
|
+
const lastTurnKey = typeof lastTurnId === "string" ? lastTurnId : "";
|
|
1912
|
+
if (lastMethod !== method || lastItemKey !== itemKey || lastTurnKey !== turnKey || typeof lastDelta !== "string") {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
if (lastDelta.length + delta.length > MAX_COALESCED_DELTA_CHARS) return false;
|
|
1916
|
+
last.data = {
|
|
1917
|
+
...last.data,
|
|
1918
|
+
delta: `${lastDelta}${delta}`
|
|
1919
|
+
};
|
|
1920
|
+
last.timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1921
|
+
return true;
|
|
1922
|
+
}
|
|
1923
|
+
function evictEvents(buf) {
|
|
1924
|
+
while (buf.events.length > buf.maxSize) {
|
|
1925
|
+
const idx = buf.events.findIndex((e) => !e.pinned);
|
|
1926
|
+
if (idx === -1) break;
|
|
1927
|
+
buf.events.splice(idx, 1);
|
|
1928
|
+
}
|
|
1929
|
+
while (buf.events.length > buf.maxSize) {
|
|
1930
|
+
const idx = buf.events.findIndex((e) => e.type === "approval_result");
|
|
1931
|
+
if (idx === -1) break;
|
|
1932
|
+
buf.events.splice(idx, 1);
|
|
1933
|
+
}
|
|
1934
|
+
if (buf.events.length <= buf.hardMaxSize) return;
|
|
1935
|
+
const overflow = buf.events.length - buf.hardMaxSize;
|
|
1936
|
+
const approvalResultIdx = [];
|
|
1937
|
+
const nonPinnedIdx = [];
|
|
1938
|
+
const pinnedNonCriticalIdx = [];
|
|
1939
|
+
const criticalPinnedIdx = [];
|
|
1940
|
+
for (let i = 0; i < buf.events.length; i++) {
|
|
1941
|
+
const event = buf.events[i];
|
|
1942
|
+
if (event.type === "approval_result") {
|
|
1943
|
+
approvalResultIdx.push(i);
|
|
1944
|
+
} else if (!event.pinned) {
|
|
1945
|
+
nonPinnedIdx.push(i);
|
|
1946
|
+
} else if (!isHardPinnedCriticalType(event.type)) {
|
|
1947
|
+
pinnedNonCriticalIdx.push(i);
|
|
1948
|
+
} else {
|
|
1949
|
+
criticalPinnedIdx.push(i);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const drop = /* @__PURE__ */ new Set();
|
|
1953
|
+
const take = (indices) => {
|
|
1954
|
+
for (const idx of indices) {
|
|
1955
|
+
if (drop.size >= overflow) break;
|
|
1956
|
+
drop.add(idx);
|
|
1957
|
+
}
|
|
1958
|
+
};
|
|
1959
|
+
take(approvalResultIdx);
|
|
1960
|
+
take(nonPinnedIdx);
|
|
1961
|
+
take(pinnedNonCriticalIdx);
|
|
1962
|
+
const beforeCritical = drop.size;
|
|
1963
|
+
take(criticalPinnedIdx);
|
|
1964
|
+
if (drop.size > beforeCritical) {
|
|
1965
|
+
console.error(
|
|
1966
|
+
"[codex-mcp] Event buffer hard limit exceeded with only critical pinned events; evicting oldest event."
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
if (drop.size === 0) return;
|
|
1970
|
+
buf.events = buf.events.filter((_, idx) => !drop.has(idx));
|
|
1971
|
+
}
|
|
1972
|
+
function isHardPinnedCriticalType(type) {
|
|
1973
|
+
return type === "approval_request" || type === "result" || type === "error";
|
|
1974
|
+
}
|
|
1975
|
+
function toPublicInfo(session) {
|
|
1976
|
+
return {
|
|
1977
|
+
sessionId: session.sessionId,
|
|
1978
|
+
status: session.status,
|
|
1979
|
+
createdAt: session.createdAt,
|
|
1980
|
+
lastActiveAt: session.lastActiveAt,
|
|
1981
|
+
cancelledAt: session.cancelledAt,
|
|
1982
|
+
cancelledReason: session.cancelledReason,
|
|
1983
|
+
model: session.model,
|
|
1984
|
+
approvalPolicy: session.approvalPolicy,
|
|
1985
|
+
sandbox: session.sandbox,
|
|
1986
|
+
pendingRequestCount: Array.from(session.pendingRequests.values()).filter((r) => !r.resolved).length
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
function toSensitiveInfo(session) {
|
|
1990
|
+
return {
|
|
1991
|
+
...toPublicInfo(session),
|
|
1992
|
+
threadId: session.threadId,
|
|
1993
|
+
cwd: session.cwd,
|
|
1994
|
+
profile: session.profile,
|
|
1995
|
+
config: session.config
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
function buildCommandApprovalResponse(decision, execpolicyAmendment) {
|
|
1999
|
+
if (decision === "acceptWithExecpolicyAmendment") {
|
|
2000
|
+
if (!execpolicyAmendment || execpolicyAmendment.length === 0) {
|
|
2001
|
+
throw new Error(
|
|
2002
|
+
`Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: execpolicyAmendment required for acceptWithExecpolicyAmendment`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
return {
|
|
2006
|
+
decision: {
|
|
2007
|
+
acceptWithExecpolicyAmendment: {
|
|
2008
|
+
execpolicy_amendment: execpolicyAmendment
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
return { decision };
|
|
2014
|
+
}
|
|
2015
|
+
function isRecord(value) {
|
|
2016
|
+
return typeof value === "object" && value !== null;
|
|
2017
|
+
}
|
|
2018
|
+
function extractThreadId(result) {
|
|
2019
|
+
if (!isRecord(result)) {
|
|
2020
|
+
throw new Error(`Error [${"INTERNAL" /* INTERNAL */}]: Invalid thread response: expected object`);
|
|
2021
|
+
}
|
|
2022
|
+
const direct = result.threadId;
|
|
2023
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
2024
|
+
const thread = result.thread;
|
|
2025
|
+
if (isRecord(thread) && typeof thread.id === "string" && thread.id.length > 0) return thread.id;
|
|
2026
|
+
throw new Error(`Error [${"INTERNAL" /* INTERNAL */}]: Invalid thread response: missing thread id`);
|
|
2027
|
+
}
|
|
2028
|
+
function extractTurnId(result) {
|
|
2029
|
+
if (!isRecord(result)) return void 0;
|
|
2030
|
+
const direct = result.turnId;
|
|
2031
|
+
if (typeof direct === "string" && direct.length > 0) return direct;
|
|
2032
|
+
const turn = result.turn;
|
|
2033
|
+
if (isRecord(turn) && typeof turn.id === "string" && turn.id.length > 0) return turn.id;
|
|
2034
|
+
return void 0;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/utils/config.ts
|
|
2038
|
+
function extractSpawnOptions(params) {
|
|
2039
|
+
return {
|
|
2040
|
+
profile: params.profile,
|
|
2041
|
+
model: params.model,
|
|
2042
|
+
approvalPolicy: params.approvalPolicy,
|
|
2043
|
+
sandbox: params.sandbox,
|
|
2044
|
+
config: params.advanced?.config
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
// src/tools/codex.ts
|
|
2049
|
+
async function executeCodex(args, sessionManager, serverCwd) {
|
|
2050
|
+
const cwd = resolveAndValidateCwd(args.cwd, serverCwd);
|
|
2051
|
+
const spawnOpts = extractSpawnOptions(args);
|
|
2052
|
+
const effort = args.effort ?? DEFAULT_EFFORT_LEVEL;
|
|
2053
|
+
return sessionManager.createSession(args.prompt, cwd, spawnOpts, effort, args.advanced);
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/tools/codex-reply.ts
|
|
2057
|
+
async function executeCodexReply(args, sessionManager) {
|
|
2058
|
+
return sessionManager.replyToSession(args.sessionId, args.prompt, {
|
|
2059
|
+
model: args.model,
|
|
2060
|
+
approvalPolicy: args.approvalPolicy,
|
|
2061
|
+
effort: args.effort,
|
|
2062
|
+
summary: args.summary,
|
|
2063
|
+
personality: args.personality,
|
|
2064
|
+
sandbox: args.sandbox,
|
|
2065
|
+
cwd: args.cwd,
|
|
2066
|
+
outputSchema: args.outputSchema
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// src/tools/codex-session.ts
|
|
2071
|
+
async function executeCodexSession(args, sessionManager) {
|
|
2072
|
+
switch (args.action) {
|
|
2073
|
+
case "list":
|
|
2074
|
+
return { sessions: sessionManager.listSessions() };
|
|
2075
|
+
case "get":
|
|
2076
|
+
if (!args.sessionId) {
|
|
2077
|
+
return {
|
|
2078
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: sessionId required for 'get'`,
|
|
2079
|
+
isError: true
|
|
2080
|
+
};
|
|
2081
|
+
}
|
|
2082
|
+
return sessionManager.getSession(args.sessionId, args.includeSensitive);
|
|
2083
|
+
case "cancel":
|
|
2084
|
+
if (!args.sessionId) {
|
|
2085
|
+
return {
|
|
2086
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: sessionId required for 'cancel'`,
|
|
2087
|
+
isError: true
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
await sessionManager.cancelSession(args.sessionId);
|
|
2091
|
+
return { success: true, message: `Session ${args.sessionId} cancelled` };
|
|
2092
|
+
case "interrupt":
|
|
2093
|
+
if (!args.sessionId) {
|
|
2094
|
+
return {
|
|
2095
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: sessionId required for 'interrupt'`,
|
|
2096
|
+
isError: true
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
await sessionManager.interruptSession(args.sessionId);
|
|
2100
|
+
return { success: true, message: `Session ${args.sessionId} interrupted` };
|
|
2101
|
+
case "fork":
|
|
2102
|
+
if (!args.sessionId) {
|
|
2103
|
+
return {
|
|
2104
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: sessionId required for 'fork'`,
|
|
2105
|
+
isError: true
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
return await sessionManager.forkSession(args.sessionId);
|
|
2109
|
+
default:
|
|
2110
|
+
return {
|
|
2111
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: Unknown action '${args.action}'`,
|
|
2112
|
+
isError: true
|
|
2113
|
+
};
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/tools/codex-check.ts
|
|
2118
|
+
function executeCodexCheck(args, sessionManager) {
|
|
2119
|
+
const responseMode = args.responseMode ?? "minimal";
|
|
2120
|
+
const pollOptions = args.pollOptions;
|
|
2121
|
+
switch (args.action) {
|
|
2122
|
+
case "poll": {
|
|
2123
|
+
const maxEvents = typeof args.maxEvents === "number" ? Math.max(POLL_MIN_MAX_EVENTS, args.maxEvents) : POLL_DEFAULT_MAX_EVENTS;
|
|
2124
|
+
return sessionManager.pollEvents(args.sessionId, args.cursor, maxEvents, {
|
|
2125
|
+
responseMode,
|
|
2126
|
+
pollOptions
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
case "respond_permission":
|
|
2130
|
+
case "respond_approval": {
|
|
2131
|
+
if (!args.requestId || !args.decision) {
|
|
2132
|
+
return {
|
|
2133
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: requestId and decision required for respond_permission/respond_approval`,
|
|
2134
|
+
isError: true
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
try {
|
|
2138
|
+
sessionManager.resolveApproval(args.sessionId, args.requestId, args.decision, {
|
|
2139
|
+
execpolicyAmendment: args.execpolicyAmendment,
|
|
2140
|
+
denyMessage: args.denyMessage
|
|
2141
|
+
});
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2144
|
+
return { error: message, isError: true };
|
|
2145
|
+
}
|
|
2146
|
+
const maxEvents = args.maxEvents ?? RESPOND_DEFAULT_MAX_EVENTS;
|
|
2147
|
+
const result = sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
|
|
2148
|
+
responseMode,
|
|
2149
|
+
pollOptions
|
|
2150
|
+
});
|
|
2151
|
+
if (args.action === "respond_approval") {
|
|
2152
|
+
return addWarning(
|
|
2153
|
+
result,
|
|
2154
|
+
"Action 'respond_approval' is deprecated, use 'respond_permission'.",
|
|
2155
|
+
pollOptions?.maxBytes
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return result;
|
|
2159
|
+
}
|
|
2160
|
+
case "respond_user_input": {
|
|
2161
|
+
if (!args.requestId || !args.answers) {
|
|
2162
|
+
return {
|
|
2163
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: requestId and answers required for respond_user_input`,
|
|
2164
|
+
isError: true
|
|
2165
|
+
};
|
|
2166
|
+
}
|
|
2167
|
+
try {
|
|
2168
|
+
sessionManager.resolveUserInput(args.sessionId, args.requestId, args.answers);
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2171
|
+
return { error: message, isError: true };
|
|
2172
|
+
}
|
|
2173
|
+
const maxEvents = args.maxEvents ?? RESPOND_DEFAULT_MAX_EVENTS;
|
|
2174
|
+
return sessionManager.pollEventsMonotonic(args.sessionId, args.cursor, maxEvents, {
|
|
2175
|
+
responseMode,
|
|
2176
|
+
pollOptions
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
default:
|
|
2180
|
+
return {
|
|
2181
|
+
error: `Error [${"INVALID_ARGUMENT" /* INVALID_ARGUMENT */}]: Unknown action '${args.action}'`,
|
|
2182
|
+
isError: true
|
|
2183
|
+
};
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function addWarning(result, warning, maxBytes) {
|
|
2187
|
+
if (!result.compatWarnings) result.compatWarnings = [];
|
|
2188
|
+
result.compatWarnings.push(warning);
|
|
2189
|
+
if (typeof maxBytes !== "number") {
|
|
2190
|
+
return result;
|
|
2191
|
+
}
|
|
2192
|
+
const normalizedMaxBytes = Math.max(1, Math.floor(maxBytes));
|
|
2193
|
+
if (Buffer.byteLength(JSON.stringify(result), "utf8") <= normalizedMaxBytes) {
|
|
2194
|
+
return result;
|
|
2195
|
+
}
|
|
2196
|
+
if (result.compatWarnings.length > 1) {
|
|
2197
|
+
result.compatWarnings.pop();
|
|
2198
|
+
} else {
|
|
2199
|
+
result.compatWarnings = void 0;
|
|
2200
|
+
}
|
|
2201
|
+
return result;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// src/resources/register-resources.ts
|
|
2205
|
+
import { spawnSync } from "child_process";
|
|
2206
|
+
|
|
2207
|
+
// src/utils/stdio-guard.ts
|
|
2208
|
+
var STDIO_MODES = ["auto", "strict", "off"];
|
|
2209
|
+
function resolveStdioMode(env = process.env) {
|
|
2210
|
+
const raw = env.CODEX_MCP_STDIO_MODE;
|
|
2211
|
+
if (raw === void 0) {
|
|
2212
|
+
return { mode: "auto", source: "default" };
|
|
2213
|
+
}
|
|
2214
|
+
const normalized = raw.trim().toLowerCase();
|
|
2215
|
+
if (normalized === "") {
|
|
2216
|
+
return { mode: "auto", source: "default" };
|
|
2217
|
+
}
|
|
2218
|
+
if (STDIO_MODES.includes(normalized)) {
|
|
2219
|
+
return { mode: normalized, source: "env" };
|
|
2220
|
+
}
|
|
2221
|
+
return { mode: "auto", source: "env_invalid", invalidRaw: raw };
|
|
2222
|
+
}
|
|
2223
|
+
function runStdioPreflight(opts = {}) {
|
|
2224
|
+
const platform = opts.platform ?? process.platform;
|
|
2225
|
+
const env = opts.env ?? process.env;
|
|
2226
|
+
const stdinIsTTY = opts.stdinIsTTY ?? Boolean(process.stdin.isTTY);
|
|
2227
|
+
const stdoutIsTTY = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
|
|
2228
|
+
const modeResolution = resolveStdioMode(env);
|
|
2229
|
+
const notes = [];
|
|
2230
|
+
const riskReasons = [];
|
|
2231
|
+
if (modeResolution.source === "env_invalid" && modeResolution.invalidRaw) {
|
|
2232
|
+
notes.push(
|
|
2233
|
+
`Invalid CODEX_MCP_STDIO_MODE='${modeResolution.invalidRaw}'. Falling back to 'auto'.`
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
if (modeResolution.mode === "off") {
|
|
2237
|
+
return {
|
|
2238
|
+
mode: modeResolution.mode,
|
|
2239
|
+
modeSource: modeResolution.source,
|
|
2240
|
+
invalidMode: modeResolution.invalidRaw,
|
|
2241
|
+
riskLevel: "low",
|
|
2242
|
+
riskReasons: [],
|
|
2243
|
+
blockingReasons: [],
|
|
2244
|
+
notes,
|
|
2245
|
+
suggestions: [],
|
|
2246
|
+
shouldBlock: false
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
const blockingReasons = [];
|
|
2250
|
+
if (platform === "win32" && looksLikePowerShell(env)) {
|
|
2251
|
+
riskReasons.push(
|
|
2252
|
+
"PowerShell environment detected on Windows; shell profiles can print banner text to stdout."
|
|
2253
|
+
);
|
|
2254
|
+
}
|
|
2255
|
+
if (stdinIsTTY || stdoutIsTTY) {
|
|
2256
|
+
const ttyRisk = "STDIO appears attached to a terminal (TTY). MCP clients should launch codex-mcp with piped stdio.";
|
|
2257
|
+
notes.push(ttyRisk);
|
|
2258
|
+
riskReasons.push(ttyRisk);
|
|
2259
|
+
blockingReasons.push(ttyRisk);
|
|
2260
|
+
}
|
|
2261
|
+
const riskLevel = riskReasons.length > 0 ? "elevated" : "low";
|
|
2262
|
+
const shouldBlock = modeResolution.mode === "strict" && blockingReasons.length > 0;
|
|
2263
|
+
return {
|
|
2264
|
+
mode: modeResolution.mode,
|
|
2265
|
+
modeSource: modeResolution.source,
|
|
2266
|
+
invalidMode: modeResolution.invalidRaw,
|
|
2267
|
+
riskLevel,
|
|
2268
|
+
riskReasons,
|
|
2269
|
+
blockingReasons,
|
|
2270
|
+
notes,
|
|
2271
|
+
suggestions: riskReasons.length > 0 ? buildFixSuggestions(platform) : [],
|
|
2272
|
+
shouldBlock
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
function looksLikePowerShell(env) {
|
|
2276
|
+
return Boolean(
|
|
2277
|
+
env.POWERSHELL_DISTRIBUTION_CHANNEL || env.PSModulePath || env.PSExecutionPolicyPreference || env.PSModuleAnalysisCachePath
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
function buildFixSuggestions(platform) {
|
|
2281
|
+
const generic = [
|
|
2282
|
+
"Prefer direct MCP config launch: command='npx', args=['-y', '@leo000001/codex-mcp']",
|
|
2283
|
+
"Keep server stdout strictly JSON-RPC; route diagnostics to stderr only.",
|
|
2284
|
+
"codex-mcp cannot sanitize shell/profile stdout once emitted before MCP handshake."
|
|
2285
|
+
];
|
|
2286
|
+
if (platform === "win32") {
|
|
2287
|
+
return [
|
|
2288
|
+
'If shell wrapping is required, use: pwsh -NoProfile -Command "npx -y @leo000001/codex-mcp"',
|
|
2289
|
+
"Disable noisy PowerShell profile output (oh-my-posh banners, startup prompts, etc.).",
|
|
2290
|
+
...generic
|
|
2291
|
+
];
|
|
2292
|
+
}
|
|
2293
|
+
return generic;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// src/resources/register-resources.ts
|
|
2297
|
+
var RESOURCE_SCHEME = "codex-mcp";
|
|
2298
|
+
var RESOURCE_URIS = {
|
|
2299
|
+
serverInfo: `${RESOURCE_SCHEME}:///server-info`,
|
|
2300
|
+
compatReport: `${RESOURCE_SCHEME}:///compat-report`,
|
|
2301
|
+
config: `${RESOURCE_SCHEME}:///config`,
|
|
2302
|
+
gotchas: `${RESOURCE_SCHEME}:///gotchas`,
|
|
2303
|
+
quickstart: `${RESOURCE_SCHEME}:///quickstart`,
|
|
2304
|
+
errors: `${RESOURCE_SCHEME}:///errors`
|
|
2305
|
+
};
|
|
2306
|
+
var RESOURCE_CATALOG = [
|
|
2307
|
+
{
|
|
2308
|
+
key: "serverInfo",
|
|
2309
|
+
name: "server_info",
|
|
2310
|
+
title: "Server Info",
|
|
2311
|
+
description: "Server metadata and runtime capabilities",
|
|
2312
|
+
mimeType: "application/json"
|
|
2313
|
+
},
|
|
2314
|
+
{
|
|
2315
|
+
key: "compatReport",
|
|
2316
|
+
name: "compat_report",
|
|
2317
|
+
title: "Compat Report",
|
|
2318
|
+
description: "Cross-backend compatibility capability report",
|
|
2319
|
+
mimeType: "application/json"
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
key: "config",
|
|
2323
|
+
name: "config",
|
|
2324
|
+
title: "Config Guide",
|
|
2325
|
+
description: "Parameter guide and config.toml mapping",
|
|
2326
|
+
mimeType: "text/markdown"
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
key: "gotchas",
|
|
2330
|
+
name: "gotchas",
|
|
2331
|
+
title: "Gotchas",
|
|
2332
|
+
description: "Practical limits and common issues",
|
|
2333
|
+
mimeType: "text/markdown"
|
|
2334
|
+
},
|
|
2335
|
+
{
|
|
2336
|
+
key: "quickstart",
|
|
2337
|
+
name: "quickstart",
|
|
2338
|
+
title: "Quickstart",
|
|
2339
|
+
description: "Minimal end-to-end workflow",
|
|
2340
|
+
mimeType: "text/markdown"
|
|
2341
|
+
},
|
|
2342
|
+
{
|
|
2343
|
+
key: "errors",
|
|
2344
|
+
name: "errors",
|
|
2345
|
+
title: "Errors",
|
|
2346
|
+
description: "Error code reference and recovery hints",
|
|
2347
|
+
mimeType: "text/markdown"
|
|
2348
|
+
}
|
|
2349
|
+
];
|
|
2350
|
+
var ERROR_CODE_HINTS = {
|
|
2351
|
+
["INVALID_ARGUMENT" /* INVALID_ARGUMENT */]: "Input shape/value mismatch. Fix payload and retry.",
|
|
2352
|
+
["SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */]: "Unknown sessionId or already cleaned up.",
|
|
2353
|
+
["SESSION_BUSY" /* SESSION_BUSY */]: "Session is running or waiting approval. Poll until idle/error.",
|
|
2354
|
+
["SESSION_NOT_RUNNING" /* SESSION_NOT_RUNNING */]: "Action requires running/waiting_approval session.",
|
|
2355
|
+
["REQUEST_NOT_FOUND" /* REQUEST_NOT_FOUND */]: "requestId was resolved, stale, or never existed.",
|
|
2356
|
+
["TIMEOUT" /* TIMEOUT */]: "Operation timed out. Retry or use a longer timeout where supported.",
|
|
2357
|
+
["CANCELLED" /* CANCELLED */]: "Session was cancelled and cannot be resumed.",
|
|
2358
|
+
["APP_SERVER_START_FAILED" /* APP_SERVER_START_FAILED */]: "codex app-server failed to boot. Check CLI install/path.",
|
|
2359
|
+
["THREAD_FORK_RESUME_FAILED" /* THREAD_FORK_RESUME_FAILED */]: "Forked thread could not resume in new process. Retry fork from current source session.",
|
|
2360
|
+
["PROTOCOL_PARSE_ERROR" /* PROTOCOL_PARSE_ERROR */]: "Non-JSON or malformed app-server line. Check shell/profile noise and transport health.",
|
|
2361
|
+
["WRITE_QUEUE_DROPPED" /* WRITE_QUEUE_DROPPED */]: "stdin backpressure overflow. Reduce burst size and re-run in smaller turns.",
|
|
2362
|
+
["INTERNAL" /* INTERNAL */]: "Unexpected server-side failure. Inspect logs and retry safely."
|
|
2363
|
+
};
|
|
2364
|
+
function asTextResource(uri, text, mimeType) {
|
|
2365
|
+
return {
|
|
2366
|
+
contents: [
|
|
2367
|
+
{
|
|
2368
|
+
uri: uri.toString(),
|
|
2369
|
+
text,
|
|
2370
|
+
mimeType
|
|
2371
|
+
}
|
|
2372
|
+
]
|
|
2373
|
+
};
|
|
2374
|
+
}
|
|
2375
|
+
function detectCodexCliVersion(timeoutMs = 1500) {
|
|
2376
|
+
try {
|
|
2377
|
+
const run = spawnSync("codex", ["--version"], {
|
|
2378
|
+
encoding: "utf8",
|
|
2379
|
+
timeout: timeoutMs,
|
|
2380
|
+
windowsHide: true
|
|
2381
|
+
});
|
|
2382
|
+
const combined = `${run.stdout ?? ""}
|
|
2383
|
+
${run.stderr ?? ""}`.trim();
|
|
2384
|
+
if (!combined) return null;
|
|
2385
|
+
const versionToken = combined.match(/v?\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
2386
|
+
if (!versionToken) return combined.split(/\s+/)[0] ?? null;
|
|
2387
|
+
return versionToken[0].replace(/^v/, "");
|
|
2388
|
+
} catch {
|
|
2389
|
+
return null;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
function msToMinutes(ms) {
|
|
2393
|
+
return Math.floor(ms / 6e4);
|
|
2394
|
+
}
|
|
2395
|
+
function buildConfigGuideText() {
|
|
2396
|
+
return [
|
|
2397
|
+
"## Top-level parameters (`codex`)",
|
|
2398
|
+
"",
|
|
2399
|
+
"- Required: `prompt`, `approvalPolicy`, `sandbox`.",
|
|
2400
|
+
"- Optional: `effort` (default `low`), `cwd` (default server cwd), `model` (default config.toml), `profile` (default CLI profile), `advanced`.",
|
|
2401
|
+
"- Prefer passing `cwd` explicitly to avoid accidental server-cwd execution.",
|
|
2402
|
+
"",
|
|
2403
|
+
"## `advanced.*` guide",
|
|
2404
|
+
"",
|
|
2405
|
+
"- `advanced.baseInstructions`: replace default system instructions for this session (default: unchanged).",
|
|
2406
|
+
"- `advanced.developerInstructions`: append extra developer instructions (default: none).",
|
|
2407
|
+
"- `advanced.personality`: optional personality preset (default: config.toml).",
|
|
2408
|
+
"- `advanced.summary`: summary verbosity preset for turn output (default: config.toml).",
|
|
2409
|
+
"- `advanced.ephemeral`: do not persist thread state remotely (default `false`).",
|
|
2410
|
+
"- `advanced.images`: local image file paths on the same host as codex-mcp (default: none).",
|
|
2411
|
+
`- \`advanced.approvalTimeoutMs\`: auto-decline timeout for approval/user-input requests (default \`${DEFAULT_APPROVAL_TIMEOUT_MS}\` ms).`,
|
|
2412
|
+
"- `advanced.outputSchema`: JSON Schema for structured output from `codex` turns (default: none).",
|
|
2413
|
+
"",
|
|
2414
|
+
"## `advanced.config` mapping",
|
|
2415
|
+
"",
|
|
2416
|
+
"Forwarded as `-c key=value` flags to `codex app-server`.",
|
|
2417
|
+
"Primitives use `String(value)`; objects/arrays use `JSON.stringify(value)`.",
|
|
2418
|
+
"",
|
|
2419
|
+
"Prefer dedicated top-level params when available:",
|
|
2420
|
+
"",
|
|
2421
|
+
"- `codex.model` -> `-c model=...`",
|
|
2422
|
+
"- `codex.approvalPolicy` -> `-c approval_policy=...`",
|
|
2423
|
+
"- `codex.sandbox` -> `-c sandbox_mode=...`",
|
|
2424
|
+
"- `codex.effort` -> turn-level reasoning effort (do not encode in `advanced.config`)",
|
|
2425
|
+
"- `codex.profile` -> `-p ...`",
|
|
2426
|
+
"",
|
|
2427
|
+
"## `codex_reply` differences",
|
|
2428
|
+
"",
|
|
2429
|
+
"- `codex_reply.outputSchema` is top-level.",
|
|
2430
|
+
"- `codex.outputSchema` lives under `advanced.outputSchema`.",
|
|
2431
|
+
"- `codex_reply` can override `model`, `approvalPolicy`, `sandbox`, `effort`, `summary`, `personality`, and `cwd`.",
|
|
2432
|
+
"- `codex_reply` only works when session state is `idle` or `error`; otherwise returns `SESSION_BUSY`.",
|
|
2433
|
+
"- All `codex_reply` override fields default to no override when omitted.",
|
|
2434
|
+
"",
|
|
2435
|
+
"## Override persistence (`codex_reply`)",
|
|
2436
|
+
"",
|
|
2437
|
+
"- `model`, `approvalPolicy`, `sandbox`, and `cwd` update in-memory session defaults for later turns.",
|
|
2438
|
+
"- `effort`, `summary`, `personality`, and `outputSchema` apply to the submitted turn payload.",
|
|
2439
|
+
"",
|
|
2440
|
+
"## Version compatibility note",
|
|
2441
|
+
"",
|
|
2442
|
+
"Available `advanced.config` keys depend on installed Codex CLI version.",
|
|
2443
|
+
"To inspect your local CLI version, read `codex-mcp:///server-info` (`codexCliVersion`).",
|
|
2444
|
+
"",
|
|
2445
|
+
"## Other tool defaults (quick reference)",
|
|
2446
|
+
"",
|
|
2447
|
+
"- `codex_session.includeSensitive`: default `false`.",
|
|
2448
|
+
`- \`codex_check.poll.maxEvents\`: default \`${POLL_DEFAULT_MAX_EVENTS}\` (minimum \`${POLL_MIN_MAX_EVENTS}\`).`,
|
|
2449
|
+
`- \`codex_check.respond_*.maxEvents\`: default \`${RESPOND_DEFAULT_MAX_EVENTS}\`.`,
|
|
2450
|
+
"- `codex_check.responseMode`: default `minimal` (`minimal` / `delta_compact` / `full`).",
|
|
2451
|
+
"- `codex_check.pollOptions.includeEvents`: default `true`.",
|
|
2452
|
+
"- `codex_check.pollOptions.includeActions`: default `true`.",
|
|
2453
|
+
"- `codex_check.pollOptions.includeResult`: default `true`.",
|
|
2454
|
+
"- `codex_check.pollOptions.maxBytes`: default unlimited.",
|
|
2455
|
+
"- `codex_check.cursor`: default is session last consumed cursor when omitted.",
|
|
2456
|
+
""
|
|
2457
|
+
].join("\n");
|
|
2458
|
+
}
|
|
2459
|
+
function buildGotchasText() {
|
|
2460
|
+
return [
|
|
2461
|
+
"## Polling and cursors",
|
|
2462
|
+
"",
|
|
2463
|
+
'- Sessions are async. Poll `codex_check(action="poll")` until status is `idle`/`error`/`cancelled`.',
|
|
2464
|
+
"- Store `nextCursor` and pass it back to avoid replay.",
|
|
2465
|
+
`- Poll default is \`maxEvents=${POLL_DEFAULT_MAX_EVENTS}\` (authoritative: tool schema / constants).`,
|
|
2466
|
+
`- Poll enforces minimum \`maxEvents=${POLL_MIN_MAX_EVENTS}\`; sending \`0\` is normalized to \`${POLL_MIN_MAX_EVENTS}\`.`,
|
|
2467
|
+
`- \`respond_permission\` and \`respond_user_input\` default to compact ACK with \`maxEvents=${RESPOND_DEFAULT_MAX_EVENTS}\`.`,
|
|
2468
|
+
"- `respond_approval` is a deprecated alias for `respond_permission`.",
|
|
2469
|
+
"- Default response mode is `minimal`; use `full` if you need full raw event payloads.",
|
|
2470
|
+
"- respond_* uses monotonic cursor handling: `max(cursor, sessionLastCursor)`.",
|
|
2471
|
+
"- If `cursorResetTo` is present, your cursor is stale (old events were evicted); restart from that value.",
|
|
2472
|
+
"",
|
|
2473
|
+
"## Approval behavior",
|
|
2474
|
+
"",
|
|
2475
|
+
`- Pending approvals/user-input auto-decline after \`approvalTimeoutMs\` (default ${DEFAULT_APPROVAL_TIMEOUT_MS} ms).`,
|
|
2476
|
+
"- `untrusted` behavior is enforced by Codex CLI backend and may auto-allow some low-risk commands.",
|
|
2477
|
+
"- Do not assume every read-only command will always require approval across CLI versions.",
|
|
2478
|
+
"",
|
|
2479
|
+
"## Event model",
|
|
2480
|
+
"",
|
|
2481
|
+
"- Top-level `events[].type` is one of: `output`, `progress`, `approval_request`, `approval_result`, `result`, `error`.",
|
|
2482
|
+
"- Fine-grained stream semantics are in `events[].data.method` (for example command output delta, reasoning delta, turn updates).",
|
|
2483
|
+
'- Retryable interruptions surface as `progress` with `method="codex-mcp/reconnect"` and include retry fields.',
|
|
2484
|
+
"- During reconnect/retry, continue polling normally; if retries stop (`willRetry=false`), session transitions to error path.",
|
|
2485
|
+
"",
|
|
2486
|
+
"## Windows shell/profile issues",
|
|
2487
|
+
"",
|
|
2488
|
+
"- On Windows wrappers, prefer `pwsh -NoProfile` to avoid profile/banner stdout noise.",
|
|
2489
|
+
"- Profile noise can affect both MCP handshake and agent-internal command turns.",
|
|
2490
|
+
"- For mojibake, enforce UTF-8 shell output (`chcp 65001`, `$OutputEncoding = [Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()`).",
|
|
2491
|
+
"- Prefer host-native absolute paths for `cwd` and file args (Windows example: `D:\\\\Lab\\\\codex-mcp`).",
|
|
2492
|
+
"",
|
|
2493
|
+
"## Lifecycle and cleanup",
|
|
2494
|
+
"",
|
|
2495
|
+
`- Idle sessions are auto-cleaned after ${msToMinutes(DEFAULT_IDLE_CLEANUP_MS)} minutes.`,
|
|
2496
|
+
`- Running/waiting sessions are auto-cleaned after ${msToMinutes(DEFAULT_RUNNING_CLEANUP_MS)} minutes.`,
|
|
2497
|
+
`- Error/cancelled sessions are retained for about ${msToMinutes(DEFAULT_TERMINAL_CLEANUP_MS)} minutes, then removed.`,
|
|
2498
|
+
"- Session state is in-memory. Restarting codex-mcp drops all existing sessions.",
|
|
2499
|
+
"",
|
|
2500
|
+
"## Capacity",
|
|
2501
|
+
"",
|
|
2502
|
+
"- codex-mcp does not hard-code a strict concurrent-session cap.",
|
|
2503
|
+
"- Practical limit depends on machine resources and child-process load.",
|
|
2504
|
+
""
|
|
2505
|
+
].join("\n");
|
|
2506
|
+
}
|
|
2507
|
+
function buildQuickstartText() {
|
|
2508
|
+
return [
|
|
2509
|
+
"## Minimal flow",
|
|
2510
|
+
"",
|
|
2511
|
+
"1. Start session (`codex`)",
|
|
2512
|
+
"",
|
|
2513
|
+
"```json",
|
|
2514
|
+
"{",
|
|
2515
|
+
' "prompt": "List files and summarize repository purpose.",',
|
|
2516
|
+
' "approvalPolicy": "on-request",',
|
|
2517
|
+
' "sandbox": "workspace-write",',
|
|
2518
|
+
' "effort": "low",',
|
|
2519
|
+
' "cwd": "D:\\\\Lab\\\\codex-mcp"',
|
|
2520
|
+
"}",
|
|
2521
|
+
"```",
|
|
2522
|
+
"",
|
|
2523
|
+
"Typical start result:",
|
|
2524
|
+
"",
|
|
2525
|
+
"```json",
|
|
2526
|
+
"{",
|
|
2527
|
+
' "sessionId": "sess_abc123",',
|
|
2528
|
+
' "threadId": "thread_xyz",',
|
|
2529
|
+
' "status": "running",',
|
|
2530
|
+
' "pollInterval": 120000',
|
|
2531
|
+
"}",
|
|
2532
|
+
"```",
|
|
2533
|
+
"",
|
|
2534
|
+
"2. Poll incrementally (`codex_check`)",
|
|
2535
|
+
"",
|
|
2536
|
+
"```json",
|
|
2537
|
+
"{",
|
|
2538
|
+
' "action": "poll",',
|
|
2539
|
+
' "sessionId": "sess_abc123",',
|
|
2540
|
+
' "cursor": 0,',
|
|
2541
|
+
' "maxEvents": 10',
|
|
2542
|
+
"}",
|
|
2543
|
+
"```",
|
|
2544
|
+
"",
|
|
2545
|
+
"- Use `pollInterval` as a minimum delay: `running` >=120000ms (and usually longer for big tasks).",
|
|
2546
|
+
"- `waiting_approval` is the exception: poll/answer around 1000ms to avoid timeout.",
|
|
2547
|
+
"",
|
|
2548
|
+
"3. If `actions[]` contains an approval request, respond:",
|
|
2549
|
+
"",
|
|
2550
|
+
"```json",
|
|
2551
|
+
"{",
|
|
2552
|
+
' "action": "respond_permission",',
|
|
2553
|
+
' "sessionId": "sess_abc123",',
|
|
2554
|
+
' "requestId": "req_123",',
|
|
2555
|
+
' "decision": "acceptForSession"',
|
|
2556
|
+
"}",
|
|
2557
|
+
"```",
|
|
2558
|
+
"",
|
|
2559
|
+
"4. If `actions[]` contains a user-input request, respond:",
|
|
2560
|
+
"",
|
|
2561
|
+
"```json",
|
|
2562
|
+
"{",
|
|
2563
|
+
' "action": "respond_user_input",',
|
|
2564
|
+
' "sessionId": "sess_abc123",',
|
|
2565
|
+
' "requestId": "req_456",',
|
|
2566
|
+
' "answers": {',
|
|
2567
|
+
' "question-id": {',
|
|
2568
|
+
' "answers": ["Option A"]',
|
|
2569
|
+
" }",
|
|
2570
|
+
" }",
|
|
2571
|
+
"}",
|
|
2572
|
+
"```",
|
|
2573
|
+
"",
|
|
2574
|
+
"5. Continue polling until terminal status (`idle`, `error`, or `cancelled`), respecting the >=2 minute interval while `running`.",
|
|
2575
|
+
"",
|
|
2576
|
+
"## Cursor notes",
|
|
2577
|
+
"",
|
|
2578
|
+
"- Omit `cursor` to continue from session last consumed cursor.",
|
|
2579
|
+
`- Omit \`maxEvents\`: defaults are poll=${POLL_DEFAULT_MAX_EVENTS}, respond_*=${RESPOND_DEFAULT_MAX_EVENTS}.`,
|
|
2580
|
+
"- Omit `responseMode`: default is `minimal`.",
|
|
2581
|
+
"- Use returned `nextCursor` for the next call.",
|
|
2582
|
+
"- If `cursorResetTo` appears, reset to that value and continue.",
|
|
2583
|
+
""
|
|
2584
|
+
].join("\n");
|
|
2585
|
+
}
|
|
2586
|
+
function buildErrorsText() {
|
|
2587
|
+
const lines = [
|
|
2588
|
+
"## Error format",
|
|
2589
|
+
"",
|
|
2590
|
+
"Tool failures use: `Error [CODE]: message`",
|
|
2591
|
+
"",
|
|
2592
|
+
"## Codes",
|
|
2593
|
+
""
|
|
2594
|
+
];
|
|
2595
|
+
for (const code of Object.values(ErrorCode)) {
|
|
2596
|
+
lines.push(`- \`${code}\`: ${ERROR_CODE_HINTS[code]}`);
|
|
2597
|
+
}
|
|
2598
|
+
lines.push("");
|
|
2599
|
+
lines.push("## Recovery basics");
|
|
2600
|
+
lines.push("");
|
|
2601
|
+
lines.push("- `INVALID_ARGUMENT`: fix payload fields/enums and retry.");
|
|
2602
|
+
lines.push("- `SESSION_BUSY`: poll until terminal/idle before issuing incompatible action.");
|
|
2603
|
+
lines.push("- `REQUEST_NOT_FOUND`: re-poll and use latest `actions[].requestId`.");
|
|
2604
|
+
lines.push("- `PROTOCOL_PARSE_ERROR`: remove shell/profile stdout noise and restart session.");
|
|
2605
|
+
lines.push("");
|
|
2606
|
+
return lines.join("\n");
|
|
2607
|
+
}
|
|
2608
|
+
function buildCompatReport(deps, codexCliVersion) {
|
|
2609
|
+
const runtimeWarnings = [];
|
|
2610
|
+
if (!codexCliVersion) {
|
|
2611
|
+
runtimeWarnings.push("Unable to detect local codex CLI version from PATH.");
|
|
2612
|
+
}
|
|
2613
|
+
return JSON.stringify(
|
|
2614
|
+
{
|
|
2615
|
+
schemaVersion: "1.0.0",
|
|
2616
|
+
features: {
|
|
2617
|
+
respondPermission: true,
|
|
2618
|
+
respondApprovalAlias: true,
|
|
2619
|
+
respondUserInput: true,
|
|
2620
|
+
sessionInterrupt: true,
|
|
2621
|
+
responseModeMinimal: true,
|
|
2622
|
+
responseModeDeltaCompact: true,
|
|
2623
|
+
responseModeFull: true,
|
|
2624
|
+
pollOptionsBase: true,
|
|
2625
|
+
maxBytesTruncation: true,
|
|
2626
|
+
compatWarnings: true,
|
|
2627
|
+
diskResume: false,
|
|
2628
|
+
dynamicTools: false,
|
|
2629
|
+
toolPermissionControl: false
|
|
2630
|
+
},
|
|
2631
|
+
recommendedSettings: {
|
|
2632
|
+
codexCheck: {
|
|
2633
|
+
responseMode: "minimal",
|
|
2634
|
+
pollOptions: {
|
|
2635
|
+
includeEvents: true,
|
|
2636
|
+
includeActions: true,
|
|
2637
|
+
includeResult: true
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
},
|
|
2641
|
+
toolCounts: {
|
|
2642
|
+
core: 4
|
|
2643
|
+
},
|
|
2644
|
+
runtimeWarnings,
|
|
2645
|
+
detectedMismatches: [],
|
|
2646
|
+
runtime: {
|
|
2647
|
+
codexMcpVersion: deps.version,
|
|
2648
|
+
codexCliVersion,
|
|
2649
|
+
activeSessions: deps.sessionManager.getActiveSessionCount()
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
null,
|
|
2653
|
+
2
|
|
2654
|
+
);
|
|
2655
|
+
}
|
|
2656
|
+
function registerResources(server, deps) {
|
|
2657
|
+
let codexCliVersionCache;
|
|
2658
|
+
const getCodexCliVersion = () => {
|
|
2659
|
+
if (codexCliVersionCache !== void 0) return codexCliVersionCache;
|
|
2660
|
+
codexCliVersionCache = detectCodexCliVersion();
|
|
2661
|
+
return codexCliVersionCache;
|
|
2662
|
+
};
|
|
2663
|
+
const byKey = new Map(RESOURCE_CATALOG.map((entry) => [entry.key, entry]));
|
|
2664
|
+
const serverInfoMeta = byKey.get("serverInfo");
|
|
2665
|
+
const serverInfoUri = new URL(RESOURCE_URIS.serverInfo);
|
|
2666
|
+
server.registerResource(
|
|
2667
|
+
serverInfoMeta.name,
|
|
2668
|
+
serverInfoUri.toString(),
|
|
2669
|
+
{
|
|
2670
|
+
title: serverInfoMeta.title,
|
|
2671
|
+
description: serverInfoMeta.description,
|
|
2672
|
+
mimeType: serverInfoMeta.mimeType
|
|
2673
|
+
},
|
|
2674
|
+
() => {
|
|
2675
|
+
const observedModel = deps.sessionManager.getObservedDefaultModel();
|
|
2676
|
+
return asTextResource(
|
|
2677
|
+
serverInfoUri,
|
|
2678
|
+
JSON.stringify(
|
|
2679
|
+
{
|
|
2680
|
+
name: "codex-mcp",
|
|
2681
|
+
version: deps.version,
|
|
2682
|
+
codexCliVersion: getCodexCliVersion(),
|
|
2683
|
+
node: process.version,
|
|
2684
|
+
platform: process.platform,
|
|
2685
|
+
arch: process.arch,
|
|
2686
|
+
stdioMode: resolveStdioMode().mode,
|
|
2687
|
+
supportedApprovalPolicies: APPROVAL_POLICIES,
|
|
2688
|
+
supportedSandboxModes: SANDBOX_MODES,
|
|
2689
|
+
supportedEffortLevels: EFFORT_LEVELS,
|
|
2690
|
+
activeSessions: deps.sessionManager.getActiveSessionCount(),
|
|
2691
|
+
defaultModel: observedModel,
|
|
2692
|
+
defaultModelSource: observedModel ? "session-default" : "unknown",
|
|
2693
|
+
resources: RESOURCE_CATALOG.map((entry) => ({
|
|
2694
|
+
uri: RESOURCE_URIS[entry.key],
|
|
2695
|
+
title: entry.title,
|
|
2696
|
+
mimeType: entry.mimeType,
|
|
2697
|
+
description: entry.description
|
|
2698
|
+
}))
|
|
2699
|
+
},
|
|
2700
|
+
null,
|
|
2701
|
+
2
|
|
2702
|
+
),
|
|
2703
|
+
"application/json"
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2706
|
+
);
|
|
2707
|
+
const compatReportMeta = byKey.get("compatReport");
|
|
2708
|
+
const compatReportUri = new URL(RESOURCE_URIS.compatReport);
|
|
2709
|
+
server.registerResource(
|
|
2710
|
+
compatReportMeta.name,
|
|
2711
|
+
compatReportUri.toString(),
|
|
2712
|
+
{
|
|
2713
|
+
title: compatReportMeta.title,
|
|
2714
|
+
description: compatReportMeta.description,
|
|
2715
|
+
mimeType: compatReportMeta.mimeType
|
|
2716
|
+
},
|
|
2717
|
+
() => asTextResource(
|
|
2718
|
+
compatReportUri,
|
|
2719
|
+
buildCompatReport(deps, getCodexCliVersion()),
|
|
2720
|
+
"application/json"
|
|
2721
|
+
)
|
|
2722
|
+
);
|
|
2723
|
+
const configMeta = byKey.get("config");
|
|
2724
|
+
const configUri = new URL(RESOURCE_URIS.config);
|
|
2725
|
+
server.registerResource(
|
|
2726
|
+
configMeta.name,
|
|
2727
|
+
configUri.toString(),
|
|
2728
|
+
{
|
|
2729
|
+
title: configMeta.title,
|
|
2730
|
+
description: configMeta.description,
|
|
2731
|
+
mimeType: configMeta.mimeType
|
|
2732
|
+
},
|
|
2733
|
+
() => asTextResource(configUri, buildConfigGuideText(), "text/markdown")
|
|
2734
|
+
);
|
|
2735
|
+
const gotchasMeta = byKey.get("gotchas");
|
|
2736
|
+
const gotchasUri = new URL(RESOURCE_URIS.gotchas);
|
|
2737
|
+
server.registerResource(
|
|
2738
|
+
gotchasMeta.name,
|
|
2739
|
+
gotchasUri.toString(),
|
|
2740
|
+
{
|
|
2741
|
+
title: gotchasMeta.title,
|
|
2742
|
+
description: gotchasMeta.description,
|
|
2743
|
+
mimeType: gotchasMeta.mimeType
|
|
2744
|
+
},
|
|
2745
|
+
() => asTextResource(gotchasUri, buildGotchasText(), "text/markdown")
|
|
2746
|
+
);
|
|
2747
|
+
const quickstartMeta = byKey.get("quickstart");
|
|
2748
|
+
const quickstartUri = new URL(RESOURCE_URIS.quickstart);
|
|
2749
|
+
server.registerResource(
|
|
2750
|
+
quickstartMeta.name,
|
|
2751
|
+
quickstartUri.toString(),
|
|
2752
|
+
{
|
|
2753
|
+
title: quickstartMeta.title,
|
|
2754
|
+
description: quickstartMeta.description,
|
|
2755
|
+
mimeType: quickstartMeta.mimeType
|
|
2756
|
+
},
|
|
2757
|
+
() => asTextResource(quickstartUri, buildQuickstartText(), "text/markdown")
|
|
2758
|
+
);
|
|
2759
|
+
const errorsMeta = byKey.get("errors");
|
|
2760
|
+
const errorsUri = new URL(RESOURCE_URIS.errors);
|
|
2761
|
+
server.registerResource(
|
|
2762
|
+
errorsMeta.name,
|
|
2763
|
+
errorsUri.toString(),
|
|
2764
|
+
{
|
|
2765
|
+
title: errorsMeta.title,
|
|
2766
|
+
description: errorsMeta.description,
|
|
2767
|
+
mimeType: errorsMeta.mimeType
|
|
2768
|
+
},
|
|
2769
|
+
() => asTextResource(errorsUri, buildErrorsText(), "text/markdown")
|
|
2770
|
+
);
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
// src/server.ts
|
|
2774
|
+
var SERVER_VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
2775
|
+
function formatErrorMessage(err) {
|
|
2776
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2777
|
+
const m = /^Error \[([A-Z_]+)\]:\s*(.*)$/.exec(message);
|
|
2778
|
+
if (m) {
|
|
2779
|
+
const [, code, rest] = m;
|
|
2780
|
+
if (code === "INTERNAL" /* INTERNAL */) {
|
|
2781
|
+
return `Error [${"INTERNAL" /* INTERNAL */}]: ${redactPaths(rest)}`;
|
|
2782
|
+
}
|
|
2783
|
+
return message;
|
|
2784
|
+
}
|
|
2785
|
+
return `Error [${"INTERNAL" /* INTERNAL */}]: ${redactPaths(message)}`;
|
|
2786
|
+
}
|
|
2787
|
+
function toStructuredContent(value) {
|
|
2788
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
2789
|
+
return value;
|
|
2790
|
+
}
|
|
2791
|
+
return { value };
|
|
2792
|
+
}
|
|
2793
|
+
function createServer(serverCwd) {
|
|
2794
|
+
const sessionManager = new SessionManager();
|
|
2795
|
+
const server = new McpServer({
|
|
2796
|
+
name: "codex-mcp",
|
|
2797
|
+
version: SERVER_VERSION
|
|
2798
|
+
});
|
|
2799
|
+
registerResources(server, { version: SERVER_VERSION, sessionManager });
|
|
2800
|
+
const publicSessionInfoSchema = z.object({
|
|
2801
|
+
sessionId: z.string(),
|
|
2802
|
+
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]),
|
|
2803
|
+
createdAt: z.string(),
|
|
2804
|
+
lastActiveAt: z.string(),
|
|
2805
|
+
cancelledAt: z.string().optional(),
|
|
2806
|
+
cancelledReason: z.string().optional(),
|
|
2807
|
+
model: z.string().optional(),
|
|
2808
|
+
approvalPolicy: z.enum(APPROVAL_POLICIES).optional(),
|
|
2809
|
+
sandbox: z.enum(SANDBOX_MODES).optional(),
|
|
2810
|
+
pendingRequestCount: z.number().int()
|
|
2811
|
+
});
|
|
2812
|
+
const errorOutputShape = {
|
|
2813
|
+
error: z.string().optional(),
|
|
2814
|
+
isError: z.boolean().optional()
|
|
2815
|
+
};
|
|
2816
|
+
const sessionStartOutputShape = {
|
|
2817
|
+
sessionId: z.string().optional(),
|
|
2818
|
+
threadId: z.string().optional(),
|
|
2819
|
+
status: z.enum(["running", "idle"]).optional(),
|
|
2820
|
+
pollInterval: z.number().int().optional().describe(
|
|
2821
|
+
"Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
|
|
2822
|
+
),
|
|
2823
|
+
...errorOutputShape
|
|
2824
|
+
};
|
|
2825
|
+
server.registerTool(
|
|
2826
|
+
"codex",
|
|
2827
|
+
{
|
|
2828
|
+
title: "Start Codex Session",
|
|
2829
|
+
description: "Start session asynchronously and return `{ sessionId, threadId, status, pollInterval }`. Use `pollInterval` as a minimum hint: `running` >=120000ms (increase for long tasks), `waiting_approval` ~=1000ms.",
|
|
2830
|
+
inputSchema: {
|
|
2831
|
+
prompt: z.string().describe("Task or question"),
|
|
2832
|
+
approvalPolicy: z.enum(APPROVAL_POLICIES).describe("Required enum: untrusted/on-failure/on-request/never."),
|
|
2833
|
+
sandbox: z.enum(SANDBOX_MODES).describe("Required enum: read-only/workspace-write/danger-full-access."),
|
|
2834
|
+
effort: z.enum(EFFORT_LEVELS).default(DEFAULT_EFFORT_LEVEL).describe("Reasoning effort (default: low)."),
|
|
2835
|
+
cwd: z.string().optional().describe("Working directory (default: server cwd)."),
|
|
2836
|
+
model: z.string().optional().describe("Model override (default: config.toml)"),
|
|
2837
|
+
profile: z.string().optional().describe("Profile name (default: CLI default profile)."),
|
|
2838
|
+
advanced: z.object({
|
|
2839
|
+
baseInstructions: z.string().optional().describe("Replace system instructions."),
|
|
2840
|
+
developerInstructions: z.string().optional().describe("Extra developer instructions."),
|
|
2841
|
+
personality: z.enum(PERSONALITIES).optional().describe("Personality (default: config.toml)."),
|
|
2842
|
+
summary: z.enum(SUMMARY_MODES).optional().describe("Summary mode (default: config.toml)."),
|
|
2843
|
+
config: z.record(z.string(), z.unknown()).optional().describe("Override config values."),
|
|
2844
|
+
ephemeral: z.boolean().optional().describe("Do not persist thread (default: false)."),
|
|
2845
|
+
outputSchema: z.record(z.string(), z.unknown()).optional().describe("Structured output schema."),
|
|
2846
|
+
images: z.array(z.string()).optional().describe("Local image paths."),
|
|
2847
|
+
approvalTimeoutMs: z.number().int().positive().default(DEFAULT_APPROVAL_TIMEOUT_MS).optional().describe(`Auto-decline timeout in ms (default: ${DEFAULT_APPROVAL_TIMEOUT_MS})`)
|
|
2848
|
+
}).optional().describe("Advanced settings.")
|
|
2849
|
+
},
|
|
2850
|
+
outputSchema: sessionStartOutputShape,
|
|
2851
|
+
annotations: {
|
|
2852
|
+
title: "Start Codex Session",
|
|
2853
|
+
readOnlyHint: false,
|
|
2854
|
+
destructiveHint: true,
|
|
2855
|
+
idempotentHint: false,
|
|
2856
|
+
openWorldHint: true
|
|
2857
|
+
}
|
|
2858
|
+
},
|
|
2859
|
+
async (args) => {
|
|
2860
|
+
try {
|
|
2861
|
+
const result = await executeCodex(args, sessionManager, serverCwd);
|
|
2862
|
+
return {
|
|
2863
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2864
|
+
structuredContent: toStructuredContent(result),
|
|
2865
|
+
isError: false
|
|
2866
|
+
};
|
|
2867
|
+
} catch (err) {
|
|
2868
|
+
const message = formatErrorMessage(err);
|
|
2869
|
+
return {
|
|
2870
|
+
content: [{ type: "text", text: message }],
|
|
2871
|
+
structuredContent: { error: message, isError: true },
|
|
2872
|
+
isError: true
|
|
2873
|
+
};
|
|
2874
|
+
}
|
|
2875
|
+
}
|
|
2876
|
+
);
|
|
2877
|
+
server.registerTool(
|
|
2878
|
+
"codex_reply",
|
|
2879
|
+
{
|
|
2880
|
+
title: "Continue Codex Session",
|
|
2881
|
+
description: "Continue existing session. Allowed in `idle`/`error`; otherwise `SESSION_BUSY`. Returns immediately. Use `pollInterval` as a minimum hint: `running` >=120000ms, `waiting_approval` ~=1000ms.",
|
|
2882
|
+
inputSchema: {
|
|
2883
|
+
sessionId: z.string().describe("Session ID from codex tool"),
|
|
2884
|
+
prompt: z.string().describe("Follow-up message"),
|
|
2885
|
+
model: z.string().optional().describe("Override model."),
|
|
2886
|
+
approvalPolicy: z.enum(APPROVAL_POLICIES).optional().describe("Override approval policy."),
|
|
2887
|
+
effort: z.enum(EFFORT_LEVELS).optional().describe("Override effort."),
|
|
2888
|
+
summary: z.enum(SUMMARY_MODES).optional().describe("Override summary."),
|
|
2889
|
+
personality: z.enum(PERSONALITIES).optional().describe("Override personality."),
|
|
2890
|
+
sandbox: z.enum(SANDBOX_MODES).optional().describe("Override sandbox."),
|
|
2891
|
+
cwd: z.string().optional().describe("Override cwd."),
|
|
2892
|
+
outputSchema: z.record(z.string(), z.unknown()).optional().describe("Structured output schema override (top-level in codex_reply).")
|
|
2893
|
+
},
|
|
2894
|
+
outputSchema: sessionStartOutputShape,
|
|
2895
|
+
annotations: {
|
|
2896
|
+
title: "Continue Codex Session",
|
|
2897
|
+
readOnlyHint: false,
|
|
2898
|
+
destructiveHint: true,
|
|
2899
|
+
idempotentHint: false,
|
|
2900
|
+
openWorldHint: true
|
|
2901
|
+
}
|
|
2902
|
+
},
|
|
2903
|
+
async (args) => {
|
|
2904
|
+
try {
|
|
2905
|
+
const result = await executeCodexReply(args, sessionManager);
|
|
2906
|
+
return {
|
|
2907
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2908
|
+
structuredContent: toStructuredContent(result),
|
|
2909
|
+
isError: false
|
|
2910
|
+
};
|
|
2911
|
+
} catch (err) {
|
|
2912
|
+
const message = formatErrorMessage(err);
|
|
2913
|
+
return {
|
|
2914
|
+
content: [{ type: "text", text: message }],
|
|
2915
|
+
structuredContent: { error: message, isError: true },
|
|
2916
|
+
isError: true
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
);
|
|
2921
|
+
server.registerTool(
|
|
2922
|
+
"codex_session",
|
|
2923
|
+
{
|
|
2924
|
+
title: "Manage Sessions",
|
|
2925
|
+
description: `Session actions: list, get, cancel, interrupt, fork.
|
|
2926
|
+
|
|
2927
|
+
- list: sessions in memory.
|
|
2928
|
+
- get: details. includeSensitive defaults to false; true adds threadId/cwd/profile/config.
|
|
2929
|
+
- cancel: terminal.
|
|
2930
|
+
- interrupt: stop current turn.
|
|
2931
|
+
- fork: clone current thread into a new session; source remains unchanged.`,
|
|
2932
|
+
inputSchema: {
|
|
2933
|
+
action: z.enum(SESSION_ACTIONS),
|
|
2934
|
+
sessionId: z.string().optional().describe("Required for get/cancel/interrupt/fork"),
|
|
2935
|
+
includeSensitive: z.boolean().default(false).optional().describe("Include cwd/config/threadId/profile in get (default: false)")
|
|
2936
|
+
},
|
|
2937
|
+
outputSchema: {
|
|
2938
|
+
sessions: z.array(publicSessionInfoSchema).optional(),
|
|
2939
|
+
sessionId: z.string().optional(),
|
|
2940
|
+
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]).optional(),
|
|
2941
|
+
createdAt: z.string().optional(),
|
|
2942
|
+
lastActiveAt: z.string().optional(),
|
|
2943
|
+
cancelledAt: z.string().optional(),
|
|
2944
|
+
cancelledReason: z.string().optional(),
|
|
2945
|
+
model: z.string().optional(),
|
|
2946
|
+
approvalPolicy: z.enum(APPROVAL_POLICIES).optional(),
|
|
2947
|
+
sandbox: z.enum(SANDBOX_MODES).optional(),
|
|
2948
|
+
pendingRequestCount: z.number().int().optional(),
|
|
2949
|
+
threadId: z.string().optional(),
|
|
2950
|
+
cwd: z.string().optional(),
|
|
2951
|
+
profile: z.string().optional(),
|
|
2952
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
2953
|
+
pollInterval: z.number().int().optional().describe(
|
|
2954
|
+
"Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
|
|
2955
|
+
),
|
|
2956
|
+
success: z.boolean().optional(),
|
|
2957
|
+
message: z.string().optional(),
|
|
2958
|
+
...errorOutputShape
|
|
2959
|
+
},
|
|
2960
|
+
annotations: {
|
|
2961
|
+
title: "Manage Sessions",
|
|
2962
|
+
readOnlyHint: false,
|
|
2963
|
+
destructiveHint: true,
|
|
2964
|
+
idempotentHint: false,
|
|
2965
|
+
openWorldHint: false
|
|
2966
|
+
}
|
|
2967
|
+
},
|
|
2968
|
+
async (args) => {
|
|
2969
|
+
try {
|
|
2970
|
+
const result = await executeCodexSession(args, sessionManager);
|
|
2971
|
+
const isError = typeof result.isError === "boolean" ? result.isError : false;
|
|
2972
|
+
return {
|
|
2973
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
2974
|
+
structuredContent: toStructuredContent(result),
|
|
2975
|
+
isError
|
|
2976
|
+
};
|
|
2977
|
+
} catch (err) {
|
|
2978
|
+
const message = formatErrorMessage(err);
|
|
2979
|
+
return {
|
|
2980
|
+
content: [{ type: "text", text: message }],
|
|
2981
|
+
structuredContent: { error: message, isError: true },
|
|
2982
|
+
isError: true
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
);
|
|
2987
|
+
server.registerTool(
|
|
2988
|
+
"codex_check",
|
|
2989
|
+
{
|
|
2990
|
+
title: "Poll & Respond",
|
|
2991
|
+
description: `Poll session for events or respond to approval/input requests.
|
|
2992
|
+
|
|
2993
|
+
POLLING FREQUENCY: Do NOT poll every turn. Codex tasks take minutes, not seconds.
|
|
2994
|
+
- Treat pollInterval as a minimum hint, not a fixed schedule.
|
|
2995
|
+
- "running": never poll faster than 120000ms (2 minutes); use longer intervals for longer tasks.
|
|
2996
|
+
- "waiting_approval": poll about every 1000ms and respond quickly to actions[].
|
|
2997
|
+
- When status is "idle"/"error"/"cancelled": stop polling, the session is done.
|
|
2998
|
+
|
|
2999
|
+
poll: events since cursor. Default maxEvents=${POLL_DEFAULT_MAX_EVENTS}.
|
|
3000
|
+
|
|
3001
|
+
respond_permission: approval decision. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
|
|
3002
|
+
|
|
3003
|
+
respond_approval: deprecated alias for respond_permission.
|
|
3004
|
+
|
|
3005
|
+
respond_user_input: user-input answers. Default maxEvents=${RESPOND_DEFAULT_MAX_EVENTS} (compact ACK).
|
|
3006
|
+
|
|
3007
|
+
events[].type is coarse-grained; details are in events[].data.method.
|
|
3008
|
+
cursor omitted => use session last cursor. cursorResetTo => reset and continue.`,
|
|
3009
|
+
inputSchema: {
|
|
3010
|
+
action: z.enum(CHECK_ACTIONS),
|
|
3011
|
+
sessionId: z.string().describe("Target session ID"),
|
|
3012
|
+
cursor: z.number().int().nonnegative().optional().describe("Event cursor (default: session last consumed cursor)."),
|
|
3013
|
+
maxEvents: z.number().int().nonnegative().optional().describe(
|
|
3014
|
+
`Max events. Default: poll=${POLL_DEFAULT_MAX_EVENTS} (min ${POLL_MIN_MAX_EVENTS}), respond_*=${RESPOND_DEFAULT_MAX_EVENTS}.`
|
|
3015
|
+
),
|
|
3016
|
+
responseMode: z.enum(RESPONSE_MODES).optional().describe("Response mode. Default: minimal. Options: minimal/delta_compact/full."),
|
|
3017
|
+
pollOptions: z.object({
|
|
3018
|
+
includeEvents: z.boolean().optional().describe("Default: true. Include events[] in response."),
|
|
3019
|
+
includeActions: z.boolean().optional().describe("Default: true. Include actions[] in response."),
|
|
3020
|
+
includeResult: z.boolean().optional().describe("Default: true. Include result in response."),
|
|
3021
|
+
maxBytes: z.number().int().positive().optional().describe("Default: unlimited. Best-effort response payload cap in bytes."),
|
|
3022
|
+
includeTools: z.boolean().optional().describe("Default: false. Reserved for future dynamic tool metadata support.")
|
|
3023
|
+
}).optional().describe("Optional poll shaping controls."),
|
|
3024
|
+
// respond_permission/respond_approval
|
|
3025
|
+
requestId: z.string().optional().describe("Request ID from actions[]"),
|
|
3026
|
+
decision: z.enum(ALL_DECISIONS).optional().describe(
|
|
3027
|
+
"Approval decision for respond_permission/respond_approval. acceptWithExecpolicyAmendment requires execpolicyAmendment."
|
|
3028
|
+
),
|
|
3029
|
+
execpolicyAmendment: z.array(z.string()).optional().describe("For acceptWithExecpolicyAmendment only"),
|
|
3030
|
+
denyMessage: z.string().optional().describe("Deny reason (not sent to agent)"),
|
|
3031
|
+
// respond_user_input
|
|
3032
|
+
answers: z.record(
|
|
3033
|
+
z.string(),
|
|
3034
|
+
z.object({
|
|
3035
|
+
answers: z.array(z.string())
|
|
3036
|
+
})
|
|
3037
|
+
).optional().describe("questionId -> answers map (questionId from actions[] user_input request).")
|
|
3038
|
+
},
|
|
3039
|
+
outputSchema: {
|
|
3040
|
+
sessionId: z.string().optional(),
|
|
3041
|
+
status: z.enum(["running", "idle", "waiting_approval", "error", "cancelled"]).optional(),
|
|
3042
|
+
pollInterval: z.number().int().optional().describe(
|
|
3043
|
+
"Recommended minimum delay before next poll (ms): running >=120000, waiting_approval ~=1000."
|
|
3044
|
+
),
|
|
3045
|
+
events: z.array(
|
|
3046
|
+
z.object({
|
|
3047
|
+
id: z.number().int(),
|
|
3048
|
+
type: z.enum([
|
|
3049
|
+
"output",
|
|
3050
|
+
"progress",
|
|
3051
|
+
"approval_request",
|
|
3052
|
+
"approval_result",
|
|
3053
|
+
"result",
|
|
3054
|
+
"error"
|
|
3055
|
+
]),
|
|
3056
|
+
data: z.unknown(),
|
|
3057
|
+
timestamp: z.string()
|
|
3058
|
+
})
|
|
3059
|
+
).optional(),
|
|
3060
|
+
nextCursor: z.number().int().optional(),
|
|
3061
|
+
cursorResetTo: z.number().int().optional(),
|
|
3062
|
+
actions: z.array(
|
|
3063
|
+
z.object({
|
|
3064
|
+
type: z.enum(["approval", "user_input"]),
|
|
3065
|
+
requestId: z.string(),
|
|
3066
|
+
kind: z.string(),
|
|
3067
|
+
params: z.unknown(),
|
|
3068
|
+
itemId: z.string(),
|
|
3069
|
+
reason: z.string().optional(),
|
|
3070
|
+
createdAt: z.string()
|
|
3071
|
+
})
|
|
3072
|
+
).optional(),
|
|
3073
|
+
result: z.object({
|
|
3074
|
+
turnId: z.string(),
|
|
3075
|
+
output: z.string().optional(),
|
|
3076
|
+
structuredOutput: z.unknown().optional(),
|
|
3077
|
+
turn: z.unknown().optional(),
|
|
3078
|
+
status: z.string().optional(),
|
|
3079
|
+
turnError: z.unknown().optional(),
|
|
3080
|
+
error: z.string().optional(),
|
|
3081
|
+
completedAt: z.string()
|
|
3082
|
+
}).optional(),
|
|
3083
|
+
compatWarnings: z.array(z.string()).optional(),
|
|
3084
|
+
truncated: z.boolean().optional(),
|
|
3085
|
+
truncatedFields: z.array(z.string()).optional(),
|
|
3086
|
+
...errorOutputShape
|
|
3087
|
+
},
|
|
3088
|
+
annotations: {
|
|
3089
|
+
title: "Poll & Respond",
|
|
3090
|
+
readOnlyHint: false,
|
|
3091
|
+
destructiveHint: false,
|
|
3092
|
+
idempotentHint: false,
|
|
3093
|
+
openWorldHint: false
|
|
3094
|
+
}
|
|
3095
|
+
},
|
|
3096
|
+
async (args) => {
|
|
3097
|
+
try {
|
|
3098
|
+
const result = executeCodexCheck(args, sessionManager);
|
|
3099
|
+
const isError = typeof result.isError === "boolean" ? result.isError : false;
|
|
3100
|
+
return {
|
|
3101
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3102
|
+
structuredContent: toStructuredContent(result),
|
|
3103
|
+
isError
|
|
3104
|
+
};
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
const message = formatErrorMessage(err);
|
|
3107
|
+
return {
|
|
3108
|
+
content: [{ type: "text", text: message }],
|
|
3109
|
+
structuredContent: { error: message, isError: true },
|
|
3110
|
+
isError: true
|
|
3111
|
+
};
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
);
|
|
3115
|
+
const originalClose = server.close.bind(server);
|
|
3116
|
+
server.close = async () => {
|
|
3117
|
+
sessionManager.destroy();
|
|
3118
|
+
await originalClose();
|
|
3119
|
+
};
|
|
3120
|
+
return server;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
// src/index.ts
|
|
3124
|
+
async function main() {
|
|
3125
|
+
const preflight = runStdioPreflight();
|
|
3126
|
+
for (const note of preflight.notes) {
|
|
3127
|
+
console.error(`[stdio] ${note}`);
|
|
3128
|
+
}
|
|
3129
|
+
if (preflight.riskLevel === "elevated") {
|
|
3130
|
+
console.error(`[stdio] Elevated stdout contamination risk detected (mode=${preflight.mode}).`);
|
|
3131
|
+
for (const reason of preflight.riskReasons) {
|
|
3132
|
+
console.error(`[stdio] Reason: ${reason}`);
|
|
3133
|
+
}
|
|
3134
|
+
for (const suggestion of preflight.suggestions) {
|
|
3135
|
+
console.error(`[stdio] Suggestion: ${suggestion}`);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
if (preflight.shouldBlock) {
|
|
3139
|
+
throw new Error(
|
|
3140
|
+
"STDIO preflight failed in strict mode due to blocking stdout contamination risk"
|
|
3141
|
+
);
|
|
3142
|
+
}
|
|
3143
|
+
const serverCwd = process.cwd();
|
|
3144
|
+
const server = createServer(serverCwd);
|
|
3145
|
+
const transport = new StdioServerTransport();
|
|
3146
|
+
let closing = false;
|
|
3147
|
+
const shutdown = async () => {
|
|
3148
|
+
if (closing) return;
|
|
3149
|
+
closing = true;
|
|
3150
|
+
try {
|
|
3151
|
+
await server.close();
|
|
3152
|
+
} catch {
|
|
3153
|
+
}
|
|
3154
|
+
process.exitCode = 0;
|
|
3155
|
+
const exitTimer = setTimeout(() => process.exit(0), 100);
|
|
3156
|
+
exitTimer.unref();
|
|
3157
|
+
};
|
|
3158
|
+
process.on("SIGINT", shutdown);
|
|
3159
|
+
process.on("SIGTERM", shutdown);
|
|
3160
|
+
process.on("SIGBREAK", shutdown);
|
|
3161
|
+
await server.connect(transport);
|
|
3162
|
+
console.error(`codex-mcp server started (cwd: ${serverCwd})`);
|
|
3163
|
+
}
|
|
3164
|
+
main().catch((err) => {
|
|
3165
|
+
console.error("Fatal error:", err);
|
|
3166
|
+
process.exit(1);
|
|
3167
|
+
});
|
|
3168
|
+
//# sourceMappingURL=index.js.map
|