metheus-governance-mcp-cli 0.2.8 → 0.2.10

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 +9 -1
  2. package/cli.mjs +853 -34
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -17,7 +17,7 @@ Compatibility note: legacy command alias `metheus-governance-mcp` is still suppo
17
17
  ## Install
18
18
 
19
19
  ```bash
20
- npm install -g metheus-governance-mcp-cli
20
+ npm install -g metheus-governance-mcp-cli@latest
21
21
  ```
22
22
 
23
23
  ## One command bootstrap (recommended)
@@ -61,6 +61,8 @@ Local bootstrap tools exposed by proxy:
61
61
  - `project.summary`
62
62
  - `project.describe` (alias)
63
63
  - `project.get` (alias)
64
+ - `ctxpack.merge.brief`
65
+ - `ctxpack.merge.execute`
64
66
 
65
67
  These tools accept `project_id` and return:
66
68
 
@@ -74,6 +76,12 @@ These tools accept `project_id` and return:
74
76
  - newer server version -> update local cache
75
77
  - workspace path -> follows active client workspace/root when provided by Codex/Claude
76
78
 
79
+ Ctxpack merge safety flow:
80
+ - call `ctxpack.merge.brief` first
81
+ - review who changed what (`created_by`, `changed_paths`, metadata summary)
82
+ - get explicit owner decision in chat
83
+ - then call `ctxpack.merge.execute` with `owner_confirmation`
84
+
77
85
  Manual ctxpack pull/update:
78
86
 
79
87
  ```bash
package/cli.mjs CHANGED
@@ -140,11 +140,41 @@ function firstNonEmptyString(values) {
140
140
  return "";
141
141
  }
142
142
 
143
+ function extractWorkspaceCandidateFromFolders(rawFolders) {
144
+ if (!Array.isArray(rawFolders)) return "";
145
+ for (const folder of rawFolders) {
146
+ if (typeof folder === "string") {
147
+ const direct = firstNonEmptyString([fileURIToLocalPath(folder), folder]);
148
+ if (direct) return resolveWorkspaceDir(direct);
149
+ continue;
150
+ }
151
+ if (!folder || typeof folder !== "object" || Array.isArray(folder)) continue;
152
+ const uriValue = firstNonEmptyString([
153
+ folder.uri,
154
+ folder.root_uri,
155
+ folder.rootUri,
156
+ folder.path,
157
+ folder.root_path,
158
+ folder.rootPath,
159
+ ]);
160
+ const candidate = firstNonEmptyString([fileURIToLocalPath(uriValue), uriValue]);
161
+ if (candidate) return resolveWorkspaceDir(candidate);
162
+ }
163
+ return "";
164
+ }
165
+
143
166
  function extractWorkspaceCandidateFromRequest(requestObj, toolArgs) {
144
167
  const params = safeObject(requestObj?.params);
145
168
  const meta = safeObject(params._meta);
169
+ const workspaceFromFolders = firstNonEmptyString([
170
+ extractWorkspaceCandidateFromFolders(params.workspace_folders),
171
+ extractWorkspaceCandidateFromFolders(params.workspaceFolders),
172
+ extractWorkspaceCandidateFromFolders(meta.workspace_folders),
173
+ extractWorkspaceCandidateFromFolders(meta.workspaceFolders),
174
+ ]);
146
175
  const args = safeObject(toolArgs);
147
176
  const rawCandidate = firstNonEmptyString([
177
+ workspaceFromFolders,
148
178
  args.workspace_dir,
149
179
  args.workspaceDir,
150
180
  params.workspace_dir,
@@ -172,10 +202,12 @@ function extractWorkspaceCandidateFromEnv() {
172
202
  const rawCandidate = firstNonEmptyString([
173
203
  process.env.METHEUS_WORKSPACE_DIR,
174
204
  process.env.CLAUDE_WORKSPACE_DIR,
205
+ process.env.CLAUDE_WORKSPACE_URI,
175
206
  process.env.CLAUDE_PROJECT_DIR,
176
207
  process.env.CODEX_WORKSPACE_DIR,
177
208
  process.env.WORKSPACE_DIR,
178
209
  process.env.WORKSPACE_FOLDER,
210
+ process.env.VSCODE_WORKSPACE_FOLDER,
179
211
  process.env.VSCODE_CWD,
180
212
  process.env.PWD,
181
213
  process.env.INIT_CWD,
@@ -198,6 +230,25 @@ function resolveWorkspaceDirForRequest(defaultWorkspaceDir, requestObj, toolArgs
198
230
  return resolveWorkspaceDir(candidate);
199
231
  }
200
232
 
233
+ function resolveProjectIDForRequest({
234
+ toolArgs,
235
+ args,
236
+ workspaceDir,
237
+ responseProjectID,
238
+ envelopeProjectID,
239
+ }) {
240
+ const resolvedWorkspaceDir = resolveWorkspaceDir(workspaceDir || process.cwd());
241
+ const workspaceMeta = loadWorkspaceMeta(resolvedWorkspaceDir);
242
+ return firstNonEmptyString([
243
+ toolArgs?.project_id,
244
+ toolArgs?.projectID,
245
+ responseProjectID,
246
+ envelopeProjectID,
247
+ workspaceMeta.project_id,
248
+ args?.projectID,
249
+ ]);
250
+ }
251
+
201
252
  function homeCtxpackCacheRootDir() {
202
253
  const home = String(process.env.USERPROFILE || process.env.HOME || "").trim();
203
254
  if (!home) {
@@ -1411,6 +1462,19 @@ function addDoctorCheck(rows, status, label, detail) {
1411
1462
  });
1412
1463
  }
1413
1464
 
1465
+ function isUnauthorizedLike(rawText) {
1466
+ const text = String(rawText || "").trim().toLowerCase();
1467
+ if (!text) return false;
1468
+ return (
1469
+ text.includes("unauthorized") ||
1470
+ text.includes("access denied") ||
1471
+ text.includes("status 401") ||
1472
+ text.includes("status 403") ||
1473
+ text.includes("http 401") ||
1474
+ text.includes("http 403")
1475
+ );
1476
+ }
1477
+
1414
1478
  function statusIcon(status) {
1415
1479
  if (status === "ok") return "OK";
1416
1480
  if (status === "fail") return "FAIL";
@@ -1695,12 +1759,25 @@ async function runDoctor(flags) {
1695
1759
  addDoctorCheck(rows, "ok", "server mode", "ctxpack writable");
1696
1760
  }
1697
1761
  } catch (err) {
1698
- addDoctorCheck(
1699
- rows,
1700
- "warn",
1701
- "server ctxpack policy",
1702
- `unable to read /ctxpack/stats (${String(err?.message || err)})`,
1703
- );
1762
+ const statusCode = Number(err?.statusCode || 0);
1763
+ const message = String(err?.message || err);
1764
+ if (statusCode === 404) {
1765
+ addDoctorCheck(
1766
+ rows,
1767
+ "ok",
1768
+ "server ctxpack policy",
1769
+ "ctxpack/stats endpoint is not exposed in this deployment (optional check skipped)",
1770
+ );
1771
+ } else if (statusCode === 401 || statusCode === 403 || isUnauthorizedLike(message)) {
1772
+ addDoctorCheck(
1773
+ rows,
1774
+ "warn",
1775
+ "server ctxpack policy",
1776
+ "no permission to read ctxpack/stats in this environment",
1777
+ );
1778
+ } else {
1779
+ addDoctorCheck(rows, "warn", "server ctxpack policy", `unable to read /ctxpack/stats (${message})`);
1780
+ }
1704
1781
  }
1705
1782
  }
1706
1783
 
@@ -1736,7 +1813,17 @@ async function runDoctor(flags) {
1736
1813
  },
1737
1814
  });
1738
1815
  if (!rpc.ok) {
1739
- addDoctorCheck(rows, "fail", `smoke ${row.tool}`, rpc.error || "rpc error");
1816
+ const errorText = String(rpc.error || "rpc error");
1817
+ if (isUnauthorizedLike(errorText)) {
1818
+ addDoctorCheck(
1819
+ rows,
1820
+ "warn",
1821
+ `smoke ${row.tool}`,
1822
+ "unauthorized for this tool with current token/project role",
1823
+ );
1824
+ } else {
1825
+ addDoctorCheck(rows, "fail", `smoke ${row.tool}`, errorText);
1826
+ }
1740
1827
  continue;
1741
1828
  }
1742
1829
  const envelope = parseToolEnvelopeFromRPCResult(safeObject(rpc.response).result);
@@ -1749,12 +1836,21 @@ async function runDoctor(flags) {
1749
1836
  if (ok && status >= 200 && status < 300) {
1750
1837
  addDoctorCheck(rows, "ok", `smoke ${row.tool}`, `status ${status}`);
1751
1838
  } else {
1752
- addDoctorCheck(
1753
- rows,
1754
- "fail",
1755
- `smoke ${row.tool}`,
1756
- `status ${status || "-"}, ok=${ok ? "true" : "false"}`,
1757
- );
1839
+ if (status === 401 || status === 403) {
1840
+ addDoctorCheck(
1841
+ rows,
1842
+ "warn",
1843
+ `smoke ${row.tool}`,
1844
+ `status ${status}: unauthorized for this tool with current token/project role`,
1845
+ );
1846
+ } else {
1847
+ addDoctorCheck(
1848
+ rows,
1849
+ "fail",
1850
+ `smoke ${row.tool}`,
1851
+ `status ${status || "-"}, ok=${ok ? "true" : "false"}`,
1852
+ );
1853
+ }
1758
1854
  }
1759
1855
  }
1760
1856
 
@@ -1801,7 +1897,10 @@ function postJSON(urlText, timeoutSeconds, token, payload) {
1801
1897
  resolve(text.trim());
1802
1898
  return;
1803
1899
  }
1804
- reject(new Error(text.trim() || `http ${statusCode}`));
1900
+ const err = new Error(text.trim() || `http ${statusCode}`);
1901
+ err.statusCode = statusCode;
1902
+ err.responseBody = text;
1903
+ reject(err);
1805
1904
  });
1806
1905
  },
1807
1906
  );
@@ -1887,6 +1986,7 @@ function jsonRpcResult(requestObj, result) {
1887
1986
  }
1888
1987
 
1889
1988
  const LOCAL_PROJECT_TOOL_NAMES = ["project.summary", "project.describe", "project.get"];
1989
+ const LOCAL_CTXPACK_MERGE_TOOL_NAMES = ["ctxpack.merge.brief", "ctxpack.merge.execute"];
1890
1990
 
1891
1991
  function buildProjectSummaryInputSchema() {
1892
1992
  return {
@@ -1910,6 +2010,62 @@ function buildProjectSummaryInputSchema() {
1910
2010
  };
1911
2011
  }
1912
2012
 
2013
+ function buildCtxpackMergeBriefInputSchema() {
2014
+ return {
2015
+ type: "object",
2016
+ properties: {
2017
+ project_id: {
2018
+ type: "string",
2019
+ description: "Project UUID. If omitted, current configured project_id is used.",
2020
+ },
2021
+ status: {
2022
+ type: "string",
2023
+ description: "Merge-request status filter. Default: open.",
2024
+ enum: ["open", "review", "approved", "rejected", "merged", "closed", "all"],
2025
+ },
2026
+ limit: {
2027
+ type: "integer",
2028
+ minimum: 1,
2029
+ maximum: 100,
2030
+ description: "Maximum merge requests to inspect. Default: 20.",
2031
+ },
2032
+ },
2033
+ additionalProperties: false,
2034
+ };
2035
+ }
2036
+
2037
+ function buildCtxpackMergeExecuteInputSchema() {
2038
+ return {
2039
+ type: "object",
2040
+ properties: {
2041
+ project_id: {
2042
+ type: "string",
2043
+ description: "Project UUID. If omitted, current configured project_id is used.",
2044
+ },
2045
+ merge_request_id: {
2046
+ type: "string",
2047
+ description: "Target merge request UUID.",
2048
+ },
2049
+ action: {
2050
+ type: "string",
2051
+ description: "Action to execute.",
2052
+ enum: ["review", "approve", "reject", "close", "merge"],
2053
+ },
2054
+ owner_confirmation: {
2055
+ type: "string",
2056
+ description:
2057
+ "Explicit owner confirmation text from chat (for safety). Example: confirm / approved / yes.",
2058
+ },
2059
+ note: {
2060
+ type: "string",
2061
+ description: "Optional reviewer note recorded in metadata for non-merge actions.",
2062
+ },
2063
+ },
2064
+ required: ["merge_request_id", "action", "owner_confirmation"],
2065
+ additionalProperties: false,
2066
+ };
2067
+ }
2068
+
1913
2069
  function buildLocalToolSpecs() {
1914
2070
  return [
1915
2071
  {
@@ -1930,6 +2086,18 @@ function buildLocalToolSpecs() {
1930
2086
  "Alias of project.summary. Returns project metadata and access status for the given project_id.",
1931
2087
  inputSchema: buildProjectSummaryInputSchema(),
1932
2088
  },
2089
+ {
2090
+ name: "ctxpack.merge.brief",
2091
+ description:
2092
+ "Before merge, summarize pending ctxpack merge requests (who changed what), show risk/recommendation, and provide owner confirmation checklist.",
2093
+ inputSchema: buildCtxpackMergeBriefInputSchema(),
2094
+ },
2095
+ {
2096
+ name: "ctxpack.merge.execute",
2097
+ description:
2098
+ "Execute ctxpack merge-request action (review/approve/reject/close/merge) after explicit owner confirmation.",
2099
+ inputSchema: buildCtxpackMergeExecuteInputSchema(),
2100
+ },
1933
2101
  ];
1934
2102
  }
1935
2103
 
@@ -2233,6 +2401,10 @@ async function loadProjectSummaryForTool({
2233
2401
  visibility: String(project.visibility || "").trim() || "private",
2234
2402
  template: String(project.template || "").trim() || "blank",
2235
2403
  template_label: normalizeTemplateLabel(project.template),
2404
+ owner_user_id: String(project.owner_user_id || "").trim(),
2405
+ program_owner_user_id: String(project.program_owner_user_id || "").trim(),
2406
+ tech_owner_user_id: String(project.tech_owner_user_id || "").trim(),
2407
+ governance_owner_user_id: String(project.governance_owner_user_id || "").trim(),
2236
2408
  owner_name: String(project.owner_name || "").trim(),
2237
2409
  program_owner_name: String(project.program_owner_name || "").trim(),
2238
2410
  tech_owner_name: String(project.tech_owner_name || "").trim(),
@@ -2255,6 +2427,522 @@ async function loadProjectSummaryForTool({
2255
2427
  };
2256
2428
  }
2257
2429
 
2430
+ function normalizeMergeStatusFilter(rawValue) {
2431
+ const value = String(rawValue || "").trim().toLowerCase();
2432
+ if (!value) return "open";
2433
+ if (["open", "review", "approved", "rejected", "merged", "closed", "all"].includes(value)) {
2434
+ return value;
2435
+ }
2436
+ return "";
2437
+ }
2438
+
2439
+ function normalizeMergeAction(rawValue) {
2440
+ const value = String(rawValue || "").trim().toLowerCase();
2441
+ if (["review", "approve", "reject", "close", "merge"].includes(value)) {
2442
+ return value;
2443
+ }
2444
+ return "";
2445
+ }
2446
+
2447
+ function parseMergeMetadata(rawMetadata) {
2448
+ if (!rawMetadata) return {};
2449
+ if (typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) {
2450
+ return rawMetadata;
2451
+ }
2452
+ if (typeof rawMetadata !== "string") return {};
2453
+ try {
2454
+ const parsed = JSON.parse(rawMetadata);
2455
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
2456
+ return parsed;
2457
+ } catch {
2458
+ return {};
2459
+ }
2460
+ }
2461
+
2462
+ function collectMergeChangedPaths(metadataObj) {
2463
+ const meta = safeObject(metadataObj);
2464
+ const out = [];
2465
+ const pushPath = (raw) => {
2466
+ const value = String(raw || "").trim();
2467
+ if (!value) return;
2468
+ if (!out.includes(value)) out.push(value);
2469
+ };
2470
+ for (const pathValue of ensureArray(meta.changed_paths)) {
2471
+ pushPath(pathValue);
2472
+ }
2473
+ for (const pathValue of ensureArray(meta.conflict_paths)) {
2474
+ pushPath(pathValue);
2475
+ }
2476
+ const files = ensureArray(meta.files);
2477
+ for (const file of files) {
2478
+ pushPath(safeObject(file).path);
2479
+ }
2480
+ const changeSet = safeObject(meta.change_set);
2481
+ const changeSetFiles = ensureArray(changeSet.files);
2482
+ for (const file of changeSetFiles) {
2483
+ pushPath(safeObject(file).path);
2484
+ }
2485
+ return out;
2486
+ }
2487
+
2488
+ function summarizeMergeMetadata(metadataObj) {
2489
+ const meta = safeObject(metadataObj);
2490
+ const parts = [];
2491
+ const changeSet = safeObject(meta.change_set);
2492
+ const summary = safeObject(changeSet.summary);
2493
+ const changedCount = Number.parseInt(String(summary.changed || 0), 10);
2494
+ if (Number.isFinite(changedCount) && changedCount > 0) {
2495
+ parts.push(`changed files: ${changedCount}`);
2496
+ }
2497
+ const title = String(meta.title || meta.summary || meta.reason || "").trim();
2498
+ if (title) {
2499
+ parts.push(title);
2500
+ }
2501
+ if (!parts.length) return "";
2502
+ return parts.join(" | ");
2503
+ }
2504
+
2505
+ function parseISOTime(rawValue) {
2506
+ const text = String(rawValue || "").trim();
2507
+ if (!text) return 0;
2508
+ const ms = Date.parse(text);
2509
+ if (!Number.isFinite(ms)) return 0;
2510
+ return ms;
2511
+ }
2512
+
2513
+ function extractActorFromToken(token) {
2514
+ const payload = safeObject(decodeJwtPayload(token));
2515
+ const userID = firstNonEmptyString([payload.sub, payload.user_id, payload.uid]);
2516
+ const email = firstNonEmptyString([payload.email, payload.preferred_username]);
2517
+ const name = firstNonEmptyString([payload.name, payload.given_name]);
2518
+ return { user_id: userID, email, name };
2519
+ }
2520
+
2521
+ async function loadProjectMemberMap({ siteBaseURL, projectID, token, timeoutSeconds }) {
2522
+ const encodedProjectID = encodeURIComponent(projectID);
2523
+ try {
2524
+ const raw = await getJSONWithAuth(
2525
+ `${siteBaseURL}/api/v1/projects/${encodedProjectID}/members?limit=500&offset=0`,
2526
+ timeoutSeconds,
2527
+ token,
2528
+ );
2529
+ const members = ensureArray(raw);
2530
+ const map = new Map();
2531
+ for (const rowRaw of members) {
2532
+ const row = safeObject(rowRaw);
2533
+ const userID = String(row.user_id || "").trim();
2534
+ if (!userID) continue;
2535
+ map.set(userID, {
2536
+ user_id: userID,
2537
+ name: String(row.name || "").trim(),
2538
+ email: String(row.email || "").trim(),
2539
+ role: String(row.role || "").trim(),
2540
+ });
2541
+ }
2542
+ return map;
2543
+ } catch {
2544
+ return new Map();
2545
+ }
2546
+ }
2547
+
2548
+ async function loadOrgOwnerUserID({ siteBaseURL, orgID, token, timeoutSeconds }) {
2549
+ const normalizedOrgID = String(orgID || "").trim();
2550
+ if (!normalizedOrgID) return "";
2551
+ try {
2552
+ const raw = await getJSONWithAuth(
2553
+ `${siteBaseURL}/api/v1/orgs/${encodeURIComponent(normalizedOrgID)}`,
2554
+ timeoutSeconds,
2555
+ token,
2556
+ );
2557
+ return String(safeObject(raw).owner_user_id || "").trim();
2558
+ } catch {
2559
+ return "";
2560
+ }
2561
+ }
2562
+
2563
+ function resolveActorDisplay(userID, membersByUserID) {
2564
+ const id = String(userID || "").trim();
2565
+ if (!id) return "-";
2566
+ const row = membersByUserID.get(id);
2567
+ if (!row) return id;
2568
+ const name = String(row.name || "").trim();
2569
+ const email = String(row.email || "").trim();
2570
+ if (name && email) return `${name} <${email}>`;
2571
+ if (name) return name;
2572
+ if (email) return email;
2573
+ return id;
2574
+ }
2575
+
2576
+ function isOwnerConfirmationAccepted(rawValue) {
2577
+ const text = String(rawValue || "").trim().toLowerCase();
2578
+ if (!text) return false;
2579
+ return ["yes", "y", "ok", "approve", "approved", "confirm", "confirmed"].includes(text);
2580
+ }
2581
+
2582
+ async function loadCtxpackMergeBriefForTool({
2583
+ siteBaseURL,
2584
+ projectID,
2585
+ token,
2586
+ timeoutSeconds,
2587
+ statusFilter,
2588
+ limit,
2589
+ workspaceDir,
2590
+ }) {
2591
+ const summary = await loadProjectSummaryForTool({
2592
+ siteBaseURL,
2593
+ projectID,
2594
+ token,
2595
+ timeoutSeconds,
2596
+ includeCtxpack: false,
2597
+ syncCtxpackLocal: false,
2598
+ workspaceDir,
2599
+ });
2600
+ if (String(summary.access || "") !== "granted") {
2601
+ return {
2602
+ project_id: projectID,
2603
+ access: summary.access || "error",
2604
+ status_code: summary.status_code || 500,
2605
+ message: summary.message || "Unable to access project.",
2606
+ merge_requests: [],
2607
+ recommendation: {},
2608
+ };
2609
+ }
2610
+
2611
+ const normalizedStatus = normalizeMergeStatusFilter(statusFilter) || "open";
2612
+ const cappedLimit = Math.max(1, Math.min(100, Number.parseInt(String(limit || 20), 10) || 20));
2613
+ const encodedProjectID = encodeURIComponent(projectID);
2614
+ const statusParam =
2615
+ normalizedStatus === "all" || normalizedStatus === "open"
2616
+ ? ""
2617
+ : `&status=${encodeURIComponent(normalizedStatus)}`;
2618
+ const listURL = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests?limit=${cappedLimit}&offset=0${statusParam}`;
2619
+ let mergeFeatureAvailable = true;
2620
+ let mergeFeatureWarning = "";
2621
+ let allItems = [];
2622
+ try {
2623
+ const rawList = await getJSONWithAuth(listURL, timeoutSeconds, token);
2624
+ allItems = ensureArray(rawList);
2625
+ } catch (err) {
2626
+ const statusCode = Number(err?.statusCode || 0) || 500;
2627
+ if (statusCode === 404) {
2628
+ mergeFeatureAvailable = false;
2629
+ mergeFeatureWarning = "Merge-request API is not enabled on this server yet.";
2630
+ allItems = [];
2631
+ } else {
2632
+ throw err;
2633
+ }
2634
+ }
2635
+ const statusOpenSet = new Set(["open", "review", "approved"]);
2636
+ const filteredItems =
2637
+ normalizedStatus === "open"
2638
+ ? allItems.filter((rowRaw) => statusOpenSet.has(String(safeObject(rowRaw).status || "").trim().toLowerCase()))
2639
+ : allItems;
2640
+
2641
+ const membersByUserID = await loadProjectMemberMap({
2642
+ siteBaseURL,
2643
+ projectID,
2644
+ token,
2645
+ timeoutSeconds,
2646
+ });
2647
+
2648
+ const actor = extractActorFromToken(token);
2649
+ const orgOwnerUserID = await loadOrgOwnerUserID({
2650
+ siteBaseURL,
2651
+ orgID: summary.org_id,
2652
+ token,
2653
+ timeoutSeconds,
2654
+ });
2655
+ const projectOwnerIDs = new Set(
2656
+ [summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
2657
+ .map((value) => String(value || "").trim())
2658
+ .filter(Boolean),
2659
+ );
2660
+ const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
2661
+ const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
2662
+ const ownerGate = isProjectOwner || isOrgOwner;
2663
+
2664
+ const normalizedItems = filteredItems
2665
+ .map((rowRaw) => {
2666
+ const row = safeObject(rowRaw);
2667
+ const metadata = parseMergeMetadata(row.metadata);
2668
+ const changedPaths = collectMergeChangedPaths(metadata);
2669
+ const status = String(row.status || "").trim().toLowerCase();
2670
+ return {
2671
+ merge_request_id: String(row.merge_request_id || "").trim(),
2672
+ status,
2673
+ created_by_user_id: String(row.created_by || "").trim(),
2674
+ created_by: resolveActorDisplay(String(row.created_by || "").trim(), membersByUserID),
2675
+ reviewed_by_user_id: String(row.reviewed_by || "").trim(),
2676
+ reviewed_by: resolveActorDisplay(String(row.reviewed_by || "").trim(), membersByUserID),
2677
+ approved_at: String(row.approved_at || "").trim(),
2678
+ created_at: String(row.created_at || "").trim(),
2679
+ updated_at: String(row.updated_at || "").trim(),
2680
+ source_version_id: String(row.source_version_id || "").trim(),
2681
+ target_version_id: String(row.target_version_id || "").trim(),
2682
+ metadata_summary: summarizeMergeMetadata(metadata),
2683
+ changed_paths: changedPaths,
2684
+ changed_paths_count: changedPaths.length,
2685
+ };
2686
+ })
2687
+ .sort((a, b) => parseISOTime(b.updated_at) - parseISOTime(a.updated_at));
2688
+
2689
+ const statusCount = {
2690
+ open: normalizedItems.filter((row) => row.status === "open").length,
2691
+ review: normalizedItems.filter((row) => row.status === "review").length,
2692
+ approved: normalizedItems.filter((row) => row.status === "approved").length,
2693
+ rejected: normalizedItems.filter((row) => row.status === "rejected").length,
2694
+ merged: normalizedItems.filter((row) => row.status === "merged").length,
2695
+ closed: normalizedItems.filter((row) => row.status === "closed").length,
2696
+ };
2697
+
2698
+ const approvedCandidates = normalizedItems.filter((row) => row.status === "approved");
2699
+ const reviewCandidates = normalizedItems.filter((row) => row.status === "review" || row.status === "open");
2700
+ let recommendationAction = "no_action";
2701
+ let recommendationReason = "No pending merge requests.";
2702
+ let ownerPrompt = "";
2703
+ if (!mergeFeatureAvailable) {
2704
+ recommendationAction = "merge_api_unavailable";
2705
+ recommendationReason = mergeFeatureWarning;
2706
+ ownerPrompt =
2707
+ "Ask platform owner/admin to enable ctxpack merge-request API before merge approval workflow.";
2708
+ } else if (approvedCandidates.length > 0) {
2709
+ recommendationAction = ownerGate ? "merge_ready" : "owner_approval_required";
2710
+ recommendationReason =
2711
+ approvedCandidates.length === 1
2712
+ ? "There is 1 approved merge request ready for merge."
2713
+ : `There are ${approvedCandidates.length} approved merge requests ready for merge.`;
2714
+ const ids = approvedCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
2715
+ ownerPrompt = `Ask owner decision: merge now for ${ids.join(", ")} ?`;
2716
+ } else if (reviewCandidates.length > 0) {
2717
+ recommendationAction = ownerGate ? "review_then_approve" : "owner_review_required";
2718
+ recommendationReason =
2719
+ reviewCandidates.length === 1
2720
+ ? "There is 1 merge request waiting for review/approval."
2721
+ : `There are ${reviewCandidates.length} merge requests waiting for review/approval.`;
2722
+ const ids = reviewCandidates.slice(0, 5).map((row) => row.merge_request_id).filter(Boolean);
2723
+ ownerPrompt = `Ask owner decision: approve or reject ${ids.join(", ")} ?`;
2724
+ }
2725
+
2726
+ return {
2727
+ project_id: projectID,
2728
+ access: "granted",
2729
+ status_code: 200,
2730
+ message: mergeFeatureAvailable
2731
+ ? "Ctxpack merge briefing is ready."
2732
+ : "Ctxpack merge briefing is ready, but merge-request API is unavailable on this server.",
2733
+ merge_feature_available: mergeFeatureAvailable,
2734
+ merge_feature_warning: mergeFeatureWarning,
2735
+ owner_gate: {
2736
+ actor_user_id: actor.user_id,
2737
+ actor_email: actor.email,
2738
+ actor_name: actor.name,
2739
+ project_owner_user_id: String(summary.owner_user_id || "").trim(),
2740
+ org_owner_user_id: orgOwnerUserID,
2741
+ is_project_owner: isProjectOwner,
2742
+ is_org_owner: isOrgOwner,
2743
+ can_owner_finalize: ownerGate,
2744
+ },
2745
+ status_filter: normalizedStatus,
2746
+ merge_request_count: normalizedItems.length,
2747
+ status_count: statusCount,
2748
+ merge_requests: normalizedItems,
2749
+ recommendation: {
2750
+ action: recommendationAction,
2751
+ reason: recommendationReason,
2752
+ owner_prompt: ownerPrompt,
2753
+ owner_confirmation_required: true,
2754
+ },
2755
+ };
2756
+ }
2757
+
2758
+ function buildCtxpackMergeBriefText(brief) {
2759
+ if (String(brief?.access || "") !== "granted") {
2760
+ return [
2761
+ `Project ID: ${brief?.project_id || "-"}`,
2762
+ `Access: ${brief?.access || "error"}`,
2763
+ `Status: ${brief?.status_code || "-"}`,
2764
+ `${brief?.message || "Unable to load ctxpack merge briefing."}`,
2765
+ ].join("\n");
2766
+ }
2767
+
2768
+ const lines = [
2769
+ `Project ID: ${brief.project_id || "-"}`,
2770
+ `Merge Requests: ${Number(brief.merge_request_count || 0)}`,
2771
+ `Status Filter: ${brief.status_filter || "open"}`,
2772
+ `Owner Gate: ${brief.owner_gate?.can_owner_finalize ? "owner-confirmation possible" : "owner token required"}`,
2773
+ "",
2774
+ "Status Summary:",
2775
+ `- Open: ${Number(brief.status_count?.open || 0)}`,
2776
+ `- Review: ${Number(brief.status_count?.review || 0)}`,
2777
+ `- Approved: ${Number(brief.status_count?.approved || 0)}`,
2778
+ `- Rejected: ${Number(brief.status_count?.rejected || 0)}`,
2779
+ `- Merged: ${Number(brief.status_count?.merged || 0)}`,
2780
+ `- Closed: ${Number(brief.status_count?.closed || 0)}`,
2781
+ ];
2782
+ if (brief.merge_feature_available === false) {
2783
+ lines.push("");
2784
+ lines.push("Merge API:");
2785
+ lines.push(`- ${brief.merge_feature_warning || "Merge-request API is unavailable on this server."}`);
2786
+ }
2787
+ lines.push("");
2788
+ lines.push("Recommendation:");
2789
+ lines.push(`- Action: ${brief.recommendation?.action || "no_action"}`);
2790
+ lines.push(`- Reason: ${brief.recommendation?.reason || "-"}`);
2791
+ const ownerPrompt = String(brief.recommendation?.owner_prompt || "").trim();
2792
+ if (ownerPrompt) {
2793
+ lines.push(`- Owner Prompt: ${ownerPrompt}`);
2794
+ }
2795
+ const requests = ensureArray(brief.merge_requests);
2796
+ if (requests.length > 0) {
2797
+ lines.push("");
2798
+ lines.push("Pending Details:");
2799
+ for (const row of requests.slice(0, 20)) {
2800
+ const changedPreview =
2801
+ row.changed_paths_count > 0 ? `${row.changed_paths_count} path(s)` : "no changed_paths metadata";
2802
+ const changedList =
2803
+ row.changed_paths_count > 0 ? ` [${ensureArray(row.changed_paths).slice(0, 5).join(", ")}]` : "";
2804
+ lines.push(
2805
+ `- ${row.merge_request_id} | ${row.status} | by ${row.created_by || row.created_by_user_id || "-"} | ${changedPreview}${changedList}`,
2806
+ );
2807
+ if (row.metadata_summary) {
2808
+ lines.push(` summary: ${row.metadata_summary}`);
2809
+ }
2810
+ }
2811
+ }
2812
+ lines.push("");
2813
+ lines.push(
2814
+ "Before execute, ask owner for explicit confirmation, then run ctxpack.merge.execute with owner_confirmation.",
2815
+ );
2816
+ return lines.join("\n");
2817
+ }
2818
+
2819
+ async function executeCtxpackMergeActionForTool({
2820
+ siteBaseURL,
2821
+ projectID,
2822
+ mergeRequestID,
2823
+ action,
2824
+ ownerConfirmation,
2825
+ token,
2826
+ timeoutSeconds,
2827
+ workspaceDir,
2828
+ }) {
2829
+ if (!isOwnerConfirmationAccepted(ownerConfirmation)) {
2830
+ return {
2831
+ project_id: projectID,
2832
+ merge_request_id: mergeRequestID,
2833
+ action,
2834
+ ok: false,
2835
+ status_code: 400,
2836
+ message: "Owner confirmation missing. Ask owner and pass owner_confirmation (confirm/approved/yes).",
2837
+ };
2838
+ }
2839
+
2840
+ const summary = await loadProjectSummaryForTool({
2841
+ siteBaseURL,
2842
+ projectID,
2843
+ token,
2844
+ timeoutSeconds,
2845
+ includeCtxpack: false,
2846
+ syncCtxpackLocal: false,
2847
+ workspaceDir,
2848
+ });
2849
+ if (String(summary.access || "") !== "granted") {
2850
+ return {
2851
+ project_id: projectID,
2852
+ merge_request_id: mergeRequestID,
2853
+ action,
2854
+ ok: false,
2855
+ status_code: summary.status_code || 500,
2856
+ message: summary.message || "Project access denied.",
2857
+ };
2858
+ }
2859
+
2860
+ const actor = extractActorFromToken(token);
2861
+ const orgOwnerUserID = await loadOrgOwnerUserID({
2862
+ siteBaseURL,
2863
+ orgID: summary.org_id,
2864
+ token,
2865
+ timeoutSeconds,
2866
+ });
2867
+ const projectOwnerIDs = new Set(
2868
+ [summary.owner_user_id, summary.program_owner_user_id, summary.tech_owner_user_id, summary.governance_owner_user_id]
2869
+ .map((value) => String(value || "").trim())
2870
+ .filter(Boolean),
2871
+ );
2872
+ const isProjectOwner = actor.user_id ? projectOwnerIDs.has(actor.user_id) : false;
2873
+ const isOrgOwner = Boolean(actor.user_id && orgOwnerUserID && actor.user_id === orgOwnerUserID);
2874
+ if (!(isProjectOwner || isOrgOwner)) {
2875
+ return {
2876
+ project_id: projectID,
2877
+ merge_request_id: mergeRequestID,
2878
+ action,
2879
+ ok: false,
2880
+ status_code: 403,
2881
+ message: "Owner gate blocked: current token is not project/org owner. Ask owner account to execute.",
2882
+ };
2883
+ }
2884
+
2885
+ const encodedProjectID = encodeURIComponent(projectID);
2886
+ const encodedMRID = encodeURIComponent(mergeRequestID);
2887
+ const endpoint = `${siteBaseURL}/api/v1/projects/${encodedProjectID}/ctxpack/merge-requests/${encodedMRID}/${action}`;
2888
+
2889
+ try {
2890
+ const responseText = await postJSON(endpoint, timeoutSeconds, token, {});
2891
+ const parsed = tryJsonParse(responseText) || {};
2892
+ return {
2893
+ project_id: projectID,
2894
+ merge_request_id: mergeRequestID,
2895
+ action,
2896
+ ok: true,
2897
+ status_code: 200,
2898
+ message: `Merge request action '${action}' completed.`,
2899
+ actor_user_id: actor.user_id,
2900
+ actor_email: actor.email,
2901
+ response: safeObject(parsed),
2902
+ };
2903
+ } catch (err) {
2904
+ const statusCode = Number(err?.statusCode || 0) || 500;
2905
+ const bodyText = String(err?.responseBody || err?.message || "").trim();
2906
+ let message = bodyText || `Failed to execute action '${action}'.`;
2907
+ if (statusCode === 403) {
2908
+ message = "Forbidden by server policy/role. Current account cannot execute this action.";
2909
+ } else if (statusCode === 409) {
2910
+ message =
2911
+ "Merge conflict or approval state mismatch. Refresh briefing (ctxpack.merge.brief), then re-review before merge.";
2912
+ } else if (statusCode === 404) {
2913
+ message = "Merge request not found for this project.";
2914
+ }
2915
+ return {
2916
+ project_id: projectID,
2917
+ merge_request_id: mergeRequestID,
2918
+ action,
2919
+ ok: false,
2920
+ status_code: statusCode,
2921
+ message,
2922
+ error: bodyText,
2923
+ };
2924
+ }
2925
+ }
2926
+
2927
+ function buildCtxpackMergeExecuteText(result) {
2928
+ const lines = [
2929
+ `Project ID: ${result.project_id || "-"}`,
2930
+ `Merge Request ID: ${result.merge_request_id || "-"}`,
2931
+ `Action: ${result.action || "-"}`,
2932
+ `Result: ${result.ok ? "ok" : "failed"} (status ${result.status_code || "-"})`,
2933
+ `Message: ${result.message || "-"}`,
2934
+ ];
2935
+ if (result.ok) {
2936
+ const mergedVersionID = String(result.response?.merged_version_id || "").trim();
2937
+ if (mergedVersionID) {
2938
+ lines.push(`Merged Version ID: ${mergedVersionID}`);
2939
+ }
2940
+ } else if (result.error) {
2941
+ lines.push(`Server: ${result.error}`);
2942
+ }
2943
+ return lines.join("\n");
2944
+ }
2945
+
2258
2946
  function buildProjectSummaryText(summary) {
2259
2947
  if (String(summary?.access || "") !== "granted") {
2260
2948
  return [
@@ -2327,6 +3015,7 @@ function appendProjectHintToInitialize(responseObj, args) {
2327
3015
  "- MUST call `project.summary` first when the user provides only a Project ID or asks project overview/agenda.",
2328
3016
  "- `project.describe` and `project.get` are aliases of `project.summary`.",
2329
3017
  "- After project summary, use workitem/evidence/decision tools as follow-up.",
3018
+ "- Before any ctxpack merge, call `ctxpack.merge.brief` first, share recommendation to owner, get explicit owner confirmation, then call `ctxpack.merge.execute`.",
2330
3019
  ];
2331
3020
  if (args.projectID) {
2332
3021
  hintLines.splice(1, 0, `- Default project_id is ${args.projectID}.`);
@@ -2348,14 +3037,14 @@ async function maybeAutoSyncCtxpackForCall({
2348
3037
  }) {
2349
3038
  if (!isJsonRpcMethod(requestObj, "tools/call")) return null;
2350
3039
  if (!token) return null;
2351
- if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) return null;
3040
+ if (LOCAL_PROJECT_TOOL_NAMES.includes(toolName) || LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) return null;
2352
3041
  if (String(toolName || "").trim().toLowerCase() === "ctxpack.ensure") return null;
2353
3042
 
2354
- const projectID = firstNonEmptyString([
2355
- toolArgs?.project_id,
2356
- toolArgs?.projectID,
2357
- args.projectID,
2358
- ]);
3043
+ const projectID = resolveProjectIDForRequest({
3044
+ toolArgs,
3045
+ args,
3046
+ workspaceDir,
3047
+ });
2359
3048
  if (!isUUID(projectID)) return null;
2360
3049
 
2361
3050
  const workspacePath = resolveWorkspaceDir(workspaceDir || process.cwd());
@@ -2448,7 +3137,12 @@ async function appendWorkitemListHints(responseObj, args, toolArgs, token) {
2448
3137
  }
2449
3138
 
2450
3139
  const projectID = String(
2451
- toolArgs?.project_id || toolArgs?.projectID || responseProjectID || args.projectID || "",
3140
+ resolveProjectIDForRequest({
3141
+ toolArgs,
3142
+ args,
3143
+ workspaceDir: args.workspaceDir,
3144
+ responseProjectID,
3145
+ }),
2452
3146
  ).trim();
2453
3147
 
2454
3148
  if (projectID && isUUID(projectID)) {
@@ -2517,7 +3211,12 @@ function appendCtxpackEnsureSyncHints(responseObj, args, toolArgs, requestObj) {
2517
3211
  if (!Object.keys(body).length) return responseObj;
2518
3212
 
2519
3213
  const projectID = String(
2520
- toolArgs?.project_id || toolArgs?.projectID || envelope.project_id || args.projectID || "",
3214
+ resolveProjectIDForRequest({
3215
+ toolArgs,
3216
+ args,
3217
+ workspaceDir: args.workspaceDir,
3218
+ envelopeProjectID: envelope.project_id,
3219
+ }),
2521
3220
  ).trim();
2522
3221
  if (!projectID || !isUUID(projectID)) return responseObj;
2523
3222
  const workspaceDir = resolveWorkspaceDirForRequest(args.workspaceDir || process.cwd(), requestObj, toolArgs);
@@ -2632,13 +3331,15 @@ async function injectCtxpackPreflightToken(requestObj, toolName, toolArgs, args,
2632
3331
  return requestObj;
2633
3332
  }
2634
3333
 
2635
- const projectID = firstNonEmptyString([
2636
- rawArgs.project_id,
2637
- rawArgs.projectID,
2638
- toolArgs?.project_id,
2639
- toolArgs?.projectID,
2640
- args.projectID,
2641
- ]);
3334
+ const mergedToolArgs = {
3335
+ ...safeObject(toolArgs),
3336
+ project_id: firstNonEmptyString([rawArgs.project_id, rawArgs.projectID, toolArgs?.project_id, toolArgs?.projectID]),
3337
+ };
3338
+ const projectID = resolveProjectIDForRequest({
3339
+ toolArgs: mergedToolArgs,
3340
+ args,
3341
+ workspaceDir,
3342
+ });
2642
3343
  if (!isUUID(projectID)) {
2643
3344
  return requestObj;
2644
3345
  }
@@ -2728,12 +3429,13 @@ async function appendCtxpackConflictHintToErrorResponse(responseObj, args, toolN
2728
3429
  }
2729
3430
 
2730
3431
  async function runProxy(flags) {
2731
- const workspaceMeta = loadWorkspaceMeta(process.cwd());
3432
+ const explicitWorkspaceDirRaw = String(flags["workspace-dir"] || "").trim();
3433
+ const explicitWorkspaceDir = explicitWorkspaceDirRaw ? resolveWorkspaceDir(explicitWorkspaceDirRaw) : "";
2732
3434
  const args = {
2733
3435
  baseURL: flags["base-url"] || DEFAULT_BASE_URL,
2734
- projectID: String(flags["project-id"] || workspaceMeta.project_id || "").trim(),
2735
- ctxpackKey: String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim(),
2736
- workspaceDir: resolveWorkspaceDir(flags["workspace-dir"] || process.cwd()),
3436
+ projectID: String(flags["project-id"] || "").trim(),
3437
+ ctxpackKey: String(flags["ctxpack-key"] || "").trim(),
3438
+ workspaceDir: explicitWorkspaceDir,
2737
3439
  includeDrafts: boolFromRaw(flags["include-drafts"], true),
2738
3440
  autoPullOnConflict: boolFromRaw(flags["auto-pull-on-conflict"], true),
2739
3441
  timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
@@ -2800,12 +3502,16 @@ async function runProxy(flags) {
2800
3502
 
2801
3503
  const { name: toolName, args: toolArgs } = extractToolCall(requestObj);
2802
3504
  const requestWorkspaceCandidate = extractWorkspaceCandidateFromRequest(requestObj, toolArgs);
3505
+ const envWorkspaceCandidate = extractWorkspaceCandidateFromEnv();
2803
3506
  if (requestWorkspaceCandidate) {
2804
3507
  sessionWorkspaceDir = requestWorkspaceCandidate;
3508
+ } else if (envWorkspaceCandidate) {
3509
+ sessionWorkspaceDir = envWorkspaceCandidate;
2805
3510
  }
2806
3511
  const requestWorkspaceDir = resolveWorkspaceDir(
2807
3512
  firstNonEmptyString([
2808
3513
  requestWorkspaceCandidate,
3514
+ envWorkspaceCandidate,
2809
3515
  sessionWorkspaceDir,
2810
3516
  args.workspaceDir,
2811
3517
  process.cwd(),
@@ -2823,7 +3529,13 @@ async function runProxy(flags) {
2823
3529
  });
2824
3530
  }
2825
3531
  if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_PROJECT_TOOL_NAMES.includes(toolName)) {
2826
- const projectID = String(toolArgs.project_id || toolArgs.projectID || args.projectID || "").trim();
3532
+ const projectID = String(
3533
+ resolveProjectIDForRequest({
3534
+ toolArgs,
3535
+ args,
3536
+ workspaceDir: requestWorkspaceDir,
3537
+ }),
3538
+ ).trim();
2827
3539
  if (!projectID) {
2828
3540
  process.stdout.write(
2829
3541
  `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
@@ -2879,6 +3591,113 @@ async function runProxy(flags) {
2879
3591
  }
2880
3592
  return;
2881
3593
  }
3594
+ if (isJsonRpcMethod(requestObj, "tools/call") && LOCAL_CTXPACK_MERGE_TOOL_NAMES.includes(toolName)) {
3595
+ const projectID = String(
3596
+ resolveProjectIDForRequest({
3597
+ toolArgs,
3598
+ args,
3599
+ workspaceDir: requestWorkspaceDir,
3600
+ }),
3601
+ ).trim();
3602
+ if (!projectID) {
3603
+ process.stdout.write(
3604
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id is required (or set --project-id during setup)"))}\n`,
3605
+ );
3606
+ return;
3607
+ }
3608
+ if (!isUUID(projectID)) {
3609
+ process.stdout.write(
3610
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, "project_id must be a valid UUID"))}\n`,
3611
+ );
3612
+ return;
3613
+ }
3614
+
3615
+ try {
3616
+ if (toolName === "ctxpack.merge.brief") {
3617
+ const statusRaw = String(toolArgs.status || "").trim().toLowerCase();
3618
+ const statusFilter = normalizeMergeStatusFilter(statusRaw);
3619
+ if (statusRaw && !statusFilter) {
3620
+ process.stdout.write(
3621
+ `${JSON.stringify(
3622
+ jsonRpcError(
3623
+ requestObj,
3624
+ -32001,
3625
+ "status must be one of: open, review, approved, rejected, merged, closed, all",
3626
+ ),
3627
+ )}\n`,
3628
+ );
3629
+ return;
3630
+ }
3631
+ const limit = Number.parseInt(String(toolArgs.limit || 20), 10) || 20;
3632
+ const brief = await loadCtxpackMergeBriefForTool({
3633
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
3634
+ projectID,
3635
+ token,
3636
+ timeoutSeconds: args.timeoutSeconds,
3637
+ statusFilter: statusFilter || "open",
3638
+ limit,
3639
+ workspaceDir: requestWorkspaceDir,
3640
+ });
3641
+ const text = buildCtxpackMergeBriefText(brief);
3642
+ process.stdout.write(
3643
+ `${JSON.stringify(
3644
+ jsonRpcResult(requestObj, {
3645
+ content: [{ type: "text", text }],
3646
+ structuredContent: brief,
3647
+ }),
3648
+ )}\n`,
3649
+ );
3650
+ return;
3651
+ }
3652
+
3653
+ if (toolName === "ctxpack.merge.execute") {
3654
+ const mergeRequestID = String(toolArgs.merge_request_id || toolArgs.mergeRequestID || "").trim();
3655
+ const action = normalizeMergeAction(toolArgs.action);
3656
+ const ownerConfirmation = String(
3657
+ toolArgs.owner_confirmation || toolArgs.ownerConfirmation || "",
3658
+ ).trim();
3659
+ if (!mergeRequestID || !isUUID(mergeRequestID)) {
3660
+ process.stdout.write(
3661
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, "merge_request_id must be a valid UUID"))}\n`,
3662
+ );
3663
+ return;
3664
+ }
3665
+ if (!action) {
3666
+ process.stdout.write(
3667
+ `${JSON.stringify(
3668
+ jsonRpcError(requestObj, -32001, "action must be one of: review, approve, reject, close, merge"),
3669
+ )}\n`,
3670
+ );
3671
+ return;
3672
+ }
3673
+ const result = await executeCtxpackMergeActionForTool({
3674
+ siteBaseURL: normalizeSiteBaseURL(args.baseURL),
3675
+ projectID,
3676
+ mergeRequestID,
3677
+ action,
3678
+ ownerConfirmation,
3679
+ token,
3680
+ timeoutSeconds: args.timeoutSeconds,
3681
+ workspaceDir: requestWorkspaceDir,
3682
+ });
3683
+ const text = buildCtxpackMergeExecuteText(result);
3684
+ process.stdout.write(
3685
+ `${JSON.stringify(
3686
+ jsonRpcResult(requestObj, {
3687
+ content: [{ type: "text", text }],
3688
+ structuredContent: result,
3689
+ }),
3690
+ )}\n`,
3691
+ );
3692
+ return;
3693
+ }
3694
+ } catch (err) {
3695
+ process.stdout.write(
3696
+ `${JSON.stringify(jsonRpcError(requestObj, -32001, String(err?.message || err)))}\n`,
3697
+ );
3698
+ return;
3699
+ }
3700
+ }
2882
3701
 
2883
3702
  try {
2884
3703
  const requestWithDefaults = injectCtxpackPushDefaults(requestObj, toolName, requestWorkspaceDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metheus-governance-mcp-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "Metheus Governance MCP CLI (setup + stdio proxy)",
5
5
  "type": "module",
6
6
  "files": [