metheus-governance-mcp-cli 0.2.34 → 0.2.36

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 (2) hide show
  1. package/cli.mjs +147 -79
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -4045,13 +4045,95 @@ async function runProxy(flags) {
4045
4045
  let sessionWorkspaceDir = "";
4046
4046
  let sessionWorkspaceTrusted = false;
4047
4047
 
4048
+ // Proxy-initiated requests (e.g., roots/list) pending client responses.
4049
+ const pendingProxyRequests = new Map(); // id → callback(responseObj)
4050
+ let proxyRequestIdCounter = 0;
4051
+ let stdoutEnvelopeMode = "line"; // "line" | "framed"
4052
+
4053
+ function writeProxyMessage(messageText) {
4054
+ const normalized = String(messageText || "").trim();
4055
+ if (!normalized) return;
4056
+ if (stdoutEnvelopeMode === "framed") {
4057
+ const contentLength = Buffer.byteLength(normalized, "utf8");
4058
+ process.stdout.write(`Content-Length: ${contentLength}\r\n\r\n${normalized}`);
4059
+ return;
4060
+ }
4061
+ process.stdout.write(`${normalized}\n`);
4062
+ }
4063
+
4064
+ function writeProxyJson(payload) {
4065
+ writeProxyMessage(JSON.stringify(payload));
4066
+ }
4067
+
4068
+ function clientSupportsRootsList(initParamsRaw) {
4069
+ const initParams = safeObject(initParamsRaw);
4070
+ const capabilities = safeObject(initParams.capabilities);
4071
+ return Object.prototype.hasOwnProperty.call(capabilities, "roots");
4072
+ }
4073
+
4074
+ /**
4075
+ * Probe the MCP client for workspace roots via the standard roots/list request.
4076
+ * This allows the proxy to discover the actual workspace folder even when the client
4077
+ * (e.g., Antigravity) sets process.cwd() to its install directory instead of the workspace.
4078
+ */
4079
+ function sendRootsListProbe() {
4080
+ const id = `_proxy_roots_${++proxyRequestIdCounter}`;
4081
+ pendingProxyRequests.set(id, (response) => {
4082
+ const roots = Array.isArray(response?.result?.roots) ? response.result.roots : [];
4083
+ let foundWorkspace = "";
4084
+ for (const root of roots) {
4085
+ const candidate = sanitizeWorkspaceCandidate(String(root?.uri || root?.path || root?.name || ""));
4086
+ if (candidate) {
4087
+ foundWorkspace = candidate;
4088
+ break;
4089
+ }
4090
+ }
4091
+ try {
4092
+ const _diagDir = path.join(String(process.env.USERPROFILE || process.env.HOME || "."), ".metheus");
4093
+ fs.appendFileSync(
4094
+ path.join(_diagDir, "mcp-proxy-diag.log"),
4095
+ foundWorkspace
4096
+ ? `[${new Date().toISOString()}] roots/list: workspace=${foundWorkspace} from ${roots.length} root(s)\n`
4097
+ : `[${new Date().toISOString()}] roots/list: no valid workspace from ${roots.length} root(s), raw=${JSON.stringify(roots).substring(0, 300)}\n`,
4098
+ );
4099
+ } catch { /* best-effort */ }
4100
+ if (foundWorkspace) {
4101
+ sessionWorkspaceDir = foundWorkspace;
4102
+ sessionWorkspaceTrusted = true;
4103
+ }
4104
+ });
4105
+ // Timeout: if client does not respond within 10 seconds, discard the pending callback.
4106
+ setTimeout(() => {
4107
+ if (pendingProxyRequests.has(id)) {
4108
+ pendingProxyRequests.delete(id);
4109
+ try {
4110
+ const _diagDir = path.join(String(process.env.USERPROFILE || process.env.HOME || "."), ".metheus");
4111
+ fs.appendFileSync(
4112
+ path.join(_diagDir, "mcp-proxy-diag.log"),
4113
+ `[${new Date().toISOString()}] roots/list: timeout (client did not respond)\n`,
4114
+ );
4115
+ } catch { /* best-effort */ }
4116
+ }
4117
+ }, 10000);
4118
+ writeProxyJson({ jsonrpc: "2.0", id, method: "roots/list" });
4119
+ }
4120
+
4048
4121
  const handleIncomingMessage = async (lineRaw) => {
4049
4122
  const line = String(lineRaw || "").trim();
4050
4123
  if (!line) return;
4051
4124
 
4052
4125
  const requestObj = tryJsonParse(line);
4053
4126
  if (requestObj == null) {
4054
- process.stdout.write(`${JSON.stringify(jsonRpcError(null, -32700, "parse error"))}\n`);
4127
+ writeProxyJson(jsonRpcError(null, -32700, "parse error"));
4128
+ return;
4129
+ }
4130
+
4131
+ // Intercept responses to proxy-initiated requests (e.g., roots/list).
4132
+ // These have an id but no method field — they are client responses, not new requests.
4133
+ if (!requestObj.method && requestObj.id != null && pendingProxyRequests.has(String(requestObj.id))) {
4134
+ const callback = pendingProxyRequests.get(String(requestObj.id));
4135
+ pendingProxyRequests.delete(String(requestObj.id));
4136
+ try { callback(requestObj); } catch { /* best-effort */ }
4055
4137
  return;
4056
4138
  }
4057
4139
 
@@ -4081,16 +4163,14 @@ async function runProxy(flags) {
4081
4163
  ? `; expired token ignored: ${resolved.expired.source}${resolved.expired.expires ? ` (${resolved.expired.expires})` : ""}`
4082
4164
  : "";
4083
4165
  const refreshHint = lastRefreshError ? `; refresh failed: ${lastRefreshError}` : "";
4084
- process.stdout.write(
4085
- `${JSON.stringify(
4086
- jsonRpcError(
4087
- requestObj,
4088
- -32001,
4089
- `missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
4090
- args.baseURL,
4091
- )}'; login: ${helpURL}${expiredHint}${refreshHint})`,
4092
- ),
4093
- )}\n`,
4166
+ writeProxyJson(
4167
+ jsonRpcError(
4168
+ requestObj,
4169
+ -32001,
4170
+ `missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
4171
+ args.baseURL,
4172
+ )}'; login: ${helpURL}${expiredHint}${refreshHint})`,
4173
+ ),
4094
4174
  );
4095
4175
  return;
4096
4176
  }
@@ -4179,15 +4259,11 @@ async function runProxy(flags) {
4179
4259
  }),
4180
4260
  ).trim();
4181
4261
  if (!projectID) {
4182
- process.stdout.write(
4183
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
4184
- );
4262
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"));
4185
4263
  return;
4186
4264
  }
4187
4265
  if (!isUUID(projectID)) {
4188
- process.stdout.write(
4189
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
4190
- );
4266
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"));
4191
4267
  return;
4192
4268
  }
4193
4269
  try {
@@ -4214,23 +4290,19 @@ async function runProxy(flags) {
4214
4290
  workspaceSignalTrusted,
4215
4291
  });
4216
4292
  const text = buildProjectSummaryText(summary);
4217
- process.stdout.write(
4218
- `${JSON.stringify(
4219
- jsonRpcResult(requestObj, {
4220
- content: [
4221
- {
4222
- type: "text",
4223
- text,
4224
- },
4225
- ],
4226
- structuredContent: summary,
4227
- }),
4228
- )}\n`,
4293
+ writeProxyJson(
4294
+ jsonRpcResult(requestObj, {
4295
+ content: [
4296
+ {
4297
+ type: "text",
4298
+ text,
4299
+ },
4300
+ ],
4301
+ structuredContent: summary,
4302
+ }),
4229
4303
  );
4230
4304
  } catch (err) {
4231
- process.stdout.write(
4232
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4233
- );
4305
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4234
4306
  }
4235
4307
  return;
4236
4308
  }
@@ -4243,15 +4315,11 @@ async function runProxy(flags) {
4243
4315
  }),
4244
4316
  ).trim();
4245
4317
  if (!projectID) {
4246
- process.stdout.write(
4247
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
4248
- );
4318
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"));
4249
4319
  return;
4250
4320
  }
4251
4321
  if (!isUUID(projectID)) {
4252
- process.stdout.write(
4253
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
4254
- );
4322
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"));
4255
4323
  return;
4256
4324
  }
4257
4325
 
@@ -4260,14 +4328,12 @@ async function runProxy(flags) {
4260
4328
  const statusRaw = String(toolArgs.status || "").trim().toLowerCase();
4261
4329
  const statusFilter = normalizeMergeStatusFilter(statusRaw);
4262
4330
  if (statusRaw && !statusFilter) {
4263
- process.stdout.write(
4264
- `${JSON.stringify(
4265
- jsonRpcError(
4266
- requestObj,
4267
- -32001,
4268
- "status must be one of: open, review, approved, rejected, merged, closed, all",
4269
- ),
4270
- )}\n`,
4331
+ writeProxyJson(
4332
+ jsonRpcError(
4333
+ requestObj,
4334
+ -32001,
4335
+ "status must be one of: open, review, approved, rejected, merged, closed, all",
4336
+ ),
4271
4337
  );
4272
4338
  return;
4273
4339
  }
@@ -4282,13 +4348,11 @@ async function runProxy(flags) {
4282
4348
  workspaceDir: requestWorkspaceDir,
4283
4349
  });
4284
4350
  const text = buildCtxpackMergeBriefText(brief);
4285
- process.stdout.write(
4286
- `${JSON.stringify(
4287
- jsonRpcResult(requestObj, {
4288
- content: [{ type: "text", text }],
4289
- structuredContent: brief,
4290
- }),
4291
- )}\n`,
4351
+ writeProxyJson(
4352
+ jsonRpcResult(requestObj, {
4353
+ content: [{ type: "text", text }],
4354
+ structuredContent: brief,
4355
+ }),
4292
4356
  );
4293
4357
  return;
4294
4358
  }
@@ -4300,16 +4364,12 @@ async function runProxy(flags) {
4300
4364
  toolArgs.owner_confirmation || toolArgs.ownerConfirmation || "",
4301
4365
  ).trim();
4302
4366
  if (!mergeRequestID || !isUUID(mergeRequestID)) {
4303
- process.stdout.write(
4304
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"))}\n`,
4305
- );
4367
+ writeProxyJson(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"));
4306
4368
  return;
4307
4369
  }
4308
4370
  if (!action) {
4309
- process.stdout.write(
4310
- `${JSON.stringify(
4311
- jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
4312
- )}\n`,
4371
+ writeProxyJson(
4372
+ jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
4313
4373
  );
4314
4374
  return;
4315
4375
  }
@@ -4324,20 +4384,16 @@ async function runProxy(flags) {
4324
4384
  workspaceDir: requestWorkspaceDir,
4325
4385
  });
4326
4386
  const text = buildCtxpackMergeExecuteText(result);
4327
- process.stdout.write(
4328
- `${JSON.stringify(
4329
- jsonRpcResult(requestObj, {
4330
- content: [{ type: "text", text }],
4331
- structuredContent: result,
4332
- }),
4333
- )}\n`,
4387
+ writeProxyJson(
4388
+ jsonRpcResult(requestObj, {
4389
+ content: [{ type: "text", text }],
4390
+ structuredContent: result,
4391
+ }),
4334
4392
  );
4335
4393
  return;
4336
4394
  }
4337
4395
  } catch (err) {
4338
- process.stdout.write(
4339
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4340
- );
4396
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4341
4397
  return;
4342
4398
  }
4343
4399
  }
@@ -4369,13 +4425,28 @@ async function runProxy(flags) {
4369
4425
  requestWorkspaceDir,
4370
4426
  workspaceSignalTrusted,
4371
4427
  );
4372
- process.stdout.write(`${JSON.stringify(patched)}\n`);
4428
+ writeProxyJson(patched);
4373
4429
  return;
4374
4430
  }
4375
4431
  if (isJsonRpcMethod(requestObj, "tools/list")) {
4376
4432
  patched = appendLocalToolToToolsList(patched);
4377
4433
  } else if (isJsonRpcMethod(requestObj, "initialize")) {
4378
4434
  patched = appendProjectHintToInitialize(patched, args);
4435
+ // Log initialize params for workspace debugging.
4436
+ try {
4437
+ const _diagDir = path.join(String(process.env.USERPROFILE || process.env.HOME || "."), ".metheus");
4438
+ const initParams = safeObject(requestObj?.params);
4439
+ fs.appendFileSync(
4440
+ path.join(_diagDir, "mcp-proxy-diag.log"),
4441
+ `[${new Date().toISOString()}] initialize: clientInfo=${JSON.stringify(initParams.clientInfo || {})} capabilities=${JSON.stringify(initParams.capabilities || {})} rootUri=${initParams.rootUri || "(none)"} workspaceFolders=${JSON.stringify(initParams.workspaceFolders || []).substring(0, 300)}\n`,
4442
+ );
4443
+ } catch { /* best-effort */ }
4444
+ // Probe roots/list only when client advertised roots capability.
4445
+ // Some MCP clients reject unsolicited server requests and then hide tools.
4446
+ if (clientSupportsRootsList(requestObj?.params)) {
4447
+ // Use setImmediate so the initialize response is flushed first.
4448
+ setImmediate(() => sendRootsListProbe());
4449
+ }
4379
4450
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
4380
4451
  patched = await appendWorkitemListHints(patched, args, toolArgs, token, workspaceSignalTrusted);
4381
4452
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
@@ -4390,21 +4461,17 @@ async function runProxy(flags) {
4390
4461
  if (autoSyncSummary) {
4391
4462
  patched = appendAutoCtxpackSyncHint(patched, autoSyncSummary);
4392
4463
  }
4393
- process.stdout.write(`${JSON.stringify(patched)}\n`);
4464
+ writeProxyJson(patched);
4394
4465
  return;
4395
4466
  }
4396
- process.stdout.write(`${responseText}\n`);
4467
+ writeProxyMessage(responseText);
4397
4468
  return;
4398
4469
  }
4399
4470
  if (Object.prototype.hasOwnProperty.call(requestObj, "id")) {
4400
- process.stdout.write(
4401
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "empty gateway response"))}\n`,
4402
- );
4471
+ writeProxyJson(jsonRpcError(requestObj, -32001, "empty gateway response"));
4403
4472
  }
4404
4473
  } catch (err) {
4405
- process.stdout.write(
4406
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4407
- );
4474
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4408
4475
  }
4409
4476
  };
4410
4477
 
@@ -4503,6 +4570,7 @@ async function runProxy(flags) {
4503
4570
  if (pendingInputBuffer.length === 0) break;
4504
4571
 
4505
4572
  if (startsWithContentLengthHeader(pendingInputBuffer)) {
4573
+ stdoutEnvelopeMode = "framed";
4506
4574
  const framed = extractFramedMessage(pendingInputBuffer);
4507
4575
  if (!framed) break;
4508
4576
  pendingInputBuffer = framed.remaining;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.34",
3
+ "version": "0.2.36",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [