metheus-governance-mcp-cli 0.2.1 → 0.2.3

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 +40 -0
  2. package/cli.mjs +1007 -6
  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,10 +36,49 @@ 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.
41
56
 
57
+ Local bootstrap tools exposed by proxy:
58
+
59
+ - `project.summary`
60
+ - `project.describe` (alias)
61
+ - `project.get` (alias)
62
+
63
+ These tools accept `project_id` and return:
64
+
65
+ - access state (`granted`, `unauthorized`, `denied`, `not_found`)
66
+ - project metadata (name, org, template, owners, visibility)
67
+ - agenda preview from ctxpack (when available)
68
+
69
+ `project.summary` automatically syncs ctxpack to local cache:
70
+ - missing local cache -> download
71
+ - same version -> keep current
72
+ - newer server version -> update local cache
73
+
74
+ Manual ctxpack pull/update:
75
+
76
+ ```bash
77
+ metheus-governance-mcp ctxpack pull --project-id <project_uuid> --base-url https://metheus.gesiaplatform.com
78
+ ```
79
+
80
+ When `workitem.list` returns empty, proxy appends a hint to call `project.summary` first.
81
+
42
82
  ## Auth flow (recommended)
43
83
 
44
84
  1. Auto login and save token once (default flow: `device -> callback -> manual hint`):
package/cli.mjs CHANGED
@@ -14,6 +14,8 @@ const DEFAULT_SITE_URL = "https://metheus.gesiaplatform.com";
14
14
  const DEFAULT_BASE_URL = `${DEFAULT_SITE_URL}/governance/mcp`;
15
15
  const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
16
16
  const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
17
+ const CTXPACK_CACHE_RELATIVE_DIR = path.join(".metheus", "ctxpack-cache");
18
+ const CTXPACK_META_FILENAME = ".metheus_ctxpack_sync.json";
17
19
  const CLI_META = loadCLIMeta();
18
20
  const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
19
21
  const CLI_VERSION = CLI_META.version || "0.0.0";
@@ -27,7 +29,9 @@ function printUsage() {
27
29
  " metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
28
30
  " metheus-governance-mcp init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
29
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>]",
30
33
  " metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
34
+ " metheus-governance-mcp ctxpack pull [--project-id <uuid>] [--base-url <url>] [--timeout-seconds <n>]",
31
35
  " metheus-governance-mcp auth status",
32
36
  " metheus-governance-mcp 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>]",
33
37
  " metheus-governance-mcp auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]",
@@ -97,6 +101,14 @@ function authStoreFilePath() {
97
101
  return path.join(home, AUTH_STORE_RELATIVE_PATH);
98
102
  }
99
103
 
104
+ function ctxpackCacheRootDir() {
105
+ const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
106
+ if (!home) {
107
+ return path.resolve(process.cwd(), CTXPACK_CACHE_RELATIVE_DIR);
108
+ }
109
+ return path.join(home, CTXPACK_CACHE_RELATIVE_DIR);
110
+ }
111
+
100
112
  function ensureParentDir(filePath) {
101
113
  const parent = path.dirname(filePath);
102
114
  fs.mkdirSync(parent, { recursive: true });
@@ -857,6 +869,11 @@ function intFromRaw(raw, fallback) {
857
869
  return n;
858
870
  }
859
871
 
872
+ function isUUID(raw) {
873
+ const value = String(raw || "").trim();
874
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
875
+ }
876
+
860
877
  async function runAuthStatus() {
861
878
  const resolved = resolveCurrentAccessToken();
862
879
  const token = resolved.token;
@@ -1207,6 +1224,330 @@ async function runAuth(argv) {
1207
1224
  process.exitCode = 1;
1208
1225
  }
1209
1226
 
1227
+ async function resolveAccessTokenForCommand(baseURL, timeoutSeconds) {
1228
+ let resolved = resolveCurrentAccessToken();
1229
+ if (resolved.token) return resolved;
1230
+
1231
+ const refreshed = await tryRefreshStoredAccessToken({
1232
+ baseURL,
1233
+ timeoutSeconds,
1234
+ });
1235
+ if (refreshed.ok) {
1236
+ resolved = resolveCurrentAccessToken();
1237
+ }
1238
+ return resolved;
1239
+ }
1240
+
1241
+ async function runCtxpackPull(flags) {
1242
+ const workspaceMeta = loadWorkspaceMeta(process.cwd());
1243
+ const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
1244
+ if (!projectID) {
1245
+ process.stderr.write("Missing project_id. Use --project-id <uuid> or run inside a synced ctxpack workspace.\n");
1246
+ process.exitCode = 1;
1247
+ return;
1248
+ }
1249
+ if (!isUUID(projectID)) {
1250
+ process.stderr.write("project_id must be a valid UUID.\n");
1251
+ process.exitCode = 1;
1252
+ return;
1253
+ }
1254
+
1255
+ const siteBaseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
1256
+ const timeoutSeconds = intFromRaw(flags["timeout-seconds"], 30);
1257
+ const resolved = await resolveAccessTokenForCommand(siteBaseURL, timeoutSeconds);
1258
+ const token = resolved.token;
1259
+ if (!token) {
1260
+ process.stderr.write(
1261
+ `Missing auth token. Run: metheus-governance-mcp auth login --base-url ${siteBaseURL}\n`,
1262
+ );
1263
+ process.exitCode = 1;
1264
+ return;
1265
+ }
1266
+
1267
+ const summary = await loadProjectSummaryForTool({
1268
+ siteBaseURL,
1269
+ projectID,
1270
+ token,
1271
+ timeoutSeconds,
1272
+ includeCtxpack: true,
1273
+ syncCtxpackLocal: true,
1274
+ });
1275
+ process.stdout.write(`${buildProjectSummaryText(summary)}\n`);
1276
+
1277
+ if (String(summary.access || "") !== "granted") {
1278
+ process.exitCode = 1;
1279
+ return;
1280
+ }
1281
+ const syncStatus = String(summary.ctxpack_sync_status || "").trim();
1282
+ if (syncStatus === "error" || syncStatus === "not_available") {
1283
+ process.exitCode = 1;
1284
+ }
1285
+ }
1286
+
1287
+ async function runCtxpack(argv) {
1288
+ const { subcommand, flags } = parseCommandAndFlags(argv);
1289
+ if (!subcommand || subcommand === "pull") {
1290
+ await runCtxpackPull(flags);
1291
+ return;
1292
+ }
1293
+ process.stderr.write(`Unknown ctxpack subcommand: ${subcommand}\n`);
1294
+ process.exitCode = 1;
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
+
1210
1551
  function buildGatewayURL(args) {
1211
1552
  const base = String(args.baseURL || DEFAULT_BASE_URL).trim() || DEFAULT_BASE_URL;
1212
1553
  const url = new URL(base);
@@ -1259,6 +1600,592 @@ function postJSON(urlText, timeoutSeconds, token, payload) {
1259
1600
  });
1260
1601
  }
1261
1602
 
1603
+ function getJSONWithAuth(urlText, timeoutSeconds, token) {
1604
+ return new Promise((resolve, reject) => {
1605
+ const url = new URL(urlText);
1606
+ const transport = url.protocol === "http:" ? http : https;
1607
+ const req = transport.request(
1608
+ {
1609
+ protocol: url.protocol,
1610
+ hostname: url.hostname,
1611
+ port: url.port || (url.protocol === "http:" ? 80 : 443),
1612
+ path: `${url.pathname}${url.search}`,
1613
+ method: "GET",
1614
+ headers: {
1615
+ accept: "application/json",
1616
+ authorization: `Bearer ${token}`,
1617
+ },
1618
+ timeout: Math.max(3, timeoutSeconds) * 1000,
1619
+ },
1620
+ (res) => {
1621
+ const chunks = [];
1622
+ res.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
1623
+ res.on("end", () => {
1624
+ const text = Buffer.concat(chunks).toString("utf8");
1625
+ const statusCode = Number(res.statusCode || 0);
1626
+ if (statusCode >= 200 && statusCode < 300) {
1627
+ try {
1628
+ resolve(JSON.parse(text));
1629
+ } catch {
1630
+ reject(new Error("invalid json response"));
1631
+ }
1632
+ return;
1633
+ }
1634
+ const err = new Error(text.trim() || `http ${statusCode}`);
1635
+ err.statusCode = statusCode;
1636
+ err.responseBody = text;
1637
+ reject(err);
1638
+ });
1639
+ },
1640
+ );
1641
+ req.on("timeout", () => {
1642
+ req.destroy(new Error("http timeout"));
1643
+ });
1644
+ req.on("error", reject);
1645
+ req.end();
1646
+ });
1647
+ }
1648
+
1649
+ function safeObject(value) {
1650
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1651
+ return value;
1652
+ }
1653
+
1654
+ function extractToolCall(requestObj) {
1655
+ const params = safeObject(requestObj?.params);
1656
+ const rawName = params.name ?? params.tool_name ?? params.toolName;
1657
+ const name = String(rawName || "").trim();
1658
+ const rawArgs = params.arguments ?? params.args ?? {};
1659
+ const args = safeObject(rawArgs);
1660
+ return { name, args };
1661
+ }
1662
+
1663
+ function isJsonRpcMethod(requestObj, expected) {
1664
+ return String(requestObj?.method || "").trim() === expected;
1665
+ }
1666
+
1667
+ function jsonRpcResult(requestObj, result) {
1668
+ const out = { jsonrpc: "2.0", result };
1669
+ if (Object.prototype.hasOwnProperty.call(requestObj || {}, "id")) {
1670
+ out.id = requestObj.id;
1671
+ }
1672
+ return out;
1673
+ }
1674
+
1675
+ const LOCAL_PROJECT_TOOL_NAMES = ["project.summary", "project.describe", "project.get"];
1676
+
1677
+ function buildProjectSummaryInputSchema() {
1678
+ return {
1679
+ type: "object",
1680
+ properties: {
1681
+ project_id: {
1682
+ type: "string",
1683
+ description: "Project UUID. If omitted, current configured project_id is used.",
1684
+ },
1685
+ include_ctxpack: {
1686
+ type: "boolean",
1687
+ description: "If true, include ctxpack file/agenda preview when available.",
1688
+ },
1689
+ sync_ctxpack_local: {
1690
+ type: "boolean",
1691
+ description:
1692
+ "If true (default), auto-download/update ctxpack files into local cache when project.summary is called.",
1693
+ },
1694
+ },
1695
+ additionalProperties: false,
1696
+ };
1697
+ }
1698
+
1699
+ function buildLocalToolSpecs() {
1700
+ return [
1701
+ {
1702
+ name: "project.summary",
1703
+ description:
1704
+ "Get project summary and access status for a project ID. Includes agenda preview and auto-syncs local ctxpack cache when available.",
1705
+ inputSchema: buildProjectSummaryInputSchema(),
1706
+ },
1707
+ {
1708
+ name: "project.describe",
1709
+ description:
1710
+ "Alias of project.summary. Use when user asks to describe/explain a project by Project ID.",
1711
+ inputSchema: buildProjectSummaryInputSchema(),
1712
+ },
1713
+ {
1714
+ name: "project.get",
1715
+ description:
1716
+ "Alias of project.summary. Returns project metadata and access status for the given project_id.",
1717
+ inputSchema: buildProjectSummaryInputSchema(),
1718
+ },
1719
+ ];
1720
+ }
1721
+
1722
+ function normalizeTemplateLabel(rawTemplate) {
1723
+ const value = String(rawTemplate || "").trim().toLowerCase();
1724
+ if (value === "standard") return "Standard Governance";
1725
+ if (value === "agile") return "Agile Workflow";
1726
+ if (value === "blank") return "Blank Project";
1727
+ return value || "Unknown";
1728
+ }
1729
+
1730
+ function extractAgendaFromFiles(files) {
1731
+ const list = Array.isArray(files) ? files : [];
1732
+ if (!list.length) {
1733
+ return { agenda: "", agendaSource: "", ctxpackFileCount: 0 };
1734
+ }
1735
+ const byPathPriority = [
1736
+ "agenda.md",
1737
+ "readme.md",
1738
+ "docs/agenda.md",
1739
+ "docs/readme.md",
1740
+ ];
1741
+ const normalized = list
1742
+ .map((file) => ({
1743
+ path: String(file?.path || "").trim(),
1744
+ docType: String(file?.doc_type || "").trim().toLowerCase(),
1745
+ content: String(file?.content || "").trim(),
1746
+ }))
1747
+ .filter((file) => file.path && file.content);
1748
+ let selected = normalized.find((file) => byPathPriority.includes(file.path.toLowerCase()));
1749
+ if (!selected) {
1750
+ selected = normalized.find((file) => file.docType === "agenda" || file.docType === "readme");
1751
+ }
1752
+ if (!selected) {
1753
+ selected = normalized[0];
1754
+ }
1755
+ if (!selected) {
1756
+ return { agenda: "", agendaSource: "", ctxpackFileCount: list.length };
1757
+ }
1758
+
1759
+ const lines = selected.content.split(/\r?\n/);
1760
+ let start = -1;
1761
+ for (let i = 0; i < lines.length; i += 1) {
1762
+ if (/^\s{0,3}#{1,6}\s*agenda\b/i.test(lines[i])) {
1763
+ start = i + 1;
1764
+ break;
1765
+ }
1766
+ }
1767
+ let agendaText = "";
1768
+ if (start >= 0) {
1769
+ const picked = [];
1770
+ for (let i = start; i < lines.length; i += 1) {
1771
+ if (/^\s{0,3}#{1,6}\s+/.test(lines[i])) break;
1772
+ picked.push(lines[i]);
1773
+ }
1774
+ agendaText = picked.join("\n").trim();
1775
+ }
1776
+ if (!agendaText) {
1777
+ agendaText = selected.content.slice(0, 700).trim();
1778
+ }
1779
+ if (agendaText.length > 700) {
1780
+ agendaText = `${agendaText.slice(0, 700)}...`;
1781
+ }
1782
+ return {
1783
+ agenda: agendaText,
1784
+ agendaSource: selected.path,
1785
+ ctxpackFileCount: list.length,
1786
+ };
1787
+ }
1788
+
1789
+ function sanitizeCtxpackRelativePath(rawPath) {
1790
+ const input = String(rawPath || "").trim().replace(/\\/g, "/");
1791
+ if (!input) return "";
1792
+ const withoutLeadingSlash = input.replace(/^\/+/, "");
1793
+ const normalized = path.posix.normalize(withoutLeadingSlash);
1794
+ if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) {
1795
+ return "";
1796
+ }
1797
+ return normalized;
1798
+ }
1799
+
1800
+ function normalizeCtxpackFiles(files) {
1801
+ const list = Array.isArray(files) ? files : [];
1802
+ const out = [];
1803
+ for (const file of list) {
1804
+ const relPath = sanitizeCtxpackRelativePath(file?.path);
1805
+ if (!relPath) continue;
1806
+ out.push({
1807
+ path: relPath,
1808
+ docType: String(file?.doc_type || "").trim(),
1809
+ content: String(file?.content || ""),
1810
+ });
1811
+ }
1812
+ return out;
1813
+ }
1814
+
1815
+ function loadCtxpackMeta(metaPath) {
1816
+ try {
1817
+ if (!fs.existsSync(metaPath)) return null;
1818
+ const raw = fs.readFileSync(metaPath, "utf8");
1819
+ const parsed = JSON.parse(raw);
1820
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
1821
+ return parsed;
1822
+ } catch {
1823
+ return null;
1824
+ }
1825
+ }
1826
+
1827
+ function hasAllCtxpackFiles(baseDir, files) {
1828
+ for (const file of files) {
1829
+ const target = path.join(baseDir, file.path);
1830
+ if (!fs.existsSync(target)) return false;
1831
+ }
1832
+ return true;
1833
+ }
1834
+
1835
+ function syncCtxpackToLocalCache({ siteBaseURL, projectID, ctxpack }) {
1836
+ const ctxpackID = String(ctxpack?.ctxpack_id || "").trim();
1837
+ const version = String(ctxpack?.version || "").trim();
1838
+ const status = String(ctxpack?.status || "").trim() || "draft";
1839
+ const files = normalizeCtxpackFiles(ctxpack?.files);
1840
+ const cacheDir = path.join(ctxpackCacheRootDir(), projectID);
1841
+ const metaPath = path.join(cacheDir, CTXPACK_META_FILENAME);
1842
+
1843
+ if (!ctxpackID || !version || files.length === 0) {
1844
+ return {
1845
+ sync_status: "not_available",
1846
+ sync_message: "Ctxpack is missing or has no files on server.",
1847
+ local_path: cacheDir,
1848
+ local_file_count: 0,
1849
+ };
1850
+ }
1851
+
1852
+ const remoteSig = `${ctxpackID}|${version}|${status}|${files.length}`;
1853
+ const previousMeta = loadCtxpackMeta(metaPath);
1854
+ const previousSig = previousMeta
1855
+ ? `${String(previousMeta.ctxpack_id || "").trim()}|${String(previousMeta.version || "").trim()}|${String(previousMeta.status || "").trim()}|${Number.parseInt(String(previousMeta.files_count || 0), 10) || 0}`
1856
+ : "";
1857
+
1858
+ if (previousSig === remoteSig && hasAllCtxpackFiles(cacheDir, files)) {
1859
+ return {
1860
+ sync_status: "current",
1861
+ sync_message: "Local ctxpack cache is up to date.",
1862
+ local_path: cacheDir,
1863
+ local_file_count: files.length,
1864
+ };
1865
+ }
1866
+
1867
+ try {
1868
+ fs.rmSync(cacheDir, { recursive: true, force: true });
1869
+ fs.mkdirSync(cacheDir, { recursive: true });
1870
+
1871
+ for (const file of files) {
1872
+ const target = path.join(cacheDir, file.path);
1873
+ ensureParentDir(target);
1874
+ fs.writeFileSync(target, file.content, "utf8");
1875
+ }
1876
+
1877
+ const payload = {
1878
+ project_id: projectID,
1879
+ ctxpack_id: ctxpackID,
1880
+ version,
1881
+ status,
1882
+ files_count: files.length,
1883
+ files: files.map((f) => ({ path: f.path, doc_type: f.docType || "" })),
1884
+ source: `${String(siteBaseURL || "").replace(/\/+$/, "")}/api/v1/projects/${encodeURIComponent(projectID)}/ctxpack`,
1885
+ synced_at: new Date().toISOString(),
1886
+ };
1887
+ fs.writeFileSync(metaPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1888
+
1889
+ return {
1890
+ sync_status: previousMeta ? "updated" : "downloaded",
1891
+ sync_message: previousMeta
1892
+ ? "Ctxpack updated to latest server version."
1893
+ : "Ctxpack downloaded to local cache.",
1894
+ local_path: cacheDir,
1895
+ local_file_count: files.length,
1896
+ };
1897
+ } catch (err) {
1898
+ return {
1899
+ sync_status: "error",
1900
+ sync_message: String(err?.message || err),
1901
+ local_path: cacheDir,
1902
+ local_file_count: 0,
1903
+ };
1904
+ }
1905
+ }
1906
+
1907
+ async function loadProjectSummaryForTool({
1908
+ siteBaseURL,
1909
+ projectID,
1910
+ token,
1911
+ timeoutSeconds,
1912
+ includeCtxpack,
1913
+ syncCtxpackLocal,
1914
+ }) {
1915
+ const encodedProjectID = encodeURIComponent(projectID);
1916
+ let projectRaw = null;
1917
+ try {
1918
+ projectRaw = await getJSONWithAuth(`${siteBaseURL}/api/v1/projects/${encodedProjectID}`, timeoutSeconds, token);
1919
+ } catch (err) {
1920
+ const statusCode = Number(err?.statusCode || 0);
1921
+ const access =
1922
+ statusCode === 401
1923
+ ? "unauthorized"
1924
+ : statusCode === 403
1925
+ ? "denied"
1926
+ : statusCode === 404
1927
+ ? "not_found"
1928
+ : "error";
1929
+ const message =
1930
+ access === "unauthorized"
1931
+ ? "Token is invalid or expired. Please run auth login again."
1932
+ : access === "denied"
1933
+ ? "Your token account has no access to this project."
1934
+ : access === "not_found"
1935
+ ? "Project not found or not visible with current permissions."
1936
+ : "Failed to read project summary from server.";
1937
+ return {
1938
+ project_id: projectID,
1939
+ access,
1940
+ status_code: statusCode || 500,
1941
+ message,
1942
+ can_access: false,
1943
+ };
1944
+ }
1945
+ const project = safeObject(projectRaw);
1946
+
1947
+ let agenda = "";
1948
+ let agendaSource = "";
1949
+ let ctxpackID = "";
1950
+ let ctxpackVersion = "";
1951
+ let ctxpackFileCount = 0;
1952
+ let ctxpackSyncStatus = includeCtxpack && syncCtxpackLocal ? "not_attempted" : "disabled";
1953
+ let ctxpackSyncMessage = "";
1954
+ let ctxpackLocalPath = "";
1955
+ let ctxpackLocalFileCount = 0;
1956
+ if (includeCtxpack) {
1957
+ try {
1958
+ const ctxpackRaw = await getJSONWithAuth(`${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack`, timeoutSeconds, token);
1959
+ const ctxpack = safeObject(ctxpackRaw);
1960
+ const extracted = extractAgendaFromFiles(ctxpack.files);
1961
+ agenda = extracted.agenda;
1962
+ agendaSource = extracted.agendaSource;
1963
+ ctxpackFileCount = extracted.ctxpackFileCount;
1964
+ ctxpackID = String(ctxpack.ctxpack_id || "").trim();
1965
+ ctxpackVersion = String(ctxpack.version || "").trim();
1966
+ if (syncCtxpackLocal) {
1967
+ const syncResult = syncCtxpackToLocalCache({
1968
+ siteBaseURL,
1969
+ projectID,
1970
+ ctxpack,
1971
+ });
1972
+ ctxpackSyncStatus = syncResult.sync_status || "error";
1973
+ ctxpackSyncMessage = String(syncResult.sync_message || "").trim();
1974
+ ctxpackLocalPath = String(syncResult.local_path || "").trim();
1975
+ ctxpackLocalFileCount = Number(syncResult.local_file_count || 0);
1976
+ }
1977
+ } catch {
1978
+ // ctxpack may be missing; keep project summary anyway.
1979
+ if (syncCtxpackLocal) {
1980
+ ctxpackSyncStatus = "not_available";
1981
+ ctxpackSyncMessage = "Ctxpack is not available for this project.";
1982
+ }
1983
+ }
1984
+ }
1985
+
1986
+ return {
1987
+ project_id: String(project.id || projectID),
1988
+ access: "granted",
1989
+ status_code: 200,
1990
+ message: "Project access granted",
1991
+ can_access: true,
1992
+ name: String(project.name || "").trim(),
1993
+ org_id: String(project.org_id || "").trim(),
1994
+ org_name: String(project.org_name || "").trim(),
1995
+ visibility: String(project.visibility || "").trim() || "private",
1996
+ template: String(project.template || "").trim() || "blank",
1997
+ template_label: normalizeTemplateLabel(project.template),
1998
+ owner_name: String(project.owner_name || "").trim(),
1999
+ program_owner_name: String(project.program_owner_name || "").trim(),
2000
+ tech_owner_name: String(project.tech_owner_name || "").trim(),
2001
+ governance_owner_name: String(project.governance_owner_name || "").trim(),
2002
+ member_count: Number(project.member_count || 0),
2003
+ created_at: String(project.created_at || "").trim(),
2004
+ ctxpack_id: ctxpackID,
2005
+ ctxpack_version: ctxpackVersion,
2006
+ ctxpack_file_count: ctxpackFileCount,
2007
+ ctxpack_sync_status: ctxpackSyncStatus,
2008
+ ctxpack_sync_message: ctxpackSyncMessage,
2009
+ ctxpack_local_path: ctxpackLocalPath,
2010
+ ctxpack_local_file_count: ctxpackLocalFileCount,
2011
+ agenda,
2012
+ agenda_source: agendaSource,
2013
+ };
2014
+ }
2015
+
2016
+ function buildProjectSummaryText(summary) {
2017
+ if (String(summary?.access || "") !== "granted") {
2018
+ return [
2019
+ `Project ID: ${summary?.project_id || "-"}`,
2020
+ `Access: ${summary?.access || "error"}`,
2021
+ `Status: ${summary?.status_code || "-"}`,
2022
+ `${summary?.message || "Unable to access project summary."}`,
2023
+ ].join("\n");
2024
+ }
2025
+
2026
+ const lines = [
2027
+ `Project ID: ${summary.project_id || "-"}`,
2028
+ `Name: ${summary.name || "-"}`,
2029
+ `Organization: ${summary.org_name || summary.org_id || "-"}`,
2030
+ `Visibility: ${summary.visibility || "-"}`,
2031
+ `Template: ${summary.template_label || summary.template || "-"}`,
2032
+ `Owner: ${summary.owner_name || "-"}`,
2033
+ `Program Owner: ${summary.program_owner_name || "-"}`,
2034
+ `Tech Owner: ${summary.tech_owner_name || "-"}`,
2035
+ `Governance Owner: ${summary.governance_owner_name || "-"}`,
2036
+ `Member Count: ${Number(summary.member_count || 0)}`,
2037
+ ];
2038
+ if (summary.ctxpack_id) {
2039
+ lines.push(`Ctxpack: ${summary.ctxpack_id}${summary.ctxpack_version ? ` (v${summary.ctxpack_version})` : ""}`);
2040
+ }
2041
+ if (summary.ctxpack_sync_status && summary.ctxpack_sync_status !== "disabled") {
2042
+ lines.push(
2043
+ `Ctxpack Sync: ${summary.ctxpack_sync_status}${summary.ctxpack_local_path ? ` (${summary.ctxpack_local_path})` : ""}`,
2044
+ );
2045
+ if (summary.ctxpack_sync_message) {
2046
+ lines.push(`Ctxpack Sync Message: ${summary.ctxpack_sync_message}`);
2047
+ }
2048
+ }
2049
+ if (summary.agenda) {
2050
+ lines.push("");
2051
+ lines.push(`Agenda (${summary.agenda_source || "ctxpack"}):`);
2052
+ lines.push(summary.agenda);
2053
+ }
2054
+ return lines.join("\n");
2055
+ }
2056
+
2057
+ function parseGatewayResponseText(text) {
2058
+ try {
2059
+ const obj = JSON.parse(String(text || ""));
2060
+ return safeObject(obj);
2061
+ } catch {
2062
+ return null;
2063
+ }
2064
+ }
2065
+
2066
+ function ensureArray(value) {
2067
+ return Array.isArray(value) ? value : [];
2068
+ }
2069
+
2070
+ function appendLocalToolToToolsList(responseObj) {
2071
+ const result = safeObject(responseObj.result);
2072
+ const tools = ensureArray(result.tools);
2073
+ const localSpecs = buildLocalToolSpecs();
2074
+ const localNames = new Set(localSpecs.map((spec) => spec.name));
2075
+ const filtered = tools.filter((tool) => !localNames.has(String(tool?.name || "").trim()));
2076
+ result.tools = [...localSpecs, ...filtered];
2077
+ responseObj.result = result;
2078
+ return responseObj;
2079
+ }
2080
+
2081
+ function appendProjectHintToInitialize(responseObj, args) {
2082
+ const result = safeObject(responseObj.result);
2083
+ const hintLines = [
2084
+ "High-priority project bootstrap rule:",
2085
+ "- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
2086
+ "- `project.describe` and `project.get` are aliases of `project.summary`.",
2087
+ "- After project summary, use workitem/evidence/decision tools as follow-up.",
2088
+ ];
2089
+ if (args.projectID) {
2090
+ hintLines.splice(1, 0, `- Default project_id is ${args.projectID}.`);
2091
+ }
2092
+ const hint = `${hintLines.join("\n")}\n`;
2093
+ const current = String(result.instructions || "").trimEnd();
2094
+ result.instructions = current ? `${hint}\n${current}` : hint;
2095
+ responseObj.result = result;
2096
+ return responseObj;
2097
+ }
2098
+
2099
+ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2100
+ const result = safeObject(responseObj.result);
2101
+ const content = ensureArray(result.content);
2102
+ if (!content.length) return responseObj;
2103
+
2104
+ const first = safeObject(content[0]);
2105
+ if (String(first.type || "") !== "text") return responseObj;
2106
+
2107
+ const text = String(first.text || "");
2108
+ if (!text) return responseObj;
2109
+ if (text.includes("Call `project.summary`")) return responseObj;
2110
+
2111
+ const parsed = parseGatewayResponseText(text);
2112
+ if (!parsed) return responseObj;
2113
+
2114
+ const tool = String(parsed.tool || "").trim();
2115
+ const status = Number(parsed.status || 0);
2116
+ const ok = Boolean(parsed.ok);
2117
+ const body = Array.isArray(parsed.body) ? parsed.body : [];
2118
+ const isEmptyBody = body.length === 0;
2119
+ if (tool !== "workitem.list" || status !== 200 || !ok) {
2120
+ return responseObj;
2121
+ }
2122
+
2123
+ const nextLines = [""];
2124
+ if (isEmptyBody) {
2125
+ nextLines.push("No work items found for this project.");
2126
+ nextLines.push("- Call `project.summary` to confirm project context/agenda and access state first.");
2127
+ }
2128
+
2129
+ let responseProjectID = "";
2130
+ try {
2131
+ const responseURL = String(parsed.url || "").trim();
2132
+ if (responseURL) {
2133
+ responseProjectID = String(new URL(responseURL).searchParams.get("project_id") || "").trim();
2134
+ }
2135
+ } catch {
2136
+ // ignore malformed URL in text payload
2137
+ }
2138
+
2139
+ const projectID = String(
2140
+ toolArgs?.project_id || toolArgs?.projectID || responseProjectID || args.projectID || "",
2141
+ ).trim();
2142
+
2143
+ if (projectID && isUUID(projectID)) {
2144
+ try {
2145
+ const summary = await loadProjectSummaryForTool({
2146
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
2147
+ projectID,
2148
+ token,
2149
+ timeoutSeconds: args.timeoutSeconds,
2150
+ includeCtxpack: isEmptyBody,
2151
+ syncCtxpackLocal: isEmptyBody,
2152
+ });
2153
+ if (String(summary.access || "") === "granted") {
2154
+ nextLines.push("Project context:");
2155
+ nextLines.push(`- Name: ${summary.name || "-"}`);
2156
+ nextLines.push(`- Organization: ${summary.org_name || summary.org_id || "-"}`);
2157
+ nextLines.push(`- Visibility: ${summary.visibility || "-"}`);
2158
+ nextLines.push(`- Template: ${summary.template_label || summary.template || "-"}`);
2159
+ if (isEmptyBody && summary.ctxpack_sync_status) {
2160
+ nextLines.push(`- Ctxpack Sync: ${summary.ctxpack_sync_status}`);
2161
+ if (summary.ctxpack_sync_message) {
2162
+ nextLines.push(`- Ctxpack Sync Message: ${summary.ctxpack_sync_message}`);
2163
+ }
2164
+ }
2165
+ } else {
2166
+ nextLines.push("Project access status:");
2167
+ nextLines.push(`- Access: ${summary.access || "error"}`);
2168
+ nextLines.push(`- Status: ${summary.status_code || "-"}`);
2169
+ }
2170
+ } catch {
2171
+ // keep gateway response even when side lookup fails
2172
+ }
2173
+ } else if (projectID) {
2174
+ nextLines.push(`Current project_id: ${projectID}`);
2175
+ nextLines.push("- project_id format is invalid. Expected UUID.");
2176
+ }
2177
+
2178
+ if (nextLines.length <= 1) return responseObj;
2179
+
2180
+ content[0] = {
2181
+ ...first,
2182
+ text: `${text}\n${nextLines.join("\n")}`,
2183
+ };
2184
+ result.content = content;
2185
+ responseObj.result = result;
2186
+ return responseObj;
2187
+ }
2188
+
1262
2189
  async function runProxy(flags) {
1263
2190
  const workspaceMeta = loadWorkspaceMeta(process.cwd());
1264
2191
  const args = {
@@ -1327,14 +2254,80 @@ async function runProxy(flags) {
1327
2254
  return;
1328
2255
  }
1329
2256
 
2257
+ const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
2258
+ if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
2259
+ const projectID = String(toolArgs.project_id || toolArgs.projectID || args.projectID || "").trim();
2260
+ if (!projectID) {
2261
+ process.stdout.write(
2262
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
2263
+ );
2264
+ return;
2265
+ }
2266
+ if (!isUUID(projectID)) {
2267
+ process.stdout.write(
2268
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
2269
+ );
2270
+ return;
2271
+ }
2272
+ try {
2273
+ const includeCtxpack = boolFromRaw(
2274
+ Object.prototype.hasOwnProperty.call(toolArgs, "include_ctxpack")
2275
+ ? toolArgs.include_ctxpack
2276
+ : toolArgs.includeCtxpack,
2277
+ true,
2278
+ );
2279
+ const syncCtxpackLocal = boolFromRaw(
2280
+ Object.prototype.hasOwnProperty.call(toolArgs, "sync_ctxpack_local")
2281
+ ? toolArgs.sync_ctxpack_local
2282
+ : toolArgs.syncCtxpackLocal,
2283
+ true,
2284
+ );
2285
+ const summary = await loadProjectSummaryForTool({
2286
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
2287
+ projectID,
2288
+ token,
2289
+ timeoutSeconds: args.timeoutSeconds,
2290
+ includeCtxpack,
2291
+ syncCtxpackLocal,
2292
+ });
2293
+ const text = buildProjectSummaryText(summary);
2294
+ process.stdout.write(
2295
+ `${JSON.stringify(
2296
+ jsonRpcResult(requestObj, {
2297
+ content: [
2298
+ {
2299
+ type: "text",
2300
+ text,
2301
+ },
2302
+ ],
2303
+ structuredContent: summary,
2304
+ }),
2305
+ )}\n`,
2306
+ );
2307
+ } catch (err) {
2308
+ process.stdout.write(
2309
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
2310
+ );
2311
+ }
2312
+ return;
2313
+ }
2314
+
1330
2315
  try {
1331
- const responseText = await postJSON(
1332
- gatewayURL,
1333
- args.timeoutSeconds,
1334
- token,
1335
- requestObj,
1336
- );
2316
+ const responseText = await postJSON(gatewayURL, args.timeoutSeconds, token, requestObj);
1337
2317
  if (responseText) {
2318
+ const parsed = parseGatewayResponseText(responseText);
2319
+ if (parsed && !parsed.error) {
2320
+ let patched = parsed;
2321
+ if (isJsonRpcMethod(requestObj, "tools/list")) {
2322
+ patched = appendLocalToolToToolsList(patched);
2323
+ } else if (isJsonRpcMethod(requestObj, "initialize")) {
2324
+ patched = appendProjectHintToInitialize(patched, args);
2325
+ } else if (isJsonRpcMethod(requestObj, "tools/call") && toolName === "workitem.list") {
2326
+ patched = await appendWorkitemListHints(patched, args, toolArgs, token);
2327
+ }
2328
+ process.stdout.write(`${JSON.stringify(patched)}\n`);
2329
+ return;
2330
+ }
1338
2331
  process.stdout.write(`${responseText}\n`);
1339
2332
  return;
1340
2333
  }
@@ -1517,10 +2510,18 @@ async function main() {
1517
2510
  runSetup(flags);
1518
2511
  return;
1519
2512
  }
2513
+ if (command === "doctor") {
2514
+ await runDoctor(flags);
2515
+ return;
2516
+ }
1520
2517
  if (command === "auth") {
1521
2518
  await runAuth(rest);
1522
2519
  return;
1523
2520
  }
2521
+ if (command === "ctxpack") {
2522
+ await runCtxpack(rest);
2523
+ return;
2524
+ }
1524
2525
  if (command === "proxy") {
1525
2526
  await runProxy(flags);
1526
2527
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [