metheus-governance-mcp-cli 0.2.2 → 0.2.4

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 +15 -0
  2. package/cli.mjs +259 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -8,6 +8,7 @@ Metheus Governance MCP helper CLI.
8
8
  - checks Codex/Claude MCP registration
9
9
  - registers only missing clients
10
10
  - `setup`: register `metheus-governance-mcp` into Codex/Claude (if installed)
11
+ - `doctor`: run end-to-end health checks (auth/registration/gateway/project/ctxpack/tools)
11
12
  - `proxy`: stdio MCP bridge to Metheus HTTPS gateway
12
13
  - `auth`: save/check/clear local Metheus token used by proxy
13
14
 
@@ -35,6 +36,20 @@ metheus-governance-mcp setup --project-id <project_uuid> --ctxpack-key "<ctxpack
35
36
 
36
37
  `project-id` can be omitted if your current folder (or parent) has `.metheus_ctxpack_sync.json`.
37
38
 
39
+ ## Doctor
40
+
41
+ ```bash
42
+ metheus-governance-mcp doctor --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
43
+ ```
44
+
45
+ Checks:
46
+ - auth token status (+ auto refresh attempt)
47
+ - codex/claude registration state
48
+ - gateway `tools/list` reachability
49
+ - `project.summary` access
50
+ - ctxpack auto sync status
51
+ - smoke calls: `workitem.list`, `evidence.list`, `decision.list`
52
+
38
53
  ## Use in MCP
39
54
 
40
55
  `setup` auto-registers this server for `codex` and `claude` when those CLIs are available.
package/cli.mjs CHANGED
@@ -29,6 +29,7 @@ function printUsage() {
29
29
  " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
30
30
  " metheus-governance-mcp init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
31
31
  " metheus-governance-mcp setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--name <server_name>]",
32
+ " metheus-governance-mcp doctor [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--timeout-seconds <n>]",
32
33
  " metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
33
34
  " metheus-governance-mcp ctxpack pull [--project-id <uuid>] [--base-url <url>] [--timeout-seconds <n>]",
34
35
  " metheus-governance-mcp auth status",
@@ -1293,6 +1294,260 @@ async function runCtxpack(argv) {
1293
1294
  process.exitCode = 1;
1294
1295
  }
1295
1296
 
1297
+ function addDoctorCheck(rows, status, label, detail) {
1298
+ rows.push({
1299
+ status: String(status || "").trim().toLowerCase() || "warn",
1300
+ label: String(label || "").trim() || "check",
1301
+ detail: String(detail || "").trim(),
1302
+ });
1303
+ }
1304
+
1305
+ function statusIcon(status) {
1306
+ if (status === "ok") return "OK";
1307
+ if (status === "fail") return "FAIL";
1308
+ return "WARN";
1309
+ }
1310
+
1311
+ function printDoctorReport({ context, rows }) {
1312
+ const total = rows.length;
1313
+ const okCount = rows.filter((r) => r.status === "ok").length;
1314
+ const warnCount = rows.filter((r) => r.status === "warn").length;
1315
+ const failCount = rows.filter((r) => r.status === "fail").length;
1316
+
1317
+ process.stdout.write("Governance MCP doctor\n");
1318
+ process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
1319
+ process.stdout.write(`Project: ${context.projectID || "(not set)"}\n`);
1320
+ process.stdout.write("\nChecks\n");
1321
+ for (const row of rows) {
1322
+ process.stdout.write(`[${statusIcon(row.status)}] ${row.label}`);
1323
+ if (row.detail) {
1324
+ process.stdout.write(` - ${row.detail}`);
1325
+ }
1326
+ process.stdout.write("\n");
1327
+ }
1328
+ process.stdout.write(
1329
+ `\nSummary: total=${total}, ok=${okCount}, warn=${warnCount}, fail=${failCount}\n`,
1330
+ );
1331
+ }
1332
+
1333
+ function parseToolEnvelopeFromRPCResult(resultObj) {
1334
+ const result = safeObject(resultObj);
1335
+ const content = ensureArray(result.content);
1336
+ if (!content.length) return null;
1337
+ const first = safeObject(content[0]);
1338
+ const text = String(first.text || "").trim();
1339
+ if (!text) return null;
1340
+ const parsed = parseGatewayResponseText(text);
1341
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1342
+ return parsed;
1343
+ }
1344
+
1345
+ async function callGatewayRPC({ gatewayURL, timeoutSeconds, token, requestObj }) {
1346
+ try {
1347
+ const responseText = await postJSON(gatewayURL, timeoutSeconds, token, requestObj);
1348
+ const parsed = parseGatewayResponseText(responseText);
1349
+ if (!parsed) {
1350
+ return { ok: false, error: "invalid JSON-RPC response from gateway" };
1351
+ }
1352
+ if (parsed.error) {
1353
+ const errObj = safeObject(parsed.error);
1354
+ const code = Number(errObj.code || 0);
1355
+ const message = String(errObj.message || "").trim() || JSON.stringify(parsed.error);
1356
+ return { ok: false, error: `rpc ${code}: ${message}`, response: parsed };
1357
+ }
1358
+ return { ok: true, response: parsed };
1359
+ } catch (err) {
1360
+ return { ok: false, error: String(err?.message || err) };
1361
+ }
1362
+ }
1363
+
1364
+ async function runDoctor(flags) {
1365
+ const context = resolveSetupContext(flags);
1366
+ const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 30);
1367
+ const gatewayURL = buildGatewayURL({
1368
+ baseURL: `${context.baseURL}/governance/mcp`,
1369
+ projectID: context.projectID,
1370
+ ctxpackKey: context.ctxpackKey,
1371
+ includeDrafts: true,
1372
+ });
1373
+ const rows = [];
1374
+
1375
+ const resolved = await resolveAccessTokenForCommand(context.baseURL, timeoutSeconds);
1376
+ const token = resolved.token;
1377
+ if (!token) {
1378
+ addDoctorCheck(
1379
+ rows,
1380
+ "fail",
1381
+ "auth token",
1382
+ `missing. Run: metheus-governance-mcp auth login --base-url ${context.baseURL}`,
1383
+ );
1384
+ } else {
1385
+ addDoctorCheck(
1386
+ rows,
1387
+ "ok",
1388
+ "auth token",
1389
+ `configured (${resolved.source || "unknown"}${tokenExpiryIso(token) ? `, expires ${tokenExpiryIso(token)}` : ""})`,
1390
+ );
1391
+ }
1392
+
1393
+ for (const cliBin of ["codex", "claude"]) {
1394
+ if (!commandExists(cliBin)) {
1395
+ addDoctorCheck(rows, "warn", `${cliBin} CLI`, "not installed; registration check skipped");
1396
+ continue;
1397
+ }
1398
+ const registered = isRegistered(cliBin, context.serverName);
1399
+ if (registered) {
1400
+ addDoctorCheck(rows, "ok", `${cliBin} registration`, `${context.serverName} registered`);
1401
+ } else {
1402
+ addDoctorCheck(
1403
+ rows,
1404
+ "fail",
1405
+ `${cliBin} registration`,
1406
+ `${context.serverName} missing. Run: metheus-governance-mcp setup --base-url ${context.baseURL}`,
1407
+ );
1408
+ }
1409
+ }
1410
+
1411
+ if (!token) {
1412
+ printDoctorReport({ context, rows });
1413
+ process.exitCode = 1;
1414
+ return;
1415
+ }
1416
+
1417
+ const toolsListCall = await callGatewayRPC({
1418
+ gatewayURL,
1419
+ timeoutSeconds,
1420
+ token,
1421
+ requestObj: { jsonrpc: "2.0", id: 1, method: "tools/list", params: {} },
1422
+ });
1423
+ if (!toolsListCall.ok) {
1424
+ addDoctorCheck(rows, "fail", "gateway tools/list", toolsListCall.error || "unknown error");
1425
+ printDoctorReport({ context, rows });
1426
+ process.exitCode = 1;
1427
+ return;
1428
+ }
1429
+
1430
+ const toolsListResult = safeObject(safeObject(toolsListCall.response).result);
1431
+ const toolsCount = ensureArray(toolsListResult.tools).length;
1432
+ addDoctorCheck(rows, "ok", "gateway tools/list", `reachable (${toolsCount} tools)`);
1433
+
1434
+ if (!context.projectID) {
1435
+ addDoctorCheck(
1436
+ rows,
1437
+ "warn",
1438
+ "project summary",
1439
+ "project_id not set. Provide --project-id to validate access and ctxpack sync.",
1440
+ );
1441
+ printDoctorReport({ context, rows });
1442
+ process.exitCode = rows.some((r) => r.status === "fail") ? 1 : 0;
1443
+ return;
1444
+ }
1445
+
1446
+ if (!isUUID(context.projectID)) {
1447
+ addDoctorCheck(rows, "fail", "project_id format", "project_id must be a valid UUID");
1448
+ printDoctorReport({ context, rows });
1449
+ process.exitCode = 1;
1450
+ return;
1451
+ }
1452
+
1453
+ const summary = await loadProjectSummaryForTool({
1454
+ siteBaseURL: context.baseURL,
1455
+ projectID: context.projectID,
1456
+ token,
1457
+ timeoutSeconds,
1458
+ includeCtxpack: true,
1459
+ syncCtxpackLocal: true,
1460
+ });
1461
+ if (String(summary.access || "") !== "granted") {
1462
+ addDoctorCheck(
1463
+ rows,
1464
+ "fail",
1465
+ "project.summary",
1466
+ `${summary.message || "access denied"} (status ${summary.status_code || "-"})`,
1467
+ );
1468
+ printDoctorReport({ context, rows });
1469
+ process.exitCode = 1;
1470
+ return;
1471
+ }
1472
+ addDoctorCheck(
1473
+ rows,
1474
+ "ok",
1475
+ "project.summary",
1476
+ `${summary.name || context.projectID} (${summary.visibility || "-"}, ${summary.template_label || summary.template || "-"})`,
1477
+ );
1478
+
1479
+ const syncStatus = String(summary.ctxpack_sync_status || "").trim();
1480
+ if (["downloaded", "updated", "current"].includes(syncStatus)) {
1481
+ addDoctorCheck(
1482
+ rows,
1483
+ "ok",
1484
+ "ctxpack sync",
1485
+ `${syncStatus}${summary.ctxpack_local_path ? ` (${summary.ctxpack_local_path})` : ""}`,
1486
+ );
1487
+ } else if (syncStatus === "not_available") {
1488
+ addDoctorCheck(rows, "warn", "ctxpack sync", summary.ctxpack_sync_message || "ctxpack not available");
1489
+ } else {
1490
+ addDoctorCheck(rows, "fail", "ctxpack sync", summary.ctxpack_sync_message || "ctxpack sync failed");
1491
+ }
1492
+
1493
+ const smokeCalls = [
1494
+ {
1495
+ tool: "workitem.list",
1496
+ args: { project_id: context.projectID, limit: 1, offset: 0 },
1497
+ },
1498
+ {
1499
+ tool: "evidence.list",
1500
+ args: { project_id: context.projectID, limit: 1, offset: 0 },
1501
+ },
1502
+ {
1503
+ tool: "decision.list",
1504
+ args: { project_id: context.projectID, limit: 1, offset: 0 },
1505
+ },
1506
+ ];
1507
+
1508
+ for (let idx = 0; idx < smokeCalls.length; idx += 1) {
1509
+ const row = smokeCalls[idx];
1510
+ const rpc = await callGatewayRPC({
1511
+ gatewayURL,
1512
+ timeoutSeconds,
1513
+ token,
1514
+ requestObj: {
1515
+ jsonrpc: "2.0",
1516
+ id: 200 + idx,
1517
+ method: "tools/call",
1518
+ params: {
1519
+ name: row.tool,
1520
+ arguments: row.args,
1521
+ },
1522
+ },
1523
+ });
1524
+ if (!rpc.ok) {
1525
+ addDoctorCheck(rows, "fail", `smoke ${row.tool}`, rpc.error || "rpc error");
1526
+ continue;
1527
+ }
1528
+ const envelope = parseToolEnvelopeFromRPCResult(safeObject(rpc.response).result);
1529
+ if (!envelope) {
1530
+ addDoctorCheck(rows, "warn", `smoke ${row.tool}`, "no parsable tool envelope in response");
1531
+ continue;
1532
+ }
1533
+ const status = Number(envelope.status || 0);
1534
+ const ok = Boolean(envelope.ok);
1535
+ if (ok && status >= 200 && status < 300) {
1536
+ addDoctorCheck(rows, "ok", `smoke ${row.tool}`, `status ${status}`);
1537
+ } else {
1538
+ addDoctorCheck(
1539
+ rows,
1540
+ "fail",
1541
+ `smoke ${row.tool}`,
1542
+ `status ${status || "-"}, ok=${ok ? "true" : "false"}`,
1543
+ );
1544
+ }
1545
+ }
1546
+
1547
+ printDoctorReport({ context, rows });
1548
+ process.exitCode = rows.some((r) => r.status === "fail") ? 1 : 0;
1549
+ }
1550
+
1296
1551
  function buildGatewayURL(args) {
1297
1552
  const base = String(args.baseURL || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL;
1298
1553
  const url = new URL(base);
@@ -2255,6 +2510,10 @@ async function main() {
2255
2510
  runSetup(flags);
2256
2511
  return;
2257
2512
  }
2513
+ if (command === "doctor") {
2514
+ await runDoctor(flags);
2515
+ return;
2516
+ }
2258
2517
  if (command === "auth") {
2259
2518
  await runAuth(rest);
2260
2519
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [