@minhspark/codex-mcp-bridge 1.12.2 → 1.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +1 -1
- package/src/native-relay-companion.mjs +7 -2
- package/src/native-relay.mjs +171 -6
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [1.12.3] - 2026-09-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Discover the native tools pipe from the launching Desktop app-server's `mcp_servers.codex_app.env` override when Desktop does not pass it to custom MCP servers. Explicit pipe settings keep precedence; discovery rejects unrelated processes, ambiguous settings, and remote Windows pipes without scanning other sessions or persisting a restart-specific address.
|
|
10
|
+
- Cancel pending pipe discovery when the client closes, and share discovery between concurrent connection requests so a closed client cannot send a delayed message.
|
|
11
|
+
- Report a reachable shared companion in native relay status when another MCP instance owns the listening endpoint.
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
|
|
15
|
+
- Added Windows/POSIX command-line parsing, parent discovery, environment precedence, real mock-pipe delivery, cancellation, concurrent connection, and shared-companion regressions.
|
|
16
|
+
|
|
5
17
|
## [1.12.2] - 2026-09-05
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -424,7 +424,7 @@ Resolution order is `CODEX_RELAY_ID` → that file → an error naming both. Nev
|
|
|
424
424
|
|
|
425
425
|
A companion that cannot be reached before sending falls back to the app-server path. Once a request has been written, a refusal, timeout, or lost acknowledgement does not trigger another delivery: the first attempt may already have succeeded. Invalid or oversized messages are also refused rather than passed to another backend.
|
|
426
426
|
|
|
427
|
-
|
|
427
|
+
The companion uses `CODEX_APP_TOOLS_PIPE_PATH` when inherited. Desktop builds that supply it only to their bundled `codex_app` MCP are also supported: the companion reads the exact `mcp_servers.codex_app.env` override from its launching app-server. It never picks a pipe from another session or saves a restart-specific address. The native connection remains separate from MCP stdio. If neither source provides a valid pipe, the relay stays unavailable; if a configured pipe is late during startup, it retries in the background. When several MCP instances start, status identifies a reachable shared companion instead of reporting that the relay is down.
|
|
428
428
|
|
|
429
429
|
> ⚠️ `codex_app.send_message_to_thread` and the native tools pipe are **Codex Desktop internals with no public documentation**, on the same footing as the Claude peer protocol above. That is why the relay is Windows/macOS, feature-detected, optional and fallback-safe. If Codex changes it, the two places to fix are `NATIVE_DISPATCH_METHOD` and `nativeDispatchParams()` in `src/native-relay.mjs`; `CODEX_NATIVE_RELAY_METHOD` overrides the method name without a release. The request the companion sends is:
|
|
430
430
|
>
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { CodexAppServerClient } from "../src/app-server-client.mjs";
|
|
|
8
8
|
import { bootstrapRelayThread, readRelayConfig, relayConfigPath, relaySocketPath } from "../src/native-relay.mjs";
|
|
9
9
|
import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
|
|
10
10
|
|
|
11
|
-
const VERSION = "1.12.
|
|
11
|
+
const VERSION = "1.12.3";
|
|
12
12
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
13
|
const entry = path.join(root, "src", "native-relay-companion.mjs");
|
|
14
14
|
const serverName = process.env.CODEX_NATIVE_RELAY_NAME ?? "codex-native-relay";
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { PLATFORM_LABEL } from "./platform.mjs";
|
|
|
8
8
|
import { PeerEndpoint, findClaudeSession, listClaudeSessions, readTranscript } from "./peer-protocol.mjs";
|
|
9
9
|
import { createThreadDelivery } from "./thread-delivery.mjs";
|
|
10
10
|
|
|
11
|
-
const VERSION = "1.12.
|
|
11
|
+
const VERSION = "1.12.3";
|
|
12
12
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
13
13
|
const FORWARD_MAX_PER_SESSION = 50;
|
|
14
14
|
|
package/src/index.mjs
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { runTurn } from "./turn.mjs";
|
|
24
24
|
import { BridgeSecurityPolicy } from "./security-policy.mjs";
|
|
25
25
|
|
|
26
|
-
const VERSION = "1.12.
|
|
26
|
+
const VERSION = "1.12.3";
|
|
27
27
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
28
28
|
|
|
29
29
|
/**
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "./native-relay.mjs";
|
|
18
18
|
import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
|
|
19
19
|
|
|
20
|
-
const VERSION = "1.12.
|
|
20
|
+
const VERSION = "1.12.3";
|
|
21
21
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
22
22
|
|
|
23
23
|
function errorResponse(code, message) {
|
|
@@ -201,6 +201,10 @@ export class RelaySocketServer {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
+
async isListening() {
|
|
205
|
+
return this.started || this.#socketIsLive();
|
|
206
|
+
}
|
|
207
|
+
|
|
204
208
|
#handleConnection(socket) {
|
|
205
209
|
let buffer = Buffer.alloc(0);
|
|
206
210
|
let handled = false;
|
|
@@ -350,6 +354,7 @@ if (invokedDirectly) {
|
|
|
350
354
|
},
|
|
351
355
|
},
|
|
352
356
|
async () => {
|
|
357
|
+
const listening = await relay.isListening();
|
|
353
358
|
let executor = "(unconfigured)";
|
|
354
359
|
try {
|
|
355
360
|
const resolved = resolveRelayThreadId();
|
|
@@ -364,7 +369,7 @@ if (invokedDirectly) {
|
|
|
364
369
|
text: [
|
|
365
370
|
`platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
|
|
366
371
|
`companion: codex-native-relay ${VERSION}`,
|
|
367
|
-
`relay socket: ${relay.started ? relay.socketPath : `${relay.socketPath} (not listening)`}`,
|
|
372
|
+
`relay socket: ${relay.started ? relay.socketPath : `${relay.socketPath} (${listening ? "shared companion listening" : "not listening"})`}`,
|
|
368
373
|
`executor: ${executor}`,
|
|
369
374
|
`dispatch: ${process.env.CODEX_NATIVE_RELAY_METHOD ?? NATIVE_DISPATCH_METHOD}`,
|
|
370
375
|
`native pipe: ${nativeTools.socketPath ?? "unavailable (requires Codex Desktop)"}`,
|
package/src/native-relay.mjs
CHANGED
|
@@ -2,6 +2,8 @@ import fs from "node:fs";
|
|
|
2
2
|
import net from "node:net";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
5
7
|
|
|
6
8
|
import { IS_MACOS, IS_WINDOWS, PLATFORM_LABEL, homeDir } from "./platform.mjs";
|
|
7
9
|
|
|
@@ -139,14 +141,168 @@ export function nativeDispatchParams({ executorThreadId, targetThreadId, message
|
|
|
139
141
|
};
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
const execFileAsync = promisify(execFile);
|
|
145
|
+
|
|
146
|
+
function splitDesktopCommandLine(commandLine, platform) {
|
|
147
|
+
const args = [];
|
|
148
|
+
let value = "";
|
|
149
|
+
let quote = null;
|
|
150
|
+
let depth = 0;
|
|
151
|
+
for (let index = 0; index < commandLine.length; index++) {
|
|
152
|
+
const char = commandLine[index];
|
|
153
|
+
if (platform === "win32" && char === "\\") {
|
|
154
|
+
let end = index;
|
|
155
|
+
while (commandLine[end] === "\\") end++;
|
|
156
|
+
const count = end - index;
|
|
157
|
+
if (commandLine[end] === '"') {
|
|
158
|
+
value += "\\".repeat(Math.floor(count / 2));
|
|
159
|
+
if (count % 2) value += '"';
|
|
160
|
+
else quote = quote ? null : '"';
|
|
161
|
+
index = end;
|
|
162
|
+
} else {
|
|
163
|
+
value += "\\".repeat(count);
|
|
164
|
+
index = end - 1;
|
|
165
|
+
}
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (platform !== "win32" && depth > 0) {
|
|
169
|
+
value += char;
|
|
170
|
+
if (quote) {
|
|
171
|
+
if (char === "\\" && quote === '"') value += commandLine[++index] ?? "";
|
|
172
|
+
else if (char === quote) quote = null;
|
|
173
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
174
|
+
else if (char === "{" || char === "[") depth++;
|
|
175
|
+
else if (char === "}" || char === "]") depth--;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (char === '"' || (platform !== "win32" && char === "'")) {
|
|
179
|
+
if (!quote) quote = char;
|
|
180
|
+
else if (quote === char) quote = null;
|
|
181
|
+
else value += char;
|
|
182
|
+
} else if (!quote && /\s/.test(char)) {
|
|
183
|
+
if (value) args.push(value);
|
|
184
|
+
value = "";
|
|
185
|
+
} else {
|
|
186
|
+
value += char;
|
|
187
|
+
if (platform !== "win32" && !quote && char === "{") depth++;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (quote || depth) return [];
|
|
191
|
+
if (value) args.push(value);
|
|
192
|
+
return args;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function inlineTableValues(table) {
|
|
196
|
+
const text = table.trim();
|
|
197
|
+
if (!text.startsWith("{") || !text.endsWith("}")) return null;
|
|
198
|
+
const entries = [];
|
|
199
|
+
let start = 1;
|
|
200
|
+
let quote = null;
|
|
201
|
+
let depth = 0;
|
|
202
|
+
for (let index = 1; index < text.length - 1; index++) {
|
|
203
|
+
const char = text[index];
|
|
204
|
+
if (quote) {
|
|
205
|
+
if (char === "\\" && quote === '"') index++;
|
|
206
|
+
else if (char === quote) quote = null;
|
|
207
|
+
} else if (char === '"' || char === "'") quote = char;
|
|
208
|
+
else if (char === "{" || char === "[") depth++;
|
|
209
|
+
else if (char === "}" || char === "]") {
|
|
210
|
+
if (--depth < 0) return null;
|
|
211
|
+
} else if (char === "," && depth === 0) {
|
|
212
|
+
entries.push(text.slice(start, index));
|
|
213
|
+
start = index + 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (quote || depth) return null;
|
|
217
|
+
entries.push(text.slice(start, -1));
|
|
218
|
+
const values = new Map();
|
|
219
|
+
for (const entry of entries) {
|
|
220
|
+
if (!entry.trim()) continue;
|
|
221
|
+
const match = entry.match(/^\s*(?:"([A-Za-z_][A-Za-z0-9_-]*)"|'([A-Za-z_][A-Za-z0-9_-]*)'|([A-Za-z_][A-Za-z0-9_-]*))\s*=\s*([\s\S]+)$/);
|
|
222
|
+
if (!match) return null;
|
|
223
|
+
const key = match[1] ?? match[2] ?? match[3];
|
|
224
|
+
if (values.has(key)) return null;
|
|
225
|
+
values.set(key, match[4].trim());
|
|
226
|
+
}
|
|
227
|
+
return values;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function nativeToolsPipeFromCommandLine(commandLine, { platform = process.platform } = {}) {
|
|
231
|
+
if (typeof commandLine !== "string" || /[\r\n\0]/.test(commandLine)) return null;
|
|
232
|
+
const args = splitDesktopCommandLine(commandLine, platform);
|
|
233
|
+
const paths = platform === "win32" ? path.win32 : path.posix;
|
|
234
|
+
if (!/^codex(?:\.exe)?$/i.test(paths.basename(args[0] ?? ""))) return null;
|
|
235
|
+
const overrides = [];
|
|
236
|
+
let appServer = false;
|
|
237
|
+
for (let index = 1; index < args.length; index++) {
|
|
238
|
+
const arg = args[index];
|
|
239
|
+
if (arg === "-c" || arg === "--config") {
|
|
240
|
+
overrides.push(args[++index] ?? "");
|
|
241
|
+
} else if (arg.startsWith("--config=")) overrides.push(arg.slice(9));
|
|
242
|
+
else if (arg === "app-server") appServer = true;
|
|
243
|
+
else if (!arg.startsWith("-") && !appServer) return null;
|
|
244
|
+
}
|
|
245
|
+
if (!appServer) return null;
|
|
246
|
+
const candidates = [];
|
|
247
|
+
for (const override of overrides) {
|
|
248
|
+
const match = override.match(/^mcp_servers\.codex_app\s*=\s*([\s\S]+)$/);
|
|
249
|
+
if (!match) continue;
|
|
250
|
+
const config = inlineTableValues(match[1]);
|
|
251
|
+
if (!config) return null;
|
|
252
|
+
const env = inlineTableValues(config.get("env") ?? "{}");
|
|
253
|
+
if (!env) return null;
|
|
254
|
+
const raw = env.get("CODEX_APP_TOOLS_PIPE_PATH");
|
|
255
|
+
if (raw === undefined) continue;
|
|
256
|
+
let candidate;
|
|
257
|
+
try {
|
|
258
|
+
candidate = raw.startsWith('"') ? JSON.parse(raw) : /^'[^']*'$/.test(raw) ? raw.slice(1, -1) : null;
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
if (typeof candidate !== "string" || /[\r\n\0]/.test(candidate)) return null;
|
|
263
|
+
if (platform === "win32" ? !/^\\\\\.\\pipe\\[^\\]/i.test(candidate) : !path.posix.isAbsolute(candidate)) return null;
|
|
264
|
+
candidates.push(candidate);
|
|
265
|
+
}
|
|
266
|
+
return candidates.length && new Set(candidates).size === 1 ? candidates[0] : null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function readParentCommandLine(parentPid, platform) {
|
|
270
|
+
if (!Number.isSafeInteger(parentPid) || parentPid <= 0) return null;
|
|
271
|
+
const options = { timeout: 5000, maxBuffer: 128 * 1024, windowsHide: true };
|
|
272
|
+
if (platform === "win32") {
|
|
273
|
+
const powershell = path.win32.join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
274
|
+
const script = `$p=Get-CimInstance Win32_Process -Filter 'ProcessId=${parentPid}'; if ($p.Name -eq 'codex.exe') { $p.CommandLine | ConvertTo-Json -Compress }`;
|
|
275
|
+
const { stdout } = await execFileAsync(powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], options);
|
|
276
|
+
return stdout.trim() ? JSON.parse(stdout.trim()) : null;
|
|
277
|
+
}
|
|
278
|
+
const { stdout } = await execFileAsync("/bin/ps", ["-ww", "-p", String(parentPid), "-o", "args="], options);
|
|
279
|
+
return stdout.trim();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export async function resolveNativeToolsPipePath({
|
|
283
|
+
env = process.env,
|
|
284
|
+
parentPid = process.ppid,
|
|
285
|
+
platform = process.platform,
|
|
286
|
+
readParentCommandLine: readParent = readParentCommandLine,
|
|
287
|
+
} = {}) {
|
|
288
|
+
if (env.CODEX_APP_TOOLS_PIPE_PATH) return env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
289
|
+
try {
|
|
290
|
+
return nativeToolsPipeFromCommandLine(await readParent(parentPid, platform), { platform });
|
|
291
|
+
} catch {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
142
296
|
export class NativeToolsClient {
|
|
143
|
-
constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
297
|
+
constructor({ env = process.env, socketPath = env.CODEX_APP_TOOLS_PIPE_PATH, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env }) } = {}) {
|
|
144
298
|
this.env = env;
|
|
145
299
|
this.socketPath = socketPath;
|
|
146
300
|
this.timeoutMs = timeoutMs;
|
|
301
|
+
this.resolveSocketPath = resolveSocketPath;
|
|
147
302
|
this.socket = null;
|
|
148
303
|
this.connectingSocket = null;
|
|
149
304
|
this.connecting = null;
|
|
305
|
+
this.connectionGeneration = 0;
|
|
150
306
|
this.pending = new Map();
|
|
151
307
|
this.nextId = 1;
|
|
152
308
|
}
|
|
@@ -154,10 +310,17 @@ export class NativeToolsClient {
|
|
|
154
310
|
async connect() {
|
|
155
311
|
if (this.connecting) return this.connecting;
|
|
156
312
|
if (this.socket && !this.socket.destroyed) return;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
313
|
+
const generation = this.connectionGeneration;
|
|
314
|
+
this.connecting = (async () => {
|
|
315
|
+
const socketPath = this.socketPath || await this.resolveSocketPath();
|
|
316
|
+
if (generation !== this.connectionGeneration) {
|
|
317
|
+
throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
|
|
318
|
+
}
|
|
319
|
+
this.socketPath = socketPath;
|
|
320
|
+
if (!this.socketPath) {
|
|
321
|
+
throw new NativeRelayError("CODEX_APP_TOOLS_PIPE_PATH is missing from the environment and parent Desktop app-server configuration; launch the companion from Codex Desktop", "NATIVE_PIPE_UNAVAILABLE");
|
|
322
|
+
}
|
|
323
|
+
return new Promise((resolve, reject) => {
|
|
161
324
|
const socket = net.connect({ path: this.socketPath });
|
|
162
325
|
this.connectingSocket = socket;
|
|
163
326
|
let buffer = Buffer.alloc(0);
|
|
@@ -215,7 +378,8 @@ export class NativeToolsClient {
|
|
|
215
378
|
});
|
|
216
379
|
socket.on("error", (err) => fail(new NativeRelayError(`Codex Desktop native pipe failed: ${err.message}`, connected ? "NATIVE_DELIVERY_UNCONFIRMED" : "NATIVE_PIPE_UNAVAILABLE")));
|
|
217
380
|
socket.on("close", () => fail(new NativeRelayError("Codex Desktop native tools pipe closed before confirming delivery", connected ? "NATIVE_DELIVERY_UNCONFIRMED" : "NATIVE_PIPE_UNAVAILABLE")));
|
|
218
|
-
|
|
381
|
+
});
|
|
382
|
+
})();
|
|
219
383
|
try {
|
|
220
384
|
await this.connecting;
|
|
221
385
|
} finally {
|
|
@@ -264,6 +428,7 @@ export class NativeToolsClient {
|
|
|
264
428
|
}
|
|
265
429
|
|
|
266
430
|
close() {
|
|
431
|
+
this.connectionGeneration++;
|
|
267
432
|
const socket = this.socket;
|
|
268
433
|
this.socket = null;
|
|
269
434
|
for (const pending of this.pending.values()) {
|