mergestorm-mcp 0.1.3 → 0.1.6

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 +5 -2
  2. package/dist/index.js +459 -112
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,11 +1,14 @@
1
1
  # mergestorm-mcp
2
2
 
3
- Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_list`, `stack_status`, `queue_status`, `settings_get`, `settings_set`.
3
+ Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
4
4
 
5
- The stack tools are read-only:
5
+ The stack tools expose reads plus one scoped policy write:
6
6
 
7
+ - `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
7
8
  - `stack_list` returns the current API key owner's registered stacks.
9
+ - `stack_set({ stack_id, auto_land?, auto_review?, auto_patch? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review` and `auto_patch` pin Vortex auto-review or Cyclone auto-patch for that stack in either direction (`true` or `false`); `null` clears the pin so the stack follows the account setting. Pass at least one key. Account settings are never changed here.
8
10
  - `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, or a structured `stack_not_found` error.
11
+ - `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots, or a waiting result with selectors to use for the next call.
9
12
 
10
13
  Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
11
14
 
package/dist/index.js CHANGED
@@ -5,8 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/server.ts
7
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
- import { z } from "zod";
9
- import { CommandError as CommandError10 } from "mergestorm/client";
8
+ import { z as z3 } from "zod";
9
+ import { CommandError as CommandError11, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
10
10
 
11
11
  // src/tools/credits.ts
12
12
  import { CommandError, getMe, loadConfig } from "mergestorm/client";
@@ -25,8 +25,10 @@ async function credits(cfg) {
25
25
  }
26
26
  const standard = me.usage.standard;
27
27
  const remaining = standard.remaining ?? (standard.limit != null ? Math.max(0, standard.limit - standard.used) : null);
28
+ const bonusRemaining = me.usage.bonus?.remaining;
29
+ const bonusSummary = bonusRemaining == null ? "" : ` \xB7 ${bonusRemaining} bonus remaining`;
28
30
  return {
29
- summary: remaining == null ? `${standard.used} used \xB7 unlimited` : `${standard.used} used \xB7 ${remaining} remaining`,
31
+ summary: remaining == null ? `${standard.used} used \xB7 unlimited${bonusSummary}` : `${standard.used} used \xB7 ${remaining} remaining${bonusSummary}`,
30
32
  data: {
31
33
  usage: me.usage,
32
34
  resets_at: me.resets_at ?? null
@@ -44,20 +46,29 @@ import {
44
46
  REVIEW_EXIT
45
47
  } from "mergestorm/client";
46
48
  function entrySummary(entry) {
47
- const repo = entry.owner && entry.repo ? `${entry.owner}/${entry.repo}` : "unknown repo";
48
- const position = entry.position === void 0 ? "queue" : `#${entry.position}`;
49
- return `${position} \xB7 ${repo} \xB7 stack ${entry.stackId ?? "unknown"} \xB7 ${entry.state ?? "unknown"}`;
49
+ const prNumber = entry.bounceDetail?.prNumber;
50
+ const repo = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
51
+ const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? void 0;
52
+ const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? void 0;
53
+ return [
54
+ entry.state,
55
+ repo,
56
+ `stack ${entry.stackId}`,
57
+ reason,
58
+ sha?.slice(0, 7)
59
+ ].filter(Boolean).join(" \xB7 ");
50
60
  }
51
61
  async function queueStatus(stackId, cfg) {
52
62
  try {
53
63
  const resolved = cfg ?? await loadConfig2();
64
+ const id = stackId?.trim();
54
65
  const { status, body, retryAfterSeconds } = await apiFetch(
55
66
  resolved,
56
- "/api/v1/stacks/queue"
67
+ id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
57
68
  );
58
69
  if (status === 404) {
59
70
  throw new CommandError2(
60
- "Merge queue API is not available on this server yet. Deploy the API update or use the dashboard."
71
+ "Merge queue route returned 404. Deploy the API."
61
72
  );
62
73
  }
63
74
  if (status === 429) {
@@ -79,11 +90,12 @@ async function queueStatus(stackId, cfg) {
79
90
  `Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
80
91
  );
81
92
  }
82
- const id = stackId?.trim();
83
93
  if (id) {
84
- const entry = entries.find((candidate) => candidate?.stackId === id);
85
- if (!entry) {
86
- const message = `Live queue entry not found for stack: ${id}`;
94
+ const matchingEntries = entries.filter(
95
+ (candidate) => candidate?.stackId?.toLowerCase() === id.toLowerCase()
96
+ );
97
+ if (matchingEntries.length === 0) {
98
+ const message = `Queue entry not found for stack: ${id}`;
87
99
  return {
88
100
  summary: message,
89
101
  data: {
@@ -97,12 +109,12 @@ async function queueStatus(stackId, cfg) {
97
109
  };
98
110
  }
99
111
  return {
100
- summary: entrySummary(entry),
101
- data: { entry }
112
+ summary: matchingEntries.map(entrySummary).join("\n"),
113
+ data: { entries: matchingEntries }
102
114
  };
103
115
  }
104
116
  return {
105
- summary: entries.length === 0 ? "No live merge queue entries" : entries.map(entrySummary).join("\n"),
117
+ summary: entries.length === 0 ? "No merge queue entries (live or bounced)" : entries.map(entrySummary).join("\n"),
106
118
  data: { entries }
107
119
  };
108
120
  } catch (err) {
@@ -141,9 +153,21 @@ function prReviewTarget(owner, repo, prNumber) {
141
153
  }
142
154
  return { owner: cleanOwner, repo: cleanRepo, prNumber };
143
155
  }
156
+ function prReviewSummary(owner, repo, prNumber, envelope) {
157
+ const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
158
+ const sha = envelope.head_sha?.trim();
159
+ if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
160
+ if (typeof envelope.finding_count === "number") {
161
+ bits.push(
162
+ `${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`
163
+ );
164
+ }
165
+ if (envelope.phase) bits.push(String(envelope.phase));
166
+ return bits.join(" \xB7 ");
167
+ }
144
168
  function payloadFromPrReview(owner, repo, prNumber, envelope) {
145
169
  return {
146
- summary: `${owner}/${repo}#${prNumber} \xB7 ${envelope.status}`,
170
+ summary: prReviewSummary(owner, repo, prNumber, envelope),
147
171
  data: { ...envelope }
148
172
  };
149
173
  }
@@ -158,14 +182,15 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
158
182
  }
159
183
  };
160
184
  }
161
- async function reviewGetPr(owner, repo, prNumber, cfg) {
185
+ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
162
186
  const target = prReviewTarget(owner, repo, prNumber);
163
187
  try {
164
188
  const envelope = await getPrVortexReview(
165
189
  target.owner,
166
190
  target.repo,
167
191
  target.prNumber,
168
- cfg
192
+ cfg,
193
+ { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
169
194
  );
170
195
  return payloadFromPrReview(
171
196
  target.owner,
@@ -259,6 +284,8 @@ import {
259
284
  REVIEW_THREAD_SLUG_RE,
260
285
  ReviewPollTimeoutError,
261
286
  collectReviewInput,
287
+ discoverTrunk,
288
+ formatReviewSubmitError,
262
289
  loadConfig as loadConfig3,
263
290
  loadReviewContext,
264
291
  pollReview,
@@ -266,6 +293,7 @@ import {
266
293
  submitReview
267
294
  } from "mergestorm/client";
268
295
  var MCP_WAIT_CEILING_S = 300;
296
+ var MCP_PR_WAIT_DEFAULT_S = 45;
269
297
  var PINNABLE_SPECIALISTS = [
270
298
  "security",
271
299
  "performance",
@@ -290,8 +318,8 @@ function assertPinnableSpecialists(specialists) {
290
318
  function mcpSandboxRoot() {
291
319
  return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
292
320
  }
293
- function waitTimeoutMs(timeout_s) {
294
- const seconds = timeout_s ?? MCP_WAIT_CEILING_S;
321
+ function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
322
+ const seconds = timeout_s ?? defaultSeconds;
295
323
  if (!Number.isFinite(seconds) || seconds <= 0) {
296
324
  throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
297
325
  }
@@ -306,13 +334,18 @@ async function resolveInput(input) {
306
334
  return {
307
335
  thread,
308
336
  branch,
309
- baseLabel: input.base?.trim() || "main",
337
+ baseLabel: input.base?.trim() || (() => {
338
+ try {
339
+ return discoverTrunk(cwd);
340
+ } catch {
341
+ return "main";
342
+ }
343
+ })(),
310
344
  headLabel: head,
311
345
  diff: input.diff,
312
346
  files: []
313
347
  };
314
348
  }
315
- const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
316
349
  const sandboxRoot = mcpSandboxRoot();
317
350
  try {
318
351
  const [sandboxReal, cwdReal] = await Promise.all([
@@ -327,27 +360,12 @@ async function resolveInput(input) {
327
360
  if (err instanceof CommandError5) throw err;
328
361
  throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
329
362
  }
330
- let lastErr;
331
- let collected = null;
332
- let yielded = false;
333
- for (const base of bases) {
334
- try {
335
- const candidate = await collectReviewInput(base, head, cwd, sandboxRoot);
336
- yielded = true;
337
- if (!collected) collected = candidate;
338
- } catch (err) {
339
- if (yielded) continue;
340
- lastErr = err;
341
- if (input.base?.trim()) throw err;
342
- }
343
- }
344
- if (yielded) return collected;
345
- if (lastErr) throw lastErr;
346
- return null;
363
+ const base = input.base?.trim() || discoverTrunk(cwd);
364
+ return collectReviewInput(base, head, cwd, sandboxRoot);
347
365
  }
348
366
  async function reviewSubmit(input, cfg, opts = {}) {
349
367
  const resolved = cfg ?? await loadConfig3();
350
- const wait = input.wait !== false;
368
+ const wait = input.wait === true;
351
369
  const timeoutMs = waitTimeoutMs(input.timeout_s);
352
370
  assertPinnableSpecialists(input.specialists);
353
371
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
@@ -395,7 +413,13 @@ async function reviewSubmit(input, cfg, opts = {}) {
395
413
  });
396
414
  }
397
415
  if (submitted.status !== 202 && submitted.status !== 200) {
398
- throw new CommandError5(JSON.stringify(submitted.row));
416
+ return {
417
+ ...payloadFromRow({ ...submitted.row, status: "failed" }, {
418
+ jobId,
419
+ error: formatReviewSubmitError(submitted.status, submitted.row)
420
+ }),
421
+ isError: true
422
+ };
399
423
  }
400
424
  if (!jobId) {
401
425
  throw new CommandError5("Review API returned no job_id");
@@ -440,8 +464,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
440
464
  target.repo,
441
465
  target.prNumber,
442
466
  {
443
- timeoutMs: waitTimeoutMs(timeout_s),
467
+ timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
444
468
  afterSha,
469
+ pass: opts.pass,
470
+ afterPass: opts.afterPass,
445
471
  intervalMs: opts.pollIntervalMs
446
472
  }
447
473
  );
@@ -455,7 +481,12 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
455
481
  if (err instanceof PrReviewPollTimeoutError) {
456
482
  const status = err.lastEnvelope?.status ?? "in_progress";
457
483
  return {
458
- summary: `${target.owner}/${target.repo}#${target.prNumber} \xB7 ${status}`,
484
+ summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
485
+ status,
486
+ head_sha: err.lastEnvelope?.head_sha ?? null,
487
+ finding_count: err.lastEnvelope?.finding_count ?? null,
488
+ phase: err.lastEnvelope?.phase ?? null
489
+ }),
459
490
  data: {
460
491
  ...err.lastEnvelope ?? {},
461
492
  status,
@@ -533,6 +564,7 @@ async function settingsGet(cfg) {
533
564
  // src/tools/settings-set.ts
534
565
  import {
535
566
  CommandError as CommandError8,
567
+ BEARER_SETTINGS,
536
568
  SETTINGS_WRITABLE_KEYS,
537
569
  patchSettings
538
570
  } from "mergestorm/client";
@@ -550,13 +582,17 @@ function settingsPatchFromArgs(args) {
550
582
  for (const key of SETTINGS_WRITABLE_KEYS) {
551
583
  const value = args[key];
552
584
  if (value === void 0) continue;
585
+ const row = BEARER_SETTINGS.find((row2) => row2.key === key);
586
+ if ("kind" in row) {
587
+ const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
588
+ if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
589
+ Object.assign(patch, { [key]: value });
590
+ continue;
591
+ }
553
592
  if (typeof value !== "boolean") {
554
593
  throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
555
594
  }
556
- if (key === "auto_patch_enabled" && value === true) {
557
- throw new CommandError8("auto_patch_enabled cannot be enabled by this tool.", 2, "usage");
558
- }
559
- patch[key] = value;
595
+ Object.assign(patch, { [key]: value });
560
596
  }
561
597
  if (Object.keys(patch).length === 0) {
562
598
  throw new CommandError8(
@@ -576,24 +612,192 @@ async function settingsSet(args, cfg) {
576
612
  };
577
613
  }
578
614
 
615
+ // src/tools/stack-adopt.ts
616
+ import { adoptStack, CommandError as CommandError9 } from "mergestorm/client";
617
+ import { z } from "zod";
618
+
619
+ // src/tools/stack-set.ts
620
+ import { setStackPolicy } from "mergestorm/client";
621
+ function triWord(value) {
622
+ return value === null ? "default" : value ? "on" : "off";
623
+ }
624
+ function stackSetPatch(input) {
625
+ const patch = {};
626
+ if (typeof input.auto_land === "boolean") patch.autoEnqueueWhenReady = input.auto_land;
627
+ if (input.auto_review !== void 0) patch.autoReviewOverride = input.auto_review;
628
+ if (input.auto_patch !== void 0) patch.autoPatchOverride = input.auto_patch;
629
+ return patch;
630
+ }
631
+ async function stackSet(stackId, input, cfg) {
632
+ const patch = stackSetPatch(input);
633
+ if (Object.keys(patch).length === 0) {
634
+ return {
635
+ summary: "stack_set needs at least one of auto_land, auto_review, auto_patch",
636
+ isError: true,
637
+ data: {
638
+ error: {
639
+ code: "invalid_input",
640
+ message: "Pass at least one of auto_land, auto_review, auto_patch.",
641
+ stack_id: stackId
642
+ }
643
+ }
644
+ };
645
+ }
646
+ const result = await setStackPolicy(stackId, patch, cfg);
647
+ const parts = [];
648
+ if (typeof input.auto_land === "boolean") {
649
+ parts.push(`Auto land ${input.auto_land ? "on" : "off"}`);
650
+ }
651
+ if (input.auto_review !== void 0) {
652
+ parts.push(`auto-review ${triWord(input.auto_review)}`);
653
+ }
654
+ if (input.auto_patch !== void 0) {
655
+ parts.push(`auto-patch ${triWord(input.auto_patch)}`);
656
+ }
657
+ return {
658
+ summary: `${parts.join(", ")} for stack ${stackId}`,
659
+ data: {
660
+ stack_id: stackId,
661
+ ...typeof input.auto_land === "boolean" ? { auto_land: input.auto_land } : {},
662
+ ...input.auto_review !== void 0 ? { auto_review: input.auto_review } : {},
663
+ ...input.auto_patch !== void 0 ? { auto_patch: input.auto_patch } : {},
664
+ result
665
+ }
666
+ };
667
+ }
668
+
669
+ // src/tools/stack-adopt.ts
670
+ var stackAdoptSchema = {
671
+ owner: z.string().trim().min(1),
672
+ repo: z.string().trim().min(1),
673
+ pr_number: z.number().int().min(1),
674
+ auto_land: z.boolean().optional(),
675
+ auto_review: z.boolean().nullable().optional(),
676
+ auto_patch: z.boolean().nullable().optional()
677
+ };
678
+ var inputSchema = z.object(stackAdoptSchema);
679
+ async function stackAdopt(input, cfg) {
680
+ const parsed = inputSchema.safeParse(input);
681
+ if (!parsed.success) {
682
+ const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
683
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
684
+ }
685
+ const { owner, repo, pr_number } = parsed.data;
686
+ try {
687
+ const result = await adoptStack(
688
+ owner,
689
+ repo,
690
+ pr_number,
691
+ cfg,
692
+ stackSetPatch(parsed.data)
693
+ );
694
+ return {
695
+ summary: `Adopted stack for ${owner}/${repo}#${pr_number}`,
696
+ data: { owner, repo, pr_number, result }
697
+ };
698
+ } catch (err) {
699
+ const message = err instanceof Error ? err.message : String(err);
700
+ return {
701
+ summary: message,
702
+ data: {
703
+ error: {
704
+ code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
705
+ message,
706
+ ...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
707
+ }
708
+ },
709
+ isError: true
710
+ };
711
+ }
712
+ }
713
+
579
714
  // src/tools/stack-list.ts
580
- import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
715
+ import {
716
+ isCommandErrorCode as isCommandErrorCode4,
717
+ listMergeQueueEntries,
718
+ listStacks
719
+ } from "mergestorm/client";
581
720
 
582
721
  // src/tools/stack-summary.ts
583
- function stackSummary(stack) {
722
+ import {
723
+ BOUNCE_KIND_LABELS,
724
+ mergeQueueBounceLabel
725
+ } from "mergestorm/client";
726
+ function sameHead(a, b) {
727
+ const left = a?.trim().toLowerCase();
728
+ const right = b?.trim().toLowerCase();
729
+ return !!left && !!right && (left.length === right.length ? left === right : left.startsWith(right) || right.startsWith(left));
730
+ }
731
+ function blockerLabel(stack, entries) {
732
+ if (stack.archivedAt) return null;
733
+ const queue = entries.filter((entry) => entry.stackId === stack.id);
734
+ if (queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state))) return null;
735
+ const open = stack.layers.filter(
736
+ (layer2) => layer2.prNumber > 0 && layer2.state !== "merged" && layer2.state !== "closed"
737
+ );
738
+ const promote = open.filter(
739
+ (layer2) => layer2.prNumber !== stack.unit?.landPrNumber && !stack.unit?.members?.some(
740
+ (member) => member.prNumber === layer2.prNumber && member.promotedHeadSha
741
+ )
742
+ ).sort((a, b) => a.position - b.position);
743
+ const layer = promote[0] ?? open.find((layer2) => layer2.prNumber === stack.unit?.landPrNumber) ?? stack.unit?.landPr;
744
+ const bounce = queue.filter((entry) => entry.state === "bounced").sort((a, b) => (Date.parse(b.finishedAt ?? "") || 0) - (Date.parse(a.finishedAt ?? "") || 0))[0];
745
+ const bounceKind = bounce?.bounceDetail?.kind;
746
+ const recoverableBounceSha = (bounceKind === "head_moved" ? bounce?.bounceDetail?.headSha ?? bounce?.verifyHeadSha : bounce?.verifyHeadSha ?? bounce?.bounceDetail?.headSha)?.trim().toLowerCase();
747
+ const currentRecoverableBounce = !!recoverableBounceSha && (bounceKind === "head_moved" || bounceKind === "must_consolidate") && !!layer && layer.state !== "merged" && layer.state !== "closed" && sameHead(layer.headSha, recoverableBounceSha);
748
+ if (layer && layer.state !== "merged" && layer.state !== "closed") {
749
+ if (layer.restackError && layer.state !== "restacking") {
750
+ return layer.restackError.kind === "rebase_conflict" ? "Conflict" : "Restack failed";
751
+ }
752
+ if (layer.state === "conflict") return "Conflict";
753
+ if (layer.draft) return "Draft PR";
754
+ if ((!layer.headSha || layer.mergeableHeadSha === layer.headSha) && (layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty")) return "Merge conflicts";
755
+ if (!currentRecoverableBounce && (layer.ciStatus === "failure" || (layer.checks?.failure ?? 0) > 0)) {
756
+ const name = layer.checks?.failingName?.trim();
757
+ return BOUNCE_KIND_LABELS.ci_failure + (name ? ` \u2014 ${name}` : "");
758
+ }
759
+ const tempest = layer.agentRuns?.find(
760
+ (run) => run.agent === "tempest" && ["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha)
761
+ );
762
+ if (tempest) return `Tempest ${tempest.status}`;
763
+ if (layer.vortexStatus === "failed") return "Review failed";
764
+ if (stack.unit?.landPrNumber === layer.prNumber) {
765
+ if (stack.unit.landingBlockReason?.trim()) return stack.unit.landingBlockReason.trim();
766
+ if (stack.unit.tempestLandStatus?.toLowerCase() === "failed") return "Tempest failed";
767
+ }
768
+ }
769
+ const kind = bounceKind;
770
+ if (!kind || kind === "head_moved" || kind === "must_consolidate") return null;
771
+ const sha = (bounce.bounceDetail?.headSha ?? bounce.verifyHeadSha)?.trim().toLowerCase();
772
+ const current = sha && layer && layer.prNumber > 0 && layer.state !== "merged" && layer.state !== "closed" && (bounce.bounceDetail?.prNumber == null || layer.prNumber === bounce.bounceDetail.prNumber) && sameHead(layer.headSha, sha);
773
+ return current ? mergeQueueBounceLabel(bounce) : null;
774
+ }
775
+ function stackSummary(stack, entries = []) {
584
776
  const count = stack.layers.length;
585
777
  const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
586
778
  const states = [...new Set(stack.layers.map((layer) => layer.state))];
587
779
  const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
588
- return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState}`;
780
+ const overrides = [];
781
+ if (typeof stack.autoReviewOverride === "boolean") {
782
+ overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
783
+ }
784
+ if (typeof stack.autoPatchOverride === "boolean") {
785
+ overrides.push(`auto-patch ${stack.autoPatchOverride ? "on" : "off"}`);
786
+ }
787
+ if (stack.cycloneOwnerMatch) {
788
+ overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
789
+ }
790
+ const blocker = blockerLabel(stack, entries);
791
+ return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${blocker ? ` \xB7 blocked: ${blocker}` : ""}`;
589
792
  }
590
793
 
591
794
  // src/tools/stack-list.ts
592
795
  async function stackList(cfg) {
593
796
  try {
594
797
  const stacks = await listStacks(cfg);
798
+ const entries = await listMergeQueueEntries(cfg);
595
799
  return {
596
- summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack)).join("\n"),
800
+ summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack, entries)).join("\n"),
597
801
  data: { stacks }
598
802
  };
599
803
  } catch (err) {
@@ -614,8 +818,62 @@ async function stackList(cfg) {
614
818
  }
615
819
  }
616
820
 
821
+ // src/tools/stack-wait.ts
822
+ import {
823
+ loadConfig as loadConfig6,
824
+ pollStackWatch,
825
+ StackWatchError,
826
+ StackWatchTimeoutError
827
+ } from "mergestorm/client";
828
+ import { z as z2 } from "zod";
829
+ var stackWaitSchema = {
830
+ stack_id: z2.string().trim().min(1),
831
+ timeout_s: z2.number().finite().min(0).max(45).default(45),
832
+ enrolled_head_sha: z2.string().nullable().optional(),
833
+ after_finished_at: z2.string().nullable().optional(),
834
+ bounce_id: z2.string().nullable().optional()
835
+ };
836
+ async function stackWait(input, cfg, deps = {}) {
837
+ const args = z2.object(stackWaitSchema).parse(input);
838
+ const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
839
+ stackId: args.stack_id,
840
+ enrolledHeadSha: args.enrolled_head_sha ?? null,
841
+ ...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
842
+ ...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
843
+ } : void 0;
844
+ let envelope;
845
+ try {
846
+ envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
847
+ timeoutMs: args.timeout_s * 1e3,
848
+ cursor,
849
+ signal: deps.signal
850
+ });
851
+ } catch (err) {
852
+ if (err instanceof StackWatchError) {
853
+ return {
854
+ summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}`,
855
+ data: {
856
+ ...err.lastEnvelope,
857
+ ...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
858
+ },
859
+ isError: true
860
+ };
861
+ }
862
+ if (!(err instanceof StackWatchTimeoutError)) throw err;
863
+ envelope = { ...err.lastEnvelope, status: "waiting" };
864
+ }
865
+ return {
866
+ summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${envelope.blocker ? ` \xB7 ${envelope.blocker}` : ""}`,
867
+ data: envelope
868
+ };
869
+ }
870
+
617
871
  // src/tools/stack-status.ts
618
- import { getEnrichedStack, isCommandErrorCode as isCommandErrorCode5 } from "mergestorm/client";
872
+ import {
873
+ getEnrichedStack,
874
+ listMergeQueueEntries as listMergeQueueEntries2,
875
+ isCommandErrorCode as isCommandErrorCode5
876
+ } from "mergestorm/client";
619
877
  async function stackStatus(stackId, cfg) {
620
878
  try {
621
879
  const stack = await getEnrichedStack(stackId, cfg);
@@ -633,8 +891,9 @@ async function stackStatus(stackId, cfg) {
633
891
  isError: true
634
892
  };
635
893
  }
894
+ const entries = await listMergeQueueEntries2(cfg);
636
895
  return {
637
- summary: stackSummary(stack),
896
+ summary: stackSummary(stack, entries),
638
897
  data: { stack }
639
898
  };
640
899
  } catch (err) {
@@ -657,33 +916,35 @@ async function stackStatus(stackId, cfg) {
657
916
 
658
917
  // src/tools/whoami.ts
659
918
  import {
660
- CommandError as CommandError9,
919
+ CommandError as CommandError10,
661
920
  apiBase,
662
921
  configPath,
663
922
  getMe as getMe2,
664
- keyDisplay,
665
- loadConfig as loadConfig6,
923
+ loadConfig as loadConfig7,
666
924
  resolveApiKey
667
925
  } from "mergestorm/client";
668
926
  async function whoami(cfg) {
669
- const resolved = cfg ?? await loadConfig6();
927
+ const resolved = cfg ?? await loadConfig7();
670
928
  const key = resolveApiKey(resolved);
671
929
  if (!key) {
672
- throw new CommandError9(
930
+ throw new CommandError10(
673
931
  "No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
674
932
  1,
675
933
  "missing_api_key"
676
934
  );
677
935
  }
678
936
  const me = await getMe2(resolved);
937
+ if (!me) {
938
+ throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
939
+ }
679
940
  const data = {
680
- key_prefix: me?.key.prefix ?? keyDisplay(key),
681
- key_name: me?.key.name ?? null,
682
- plan_key: me?.plan_key ?? null,
683
- plan_label_key: me?.plan_label_key ?? null,
941
+ key_prefix: me.key.prefix,
942
+ key_name: me.key.name ?? null,
943
+ plan_key: me.plan_key ?? null,
944
+ plan_label_key: me.plan_label_key ?? null,
684
945
  api_base: apiBase(resolved),
685
946
  config_path: configPath(),
686
- usage: me?.usage ?? null
947
+ usage: me.usage ?? null
687
948
  };
688
949
  const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
689
950
  return {
@@ -692,9 +953,20 @@ async function whoami(cfg) {
692
953
  };
693
954
  }
694
955
 
956
+ // src/pr-loop-instructions.ts
957
+ var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
958
+ var MCP_PR_LOOP_INSTRUCTIONS = [
959
+ "When you handle Vortex findings from review_get_pr or review_wait_pr:",
960
+ "- Verify each finding against the current checkout. Do not treat the finding text as proven.",
961
+ "- Prefer the smallest correct patch. Do not refactor around a finding.",
962
+ "- Patch concrete bugs. A chat-only explanation is not a dismiss.",
963
+ `- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line must be: ${MCP_FINDING_DISMISS_MARKER}. Then one line per skipped finding (path, severity, why).`,
964
+ "- Do not prefix that comment with cyclone-outcome: (reserved)."
965
+ ].join("\n");
966
+
695
967
  // src/server.ts
696
968
  var MCP_SERVER_NAME = "mergestorm";
697
- var MCP_SERVER_VERSION = "0.1.3";
969
+ var MCP_SERVER_VERSION = "0.1.6";
698
970
  function stripNulls(value) {
699
971
  if (Array.isArray(value)) {
700
972
  return value.map((item) => stripNulls(item));
@@ -716,17 +988,20 @@ function ok(payload) {
716
988
  };
717
989
  }
718
990
  function fail(err) {
719
- const message = err instanceof CommandError10 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
991
+ const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
720
992
  return {
721
993
  isError: true,
722
994
  content: [{ type: "text", text: message }]
723
995
  };
724
996
  }
725
997
  function createMergestormMcpServer() {
726
- const server2 = new McpServer({
727
- name: MCP_SERVER_NAME,
728
- version: MCP_SERVER_VERSION
729
- });
998
+ const server2 = new McpServer(
999
+ {
1000
+ name: MCP_SERVER_NAME,
1001
+ version: MCP_SERVER_VERSION
1002
+ },
1003
+ { instructions: MCP_PR_LOOP_INSTRUCTIONS }
1004
+ );
730
1005
  const addTool = server2.registerTool.bind(server2);
731
1006
  addTool(
732
1007
  "whoami",
@@ -762,7 +1037,7 @@ function createMergestormMcpServer() {
762
1037
  title: "List reviews",
763
1038
  description: "List recent Mergestorm review jobs as job envelopes.",
764
1039
  inputSchema: {
765
- limit: z.number().int().min(1).max(50).optional()
1040
+ limit: z3.number().int().min(1).max(50).optional()
766
1041
  }
767
1042
  },
768
1043
  async ({ limit }) => {
@@ -779,7 +1054,7 @@ function createMergestormMcpServer() {
779
1054
  title: "Get review",
780
1055
  description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
781
1056
  inputSchema: {
782
- job_id: z.string().min(1)
1057
+ job_id: z3.string().min(1)
783
1058
  }
784
1059
  },
785
1060
  async ({ job_id }) => {
@@ -794,16 +1069,25 @@ function createMergestormMcpServer() {
794
1069
  "review_get_pr",
795
1070
  {
796
1071
  title: "Get GitHub PR Vortex review",
797
- description: "Fetch the latest GitHub PR Vortex pass from the DB-only endpoint. This tool is read-only. On rate_limited, wait retry_after_seconds before trying again.",
1072
+ description: "Fetch the latest GitHub PR Vortex pass from the DB-only endpoint. Review identity is SHA + pass: the envelope carries pass (1 for the first review on a head; a re-review on the same head is the next pass). Pass after_sha to scope to a head, pass to read one exact attempt, or after_pass to read only a later attempt on that head. This tool is read-only. On rate_limited, wait retry_after_seconds before trying again. Verify each finding against the current code; prefer the smallest correct patch; a chat-only skip is not a dismiss (post a PR comment starting with mergestorm-loop: dismiss).",
798
1073
  inputSchema: {
799
- owner: z.string().min(1),
800
- repo: z.string().min(1),
801
- pr_number: z.number().int().min(1)
1074
+ owner: z3.string().min(1),
1075
+ repo: z3.string().min(1),
1076
+ pr_number: z3.number().int().min(1),
1077
+ after_sha: z3.string().optional(),
1078
+ pass: z3.number().int().min(1).optional(),
1079
+ after_pass: z3.number().int().min(1).optional()
802
1080
  }
803
1081
  },
804
- async ({ owner, repo, pr_number }) => {
1082
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
805
1083
  try {
806
- return ok(await reviewGetPr(owner, repo, pr_number));
1084
+ return ok(
1085
+ await reviewGetPr(owner, repo, pr_number, void 0, {
1086
+ afterSha: after_sha,
1087
+ pass,
1088
+ afterPass: after_pass
1089
+ })
1090
+ );
807
1091
  } catch (err) {
808
1092
  return fail(err);
809
1093
  }
@@ -813,20 +1097,20 @@ function createMergestormMcpServer() {
813
1097
  "review_submit",
814
1098
  {
815
1099
  title: "Submit review",
816
- description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Default waits up to 300s. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting. specialists may be security, performance, architecture, tests, data, api, frontend. governance and seam cannot be requested.",
1100
+ description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Does not wait by default; returns job_id immediately after submission. Explicit wait: true polls one 45s slice by default. Record job_id and loop review_wait until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting. specialists may be security, performance, architecture, tests, data, api, frontend. governance and seam cannot be requested.",
817
1101
  inputSchema: {
818
- cwd: z.string().optional(),
819
- base: z.string().optional(),
820
- head: z.string().optional(),
821
- diff: z.string().optional(),
822
- router: z.enum(["off", "standard", "max", "manual"]).optional(),
823
- specialists: z.array(z.string().min(1)).max(5).optional(),
824
- context: z.string().optional(),
825
- context_files: z.array(z.string().min(1)).max(10).optional(),
826
- thread: z.string().min(1).max(120).optional(),
827
- idempotency_key: z.string().min(1).max(128).optional(),
828
- wait: z.boolean().optional(),
829
- timeout_s: z.number().positive().max(300).optional()
1102
+ cwd: z3.string().optional(),
1103
+ base: z3.string().optional(),
1104
+ head: z3.string().optional(),
1105
+ diff: z3.string().optional(),
1106
+ router: z3.enum(["off", "standard", "max", "manual"]).optional(),
1107
+ specialists: z3.array(z3.string().min(1)).max(5).optional(),
1108
+ context: z3.string().optional(),
1109
+ context_files: z3.array(z3.string().min(1)).max(10).optional(),
1110
+ thread: z3.string().min(1).max(120).optional(),
1111
+ idempotency_key: z3.string().min(1).max(128).optional(),
1112
+ wait: z3.boolean().optional(),
1113
+ timeout_s: z3.number().positive().max(300).optional()
830
1114
  }
831
1115
  },
832
1116
  async (args) => {
@@ -841,10 +1125,10 @@ function createMergestormMcpServer() {
841
1125
  "review_wait",
842
1126
  {
843
1127
  title: "Wait for review",
844
- description: "Poll one Mergestorm review job until it finishes, return in_progress after 300s, or return rate_limited with retry_after_seconds.",
1128
+ description: "Poll one Mergestorm review job for one 45s slice by default (max 300s). On timeout, returns in_progress; call again with the same job_id until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again.",
845
1129
  inputSchema: {
846
- job_id: z.string().min(1),
847
- timeout_s: z.number().positive().max(300).optional()
1130
+ job_id: z3.string().min(1),
1131
+ timeout_s: z3.number().positive().max(300).optional()
848
1132
  }
849
1133
  },
850
1134
  async ({ job_id, timeout_s }) => {
@@ -859,16 +1143,18 @@ function createMergestormMcpServer() {
859
1143
  "review_wait_pr",
860
1144
  {
861
1145
  title: "Wait for GitHub PR Vortex review",
862
- description: "Poll the DB-only GitHub PR Vortex pass until it rests. This tool is read-only. On timeout, returns in_progress. On rate_limited, wait retry_after_seconds before trying again.",
1146
+ description: "Poll the DB-only GitHub PR Vortex pass for up to 45s (pass timeout_s to override, max 300). Review identity is SHA + pass. Pass after_sha for the head you pushed. When you already hold an envelope for that same head, also pass after_pass set to its pass so the wait cannot return that attempt again; keep after_pass unchanged across timeout retries and never replace it with the pass of an in-progress envelope. Omit after_pass for a head you have no envelope for (its first pass is 1). This tool is read-only. On timeout, returns in_progress so you can call again. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
863
1147
  inputSchema: {
864
- owner: z.string().min(1),
865
- repo: z.string().min(1),
866
- pr_number: z.number().int().min(1),
867
- after_sha: z.string().optional(),
868
- timeout_s: z.number().positive().max(300).optional()
1148
+ owner: z3.string().min(1),
1149
+ repo: z3.string().min(1),
1150
+ pr_number: z3.number().int().min(1),
1151
+ after_sha: z3.string().optional(),
1152
+ pass: z3.number().int().min(1).optional(),
1153
+ after_pass: z3.number().int().min(1).optional(),
1154
+ timeout_s: z3.number().positive().max(300).optional()
869
1155
  }
870
1156
  },
871
- async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
1157
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
872
1158
  try {
873
1159
  return ok(
874
1160
  await reviewWaitPr(
@@ -876,7 +1162,9 @@ function createMergestormMcpServer() {
876
1162
  repo,
877
1163
  pr_number,
878
1164
  after_sha,
879
- timeout_s
1165
+ timeout_s,
1166
+ void 0,
1167
+ { pass, afterPass: after_pass }
880
1168
  )
881
1169
  );
882
1170
  } catch (err) {
@@ -884,6 +1172,21 @@ function createMergestormMcpServer() {
884
1172
  }
885
1173
  }
886
1174
  );
1175
+ addTool(
1176
+ "stack_adopt",
1177
+ {
1178
+ title: "Adopt PR stack",
1179
+ description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption requires a Cyclone GitHub App install; without it the API returns cyclone_not_connected \u2014 do not retry, tell the human. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership.",
1180
+ inputSchema: stackAdoptSchema
1181
+ },
1182
+ async (args) => {
1183
+ try {
1184
+ return ok(await stackAdopt(args));
1185
+ } catch (err) {
1186
+ return fail(err);
1187
+ }
1188
+ }
1189
+ );
887
1190
  addTool(
888
1191
  "stack_list",
889
1192
  {
@@ -898,13 +1201,39 @@ function createMergestormMcpServer() {
898
1201
  }
899
1202
  }
900
1203
  );
1204
+ addTool(
1205
+ "stack_set",
1206
+ {
1207
+ title: "Set stack policy",
1208
+ description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review and auto_patch pin Vortex auto-review or Cyclone auto-patch for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings.",
1209
+ inputSchema: {
1210
+ stack_id: z3.string().min(1),
1211
+ auto_land: z3.boolean().optional(),
1212
+ auto_review: z3.boolean().nullable().optional(),
1213
+ auto_patch: z3.boolean().nullable().optional()
1214
+ }
1215
+ },
1216
+ async ({ stack_id, auto_land, auto_review, auto_patch }) => {
1217
+ try {
1218
+ return ok(
1219
+ await stackSet(stack_id, {
1220
+ ...auto_land !== void 0 ? { auto_land } : {},
1221
+ ...auto_review !== void 0 ? { auto_review } : {},
1222
+ ...auto_patch !== void 0 ? { auto_patch } : {}
1223
+ })
1224
+ );
1225
+ } catch (err) {
1226
+ return fail(err);
1227
+ }
1228
+ }
1229
+ );
901
1230
  addTool(
902
1231
  "stack_status",
903
1232
  {
904
1233
  title: "Get stack status",
905
1234
  description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
906
1235
  inputSchema: {
907
- stack_id: z.string().min(1)
1236
+ stack_id: z3.string().min(1)
908
1237
  }
909
1238
  },
910
1239
  async ({ stack_id }) => {
@@ -915,13 +1244,29 @@ function createMergestormMcpServer() {
915
1244
  }
916
1245
  }
917
1246
  );
1247
+ addTool(
1248
+ "stack_wait",
1249
+ {
1250
+ title: "Wait for stack attention",
1251
+ description: "Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout returns waiting. This tool is read-only. Do not pass 300; hosts drop long MCP calls.",
1252
+ inputSchema: stackWaitSchema
1253
+ },
1254
+ async (args, extra) => {
1255
+ try {
1256
+ const payload = await stackWait(args, void 0, { signal: extra.signal });
1257
+ return { ...ok(payload), structuredContent: payload.data };
1258
+ } catch (err) {
1259
+ return fail(err);
1260
+ }
1261
+ }
1262
+ );
918
1263
  addTool(
919
1264
  "queue_status",
920
1265
  {
921
1266
  title: "Get merge queue status",
922
- description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
1267
+ description: "List live merge queue entries (position >= 1) plus the newest bounced entries (position 0) with bounceDetail for the current user, optionally filtered by stack. This tool is read-only.",
923
1268
  inputSchema: {
924
- stack_id: z.string().min(1).optional()
1269
+ stack_id: z3.string().min(1).optional()
925
1270
  }
926
1271
  },
927
1272
  async ({ stack_id }) => {
@@ -950,15 +1295,17 @@ function createMergestormMcpServer() {
950
1295
  "settings_set",
951
1296
  {
952
1297
  title: "Update settings",
953
- description: "Update writable Bearer /api/v1/settings toggles and return the stored result. At least one key is required. cyclone_connected and github_connected are read-only and cannot be set.",
1298
+ description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. cyclone_connected and github_connected are read-only and cannot be set.",
954
1299
  inputSchema: {
955
- auto_review_enabled: z.boolean().optional(),
956
- auto_patch_enabled: z.boolean().optional(),
957
- vortex_show_thinking_traces: z.boolean().optional(),
958
- repo_overview_enabled: z.boolean().optional(),
959
- review_unit_land_prs_enabled: z.boolean().optional(),
960
- cyclone_review_unit_land_prs_enabled: z.boolean().optional(),
961
- vortex_seam_specialist_enabled: z.boolean().optional()
1300
+ ...Object.fromEntries(
1301
+ BEARER_SETTINGS2.map((row) => [
1302
+ row.key,
1303
+ ("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : z3.enum(row.values) : z3.boolean()).optional()
1304
+ ])
1305
+ ),
1306
+ // Preserve these args so the handler rejects them even alongside a writable key.
1307
+ cyclone_connected: z3.unknown().optional(),
1308
+ github_connected: z3.unknown().optional()
962
1309
  }
963
1310
  },
964
1311
  async (args) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.1.3",
3
+ "version": "0.1.6",
4
4
  "description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
- "mergestorm": "^0.3.15",
32
+ "mergestorm": "0.3.19",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {