@minhspark/codex-mcp-bridge 1.23.0 → 1.24.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-mcp-bridge",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
5
5
  "author": {
6
6
  "name": "Bui Dang Minh"
package/CHANGELOG.md CHANGED
@@ -4,6 +4,26 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.24.0] - 2026-10-28
8
+
9
+ ### Added
10
+
11
+ - Optional Antigravity MCP bridge for verified Codex and Claude Desktop callers. Select an exact existing Antigravity App conversation or explicitly create a bridge-owned CLI streaming session in the same project directory.
12
+ - Native App sidecar transport with current-user socket access, native workspace/project checks, busy-recipient protection, and durable request receipts. CLI streaming validates native conversation identity and preserves permission review; results and uncertain outcomes are recorded without automatic resends.
13
+ - `antigravity-mcp-bridge-install` configures Codex, Claude, and the App sidecar independently or together, previews changes with `--dry-run`, preserves unrelated configuration, and removes only its own registrations.
14
+ - Antigravity Desktop's `agent-bridge` MCP can discover, read, and send to existing Codex Desktop tasks and live Claude Desktop Code sessions in the same project. Native per-call sender metadata, process ancestry, project configuration, and permission checks preserve Antigravity authorship and recipient policy.
15
+ - Sidecars track their native parent process identity and exit when that parent stops, preventing a restarted App from retaining an orphan's stale connection. Desktop callers without workspace metadata resolve their directory through exact native conversation and project evidence.
16
+
17
+ ### Compatibility
18
+
19
+ - Node.js 22.13 or newer is required for read-only native conversation discovery. The App adapter filters mirrored CLI records rather than trusting the database directory alone.
20
+ - App receipts report native acceptance; App response forwarding and creation are not implemented. CLI support uses bridge-owned headless sessions, not attachment to existing interactive terminals. Antigravity outbound is Desktop-only; nested callers and Linux host verification are unsupported. Claude socket writes remain unconfirmed until a native receipt or reply is observed.
21
+
22
+ ### Verification
23
+
24
+ - Automated coverage includes real MCP transports, caller identity, project scope, current-user pipe access, duplicate requests, native metadata, recipient permission checks, durable receipt states, stream protocol failures, installer preservation, and file replacement during reads.
25
+ - Live Windows verification covered CLI streaming under native `request-review` permissions and both directions between Codex and Antigravity Desktop. The App replied `ANTIGRAVITY_DESKTOP_NATIVE_OK`; its outbound MCP delivered `ANTIGRAVITY_TO_CODEX_DESKTOP_OK` to the exact originating Codex task. Claude outbound routing has automated coverage; a live Claude receiver was unavailable during verification.
26
+
7
27
  ## [1.23.0] - 2026-10-26
8
28
 
9
29
  ### Added
package/README.md CHANGED
@@ -17,6 +17,8 @@
17
17
 
18
18
  Send prompts and replies between **Claude and Codex**, keeping each conversation in its own app. Supports Windows, macOS, and Linux; native Desktop integration supports Windows and macOS.
19
19
 
20
+ The optional **Antigravity bridge** also sends authorized work from Codex or Claude Desktop to an existing Antigravity App conversation or a new Antigravity CLI streaming session. [Setup and current limits](#antigravity-app-and-cli).
21
+
20
22
  https://github.com/user-attachments/assets/98b23989-826f-4d7d-9dcb-ad7dd2739095
21
23
 
22
24
  ## Installation
@@ -25,7 +27,7 @@ https://github.com/user-attachments/assets/98b23989-826f-4d7d-9dcb-ad7dd2739095
25
27
  npm install -g @minhspark/codex-mcp-bridge@latest
26
28
  ```
27
29
 
28
- Requires **Node.js 22+**. In Windows PowerShell, use `npm.cmd` instead of `npm`. Run the same command to update; reconnect MCP servers if their settings or installation paths changed.
30
+ Requires **Node.js 22.13+**. In Windows PowerShell, use `npm.cmd` instead of `npm`. Run the same command to update; reconnect MCP servers if their settings or installation paths changed.
29
31
 
30
32
  On macOS or Linux you can install with [Homebrew](https://github.com/buidangminh23/homebrew-tap) instead, which brings its own Node.js:
31
33
 
@@ -37,6 +39,35 @@ Update with `brew upgrade codex-mcp-bridge`. The installers record Homebrew's st
37
39
 
38
40
  [Windows](#windows-powershell) · [macOS](#macos-terminal) · [Linux / WSL](#linux--wsl-bash) · [ChatGPT plugin](#chatgpt-plugin)
39
41
 
42
+ ### Antigravity App and CLI
43
+
44
+ Install and sign in to the Antigravity App and/or `agy` CLI, then run:
45
+
46
+ ```bash
47
+ antigravity-mcp-bridge-install --all
48
+ ```
49
+
50
+ Use `.cmd` on Windows. Select `--codex`, `--claude`, or `--desktop` to install individual components; `--dry-run` previews changes. The installer preserves unrelated configuration and native permissions. Reload Codex/Claude to load the new MCP server. Antigravity launches its own sidecar from `~/.gemini/config/sidecars/codex-bridge/sidecar.json`; do not run that sidecar manually or copy authentication environment variables between processes.
51
+
52
+ | Tool | Behavior |
53
+ | --- | --- |
54
+ | `antigravity_bridge_status` | Reports native sidecar connectivity and CLI capabilities. |
55
+ | `list_antigravity_conversations` | Lists the selected surface in the verified sender's exact project directory. |
56
+ | `send_to_antigravity_conversation` | Sends to an exact existing App conversation or a CLI session owned by this bridge. |
57
+ | `start_antigravity_cli_session` | Starts an explicitly authorized CLI streaming session with its initial prompt. |
58
+ | `read_antigravity_delivery` | Reads the original receipt without resending. |
59
+ | `close_antigravity_cli_session` | Closes an idle CLI session owned by the sender. |
60
+
61
+ Generate a new `requestId` UUID for each intentional delivery and reuse it only when inspecting or retrying that same delivery. A pending or unknown receipt must be inspected before further action. Receipts survive process restarts; live CLI processes do not. A CLI timeout retains its conversation identity for inspection. No mode or permission-bypass flag is added; headless tool requests requiring interaction may be denied by Antigravity.
62
+
63
+ The App adapter uses the official [sidecar `agentapi`](https://antigravity.google/docs/sidecars), verifies native workspace metadata, and refuses busy or ambiguous destinations. An App `accepted` receipt confirms native acceptance, not a completed agent reply. Open the exact App conversation to read its response. The CLI adapter uses the official [streaming JSON interface](https://antigravity.google/docs/cli/headless) and returns the agent's final result. It creates bridge-owned headless sessions; it does not attach to an already running interactive terminal. The two surfaces never substitute for one another.
64
+
65
+ The installer also registers **agent-bridge inside Antigravity Desktop** in `~/.gemini/config/mcp_config.json`. Reload the App after installation to discover it. From an Antigravity conversation, ask the agent to check `agent_bridge_status`, call `list_agent_tasks` for `codex` or `claude`, then use `send_to_agent_task` with the exact destination ID, project directory, prompt, and a fresh request UUID. Claude destinations also require their native `taskId`. Read `read_agent_delivery` without resending; `read_agent_task` reads the selected Codex task's response or Claude's native task metadata.
66
+
67
+ Outbound calls verify Antigravity's per-call MCP metadata, Desktop process ancestry, native workspace/project, and effective permission settings. The sender is attributed as Antigravity. Codex keeps its native task settings; Claude's inbound policy and permission-class checks remain in effect. A socket write to Claude is `sent_unconfirmed`, not acceptance or completion. After MCP restart, a saved receipt remains available but live Claude reply tracking may be unavailable; inspect the exact native task before further action.
68
+
69
+ Outbound support targets existing local Codex tasks and live Claude Desktop Code sessions in the same project. Nested Antigravity callers, outbound CLI callers, App conversation creation, and automatic App reply forwarding are not implemented. Windows Desktop is the live verification platform; macOS ancestry has unit coverage and Linux outbound host verification fails closed. The existing Codex↔Claude bridge remains separate. Use `antigravity-mcp-bridge-install --all --remove` to remove only these registrations; delivery receipts are retained.
70
+
40
71
  ### Windows (PowerShell)
41
72
 
42
73
  Install and sign in to [Codex Desktop](https://developers.openai.com/codex/app) and [Claude Desktop](https://claude.com/download). Save the project in Codex Desktop and open the same directory in Claude Desktop's **Code tab**. Have the Codex and Claude Code CLIs available.
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "@minhspark/codex-mcp-bridge",
3
- "version": "1.23.0",
4
- "description": "Two-way MCP bridge between Claude and Codex: prompts into a live Codex thread, messages into a running Claude Code session.",
3
+ "version": "1.24.0",
4
+ "description": "MCP bridges for Claude, Codex, and Antigravity Desktop and CLI conversations.",
5
5
  "keywords": [
6
6
  "mcp",
7
7
  "codex",
8
8
  "claude-code",
9
9
  "claude-desktop",
10
10
  "app-server",
11
+ "antigravity",
11
12
  "model-context-protocol"
12
13
  ],
13
14
  "license": "MIT",
@@ -24,6 +25,8 @@
24
25
  "bin": {
25
26
  "codex-mcp-bridge": "src/codex-bridge-launcher.mjs",
26
27
  "claude-mcp-bridge": "src/claude-bridge-launcher.mjs",
28
+ "antigravity-mcp-bridge": "src/antigravity-bridge.mjs",
29
+ "antigravity-mcp-bridge-install": "scripts/install-antigravity-bridge.mjs",
27
30
  "codex-mcp-bridge-install": "scripts/install-claude-desktop.mjs",
28
31
  "codex-bridge-code-install": "scripts/install-claude-code.mjs",
29
32
  "bridge-projects": "scripts/bridge-projects.mjs",
@@ -62,6 +65,8 @@
62
65
  "analytics:publish": "node scripts/publish-repo-analytics.mjs",
63
66
  "start": "node src/codex-bridge-launcher.mjs",
64
67
  "claude-bridge": "node src/claude-bridge-launcher.mjs",
68
+ "antigravity-bridge": "node src/antigravity-bridge.mjs",
69
+ "install:antigravity": "node scripts/install-antigravity-bridge.mjs",
65
70
  "app-server": "codex app-server --listen ws://127.0.0.1:8791",
66
71
  "test": "node scripts/run-tests.mjs",
67
72
  "check": "node scripts/check.mjs",
@@ -79,7 +84,7 @@
79
84
  "version": "node scripts/sync-version.mjs && git add src/index.mjs src/claude-bridge.mjs src/native-relay-companion.mjs scripts/install-native-relay.mjs .codex-plugin/plugin.json"
80
85
  },
81
86
  "engines": {
82
- "node": ">=22"
87
+ "node": ">=22.13"
83
88
  },
84
89
  "dependencies": {
85
90
  "@modelcontextprotocol/sdk": "^1.32.1",
package/plugin.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "codex-mcp-bridge",
4
- "version": "1.23.0",
4
+ "version": "1.24.0",
5
5
  "description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
6
6
  "author": {
7
7
  "name": "Bui Dang Minh",
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import { execFileSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mcp-registration.mjs";
8
+ import { isMainModule, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
9
+ import { resolveCodexBin, spawnEnv } from "../src/platform.mjs";
10
+ import { exitForVersionRequest } from "../src/cli-version.mjs";
11
+
12
+ const NAME = "antigravity-bridge";
13
+ const SIDECAR = "codex-bridge";
14
+ const OUTBOUND = "agent-bridge";
15
+
16
+ function object(value, label) {
17
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be a JSON object`);
18
+ return value;
19
+ }
20
+
21
+ function snapshot(file) {
22
+ let descriptor;
23
+ try { descriptor = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0)); }
24
+ catch (error) {
25
+ if (error.code === "ENOENT") return { file, text: null, value: {} };
26
+ if (error.code === "ELOOP") throw new Error(`Unsafe configuration file: ${file}`);
27
+ throw error;
28
+ }
29
+ const safe = (value) => value.isFile() && !value.isSymbolicLink() && value.nlink === 1 && value.size <= 16 * 1024 * 1024;
30
+ const matches = (left, right) => safe(right) && left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
31
+ try {
32
+ const stat = fs.lstatSync(file);
33
+ const opened = fs.fstatSync(descriptor);
34
+ if (!safe(stat) || !safe(opened)) throw new Error(`Unsafe configuration file: ${file}`);
35
+ if (!matches(stat, opened)) throw new Error(`Configuration changed while opening: ${file}`);
36
+ const buffer = Buffer.alloc(opened.size + 1);
37
+ let length = 0;
38
+ while (length < buffer.length) {
39
+ const read = fs.readSync(descriptor, buffer, length, buffer.length - length, length);
40
+ if (!read) break;
41
+ length += read;
42
+ }
43
+ if (length !== opened.size || !matches(opened, fs.fstatSync(descriptor)) || !matches(opened, fs.lstatSync(file))) throw new Error(`Configuration changed while reading: ${file}`);
44
+ const text = buffer.subarray(0, length).toString("utf8");
45
+ return { file, text, value: object(JSON.parse(text), file), mode: opened.mode & 0o777, dev: opened.dev, ino: opened.ino };
46
+ } finally { fs.closeSync(descriptor); }
47
+ }
48
+
49
+ export function validateCodexExecutable(executable, { platform = process.platform } = {}) {
50
+ if (typeof executable !== "string" || !path.isAbsolute(executable) || executable.includes("\0")) throw new Error("Codex executable must be an absolute native executable path");
51
+ const resolved = fs.realpathSync.native(executable);
52
+ if (!fs.statSync(resolved).isFile()) throw new Error("Codex executable must be a regular file");
53
+ if (platform === "win32" && !/\.(exe|com)$/i.test(resolved)) throw new Error("Codex executable must be a native Windows executable, not a shell script");
54
+ return resolved;
55
+ }
56
+
57
+ function ownEntry(entry, expected, label) {
58
+ if (entry === undefined || entry === null) return;
59
+ object(entry, label);
60
+ if (!Array.isArray(entry.args) || entry.args.length !== 1 || typeof entry.args[0] !== "string" || path.resolve(entry.args[0]) !== path.resolve(expected)) {
61
+ throw new Error(`${label} is already configured with a different entrypoint; it was not changed`);
62
+ }
63
+ }
64
+
65
+ function changed(original, value) {
66
+ const text = value === null ? null : JSON.stringify(value, null, 2) + "\n";
67
+ if (text === original.text || value !== null && JSON.stringify(value) === JSON.stringify(original.value)) return null;
68
+ return { ...original, next: text };
69
+ }
70
+
71
+ export function planAntigravityInstallation({ home = os.homedir(), root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."), node = stableNodeBin(), targets = ["codex", "claude", "desktop"], remove = false, codexExisting = null } = {}) {
72
+ if (!path.isAbsolute(home) || !path.isAbsolute(root) || !path.isAbsolute(node)) throw new Error("Installer paths must be absolute");
73
+ if (!targets.length || targets.some((target) => !["codex", "claude", "desktop"].includes(target))) throw new Error("Unknown installation target");
74
+ const entry = path.join(root, "src", "antigravity-bridge.mjs");
75
+ const sidecarEntry = path.join(root, "src", "antigravity-sidecar.mjs");
76
+ const outboundEntry = path.join(root, "src", "antigravity-outbound-bridge.mjs");
77
+ const edits = [];
78
+ const notices = [];
79
+ let codex = null;
80
+ if (targets.includes("codex")) {
81
+ if (codexExisting && codexExisting.transport?.type !== "stdio") throw new Error("The existing Antigravity MCP transport is not stdio");
82
+ ownEntry(codexExisting?.transport, entry, "Codex Antigravity bridge");
83
+ const existingCaller = codexExisting?.transport?.env?.ANTIGRAVITY_BRIDGE_CALLER;
84
+ if (existingCaller !== undefined && existingCaller !== "codex") throw new Error("The existing Codex bridge has a conflicting caller identity");
85
+ codex = remove ? { remove: Boolean(codexExisting) } : stdioMcpRegistration({ name: NAME, existing: codexExisting, node, entry, envOverrides: { ANTIGRAVITY_BRIDGE_CALLER: "codex" } });
86
+ }
87
+ if (targets.includes("claude")) {
88
+ const original = snapshot(path.join(home, ".claude.json"));
89
+ const value = structuredClone(original.value);
90
+ if (value.mcpServers !== undefined) object(value.mcpServers, "Claude mcpServers");
91
+ const existing = value.mcpServers?.[NAME];
92
+ ownEntry(existing, entry, "Claude Antigravity bridge");
93
+ if (existing?.type !== undefined && existing.type !== "stdio") throw new Error("The existing Claude bridge transport is not stdio");
94
+ if (existing?.env !== undefined) object(existing.env, "Claude bridge environment");
95
+ if (existing?.env?.ANTIGRAVITY_BRIDGE_CALLER !== undefined && existing.env.ANTIGRAVITY_BRIDGE_CALLER !== "claude") throw new Error("The existing Claude bridge has a conflicting caller identity");
96
+ if (remove) { if (existing) delete value.mcpServers[NAME]; }
97
+ else {
98
+ value.mcpServers ??= {};
99
+ value.mcpServers[NAME] = { ...existing, type: "stdio", command: node, args: [entry], env: { ...existing?.env, ANTIGRAVITY_BRIDGE_CALLER: "claude" } };
100
+ }
101
+ const edit = changed(original, value);
102
+ if (edit) edits.push(edit);
103
+ }
104
+ if (targets.includes("desktop")) {
105
+ const configRoot = path.join(home, ".gemini", "config");
106
+ const originalMcp = snapshot(path.join(configRoot, "mcp_config.json"));
107
+ const mcp = structuredClone(originalMcp.value);
108
+ if (mcp.mcpServers !== undefined) object(mcp.mcpServers, "Antigravity mcpServers");
109
+ const outbound = mcp.mcpServers?.[OUTBOUND];
110
+ ownEntry(outbound, outboundEntry, "Antigravity outbound bridge");
111
+ if (outbound?.type !== undefined && outbound.type !== "stdio" || outbound?.url !== undefined || outbound?.serverUrl !== undefined || outbound?.httpUrl !== undefined) throw new Error("The existing Antigravity outbound bridge transport is not stdio");
112
+ if (outbound?.env !== undefined) object(outbound.env, "Antigravity outbound bridge environment");
113
+ if (Object.keys(outbound?.env ?? {}).some((name) => /^(?:CLAUDE|CODEX_|ANTIGRAVITY_BRIDGE_CALLER$)/i.test(name))) throw new Error("The existing Antigravity outbound bridge contains a foreign caller environment; review it before installation");
114
+ for (const name of ["codex-bridge", "claude-bridge"]) {
115
+ if (Object.hasOwn(mcp.mcpServers ?? {}, name)) notices.push(`Existing Antigravity MCP entry ${name} was preserved; it is separate from the native agent-bridge sender integration.`);
116
+ }
117
+ if (remove) { if (outbound) delete mcp.mcpServers[OUTBOUND]; }
118
+ else {
119
+ mcp.mcpServers ??= {};
120
+ mcp.mcpServers[OUTBOUND] = { ...outbound, command: node, args: [outboundEntry] };
121
+ }
122
+ const mcpEdit = changed(originalMcp, mcp);
123
+ if (mcpEdit) edits.push(mcpEdit);
124
+ const originalSidecar = snapshot(path.join(configRoot, "sidecars", SIDECAR, "sidecar.json"));
125
+ if (originalSidecar.text !== null) ownEntry(originalSidecar.value, sidecarEntry, "Antigravity sidecar");
126
+ const originalConfig = snapshot(path.join(configRoot, "config.json"));
127
+ const config = structuredClone(originalConfig.value);
128
+ if (config.sidecars !== undefined) object(config.sidecars, "Antigravity sidecars");
129
+ if (config.sidecars?.[SIDECAR] !== undefined) object(config.sidecars[SIDECAR], "Antigravity bridge sidecar settings");
130
+ if (originalSidecar.text === null && config.sidecars?.[SIDECAR] !== undefined) throw new Error("An Antigravity sidecar with this name exists without a matching bridge definition");
131
+ let definition = null;
132
+ if (remove) { if (config.sidecars) delete config.sidecars[SIDECAR]; }
133
+ else {
134
+ definition = { ...originalSidecar.value, command: node, args: [sidecarEntry], restart_policy: originalSidecar.value.restart_policy ?? "on-failure", display_name: "Codex / Claude Antigravity Bridge" };
135
+ if (definition.builtin !== undefined) throw new Error("A builtin Antigravity sidecar cannot be replaced");
136
+ config.sidecars ??= {};
137
+ config.sidecars[SIDECAR] = { ...config.sidecars[SIDECAR], enabled: true };
138
+ }
139
+ const sidecarEdit = changed(originalSidecar, definition);
140
+ const configEdit = changed(originalConfig, config);
141
+ if (sidecarEdit) edits.push(sidecarEdit);
142
+ if (configEdit) edits.push(configEdit);
143
+ }
144
+ return { name: NAME, targets: [...new Set(targets)], remove, edits, notices, codex, entry, sidecarEntry, outboundEntry };
145
+ }
146
+
147
+ function assertCurrent(edit) {
148
+ const current = snapshot(edit.file);
149
+ if (current.text !== edit.text || current.dev !== edit.dev || current.ino !== edit.ino) throw new Error(`Configuration changed while preparing installation: ${edit.file}`);
150
+ }
151
+
152
+ export function applyJsonEdits(edits) {
153
+ for (const edit of edits) assertCurrent(edit);
154
+ const completed = [];
155
+ try {
156
+ for (const edit of edits) {
157
+ assertCurrent(edit);
158
+ if (edit.next === null) fs.unlinkSync(edit.file);
159
+ else {
160
+ fs.mkdirSync(path.dirname(edit.file), { recursive: true, mode: 0o700 });
161
+ const temporary = `${edit.file}.${process.pid}.bridge.tmp`;
162
+ try {
163
+ fs.writeFileSync(temporary, edit.next, { mode: edit.mode ?? 0o600, flag: "wx" });
164
+ assertCurrent(edit);
165
+ fs.renameSync(temporary, edit.file);
166
+ } finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
167
+ }
168
+ completed.push(edit);
169
+ if (snapshot(edit.file).text !== edit.next) throw new Error(`Configuration readback did not match the saved update: ${edit.file}`);
170
+ }
171
+ } catch (error) {
172
+ for (const edit of completed.reverse()) {
173
+ if (snapshot(edit.file).text !== edit.next) continue;
174
+ if (edit.text === null) fs.unlinkSync(edit.file);
175
+ else fs.writeFileSync(edit.file, edit.text, { mode: edit.mode ?? 0o600 });
176
+ }
177
+ throw error;
178
+ }
179
+ }
180
+
181
+ export function installerArguments(args) {
182
+ const allowed = new Set(["--codex", "--claude", "--desktop", "--all", "--remove", "--dry-run", "--help"]);
183
+ if (args.some((arg) => !allowed.has(arg))) throw new Error("Unknown option; use --help");
184
+ const selected = ["codex", "claude", "desktop"].filter((target) => args.includes(`--${target}`));
185
+ return { targets: args.includes("--all") || !selected.length ? ["codex", "claude", "desktop"] : selected, remove: args.includes("--remove"), dryRun: args.includes("--dry-run"), help: args.includes("--help") };
186
+ }
187
+
188
+ export function runAntigravityInstaller(args = process.argv.slice(2)) {
189
+ const options = installerArguments(args);
190
+ if (options.help) {
191
+ console.log("Usage: antigravity-mcp-bridge-install [--all|--codex|--claude|--desktop] [--remove] [--dry-run]");
192
+ return;
193
+ }
194
+ const home = os.homedir();
195
+ const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
196
+ const node = stableNodeBin();
197
+ let run;
198
+ let codexExisting = null;
199
+ if (options.targets.includes("codex")) {
200
+ const codexBin = validateCodexExecutable(resolveCodexBin(process.env.CODEX_EXE));
201
+ run = (command) => execFileSync(codexBin, command, { encoding: "utf8", env: spawnEnv(), windowsHide: true, shell: false, timeout: 30000, stdio: "pipe" });
202
+ const inventory = JSON.parse(run(["mcp", "list", "--json"]));
203
+ if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
204
+ if (inventory.some((item) => item.name === NAME)) codexExisting = JSON.parse(run(["mcp", "get", NAME, "--json"]));
205
+ }
206
+ const plan = planAntigravityInstallation({ ...options, home, root, node, codexExisting });
207
+ if (!options.remove) {
208
+ for (const entry of [plan.entry, ...(options.targets.includes("desktop") ? [plan.sidecarEntry, plan.outboundEntry] : [])]) {
209
+ if (!fs.existsSync(entry)) throw new Error(`Missing bridge entrypoint: ${entry}`);
210
+ }
211
+ }
212
+ if (options.dryRun) {
213
+ console.log(JSON.stringify({ dryRun: true, remove: plan.remove, targets: plan.targets, files: plan.edits.map((edit) => ({ path: edit.file, action: edit.next === null ? "remove" : "write" })), notices: plan.notices, codex: plan.codex ? { name: NAME, action: options.remove ? "remove" : "register" } : null }, null, 2));
214
+ return plan;
215
+ }
216
+ for (const edit of plan.edits) assertCurrent(edit);
217
+ if (plan.codex) {
218
+ if (options.remove) { if (plan.codex.remove) run(["mcp", "remove", NAME]); }
219
+ else applyStdioMcpRegistration({ name: NAME, registration: plan.codex, existing: codexExisting, run, configPath: path.join(process.env.CODEX_HOME ?? path.join(home, ".codex"), "config.toml") });
220
+ }
221
+ applyJsonEdits(plan.edits);
222
+ console.log(`${options.remove ? "Removed" : "Installed"} Antigravity bridge: ${plan.targets.join(", ")}.`);
223
+ for (const notice of plan.notices) console.log(notice);
224
+ if (!options.remove && options.targets.includes("desktop")) console.log("In Antigravity Customizations > Installed, find agent-bridge and choose More actions > Restart. If it is absent, reopen the App to load its new registration. In the CLI, use /mcp to reload server configurations.");
225
+ if (!options.remove) console.log("Reload the selected clients to load the bridge. Antigravity Desktop must launch its sidecar; installation alone does not prove a live connection. Native permissions were preserved.");
226
+ return plan;
227
+ }
228
+
229
+ if (isMainModule(process.argv[1], import.meta.url)) {
230
+ exitForVersionRequest(import.meta.url);
231
+ try { runAntigravityInstaller(); }
232
+ catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
233
+ }
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
25
25
 
26
26
  exitForVersionRequest(import.meta.url);
27
27
 
28
- const VERSION = "1.23.0";
28
+ const VERSION = "1.24.0";
29
29
  const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
30
30
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
31
31
  const entryArgs = ["native-relay-companion.mjs"];
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import { z } from "zod";
7
+ import { resolveCodexSenderContext } from "./codex-sender-context.mjs";
8
+ import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspectors } from "./claude-sender-context.mjs";
9
+ import { readClaudeAccountContext } from "./desktop-account-context.mjs";
10
+ import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
11
+ import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
12
+ import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
13
+ import { AntigravityCliSession } from "./antigravity-cli.mjs";
14
+ import { exitForVersionRequest } from "./cli-version.mjs";
15
+ import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
16
+ import { requireBridgeAccounts, readBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
17
+
18
+ exitForVersionRequest(import.meta.url);
19
+
20
+ const { version } = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
21
+ const rootPolicy = createHardenedRootPolicy();
22
+ const directory = antigravityBridgeDirectory();
23
+ const deliveries = new AntigravityDeliveries(path.join(directory, "cli-deliveries"));
24
+ const sessions = new Map();
25
+ let startingSessions = 0;
26
+ const caller = process.env.ANTIGRAVITY_BRIDGE_CALLER;
27
+ const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() });
28
+ const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
29
+ const schema = { requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(65536) };
30
+
31
+ async function sender(extra) {
32
+ if (caller === "codex") {
33
+ const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
34
+ const accounts = requireBridgeAccounts(readBridgeAccounts());
35
+ const { result } = await senderRelay.requestDesktop("read_thread", { threadId, hostId: "local", turnLimit: 1, includeOutputs: false, maxOutputCharsPerItem: 1 }, { accountContext: accounts, beforeSend: () => assertAccountIdentity(accounts) });
36
+ assertAccountIdentity(accounts);
37
+ return { thread: result?.thread, latestTurnId: result?.turns?.[0]?.id ?? null };
38
+ } });
39
+ if (context.status !== "verified") throw new Error(context.reason ?? "Unverified Codex sender");
40
+ return { provider: "codex", id: context.threadId, cwd: context.cwd };
41
+ }
42
+ if (caller === "claude") {
43
+ const context = requireClaudeSenderContext(await readClaudeSenderContext({ account: readClaudeAccountContext() }));
44
+ return { provider: "claude", id: context.sessionId, cwd: context.cwd };
45
+ }
46
+ throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
47
+ }
48
+
49
+ function sameCwd(expected, actual) {
50
+ if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
51
+ const left = fs.realpathSync.native(expected);
52
+ const right = fs.realpathSync.native(actual);
53
+ if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
54
+ rootPolicy.assert(left, "Antigravity project");
55
+ return left;
56
+ }
57
+
58
+ function owns(record, identity) {
59
+ if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw new Error("Delivery does not belong to this sending conversation");
60
+ sameCwd(identity.cwd, record.cwd);
61
+ return record;
62
+ }
63
+
64
+ function handoff(prompt, identity) {
65
+ return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
66
+ }
67
+
68
+ const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force. Do not claim support for Antigravity-origin handoffs into Codex or Claude; this server receives from those hosts." });
69
+
70
+ function register(name, description, inputSchema, readOnly, handler) {
71
+ server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
72
+ try { return result(await handler(args, extra)); }
73
+ catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
74
+ });
75
+ }
76
+
77
+ register("antigravity_bridge_status", "Inspect connection capabilities without sending prompts.", {}, true, async () => {
78
+ let desktop;
79
+ try { desktop = await callAntigravityDesktop({ operation: "status" }, { timeoutMs: 5000 }); }
80
+ catch { desktop = { available: false, reason: "Open Antigravity with the installed codex-bridge sidecar enabled" }; }
81
+ return { version, caller: caller ?? "unconfigured", desktop, cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false } };
82
+ });
83
+
84
+ register("list_antigravity_conversations", "List native Desktop conversations and bridge-owned CLI sessions in the sending project.", { surface: z.enum(["desktop", "cli"]) }, true, async ({ surface }, extra) => {
85
+ const identity = await sender(extra);
86
+ const records = surface === "desktop" ? await callAntigravityDesktop({ operation: "list" }) : [...sessions.values()].map((entry) => ({ ...entry.session.read(), sender: entry.sender }));
87
+ return records.filter((record) => {
88
+ try { sameCwd(identity.cwd, record.cwd); return surface !== "cli" || record.sender.id === identity.id && record.sender.provider === identity.provider; }
89
+ catch { return false; }
90
+ });
91
+ });
92
+
93
+ register("send_to_antigravity_conversation", "Send one authorized prompt to an exact existing Desktop conversation or a bridge-owned CLI session. Retain requestId and poll the delivery receipt.", { ...schema, surface: z.enum(["desktop", "cli"]), conversationId: z.string().uuid() }, false, async (args, extra) => {
94
+ const identity = await sender(extra);
95
+ const cwd = sameCwd(identity.cwd, args.cwd);
96
+ const prompt = handoff(args.prompt, identity);
97
+ if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw new Error("Desktop handoff must fit within 30000 characters and contain no NUL");
98
+ if (args.surface === "desktop") return callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
99
+ const entry = sessions.get(args.conversationId);
100
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender and bridge process");
101
+ sameCwd(cwd, entry.session.read().cwd);
102
+ return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
103
+ const current = await sender(extra);
104
+ sameCwd(cwd, current.cwd);
105
+ if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
106
+ return entry.session.send(prompt);
107
+ });
108
+ });
109
+
110
+ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity CLI streaming session for explicitly authorized new work; sends the initial prompt and returns a durable delivery receipt.", schema, false, async (args, extra) => {
111
+ const identity = await sender(extra);
112
+ const cwd = sameCwd(identity.cwd, args.cwd);
113
+ if (sessions.size + startingSessions >= 4) throw new Error("At most four CLI sessions may be open in one bridge");
114
+ return deliveries.dispatch({ ...args, cwd, sender: identity, surface: "cli" }, async () => {
115
+ if (sessions.size + startingSessions >= 4) throw Object.assign(new Error("CLI session capacity reached"), { deliveryState: "not_sent" });
116
+ startingSessions++;
117
+ let reserved = true;
118
+ let session;
119
+ try {
120
+ const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_)/i.test(name)));
121
+ session = new AntigravityCliSession({ cwd, env });
122
+ const started = await session.start();
123
+ deliveries.identify(args.requestId, started.conversationId);
124
+ sessions.set(started.conversationId, { session, sender: identity });
125
+ startingSessions--;
126
+ reserved = false;
127
+ const current = await sender(extra);
128
+ sameCwd(cwd, current.cwd);
129
+ if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
130
+ return await session.send(handoff(args.prompt, identity));
131
+ } catch (error) {
132
+ if (error.deliveryState !== "unknown") {
133
+ for (const [id, entry] of sessions) if (entry.session === session) sessions.delete(id);
134
+ await session?.close();
135
+ }
136
+ throw error;
137
+ } finally {
138
+ if (reserved) startingSessions--;
139
+ }
140
+ });
141
+ });
142
+
143
+ register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means the native API accepted it, not that the agent completed a turn.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
144
+ const identity = await sender(extra);
145
+ const record = args.surface === "desktop" ? await callAntigravityDesktop({ operation: "receipt", requestId: args.requestId }) : deliveries.read(args.requestId);
146
+ return owns(record, identity);
147
+ });
148
+
149
+ register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
150
+ const identity = await sender(extra);
151
+ const entry = sessions.get(conversationId);
152
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender");
153
+ sameCwd(identity.cwd, entry.session.read().cwd);
154
+ if (["running", "starting"].includes(entry.session.read().status)) throw new Error("Wait for the active CLI delivery before closing");
155
+ const closed = await entry.session.close();
156
+ if (closed.status !== "closing") sessions.delete(conversationId);
157
+ return { closed: closed.status !== "closing", conversationId, status: closed.status };
158
+ });
159
+
160
+ let stopping = false;
161
+ async function stop() {
162
+ if (stopping) return;
163
+ stopping = true;
164
+ await Promise.allSettled([...sessions.values()].map(({ session }) => session.close()));
165
+ await stopProcessInspectors();
166
+ await server.close();
167
+ }
168
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
169
+ process.stdin.once("end", () => { void stop(); });
170
+ await server.connect(new StdioServerTransport());