metheus-governance-mcp-cli 0.2.37 → 0.2.39

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 +28 -0
  2. package/cli.mjs +185 -13
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -103,6 +103,11 @@ Antigravity note:
103
103
  Cursor note:
104
104
  - this CLI manages MCP registration via Cursor global MCP config (`~/.cursor/mcp.json`).
105
105
 
106
+ Tool naming compatibility:
107
+ - Claude/Codex/Gemini keep canonical MCP tool names (`project.summary`, `ctxpack.merge.brief`, ...).
108
+ - Cursor/Antigravity sessions may receive safe aliases (`project_summary`, `ctxpack_merge_brief`, ...).
109
+ - Proxy maps alias calls back to canonical names automatically.
110
+
106
111
  Local bootstrap tools exposed by proxy:
107
112
 
108
113
  - `project.summary`
@@ -239,6 +244,7 @@ npm publish --access public
239
244
 
240
245
  ```bash
241
246
  npm run check
247
+ npm run test:compat
242
248
  npm run publish:dry
243
249
  ```
244
250
 
@@ -255,3 +261,25 @@ If npm account uses 2FA, pass OTP:
255
261
  ```bash
256
262
  node release.mjs --otp <6-digit-code>
257
263
  ```
264
+
265
+ ## Regression and smoke checks
266
+
267
+ Local compatibility selftest (no network):
268
+
269
+ ```bash
270
+ npm run test:compat
271
+ ```
272
+
273
+ Proxy smoke test (initialize -> tools/list -> project summary -> ctxpack local sync):
274
+
275
+ ```bash
276
+ npm run smoke:proxy -- --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --workspace-dir <workspace_path>
277
+ ```
278
+
279
+ Optional:
280
+ - `--client cursor-vscode` (default) or `--client antigravity`
281
+ - `--base-url https://metheus.gesiaplatform.com/governance/mcp`
282
+
283
+ Workspace fallback guardrail:
284
+ - If `METHEUS_WORKSPACE_DIR` is accidentally set to a non-existing boolean suffix path like `C:\code_test\true`,
285
+ proxy now recovers to the parent workspace (`C:\code_test`) instead of pinning to the bad path.
package/cli.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from "node:fs";
4
+ import os from "node:os";
4
5
  import path from "node:path";
5
6
  import process from "node:process";
6
7
  import readline from "node:readline";
@@ -42,6 +43,7 @@ function printUsage() {
42
43
  ` ${cmd} setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--workspace-fallback-dir <path>] [--name <server_name>]`,
43
44
  ` ${cmd} doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]`,
44
45
  ` ${cmd} proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--workspace-dir <path|auto>] [--include-drafts <true|false>] [--auto-pull-on-conflict <true|false>] [--timeout-seconds <n>]`,
46
+ ` ${cmd} selftest [--json <true|false>]`,
45
47
  ` ${cmd} ctxpack pull [--project-id <uuid>] [--base-url <url>] [--workspace-dir <path|auto>] [--paths <csv>] [--timeout-seconds <n>]`,
46
48
  ` ${cmd} auth status`,
47
49
  ` ${cmd} auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]`,
@@ -604,7 +606,7 @@ function extractWeakWorkspaceCandidateFromRequest(requestObj) {
604
606
  }
605
607
 
606
608
  function extractStrongWorkspaceCandidateFromEnv() {
607
- const rawCandidate = firstNonEmptyString([
609
+ const rawCandidates = [
608
610
  process.env.METHEUS_WORKSPACE_DIR,
609
611
  process.env.METHEUS_WORKSPACE_URI,
610
612
  process.env.CODEX_WORKSPACE_DIR,
@@ -612,8 +614,12 @@ function extractStrongWorkspaceCandidateFromEnv() {
612
614
  process.env.CLAUDE_WORKSPACE_DIR,
613
615
  process.env.CLAUDE_WORKSPACE_URI,
614
616
  process.env.CLAUDE_PROJECT_DIR,
615
- ]);
616
- return sanitizeWorkspaceCandidate(rawCandidate);
617
+ ];
618
+ for (const rawCandidate of rawCandidates) {
619
+ const normalized = sanitizeWorkspaceFallbackEnvCandidate(rawCandidate);
620
+ if (normalized) return normalized;
621
+ }
622
+ return "";
617
623
  }
618
624
 
619
625
  function extractWorkspaceCandidateFromEnv() {
@@ -635,6 +641,35 @@ function extractWeakWorkspaceCandidateFromEnv() {
635
641
  return sanitizeWorkspaceCandidate(rawCandidate);
636
642
  }
637
643
 
644
+ function sanitizeWorkspaceFallbackEnvCandidate(rawCandidate) {
645
+ const input = String(rawCandidate || "").trim();
646
+ if (!input) return "";
647
+
648
+ const direct = sanitizeWorkspaceCandidate(input);
649
+ if (direct && fs.existsSync(direct)) {
650
+ return direct;
651
+ }
652
+
653
+ const fileCandidate = fileURIToLocalPath(input);
654
+ const candidate = firstNonEmptyString([fileCandidate, input]);
655
+ if (!candidate) return direct;
656
+
657
+ const resolved = resolveWorkspaceDir(candidate);
658
+ if (!resolved || isEditorInstallDirectory(resolved)) {
659
+ return direct;
660
+ }
661
+
662
+ const leaf = path.basename(resolved).trim().toLowerCase();
663
+ if ((leaf === "true" || leaf === "false") && !fs.existsSync(resolved)) {
664
+ const parent = sanitizeWorkspaceCandidate(path.dirname(resolved));
665
+ if (parent && fs.existsSync(parent)) {
666
+ return parent;
667
+ }
668
+ }
669
+
670
+ return direct;
671
+ }
672
+
638
673
  function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs) {
639
674
  const strongRequestCandidate = extractStrongWorkspaceCandidateFromRequest(requestObj, toolArgs);
640
675
  const strongEnvCandidate = extractStrongWorkspaceCandidateFromEnv();
@@ -3568,6 +3603,14 @@ function shouldUseSafeToolAliasesForClient(initParamsRaw) {
3568
3603
  return name.includes("cursor") || name.includes("antigravity");
3569
3604
  }
3570
3605
 
3606
+ function displayToolNameForClient(canonicalName, useSafeToolAliases = false) {
3607
+ const canonical = String(canonicalName || "").trim();
3608
+ if (!canonical) return "";
3609
+ if (!useSafeToolAliases) return canonical;
3610
+ const alias = normalizeSafeToolAliasName(canonical);
3611
+ return alias || canonical;
3612
+ }
3613
+
3571
3614
  function injectWorkspaceDirIntoToolSchemas(tools) {
3572
3615
  const workspaceDirProp = {
3573
3616
  type: "string",
@@ -3598,14 +3641,25 @@ function appendLocalToolToToolsList(responseObj) {
3598
3641
  return responseObj;
3599
3642
  }
3600
3643
 
3601
- function appendProjectHintToInitialize(responseObj, args) {
3644
+ function appendProjectHintToInitialize(responseObj, args, options = {}) {
3645
+ const useSafeToolAliases = Boolean(options.useSafeToolAliases);
3602
3646
  const result = safeObject(responseObj.result);
3647
+ const projectSummaryTool = displayToolNameForClient("project.summary", useSafeToolAliases);
3648
+ const projectDescribeTool = displayToolNameForClient("project.describe", useSafeToolAliases);
3649
+ const projectGetTool = displayToolNameForClient("project.get", useSafeToolAliases);
3650
+ const ctxpackMergeBriefTool = displayToolNameForClient("ctxpack.merge.brief", useSafeToolAliases);
3651
+ const ctxpackMergeExecuteTool = displayToolNameForClient("ctxpack.merge.execute", useSafeToolAliases);
3603
3652
  const hintLines = [
3604
3653
  "High-priority project bootstrap rule:",
3605
- "- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
3606
- "- `project.describe` and `project.get` are aliases of `project.summary`.",
3654
+ ...(useSafeToolAliases
3655
+ ? [
3656
+ "- This session uses safe MCP tool aliases (underscore format). Use `project_summary` / `ctxpack_ensure` / `workitem_list` style names, not dotted names.",
3657
+ ]
3658
+ : []),
3659
+ `- MUST call \`${projectSummaryTool}\` first when the user provides only a Project ID or asks project overview/agenda.`,
3660
+ `- \`${projectDescribeTool}\` and \`${projectGetTool}\` are aliases of \`${projectSummaryTool}\`.`,
3607
3661
  "- After project summary, use workitem/evidence/decision tools as follow-up.",
3608
- "- Before any ctxpack merge, call `ctxpack.merge.brief` first, share recommendation to owner, get explicit owner confirmation, then call `ctxpack.merge.execute`.",
3662
+ `- Before any ctxpack merge, call \`${ctxpackMergeBriefTool}\` first, share recommendation to owner, get explicit owner confirmation, then call \`${ctxpackMergeExecuteTool}\`.`,
3609
3663
  "- IMPORTANT: When calling ANY tool, always include your current working directory / project folder path as the `workspace_dir` argument. This enables ctxpack files to be synced to the correct workspace location.",
3610
3664
  ];
3611
3665
  if (args.projectID) {
@@ -3732,7 +3786,16 @@ function appendAutoCtxpackSyncHint(responseObj, summary) {
3732
3786
  return responseObj;
3733
3787
  }
3734
3788
 
3735
- async function appendWorkitemListHints(responseObj, args, toolArgs, token, workspaceSignalTrusted = true) {
3789
+ async function appendWorkitemListHints(
3790
+ responseObj,
3791
+ args,
3792
+ toolArgs,
3793
+ token,
3794
+ workspaceSignalTrusted = true,
3795
+ options = {},
3796
+ ) {
3797
+ const useSafeToolAliases = Boolean(options.useSafeToolAliases);
3798
+ const projectSummaryTool = displayToolNameForClient("project.summary", useSafeToolAliases);
3736
3799
  const result = safeObject(responseObj.result);
3737
3800
  const content = ensureArray(result.content);
3738
3801
  if (!content.length) return responseObj;
@@ -3742,7 +3805,7 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token, works
3742
3805
 
3743
3806
  const text = String(first.text || "");
3744
3807
  if (!text) return responseObj;
3745
- if (text.includes("Call `project.summary`")) return responseObj;
3808
+ if (text.includes(`Call \`${projectSummaryTool}\``) || text.includes("Call `project.summary`")) return responseObj;
3746
3809
 
3747
3810
  const parsed = parseGatewayResponseText(text);
3748
3811
  if (!parsed) return responseObj;
@@ -3759,7 +3822,7 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token, works
3759
3822
  const nextLines = [""];
3760
3823
  if (isEmptyBody) {
3761
3824
  nextLines.push("No work items found for this project.");
3762
- nextLines.push("- Call `project.summary` to confirm project context/agenda and access state first.");
3825
+ nextLines.push(`- Call \`${projectSummaryTool}\` to confirm project context/agenda and access state first.`);
3763
3826
  }
3764
3827
 
3765
3828
  let responseProjectID = "";
@@ -4525,7 +4588,9 @@ async function runProxy(flags) {
4525
4588
  patched = applyToolAliasesToToolsListResponse(patched, sessionToolCanonicalToAlias);
4526
4589
  }
4527
4590
  } else if (isJsonRpcMethod(requestObj, "initialize")) {
4528
- patched = appendProjectHintToInitialize(patched, args);
4591
+ patched = appendProjectHintToInitialize(patched, args, {
4592
+ useSafeToolAliases: sessionUseSafeToolAliases,
4593
+ });
4529
4594
  // Log initialize params for workspace debugging.
4530
4595
  try {
4531
4596
  const _diagDir = path.join(String(process.env.USERPROFILE || process.env.HOME || "."), ".metheus");
@@ -4542,7 +4607,14 @@ async function runProxy(flags) {
4542
4607
  setImmediate(() => sendRootsListProbe());
4543
4608
  }
4544
4609
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
4545
- patched = await appendWorkitemListHints(patched, args, toolArgs, token, workspaceSignalTrusted);
4610
+ patched = await appendWorkitemListHints(
4611
+ patched,
4612
+ args,
4613
+ toolArgs,
4614
+ token,
4615
+ workspaceSignalTrusted,
4616
+ { useSafeToolAliases: sessionUseSafeToolAliases },
4617
+ );
4546
4618
  } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "ctxpack.ensure") {
4547
4619
  patched = appendCtxpackEnsureSyncHints(
4548
4620
  patched,
@@ -4984,7 +5056,7 @@ function resolveSetupContext(flags) {
4984
5056
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
4985
5057
  const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
4986
5058
  const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
4987
- const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
5059
+ const baseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
4988
5060
  const workspaceDirRaw = String(flags["workspace-dir"] || "").trim();
4989
5061
  const workspaceFallbackDirRaw = String(flags["workspace-fallback-dir"] || "").trim();
4990
5062
  const hasWorkspaceDirFlag = Object.prototype.hasOwnProperty.call(flags, "workspace-dir");
@@ -5099,6 +5171,102 @@ function runSetup(flags) {
5099
5171
  runSetupInternal(flags, { ensureOnly: false });
5100
5172
  }
5101
5173
 
5174
+ function runSelftest(flags = {}) {
5175
+ const jsonMode = boolFromRaw(flags.json, false);
5176
+ const checks = [];
5177
+ const push = (name, ok, detail = "") => checks.push({ name, ok: Boolean(ok), detail: String(detail || "") });
5178
+
5179
+ const aliasMaps = buildToolAliasMaps([
5180
+ { name: "project.summary" },
5181
+ { name: "ctxpack.merge.brief" },
5182
+ { name: "workitem.list" },
5183
+ ]);
5184
+ const summaryAlias = String(aliasMaps.canonicalToAlias.get("project.summary") || "");
5185
+ const roundTrip = rewriteAliasedToolCallToCanonical(
5186
+ {
5187
+ jsonrpc: "2.0",
5188
+ id: 1,
5189
+ method: "tools/call",
5190
+ params: {
5191
+ name: summaryAlias,
5192
+ arguments: {},
5193
+ },
5194
+ },
5195
+ aliasMaps.aliasToCanonical,
5196
+ );
5197
+ const roundTripOk = summaryAlias === "project_summary" && String(roundTrip?.params?.name || "") === "project.summary";
5198
+ push(
5199
+ "alias_roundtrip_project_summary",
5200
+ roundTripOk,
5201
+ `alias=${summaryAlias || "(none)"} canonical=${String(roundTrip?.params?.name || "(none)")}`,
5202
+ );
5203
+
5204
+ const collisionMaps = buildToolAliasMaps([{ name: "a.b" }, { name: "a_b" }]);
5205
+ const aliasDot = String(collisionMaps.canonicalToAlias.get("a.b") || "");
5206
+ const aliasUnderscore = String(collisionMaps.canonicalToAlias.get("a_b") || "");
5207
+ const collisionOk = aliasDot === "a_b_2" && aliasUnderscore === "";
5208
+ push(
5209
+ "alias_collision_safe_suffix",
5210
+ collisionOk,
5211
+ `a.b->${aliasDot || "(none)"} a_b->${aliasUnderscore || "(none)"}`,
5212
+ );
5213
+
5214
+ let tempRoot = "";
5215
+ try {
5216
+ tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "metheus-selftest-"));
5217
+ const parent = path.join(tempRoot, "workspace");
5218
+ fs.mkdirSync(parent, { recursive: true });
5219
+ const suspicious = path.join(parent, "true");
5220
+ const recovered = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
5221
+ const recoveredOk = normalizedPathForCompare(recovered) === normalizedPathForCompare(parent);
5222
+ push(
5223
+ "workspace_env_true_suffix_recovery",
5224
+ recoveredOk,
5225
+ `input=${suspicious} recovered=${recovered || "(none)"}`,
5226
+ );
5227
+
5228
+ fs.mkdirSync(suspicious, { recursive: true });
5229
+ const keptExisting = sanitizeWorkspaceFallbackEnvCandidate(suspicious);
5230
+ const keptExistingOk = normalizedPathForCompare(keptExisting) === normalizedPathForCompare(suspicious);
5231
+ push(
5232
+ "workspace_env_existing_true_dir_kept",
5233
+ keptExistingOk,
5234
+ `input=${suspicious} recovered=${keptExisting || "(none)"}`,
5235
+ );
5236
+ } catch (err) {
5237
+ push("workspace_env_guard_test_setup", false, String(err?.message || err));
5238
+ } finally {
5239
+ if (tempRoot) {
5240
+ try {
5241
+ fs.rmSync(tempRoot, { recursive: true, force: true });
5242
+ } catch {
5243
+ // ignore selftest cleanup error
5244
+ }
5245
+ }
5246
+ }
5247
+
5248
+ const failed = checks.filter((row) => !row.ok);
5249
+ const payload = {
5250
+ ok: failed.length === 0,
5251
+ pass_count: checks.length - failed.length,
5252
+ fail_count: failed.length,
5253
+ checks,
5254
+ };
5255
+
5256
+ if (jsonMode) {
5257
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
5258
+ } else {
5259
+ process.stdout.write(`Selftest: ${payload.ok ? "PASS" : "FAIL"} (${payload.pass_count}/${checks.length})\n`);
5260
+ for (const row of checks) {
5261
+ process.stdout.write(`- ${row.ok ? "PASS" : "FAIL"} ${row.name}${row.detail ? ` :: ${row.detail}` : ""}\n`);
5262
+ }
5263
+ }
5264
+
5265
+ if (!payload.ok) {
5266
+ process.exitCode = 1;
5267
+ }
5268
+ }
5269
+
5102
5270
  async function runBootstrap(flags) {
5103
5271
  process.stdout.write("Bootstrap start.\n");
5104
5272
  let resolved = resolveCurrentAccessToken();
@@ -5159,6 +5327,10 @@ async function main() {
5159
5327
  await runDoctor(flags);
5160
5328
  return;
5161
5329
  }
5330
+ if (command === "selftest") {
5331
+ runSelftest(flags);
5332
+ return;
5333
+ }
5162
5334
  if (command === "auth") {
5163
5335
  await runAuth(rest);
5164
5336
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.37",
3
+ "version": "0.2.39",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [
@@ -11,6 +11,8 @@
11
11
  ],
12
12
  "scripts": {
13
13
  "check": "node --check cli.mjs && node --check release.mjs",
14
+ "test:compat": "node cli.mjs selftest --json",
15
+ "smoke:proxy": "node scripts/smoke-proxy.mjs",
14
16
  "pack:dry": "npm pack --dry-run",
15
17
  "publish:dry": "node release.mjs --dry-run",
16
18
  "publish:public": "node release.mjs"