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.
- package/README.md +20 -6
- package/cli.mjs +468 -22
- 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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
4544
|
+
if (envArgs.length > 0) {
|
|
4164
4545
|
const envAttempts = [];
|
|
4165
|
-
envAttempts.push([...baseAddArgs, ...
|
|
4166
|
-
envAttempts.push([...baseAddArgs, ...
|
|
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 = [
|
|
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) {
|