@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/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