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.
- package/README.md +4 -1
- package/cli.mjs +208 -23
- 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
|
|
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
|
|
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.
|
|
4334
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
4402
|
-
|
|
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 || "")}`;
|