metheus-governance-mcp-cli 0.2.26 → 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 +4 -1
  2. package/cli.mjs +208 -23
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -48,6 +48,7 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
48
48
 
49
49
  Gemini CLI note:
50
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.
51
52
 
52
53
  When a client does not send workspace metadata (for example some Codex sessions),
53
54
  set a stable fallback root once:
@@ -58,7 +59,7 @@ metheus-governance-mcp-cli setup --project-id <project_uuid> --ctxpack-key "<ctx
58
59
 
59
60
  This sets fallback workspace context for clients that do not pass workspace metadata:
60
61
  - Codex/Antigravity/Cursor: `METHEUS_WORKSPACE_DIR` env
61
- - Gemini: pinned `--workspace-dir <fallback>` in registered MCP command
62
+ - Gemini: pinned `--workspace-dir <fallback>` and `METHEUS_WORKSPACE_DIR` in MCP registration
62
63
 
63
64
  Guardrail note:
64
65
  - By default, CLI blocks reading/writing ctxpack sync metadata when workspace root resolves to the home directory.
@@ -93,6 +94,8 @@ Checks:
93
94
 
94
95
  Antigravity note:
95
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.
96
99
 
97
100
  Cursor note:
98
101
  - this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
package/cli.mjs CHANGED
@@ -124,8 +124,8 @@ function loadAntigravityMcpConfig() {
124
124
  return {
125
125
  filePath,
126
126
  config: {
127
+ mcpServers: {},
127
128
  servers: {},
128
- inputs: [],
129
129
  },
130
130
  };
131
131
  }
@@ -142,12 +142,58 @@ function saveAntigravityMcpConfig(filePath, config) {
142
142
 
143
143
  function getAntigravityServerEntry(serverName) {
144
144
  const { config } = loadAntigravityMcpConfig();
145
- const servers = safeObject(config?.servers);
145
+ const servers = safeObject(config?.mcpServers ?? config?.servers);
146
146
  const entry = safeObject(servers[serverName]);
147
147
  if (!entry.command && !entry.url) return null;
148
148
  return entry;
149
149
  }
150
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
+
151
197
  function cursorMcpConfigFilePath() {
152
198
  const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
153
199
  if (home) {
@@ -3898,12 +3944,7 @@ async function runProxy(flags) {
3898
3944
  let sessionWorkspaceDir = "";
3899
3945
  let sessionWorkspaceTrusted = false;
3900
3946
 
3901
- const rl = readline.createInterface({
3902
- input: process.stdin,
3903
- crlfDelay: Infinity,
3904
- });
3905
-
3906
- rl.on("line", async (lineRaw) => {
3947
+ const handleIncomingMessage = async (lineRaw) => {
3907
3948
  const line = String(lineRaw || "").trim();
3908
3949
  if (!line) return;
3909
3950
 
@@ -4234,7 +4275,137 @@ async function runProxy(flags) {
4234
4275
  `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4235
4276
  );
4236
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
+ }
4237
4406
  });
4407
+
4408
+ process.stdin.resume();
4238
4409
  }
4239
4410
 
4240
4411
  function runCLICommand(cliBin, args, options = {}) {
@@ -4306,7 +4477,7 @@ function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4306
4477
  if (cliBin === "antigravity") {
4307
4478
  const { filePath, config } = loadAntigravityMcpConfig();
4308
4479
  const nextConfig = safeObject(config);
4309
- const nextServers = safeObject(nextConfig.servers);
4480
+ const nextServers = safeObject(nextConfig.mcpServers ?? nextConfig.servers);
4310
4481
  const existing = safeObject(nextServers[serverName]);
4311
4482
  const args = [selfPath, "proxy", ...proxyArgs];
4312
4483
  const entry = {
@@ -4330,25 +4501,36 @@ function tryRegister(cliBin, serverName, proxyArgs, options = {}) {
4330
4501
  }
4331
4502
  }
4332
4503
  nextServers[serverName] = entry;
4333
- nextConfig.servers = nextServers;
4334
- if (!Array.isArray(nextConfig.inputs)) {
4335
- nextConfig.inputs = [];
4336
- }
4504
+ nextConfig.mcpServers = nextServers;
4505
+ nextConfig.servers = { ...nextServers };
4337
4506
  return saveAntigravityMcpConfig(filePath, nextConfig);
4338
4507
  }
4339
4508
  if (cliBin === "gemini") {
4340
- // Gemini CLI (0.31.x) currently supports:
4341
- // gemini mcp add <name> <commandOrUrl> [args...]
4342
- // It does not expose --env for MCP registration, so pin workspace-dir directly.
4509
+ // Register in both user+project scopes for broader auto-discovery across folders.
4343
4510
  const geminiProxyArgs = workspaceEnv
4344
4511
  ? withWorkspaceDirArg(proxyArgs, workspaceEnv)
4345
4512
  : [...proxyArgs];
4346
- const run = runCLICommand(
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(
4347
4529
  cliBin,
4348
4530
  ["mcp", "add", serverName, process.execPath, selfPath, "proxy", ...geminiProxyArgs],
4349
4531
  { stdio: "inherit" },
4350
4532
  );
4351
- return run.status === 0;
4533
+ return legacyRun.status === 0;
4352
4534
  }
4353
4535
 
4354
4536
  const baseAddArgs = (() => {
@@ -4395,13 +4577,11 @@ function runRemove(cliBin, serverName) {
4395
4577
  if (cliBin === "antigravity") {
4396
4578
  const { filePath, config } = loadAntigravityMcpConfig();
4397
4579
  const nextConfig = safeObject(config);
4398
- const nextServers = safeObject(nextConfig.servers);
4580
+ const nextServers = safeObject(nextConfig.mcpServers ?? nextConfig.servers);
4399
4581
  if (Object.prototype.hasOwnProperty.call(nextServers, serverName)) {
4400
4582
  delete nextServers[serverName];
4401
- nextConfig.servers = nextServers;
4402
- if (!Array.isArray(nextConfig.inputs)) {
4403
- nextConfig.inputs = [];
4404
- }
4583
+ nextConfig.mcpServers = nextServers;
4584
+ nextConfig.servers = { ...nextServers };
4405
4585
  saveAntigravityMcpConfig(filePath, nextConfig);
4406
4586
  }
4407
4587
  return;
@@ -4416,6 +4596,8 @@ function runRemove(cliBin, serverName) {
4416
4596
  return;
4417
4597
  }
4418
4598
  if (cliBin === "gemini") {
4599
+ runCLICommand(cliBin, ["mcp", "remove", "-s", "project", serverName], { stdio: "ignore" });
4600
+ runCLICommand(cliBin, ["mcp", "remove", "-s", "user", serverName], { stdio: "ignore" });
4419
4601
  runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
4420
4602
  return;
4421
4603
  }
@@ -4430,6 +4612,9 @@ function isRegistered(cliBin, serverName) {
4430
4612
  return Boolean(getAntigravityServerEntry(serverName));
4431
4613
  }
4432
4614
  if (cliBin === "gemini") {
4615
+ if (getGeminiServerEntry(serverName)) {
4616
+ return true;
4617
+ }
4433
4618
  const listRun = runCLICommand(cliBin, ["mcp", "list"], { stdio: "pipe" });
4434
4619
  if (listRun.status !== 0) return false;
4435
4620
  const text = `${String(listRun.stdout || "")}\n${String(listRun.stderr || "")}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [