metheus-governance-mcp-cli 0.2.25 → 0.2.28

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.
Files changed (3) hide show
  1. package/README.md +20 -6
  2. package/cli.mjs +468 -22
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -7,9 +7,9 @@ Compatibility note: legacy command alias `metheus-governance-mcp` is still suppo
7
7
  - `metheus-governance-mcp-cli` (no args): bootstrap mode
8
8
  - checks auth token
9
9
  - if token is missing/expired, starts `auth login`
10
- - checks Codex/Claude MCP registration
10
+ - checks Codex/Claude/Gemini/Antigravity/Cursor MCP registration
11
11
  - registers only missing clients
12
- - `setup`: register `metheus-governance-mcp` into Codex/Claude (if installed)
12
+ - `setup`: register `metheus-governance-mcp` into Codex/Claude/Gemini/Antigravity/Cursor (if installed)
13
13
  - `doctor`: run end-to-end health checks (auth/registration/gateway/project/ctxpack/tools)
14
14
  - `proxy`: stdio MCP bridge to Metheus HTTPS gateway
15
15
  - `auth`: save/check/clear local Metheus token used by proxy
@@ -40,12 +40,16 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
40
40
  `setup` defaults to `--workspace-dir auto` (dynamic workspace detection).
41
41
  Use an explicit path only when you intentionally want a fixed workspace.
42
42
 
43
- Recommended for Codex/Claude multi-workspace sessions:
43
+ Recommended for Codex/Claude/Gemini/Antigravity/Cursor multi-workspace sessions:
44
44
 
45
45
  ```bash
46
46
  metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com --workspace-dir auto
47
47
  ```
48
48
 
49
+ Gemini CLI note:
50
+ - `gemini mcp` commands require Gemini auth to be configured first (`GEMINI_API_KEY` or `~/.gemini/settings.json` auth).
51
+ - `setup` registers Gemini in both `user` and `project` scopes to improve auto-discovery across folders.
52
+
49
53
  When a client does not send workspace metadata (for example some Codex sessions),
50
54
  set a stable fallback root once:
51
55
 
@@ -53,7 +57,9 @@ set a stable fallback root once:
53
57
  metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com --workspace-dir auto --workspace-fallback-dir C:\code_test
54
58
  ```
55
59
 
56
- This registers `METHEUS_WORKSPACE_DIR` for Codex MCP so ctxpack sync still resolves safely.
60
+ This sets fallback workspace context for clients that do not pass workspace metadata:
61
+ - Codex/Antigravity/Cursor: `METHEUS_WORKSPACE_DIR` env
62
+ - Gemini: pinned `--workspace-dir <fallback>` and `METHEUS_WORKSPACE_DIR` in MCP registration
57
63
 
58
64
  Guardrail note:
59
65
  - By default, CLI blocks reading/writing ctxpack sync metadata when workspace root resolves to the home directory.
@@ -76,7 +82,7 @@ metheus-governance-mcp-cli doctor --project-id <project_uuid> --base-url https:/
76
82
 
77
83
  Checks:
78
84
  - auth token status (+ auto refresh attempt)
79
- - codex/claude registration state
85
+ - codex/claude/gemini/antigravity/cursor registration state
80
86
  - gateway `tools/list` reachability
81
87
  - `project.summary` access
82
88
  - ctxpack auto sync status
@@ -84,7 +90,15 @@ Checks:
84
90
 
85
91
  ## Use in MCP
86
92
 
87
- `setup` auto-registers this server for `codex` and `claude` when those CLIs are available.
93
+ `setup` auto-registers this server for `codex`, `claude`, `gemini`, `antigravity`, and `cursor` when those CLIs are available.
94
+
95
+ Antigravity note:
96
+ - this CLI manages MCP registration via Antigravity local config file (`mcp.json`) because Antigravity does not provide full `mcp list/get/remove` subcommands.
97
+ - for compatibility across Antigravity variants, setup writes both `mcpServers` and `servers` keys.
98
+ - proxy stdio now supports both `Content-Length` framed MCP and JSONL input for VS Code-family clients.
99
+
100
+ Cursor note:
101
+ - this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
88
102
 
89
103
  Local bootstrap tools exposed by proxy:
90
104
 
package/cli.mjs CHANGED
@@ -27,6 +27,7 @@ const SELF_UPDATE_ENV_KEY = "METHEUS_CLI_AUTO_UPDATE";
27
27
  const SELF_UPDATE_GUARD_ENV_KEY = "METHEUS_CLI_SELF_UPDATE_ATTEMPTED";
28
28
  const ALLOW_HOME_WORKSPACE_ENV_KEY = "METHEUS_ALLOW_HOME_WORKSPACE";
29
29
  const AUTO_CTXPACK_SYNC_INTERVAL_MS = 60 * 1000;
30
+ const MCP_CLIENTS = ["codex", "claude", "gemini", "antigravity", "cursor"];
30
31
  const autoCtxpackSyncTracker = new Map();
31
32
 
32
33
  function printUsage() {
@@ -85,6 +86,159 @@ function resolveHomeFilePath(relativePath) {
85
86
  return path.join(home, relativePath);
86
87
  }
87
88
 
89
+ function antigravityMcpConfigFilePath() {
90
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
91
+ if (process.platform === "win32") {
92
+ const appData = String(process.env.APPDATA || "").trim();
93
+ if (appData) {
94
+ return path.join(appData, "Antigravity", "User", "mcp.json");
95
+ }
96
+ if (home) {
97
+ return path.join(home, "AppData", "Roaming", "Antigravity", "User", "mcp.json");
98
+ }
99
+ }
100
+ if (process.platform === "darwin" && home) {
101
+ return path.join(home, "Library", "Application Support", "Antigravity", "User", "mcp.json");
102
+ }
103
+ const xdgConfig = String(process.env.XDG_CONFIG_HOME || "").trim();
104
+ if (xdgConfig) {
105
+ return path.join(xdgConfig, "Antigravity", "User", "mcp.json");
106
+ }
107
+ if (home) {
108
+ return path.join(home, ".config", "Antigravity", "User", "mcp.json");
109
+ }
110
+ return path.resolve(process.cwd(), ".antigravity", "mcp.json");
111
+ }
112
+
113
+ function loadAntigravityMcpConfig() {
114
+ const filePath = antigravityMcpConfigFilePath();
115
+ try {
116
+ const raw = fs.readFileSync(filePath, "utf8");
117
+ const parsed = tryJsonParse(raw);
118
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
119
+ return { filePath, config: parsed };
120
+ }
121
+ } catch {
122
+ // no-op
123
+ }
124
+ return {
125
+ filePath,
126
+ config: {
127
+ mcpServers: {},
128
+ servers: {},
129
+ },
130
+ };
131
+ }
132
+
133
+ function saveAntigravityMcpConfig(filePath, config) {
134
+ try {
135
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
136
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, "\t")}\n`, "utf8");
137
+ return true;
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+
143
+ function getAntigravityServerEntry(serverName) {
144
+ const { config } = loadAntigravityMcpConfig();
145
+ const servers = safeObject(config?.mcpServers ?? config?.servers);
146
+ const entry = safeObject(servers[serverName]);
147
+ if (!entry.command && !entry.url) return null;
148
+ return entry;
149
+ }
150
+
151
+ function geminiSettingsFilePath(scope = "project") {
152
+ const normalized = String(scope || "project").trim().toLowerCase();
153
+ if (normalized === "user") {
154
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
155
+ if (home) {
156
+ return path.join(home, ".gemini", "settings.json");
157
+ }
158
+ }
159
+ return path.resolve(process.cwd(), ".gemini", "settings.json");
160
+ }
161
+
162
+ function loadGeminiSettings(scope = "project") {
163
+ const filePath = geminiSettingsFilePath(scope);
164
+ try {
165
+ const raw = fs.readFileSync(filePath, "utf8");
166
+ const parsed = tryJsonParse(raw);
167
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
168
+ return { filePath, config: parsed };
169
+ }
170
+ } catch {
171
+ // no-op
172
+ }
173
+ return {
174
+ filePath,
175
+ config: {
176
+ mcpServers: {},
177
+ },
178
+ };
179
+ }
180
+
181
+ function getGeminiServerEntryByScope(serverName, scope = "project") {
182
+ const { config } = loadGeminiSettings(scope);
183
+ const servers = safeObject(config?.mcpServers);
184
+ const entry = safeObject(servers[serverName]);
185
+ if (!entry.command && !entry.url) return null;
186
+ return entry;
187
+ }
188
+
189
+ function getGeminiServerEntry(serverName) {
190
+ return (
191
+ getGeminiServerEntryByScope(serverName, "project")
192
+ || getGeminiServerEntryByScope(serverName, "user")
193
+ || null
194
+ );
195
+ }
196
+
197
+ function cursorMcpConfigFilePath() {
198
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
199
+ if (home) {
200
+ return path.join(home, ".cursor", "mcp.json");
201
+ }
202
+ return path.resolve(process.cwd(), ".cursor", "mcp.json");
203
+ }
204
+
205
+ function loadCursorMcpConfig() {
206
+ const filePath = cursorMcpConfigFilePath();
207
+ try {
208
+ const raw = fs.readFileSync(filePath, "utf8");
209
+ const parsed = tryJsonParse(raw);
210
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
211
+ return { filePath, config: parsed };
212
+ }
213
+ } catch {
214
+ // no-op
215
+ }
216
+ return {
217
+ filePath,
218
+ config: {
219
+ mcpServers: {},
220
+ },
221
+ };
222
+ }
223
+
224
+ function saveCursorMcpConfig(filePath, config) {
225
+ try {
226
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
227
+ fs.writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
228
+ return true;
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ function getCursorServerEntry(serverName) {
235
+ const { config } = loadCursorMcpConfig();
236
+ const servers = safeObject(config?.mcpServers);
237
+ const entry = safeObject(servers[serverName]);
238
+ if (!entry.command && !entry.url) return null;
239
+ return entry;
240
+ }
241
+
88
242
  function updateStateFilePath() {
89
243
  return resolveHomeFilePath(SELF_UPDATE_STATE_RELATIVE_PATH);
90
244
  }
@@ -1392,7 +1546,7 @@ async function runAuthLoginManual(flags) {
1392
1546
  if (exp) {
1393
1547
  process.stdout.write(`Token expires: ${exp}\n`);
1394
1548
  }
1395
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1549
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1396
1550
  }
1397
1551
 
1398
1552
  async function runAuthLoginCallback(flags, baseURL, oidc) {
@@ -1474,7 +1628,7 @@ async function runAuthLoginCallback(flags, baseURL, oidc) {
1474
1628
  process.stdout.write("Refresh token saved.\n");
1475
1629
  }
1476
1630
  process.stdout.write("Auth login complete (callback flow).\n");
1477
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1631
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1478
1632
  }
1479
1633
 
1480
1634
  async function runAuthLoginDevice(flags, baseURL, oidc) {
@@ -1541,7 +1695,7 @@ async function runAuthLoginDevice(flags, baseURL, oidc) {
1541
1695
  process.stdout.write("Refresh token saved.\n");
1542
1696
  }
1543
1697
  process.stdout.write("Auth login complete (device flow).\n");
1544
- process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
1698
+ process.stdout.write("Restart Codex/Claude/Gemini/Antigravity/Cursor session to apply new token to MCP proxy.\n");
1545
1699
  }
1546
1700
 
1547
1701
  async function runAuthLogin(flags) {
@@ -1825,7 +1979,7 @@ async function runDoctor(flags) {
1825
1979
  );
1826
1980
  }
1827
1981
 
1828
- for (const cliBin of ["codex", "claude"]) {
1982
+ for (const cliBin of MCP_CLIENTS) {
1829
1983
  if (!commandExists(cliBin)) {
1830
1984
  addDoctorCheck(rows, "warn", `${cliBin} CLI`, "not installed; registration check skipped");
1831
1985
  continue;
@@ -3790,12 +3944,7 @@ async function runProxy(flags) {
3790
3944
  let sessionWorkspaceDir = "";
3791
3945
  let sessionWorkspaceTrusted = false;
3792
3946
 
3793
- const rl = readline.createInterface({
3794
- input: process.stdin,
3795
- crlfDelay: Infinity,
3796
- });
3797
-
3798
- rl.on("line", async (lineRaw) => {
3947
+ const handleIncomingMessage = async (lineRaw) => {
3799
3948
  const line = String(lineRaw || "").trim();
3800
3949
  if (!line) return;
3801
3950
 
@@ -4126,7 +4275,137 @@ async function runProxy(flags) {
4126
4275
  `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4127
4276
  );
4128
4277
  }
4278
+ };
4279
+
4280
+ let pendingInputBuffer = Buffer.alloc(0);
4281
+ let messageQueue = Promise.resolve();
4282
+
4283
+ function queueMessage(messageText) {
4284
+ const nextText = String(messageText || "");
4285
+ if (!nextText.trim()) return;
4286
+ messageQueue = messageQueue
4287
+ .then(() => handleIncomingMessage(nextText))
4288
+ .catch((err) => {
4289
+ process.stderr.write(`${String(err?.stack || err)}\n`);
4290
+ });
4291
+ }
4292
+
4293
+ function indexOfBuffer(haystack, needle) {
4294
+ return haystack.indexOf(needle);
4295
+ }
4296
+
4297
+ function startsWithContentLengthHeader(buffer) {
4298
+ if (!Buffer.isBuffer(buffer) || buffer.length === 0) return false;
4299
+ let start = 0;
4300
+ while (start < buffer.length && (buffer[start] === 0x0d || buffer[start] === 0x0a)) {
4301
+ start += 1;
4302
+ }
4303
+ const previewLength = Math.min(96, buffer.length - start);
4304
+ if (previewLength <= 0) return false;
4305
+ const preview = buffer.subarray(start, start + previewLength).toString("utf8");
4306
+ return /^\s*content-length\s*:/i.test(preview);
4307
+ }
4308
+
4309
+ function extractFramedMessage(buffer) {
4310
+ const crlfDelimiter = Buffer.from("\r\n\r\n");
4311
+ const lfDelimiter = Buffer.from("\n\n");
4312
+ let headerEnd = indexOfBuffer(buffer, crlfDelimiter);
4313
+ let delimiterLength = crlfDelimiter.length;
4314
+ if (headerEnd < 0) {
4315
+ headerEnd = indexOfBuffer(buffer, lfDelimiter);
4316
+ delimiterLength = lfDelimiter.length;
4317
+ }
4318
+ if (headerEnd < 0) return null;
4319
+
4320
+ const headerText = buffer.subarray(0, headerEnd).toString("utf8");
4321
+ const match = /content-length\s*:\s*(\d+)/i.exec(headerText);
4322
+ if (!match) {
4323
+ // Malformed framed payload; drop header block and continue.
4324
+ return {
4325
+ message: "",
4326
+ remaining: buffer.subarray(headerEnd + delimiterLength),
4327
+ };
4328
+ }
4329
+
4330
+ const bodyLength = Number.parseInt(String(match[1] || "0"), 10);
4331
+ if (!Number.isFinite(bodyLength) || bodyLength < 0) {
4332
+ return {
4333
+ message: "",
4334
+ remaining: buffer.subarray(headerEnd + delimiterLength),
4335
+ };
4336
+ }
4337
+
4338
+ const bodyStart = headerEnd + delimiterLength;
4339
+ const bodyEnd = bodyStart + bodyLength;
4340
+ if (buffer.length < bodyEnd) return null;
4341
+ const message = buffer.subarray(bodyStart, bodyEnd).toString("utf8");
4342
+ return {
4343
+ message,
4344
+ remaining: buffer.subarray(bodyEnd),
4345
+ };
4346
+ }
4347
+
4348
+ function extractLineMessage(buffer) {
4349
+ const newlineIndex = buffer.indexOf(0x0a);
4350
+ if (newlineIndex < 0) return null;
4351
+ let lineBuffer = buffer.subarray(0, newlineIndex);
4352
+ if (lineBuffer.length > 0 && lineBuffer[lineBuffer.length - 1] === 0x0d) {
4353
+ lineBuffer = lineBuffer.subarray(0, lineBuffer.length - 1);
4354
+ }
4355
+ return {
4356
+ message: lineBuffer.toString("utf8"),
4357
+ remaining: buffer.subarray(newlineIndex + 1),
4358
+ };
4359
+ }
4360
+
4361
+ function pumpInputBuffer() {
4362
+ // Supports both MCP framed stdio (Content-Length) and JSONL input.
4363
+ while (pendingInputBuffer.length > 0) {
4364
+ let trimmedLeading = pendingInputBuffer;
4365
+ while (
4366
+ trimmedLeading.length > 0
4367
+ && (trimmedLeading[0] === 0x0d || trimmedLeading[0] === 0x0a)
4368
+ ) {
4369
+ trimmedLeading = trimmedLeading.subarray(1);
4370
+ }
4371
+ pendingInputBuffer = trimmedLeading;
4372
+ if (pendingInputBuffer.length === 0) break;
4373
+
4374
+ if (startsWithContentLengthHeader(pendingInputBuffer)) {
4375
+ const framed = extractFramedMessage(pendingInputBuffer);
4376
+ if (!framed) break;
4377
+ pendingInputBuffer = framed.remaining;
4378
+ if (String(framed.message || "").trim()) {
4379
+ queueMessage(framed.message);
4380
+ }
4381
+ continue;
4382
+ }
4383
+
4384
+ const lineMessage = extractLineMessage(pendingInputBuffer);
4385
+ if (!lineMessage) break;
4386
+ pendingInputBuffer = lineMessage.remaining;
4387
+ if (String(lineMessage.message || "").trim()) {
4388
+ queueMessage(lineMessage.message);
4389
+ }
4390
+ }
4391
+ }
4392
+
4393
+ process.stdin.on("data", (chunk) => {
4394
+ const nextChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
4395
+ pendingInputBuffer = pendingInputBuffer.length
4396
+ ? Buffer.concat([pendingInputBuffer, nextChunk])
4397
+ : nextChunk;
4398
+ pumpInputBuffer();
4399
+ });
4400
+
4401
+ process.stdin.on("end", () => {
4402
+ const trailing = String(pendingInputBuffer.toString("utf8") || "").trim();
4403
+ if (trailing) {
4404
+ queueMessage(trailing);
4405
+ }
4129
4406
  });
4407
+
4408
+ process.stdin.resume();
4130
4409
  }
4131
4410
 
4132
4411
  function runCLICommand(cliBin, args, options = {}) {
@@ -4140,7 +4419,7 @@ function runCLICommand(cliBin, args, options = {}) {
4140
4419
  return direct;
4141
4420
  }
4142
4421
  // On Windows, npm global CLIs are often .cmd wrappers.
4143
- // Fallback through cmd.exe so tools like "claude" are discoverable.
4422
+ // Fallback through cmd.exe so tools like "claude"/"gemini"/"antigravity"/"cursor" are discoverable.
4144
4423
  return spawnSync("cmd.exe", ["/d", "/s", "/c", cliBin, ...args], execOptions);
4145
4424
  }
4146
4425
 
@@ -4149,21 +4428,123 @@ function commandExists(bin) {
4149
4428
  return check.status === 0;
4150
4429
  }
4151
4430
 
4431
+ function escapeRegExp(value) {
4432
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4433
+ }
4434
+
4435
+ function hasServerNameInListJSON(value, serverName) {
4436
+ if (Array.isArray(value)) {
4437
+ return value.some((entry) => hasServerNameInListJSON(entry, serverName));
4438
+ }
4439
+ if (!value || typeof value !== "object") return false;
4440
+ if (String(value.name || "").trim() === serverName) return true;
4441
+ return Object.values(value).some((entry) => hasServerNameInListJSON(entry, serverName));
4442
+ }
4443
+
4152
4444
  function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4153
4445
  const selfPath = fileURLToPath(import.meta.url);
4154
- const baseAddArgs =
4155
- cliBin === "claude"
4156
- ? ["mcp", "add", "--scope", "user", serverName]
4157
- : ["mcp", "add", serverName];
4158
4446
  const workspaceEnv = String(options.workspaceDir || "").trim();
4159
- const codexEnvArgs =
4447
+ if (cliBin === "cursor") {
4448
+ const { filePath, config } = loadCursorMcpConfig();
4449
+ const nextConfig = safeObject(config);
4450
+ const nextServers = safeObject(nextConfig.mcpServers);
4451
+ const existing = safeObject(nextServers[serverName]);
4452
+ const args = [selfPath, "proxy", ...proxyArgs];
4453
+ const entry = {
4454
+ ...existing,
4455
+ command: process.execPath,
4456
+ args,
4457
+ };
4458
+ const existingEnv = safeObject(existing.env);
4459
+ if (workspaceEnv) {
4460
+ entry.env = {
4461
+ ...existingEnv,
4462
+ METHEUS_WORKSPACE_DIR: workspaceEnv,
4463
+ };
4464
+ } else if (existingEnv.METHEUS_WORKSPACE_DIR) {
4465
+ const nextEnv = { ...existingEnv };
4466
+ delete nextEnv.METHEUS_WORKSPACE_DIR;
4467
+ if (Object.keys(nextEnv).length > 0) {
4468
+ entry.env = nextEnv;
4469
+ } else {
4470
+ delete entry.env;
4471
+ }
4472
+ }
4473
+ nextServers[serverName] = entry;
4474
+ nextConfig.mcpServers = nextServers;
4475
+ return saveCursorMcpConfig(filePath, nextConfig);
4476
+ }
4477
+ if (cliBin === "antigravity") {
4478
+ const { filePath, config } = loadAntigravityMcpConfig();
4479
+ const nextConfig = safeObject(config);
4480
+ const nextServers = safeObject(nextConfig.mcpServers ?? nextConfig.servers);
4481
+ const existing = safeObject(nextServers[serverName]);
4482
+ const args = [selfPath, "proxy", ...proxyArgs];
4483
+ const entry = {
4484
+ ...existing,
4485
+ command: process.execPath,
4486
+ args,
4487
+ };
4488
+ const existingEnv = safeObject(existing.env);
4489
+ if (workspaceEnv) {
4490
+ entry.env = {
4491
+ ...existingEnv,
4492
+ METHEUS_WORKSPACE_DIR: workspaceEnv,
4493
+ };
4494
+ } else if (existingEnv.METHEUS_WORKSPACE_DIR) {
4495
+ const nextEnv = { ...existingEnv };
4496
+ delete nextEnv.METHEUS_WORKSPACE_DIR;
4497
+ if (Object.keys(nextEnv).length > 0) {
4498
+ entry.env = nextEnv;
4499
+ } else {
4500
+ delete entry.env;
4501
+ }
4502
+ }
4503
+ nextServers[serverName] = entry;
4504
+ nextConfig.mcpServers = nextServers;
4505
+ nextConfig.servers = { ...nextServers };
4506
+ return saveAntigravityMcpConfig(filePath, nextConfig);
4507
+ }
4508
+ if (cliBin === "gemini") {
4509
+ // Register in both user+project scopes for broader auto-discovery across folders.
4510
+ const geminiProxyArgs = workspaceEnv
4511
+ ? withWorkspaceDirArg(proxyArgs, workspaceEnv)
4512
+ : [...proxyArgs];
4513
+ const geminiEnvArgs = workspaceEnv
4514
+ ? ["-e", `METHEUS_WORKSPACE_DIR=${workspaceEnv}`]
4515
+ : [];
4516
+ const geminiBaseArgs = [serverName, process.execPath, selfPath, "proxy", ...geminiProxyArgs];
4517
+ let ok = false;
4518
+ const scopedAttempts = [
4519
+ ["mcp", "add", "-s", "user", ...geminiEnvArgs, ...geminiBaseArgs],
4520
+ ["mcp", "add", "-s", "project", ...geminiEnvArgs, ...geminiBaseArgs],
4521
+ ];
4522
+ for (const args of scopedAttempts) {
4523
+ const run = runCLICommand(cliBin, args, { stdio: "inherit" });
4524
+ if (run.status === 0) ok = true;
4525
+ }
4526
+ if (ok) return true;
4527
+ // Backward compatibility for older Gemini CLI variants without scope/env flags.
4528
+ const legacyRun = runCLICommand(
4529
+ cliBin,
4530
+ ["mcp", "add", serverName, process.execPath, selfPath, "proxy", ...geminiProxyArgs],
4531
+ { stdio: "inherit" },
4532
+ );
4533
+ return legacyRun.status === 0;
4534
+ }
4535
+
4536
+ const baseAddArgs = (() => {
4537
+ if (cliBin === "claude") return ["mcp", "add", "--scope", "user", serverName];
4538
+ return ["mcp", "add", serverName];
4539
+ })();
4540
+ const envArgs =
4160
4541
  cliBin === "codex" && workspaceEnv
4161
4542
  ? ["--env", `METHEUS_WORKSPACE_DIR=${workspaceEnv}`]
4162
4543
  : [];
4163
- if (codexEnvArgs.length > 0) {
4544
+ if (envArgs.length > 0) {
4164
4545
  const envAttempts = [];
4165
- envAttempts.push([...baseAddArgs, ...codexEnvArgs, "--", process.execPath, selfPath, "proxy", ...proxyArgs]);
4166
- envAttempts.push([...baseAddArgs, ...codexEnvArgs, process.execPath, selfPath, "proxy", ...proxyArgs]);
4546
+ envAttempts.push([...baseAddArgs, ...envArgs, "--", process.execPath, selfPath, "proxy", ...proxyArgs]);
4547
+ envAttempts.push([...baseAddArgs, ...envArgs, process.execPath, selfPath, "proxy", ...proxyArgs]);
4167
4548
  for (const args of envAttempts) {
4168
4549
  const run = runCLICommand(cliBin, args, { stdio: "inherit" });
4169
4550
  if (run.status === 0) return true;
@@ -4182,6 +4563,29 @@ function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4182
4563
  }
4183
4564
 
4184
4565
  function runRemove(cliBin, serverName) {
4566
+ if (cliBin === "cursor") {
4567
+ const { filePath, config } = loadCursorMcpConfig();
4568
+ const nextConfig = safeObject(config);
4569
+ const nextServers = safeObject(nextConfig.mcpServers);
4570
+ if (Object.prototype.hasOwnProperty.call(nextServers, serverName)) {
4571
+ delete nextServers[serverName];
4572
+ nextConfig.mcpServers = nextServers;
4573
+ saveCursorMcpConfig(filePath, nextConfig);
4574
+ }
4575
+ return;
4576
+ }
4577
+ if (cliBin === "antigravity") {
4578
+ const { filePath, config } = loadAntigravityMcpConfig();
4579
+ const nextConfig = safeObject(config);
4580
+ const nextServers = safeObject(nextConfig.mcpServers ?? nextConfig.servers);
4581
+ if (Object.prototype.hasOwnProperty.call(nextServers, serverName)) {
4582
+ delete nextServers[serverName];
4583
+ nextConfig.mcpServers = nextServers;
4584
+ nextConfig.servers = { ...nextServers };
4585
+ saveAntigravityMcpConfig(filePath, nextConfig);
4586
+ }
4587
+ return;
4588
+ }
4185
4589
  if (cliBin === "claude") {
4186
4590
  runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], {
4187
4591
  stdio: "ignore",
@@ -4191,10 +4595,32 @@ function runRemove(cliBin, serverName) {
4191
4595
  });
4192
4596
  return;
4193
4597
  }
4598
+ if (cliBin === "gemini") {
4599
+ runCLICommand(cliBin, ["mcp", "remove", "-s", "project", serverName], { stdio: "ignore" });
4600
+ runCLICommand(cliBin, ["mcp", "remove", "-s", "user", serverName], { stdio: "ignore" });
4601
+ runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
4602
+ return;
4603
+ }
4194
4604
  runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
4195
4605
  }
4196
4606
 
4197
4607
  function isRegistered(cliBin, serverName) {
4608
+ if (cliBin === "cursor") {
4609
+ return Boolean(getCursorServerEntry(serverName));
4610
+ }
4611
+ if (cliBin === "antigravity") {
4612
+ return Boolean(getAntigravityServerEntry(serverName));
4613
+ }
4614
+ if (cliBin === "gemini") {
4615
+ if (getGeminiServerEntry(serverName)) {
4616
+ return true;
4617
+ }
4618
+ const listRun = runCLICommand(cliBin, ["mcp", "list"], { stdio: "pipe" });
4619
+ if (listRun.status !== 0) return false;
4620
+ const text = `${String(listRun.stdout || "")}\n${String(listRun.stderr || "")}`;
4621
+ const pattern = new RegExp(`(^|[\\s"'])${escapeRegExp(serverName)}([\\s"':]|$)`, "m");
4622
+ return pattern.test(text);
4623
+ }
4198
4624
  const args =
4199
4625
  cliBin === "claude"
4200
4626
  ? ["mcp", "get", serverName]
@@ -4204,6 +4630,26 @@ function isRegistered(cliBin, serverName) {
4204
4630
  }
4205
4631
 
4206
4632
  function getRegisteredTransport(cliBin, serverName) {
4633
+ if (cliBin === "cursor") {
4634
+ const entry = getCursorServerEntry(serverName);
4635
+ if (!entry) return null;
4636
+ return {
4637
+ type: "stdio",
4638
+ command: String(entry.command || "").trim(),
4639
+ args: Array.isArray(entry.args) ? entry.args : [],
4640
+ env: safeObject(entry.env),
4641
+ };
4642
+ }
4643
+ if (cliBin === "antigravity") {
4644
+ const entry = getAntigravityServerEntry(serverName);
4645
+ if (!entry) return null;
4646
+ return {
4647
+ type: "stdio",
4648
+ command: String(entry.command || "").trim(),
4649
+ args: Array.isArray(entry.args) ? entry.args : [],
4650
+ env: safeObject(entry.env),
4651
+ };
4652
+ }
4207
4653
  if (cliBin !== "codex") return null;
4208
4654
  const run = runCLICommand(cliBin, ["mcp", "get", serverName, "--json"], { stdio: "pipe" });
4209
4655
  if (run.status !== 0) return null;
@@ -4289,7 +4735,7 @@ function resolveSetupContext(flags) {
4289
4735
  function runSetupInternal(flags, options = {}) {
4290
4736
  const ensureOnly = Boolean(options.ensureOnly);
4291
4737
  const context = resolveSetupContext(flags);
4292
- const clients = ["codex", "claude"];
4738
+ const clients = [...MCP_CLIENTS];
4293
4739
  const results = [];
4294
4740
 
4295
4741
  for (const cliBin of clients) {
@@ -4305,7 +4751,7 @@ function runSetupInternal(flags, options = {}) {
4305
4751
  ? context.workspaceDir
4306
4752
  : context.workspaceFallbackDir;
4307
4753
 
4308
- if (cliBin === "codex" && !context.hasWorkspaceDirFlag && !context.hasWorkspaceFallbackDirFlag) {
4754
+ if ((cliBin === "codex" || cliBin === "cursor" || cliBin === "antigravity") && !context.hasWorkspaceDirFlag && !context.hasWorkspaceFallbackDirFlag) {
4309
4755
  const transport = getRegisteredTransport(cliBin, context.serverName);
4310
4756
  if (transport) {
4311
4757
  const existingWorkspaceDir = extractWorkspaceDirArg(transport.args);
@@ -4348,7 +4794,7 @@ function runSetupInternal(flags, options = {}) {
4348
4794
  process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
4349
4795
  }
4350
4796
  if (results.length === 0) {
4351
- process.stdout.write("No codex/claude CLI found. Registration skipped.\n");
4797
+ process.stdout.write("No codex/claude/gemini/antigravity/cursor CLI found. Registration skipped.\n");
4352
4798
  return;
4353
4799
  }
4354
4800
  for (const row of results) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.25",
3
+ "version": "0.2.28",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [