metheus-governance-mcp-cli 0.2.35 → 0.2.37

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 +174 -85
  2. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -3491,6 +3491,83 @@ function ensureArray(value) {
3491
3491
  return Array.isArray(value) ? value : [];
3492
3492
  }
3493
3493
 
3494
+ function normalizeSafeToolAliasName(rawName) {
3495
+ const base = String(rawName || "")
3496
+ .trim()
3497
+ .replace(/[^a-zA-Z0-9_]/g, "_")
3498
+ .replace(/_+/g, "_")
3499
+ .replace(/^_+|_+$/g, "");
3500
+ if (!base) return "";
3501
+ if (/^[0-9]/.test(base)) return `tool_${base}`;
3502
+ return base;
3503
+ }
3504
+
3505
+ function buildToolAliasMaps(tools) {
3506
+ const canonicalNames = new Set(
3507
+ ensureArray(tools)
3508
+ .map((tool) => String(tool?.name || "").trim())
3509
+ .filter(Boolean),
3510
+ );
3511
+ const aliasToCanonical = new Map();
3512
+ const canonicalToAlias = new Map();
3513
+ const reserved = new Set(canonicalNames);
3514
+
3515
+ for (const canonicalName of canonicalNames) {
3516
+ const aliasBase = normalizeSafeToolAliasName(canonicalName);
3517
+ if (!aliasBase || aliasBase === canonicalName) continue;
3518
+ let alias = aliasBase;
3519
+ let counter = 2;
3520
+ while (reserved.has(alias) || aliasToCanonical.has(alias)) {
3521
+ alias = `${aliasBase}_${counter}`;
3522
+ counter += 1;
3523
+ }
3524
+ aliasToCanonical.set(alias, canonicalName);
3525
+ canonicalToAlias.set(canonicalName, alias);
3526
+ reserved.add(alias);
3527
+ }
3528
+ return { aliasToCanonical, canonicalToAlias };
3529
+ }
3530
+
3531
+ function applyToolAliasesToToolsListResponse(responseObj, canonicalToAlias) {
3532
+ const result = safeObject(responseObj.result);
3533
+ const tools = ensureArray(result.tools);
3534
+ result.tools = tools.map((tool) => {
3535
+ const safeTool = safeObject(tool);
3536
+ const currentName = String(safeTool.name || "").trim();
3537
+ const aliasName = canonicalToAlias.get(currentName);
3538
+ if (!aliasName) return safeTool;
3539
+ return { ...safeTool, name: aliasName };
3540
+ });
3541
+ responseObj.result = result;
3542
+ return responseObj;
3543
+ }
3544
+
3545
+ function rewriteAliasedToolCallToCanonical(requestObj, aliasToCanonical) {
3546
+ if (!isJsonRpcMethod(requestObj, "tools/call")) return requestObj;
3547
+ const params = safeObject(requestObj.params);
3548
+ const currentName = String(params.name ?? params.tool_name ?? params.toolName ?? "").trim();
3549
+ if (!currentName) return requestObj;
3550
+ const canonicalName = String(aliasToCanonical.get(currentName) || "").trim();
3551
+ if (!canonicalName) return requestObj;
3552
+
3553
+ const nextParams = { ...params, name: canonicalName };
3554
+ if (Object.prototype.hasOwnProperty.call(nextParams, "tool_name")) {
3555
+ nextParams.tool_name = canonicalName;
3556
+ }
3557
+ if (Object.prototype.hasOwnProperty.call(nextParams, "toolName")) {
3558
+ nextParams.toolName = canonicalName;
3559
+ }
3560
+ return { ...requestObj, params: nextParams };
3561
+ }
3562
+
3563
+ function shouldUseSafeToolAliasesForClient(initParamsRaw) {
3564
+ const initParams = safeObject(initParamsRaw);
3565
+ const clientInfo = safeObject(initParams.clientInfo);
3566
+ const name = String(clientInfo.name || "").trim().toLowerCase();
3567
+ if (!name) return false;
3568
+ return name.includes("cursor") || name.includes("antigravity");
3569
+ }
3570
+
3494
3571
  function injectWorkspaceDirIntoToolSchemas(tools) {
3495
3572
  const workspaceDirProp = {
3496
3573
  type: "string",
@@ -4044,10 +4121,35 @@ async function runProxy(flags) {
4044
4121
  let lastRefreshError = "";
4045
4122
  let sessionWorkspaceDir = "";
4046
4123
  let sessionWorkspaceTrusted = false;
4124
+ let sessionUseSafeToolAliases = false;
4125
+ let sessionToolAliasToCanonical = new Map();
4126
+ let sessionToolCanonicalToAlias = new Map();
4047
4127
 
4048
4128
  // Proxy-initiated requests (e.g., roots/list) pending client responses.
4049
4129
  const pendingProxyRequests = new Map(); // id → callback(responseObj)
4050
4130
  let proxyRequestIdCounter = 0;
4131
+ let stdoutEnvelopeMode = "line"; // "line" | "framed"
4132
+
4133
+ function writeProxyMessage(messageText) {
4134
+ const normalized = String(messageText || "").trim();
4135
+ if (!normalized) return;
4136
+ if (stdoutEnvelopeMode === "framed") {
4137
+ const contentLength = Buffer.byteLength(normalized, "utf8");
4138
+ process.stdout.write(`Content-Length: ${contentLength}\r\n\r\n${normalized}`);
4139
+ return;
4140
+ }
4141
+ process.stdout.write(`${normalized}\n`);
4142
+ }
4143
+
4144
+ function writeProxyJson(payload) {
4145
+ writeProxyMessage(JSON.stringify(payload));
4146
+ }
4147
+
4148
+ function clientSupportsRootsList(initParamsRaw) {
4149
+ const initParams = safeObject(initParamsRaw);
4150
+ const capabilities = safeObject(initParams.capabilities);
4151
+ return Object.prototype.hasOwnProperty.call(capabilities, "roots");
4152
+ }
4051
4153
 
4052
4154
  /**
4053
4155
  * Probe the MCP client for workspace roots via the standard roots/list request.
@@ -4093,16 +4195,16 @@ async function runProxy(flags) {
4093
4195
  } catch { /* best-effort */ }
4094
4196
  }
4095
4197
  }, 10000);
4096
- process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id, method: "roots/list" })}\n`);
4198
+ writeProxyJson({ jsonrpc: "2.0", id, method: "roots/list" });
4097
4199
  }
4098
4200
 
4099
4201
  const handleIncomingMessage = async (lineRaw) => {
4100
4202
  const line = String(lineRaw || "").trim();
4101
4203
  if (!line) return;
4102
4204
 
4103
- const requestObj = tryJsonParse(line);
4205
+ let requestObj = tryJsonParse(line);
4104
4206
  if (requestObj == null) {
4105
- process.stdout.write(`${JSON.stringify(jsonRpcError(null, -32700, "parse error"))}\n`);
4207
+ writeProxyJson(jsonRpcError(null, -32700, "parse error"));
4106
4208
  return;
4107
4209
  }
4108
4210
 
@@ -4141,20 +4243,25 @@ async function runProxy(flags) {
4141
4243
  ? `; expired token ignored: ${resolved.expired.source}${resolved.expired.expires ? ` (${resolved.expired.expires})` : ""}`
4142
4244
  : "";
4143
4245
  const refreshHint = lastRefreshError ? `; refresh failed: ${lastRefreshError}` : "";
4144
- process.stdout.write(
4145
- `${JSON.stringify(
4146
- jsonRpcError(
4147
- requestObj,
4148
- -32001,
4149
- `missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
4150
- args.baseURL,
4151
- )}'; login: ${helpURL}${expiredHint}${refreshHint})`,
4152
- ),
4153
- )}\n`,
4246
+ writeProxyJson(
4247
+ jsonRpcError(
4248
+ requestObj,
4249
+ -32001,
4250
+ `missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
4251
+ args.baseURL,
4252
+ )}'; login: ${helpURL}${expiredHint}${refreshHint})`,
4253
+ ),
4154
4254
  );
4155
4255
  return;
4156
4256
  }
4157
4257
 
4258
+ if (isJsonRpcMethod(requestObj, "initialize") && shouldUseSafeToolAliasesForClient(requestObj?.params)) {
4259
+ sessionUseSafeToolAliases = true;
4260
+ }
4261
+ if (sessionUseSafeToolAliases) {
4262
+ requestObj = rewriteAliasedToolCallToCanonical(requestObj, sessionToolAliasToCanonical);
4263
+ }
4264
+
4158
4265
  const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
4159
4266
  let strongRequestWorkspaceCandidate = "";
4160
4267
  let weakRequestWorkspaceCandidate = "";
@@ -4239,15 +4346,11 @@ async function runProxy(flags) {
4239
4346
  }),
4240
4347
  ).trim();
4241
4348
  if (!projectID) {
4242
- process.stdout.write(
4243
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
4244
- );
4349
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"));
4245
4350
  return;
4246
4351
  }
4247
4352
  if (!isUUID(projectID)) {
4248
- process.stdout.write(
4249
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
4250
- );
4353
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"));
4251
4354
  return;
4252
4355
  }
4253
4356
  try {
@@ -4274,23 +4377,19 @@ async function runProxy(flags) {
4274
4377
  workspaceSignalTrusted,
4275
4378
  });
4276
4379
  const text = buildProjectSummaryText(summary);
4277
- process.stdout.write(
4278
- `${JSON.stringify(
4279
- jsonRpcResult(requestObj, {
4280
- content: [
4281
- {
4282
- type: "text",
4283
- text,
4284
- },
4285
- ],
4286
- structuredContent: summary,
4287
- }),
4288
- )}\n`,
4380
+ writeProxyJson(
4381
+ jsonRpcResult(requestObj, {
4382
+ content: [
4383
+ {
4384
+ type: "text",
4385
+ text,
4386
+ },
4387
+ ],
4388
+ structuredContent: summary,
4389
+ }),
4289
4390
  );
4290
4391
  } catch (err) {
4291
- process.stdout.write(
4292
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4293
- );
4392
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4294
4393
  }
4295
4394
  return;
4296
4395
  }
@@ -4303,15 +4402,11 @@ async function runProxy(flags) {
4303
4402
  }),
4304
4403
  ).trim();
4305
4404
  if (!projectID) {
4306
- process.stdout.write(
4307
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
4308
- );
4405
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"));
4309
4406
  return;
4310
4407
  }
4311
4408
  if (!isUUID(projectID)) {
4312
- process.stdout.write(
4313
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
4314
- );
4409
+ writeProxyJson(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"));
4315
4410
  return;
4316
4411
  }
4317
4412
 
@@ -4320,14 +4415,12 @@ async function runProxy(flags) {
4320
4415
  const statusRaw = String(toolArgs.status || "").trim().toLowerCase();
4321
4416
  const statusFilter = normalizeMergeStatusFilter(statusRaw);
4322
4417
  if (statusRaw && !statusFilter) {
4323
- process.stdout.write(
4324
- `${JSON.stringify(
4325
- jsonRpcError(
4326
- requestObj,
4327
- -32001,
4328
- "status must be one of: open, review, approved, rejected, merged, closed, all",
4329
- ),
4330
- )}\n`,
4418
+ writeProxyJson(
4419
+ jsonRpcError(
4420
+ requestObj,
4421
+ -32001,
4422
+ "status must be one of: open, review, approved, rejected, merged, closed, all",
4423
+ ),
4331
4424
  );
4332
4425
  return;
4333
4426
  }
@@ -4342,13 +4435,11 @@ async function runProxy(flags) {
4342
4435
  workspaceDir: requestWorkspaceDir,
4343
4436
  });
4344
4437
  const text = buildCtxpackMergeBriefText(brief);
4345
- process.stdout.write(
4346
- `${JSON.stringify(
4347
- jsonRpcResult(requestObj, {
4348
- content: [{ type: "text", text }],
4349
- structuredContent: brief,
4350
- }),
4351
- )}\n`,
4438
+ writeProxyJson(
4439
+ jsonRpcResult(requestObj, {
4440
+ content: [{ type: "text", text }],
4441
+ structuredContent: brief,
4442
+ }),
4352
4443
  );
4353
4444
  return;
4354
4445
  }
@@ -4360,16 +4451,12 @@ async function runProxy(flags) {
4360
4451
  toolArgs.owner_confirmation || toolArgs.ownerConfirmation || "",
4361
4452
  ).trim();
4362
4453
  if (!mergeRequestID || !isUUID(mergeRequestID)) {
4363
- process.stdout.write(
4364
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"))}\n`,
4365
- );
4454
+ writeProxyJson(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"));
4366
4455
  return;
4367
4456
  }
4368
4457
  if (!action) {
4369
- process.stdout.write(
4370
- `${JSON.stringify(
4371
- jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
4372
- )}\n`,
4458
+ writeProxyJson(
4459
+ jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
4373
4460
  );
4374
4461
  return;
4375
4462
  }
@@ -4384,20 +4471,16 @@ async function runProxy(flags) {
4384
4471
  workspaceDir: requestWorkspaceDir,
4385
4472
  });
4386
4473
  const text = buildCtxpackMergeExecuteText(result);
4387
- process.stdout.write(
4388
- `${JSON.stringify(
4389
- jsonRpcResult(requestObj, {
4390
- content: [{ type: "text", text }],
4391
- structuredContent: result,
4392
- }),
4393
- )}\n`,
4474
+ writeProxyJson(
4475
+ jsonRpcResult(requestObj, {
4476
+ content: [{ type: "text", text }],
4477
+ structuredContent: result,
4478
+ }),
4394
4479
  );
4395
4480
  return;
4396
4481
  }
4397
4482
  } catch (err) {
4398
- process.stdout.write(
4399
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4400
- );
4483
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4401
4484
  return;
4402
4485
  }
4403
4486
  }
@@ -4429,11 +4512,18 @@ async function runProxy(flags) {
4429
4512
  requestWorkspaceDir,
4430
4513
  workspaceSignalTrusted,
4431
4514
  );
4432
- process.stdout.write(`${JSON.stringify(patched)}\n`);
4515
+ writeProxyJson(patched);
4433
4516
  return;
4434
4517
  }
4435
4518
  if (isJsonRpcMethod(requestObj, "tools/list")) {
4436
4519
  patched = appendLocalToolToToolsList(patched);
4520
+ if (sessionUseSafeToolAliases) {
4521
+ const tools = ensureArray(safeObject(patched.result).tools);
4522
+ const aliasMaps = buildToolAliasMaps(tools);
4523
+ sessionToolAliasToCanonical = aliasMaps.aliasToCanonical;
4524
+ sessionToolCanonicalToAlias = aliasMaps.canonicalToAlias;
4525
+ patched = applyToolAliasesToToolsListResponse(patched, sessionToolCanonicalToAlias);
4526
+ }
4437
4527
  } else if (isJsonRpcMethod(requestObj, "initialize")) {
4438
4528
  patched = appendProjectHintToInitialize(patched, args);
4439
4529
  // Log initialize params for workspace debugging.
@@ -4445,10 +4535,12 @@ async function runProxy(flags) {
4445
4535
  `[${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`,
4446
4536
  );
4447
4537
  } catch { /* best-effort */ }
4448
- // Probe client workspace roots via MCP roots/list protocol.
4449
- // Use setImmediate so the initialize response is flushed to stdout first;
4450
- // the client must see the response before it can handle server-initiated requests.
4451
- setImmediate(() => sendRootsListProbe());
4538
+ // Probe roots/list only when client advertised roots capability.
4539
+ // Some MCP clients reject unsolicited server requests and then hide tools.
4540
+ if (clientSupportsRootsList(requestObj?.params)) {
4541
+ // Use setImmediate so the initialize response is flushed first.
4542
+ setImmediate(() => sendRootsListProbe());
4543
+ }
4452
4544
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
4453
4545
  patched = await appendWorkitemListHints(patched, args, toolArgs, token, workspaceSignalTrusted);
4454
4546
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
@@ -4463,21 +4555,17 @@ async function runProxy(flags) {
4463
4555
  if (autoSyncSummary) {
4464
4556
  patched = appendAutoCtxpackSyncHint(patched, autoSyncSummary);
4465
4557
  }
4466
- process.stdout.write(`${JSON.stringify(patched)}\n`);
4558
+ writeProxyJson(patched);
4467
4559
  return;
4468
4560
  }
4469
- process.stdout.write(`${responseText}\n`);
4561
+ writeProxyMessage(responseText);
4470
4562
  return;
4471
4563
  }
4472
4564
  if (Object.prototype.hasOwnProperty.call(requestObj, "id")) {
4473
- process.stdout.write(
4474
- `${JSON.stringify(jsonRpcError(requestObj, -32001, "empty gateway response"))}\n`,
4475
- );
4565
+ writeProxyJson(jsonRpcError(requestObj, -32001, "empty gateway response"));
4476
4566
  }
4477
4567
  } catch (err) {
4478
- process.stdout.write(
4479
- `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
4480
- );
4568
+ writeProxyJson(jsonRpcError(requestObj, -32001, String(err?.message || err)));
4481
4569
  }
4482
4570
  };
4483
4571
 
@@ -4576,6 +4664,7 @@ async function runProxy(flags) {
4576
4664
  if (pendingInputBuffer.length === 0) break;
4577
4665
 
4578
4666
  if (startsWithContentLengthHeader(pendingInputBuffer)) {
4667
+ stdoutEnvelopeMode = "framed";
4579
4668
  const framed = extractFramedMessage(pendingInputBuffer);
4580
4669
  if (!framed) break;
4581
4670
  pendingInputBuffer = framed.remaining;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [