mergestorm-mcp 0.1.2 → 0.1.5

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 +4 -2
  2. package/dist/index.js +283 -93
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,10 +1,12 @@
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`, `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. `auto_patch` defaults to `false` and accepts only `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.
9
11
 
10
12
  Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
package/dist/index.js CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
- #!/usr/bin/env node
3
2
 
4
3
  // src/index.ts
5
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
5
 
7
6
  // src/server.ts
8
7
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
- import { z } from "zod";
10
- import { CommandError as CommandError10 } from "mergestorm/client";
8
+ import { z as z2 } from "zod";
9
+ import { CommandError as CommandError11, SETTINGS_WRITABLE_KEYS as SETTINGS_WRITABLE_KEYS2 } from "mergestorm/client";
11
10
 
12
11
  // src/tools/credits.ts
13
12
  import { CommandError, getMe, loadConfig } from "mergestorm/client";
@@ -26,8 +25,10 @@ async function credits(cfg) {
26
25
  }
27
26
  const standard = me.usage.standard;
28
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`;
29
30
  return {
30
- 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}`,
31
32
  data: {
32
33
  usage: me.usage,
33
34
  resets_at: me.resets_at ?? null
@@ -142,9 +143,21 @@ function prReviewTarget(owner, repo, prNumber) {
142
143
  }
143
144
  return { owner: cleanOwner, repo: cleanRepo, prNumber };
144
145
  }
146
+ function prReviewSummary(owner, repo, prNumber, envelope) {
147
+ const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
148
+ const sha = envelope.head_sha?.trim();
149
+ if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
150
+ if (typeof envelope.finding_count === "number") {
151
+ bits.push(
152
+ `${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`
153
+ );
154
+ }
155
+ if (envelope.phase) bits.push(String(envelope.phase));
156
+ return bits.join(" \xB7 ");
157
+ }
145
158
  function payloadFromPrReview(owner, repo, prNumber, envelope) {
146
159
  return {
147
- summary: `${owner}/${repo}#${prNumber} \xB7 ${envelope.status}`,
160
+ summary: prReviewSummary(owner, repo, prNumber, envelope),
148
161
  data: { ...envelope }
149
162
  };
150
163
  }
@@ -159,14 +172,15 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
159
172
  }
160
173
  };
161
174
  }
162
- async function reviewGetPr(owner, repo, prNumber, cfg) {
175
+ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
163
176
  const target = prReviewTarget(owner, repo, prNumber);
164
177
  try {
165
178
  const envelope = await getPrVortexReview(
166
179
  target.owner,
167
180
  target.repo,
168
181
  target.prNumber,
169
- cfg
182
+ cfg,
183
+ { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
170
184
  );
171
185
  return payloadFromPrReview(
172
186
  target.owner,
@@ -260,6 +274,8 @@ import {
260
274
  REVIEW_THREAD_SLUG_RE,
261
275
  ReviewPollTimeoutError,
262
276
  collectReviewInput,
277
+ discoverTrunk,
278
+ formatReviewSubmitError,
263
279
  loadConfig as loadConfig3,
264
280
  loadReviewContext,
265
281
  pollReview,
@@ -267,6 +283,7 @@ import {
267
283
  submitReview
268
284
  } from "mergestorm/client";
269
285
  var MCP_WAIT_CEILING_S = 300;
286
+ var MCP_PR_WAIT_DEFAULT_S = 45;
270
287
  var PINNABLE_SPECIALISTS = [
271
288
  "security",
272
289
  "performance",
@@ -291,8 +308,8 @@ function assertPinnableSpecialists(specialists) {
291
308
  function mcpSandboxRoot() {
292
309
  return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
293
310
  }
294
- function waitTimeoutMs(timeout_s) {
295
- const seconds = timeout_s ?? MCP_WAIT_CEILING_S;
311
+ function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
312
+ const seconds = timeout_s ?? defaultSeconds;
296
313
  if (!Number.isFinite(seconds) || seconds <= 0) {
297
314
  throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
298
315
  }
@@ -307,13 +324,18 @@ async function resolveInput(input) {
307
324
  return {
308
325
  thread,
309
326
  branch,
310
- baseLabel: input.base?.trim() || "main",
327
+ baseLabel: input.base?.trim() || (() => {
328
+ try {
329
+ return discoverTrunk(cwd);
330
+ } catch {
331
+ return "main";
332
+ }
333
+ })(),
311
334
  headLabel: head,
312
335
  diff: input.diff,
313
336
  files: []
314
337
  };
315
338
  }
316
- const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
317
339
  const sandboxRoot = mcpSandboxRoot();
318
340
  try {
319
341
  const [sandboxReal, cwdReal] = await Promise.all([
@@ -328,27 +350,12 @@ async function resolveInput(input) {
328
350
  if (err instanceof CommandError5) throw err;
329
351
  throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
330
352
  }
331
- let lastErr;
332
- let collected = null;
333
- let yielded = false;
334
- for (const base of bases) {
335
- try {
336
- const candidate = await collectReviewInput(base, head, cwd, sandboxRoot);
337
- yielded = true;
338
- if (!collected) collected = candidate;
339
- } catch (err) {
340
- if (yielded) continue;
341
- lastErr = err;
342
- if (input.base?.trim()) throw err;
343
- }
344
- }
345
- if (yielded) return collected;
346
- if (lastErr) throw lastErr;
347
- return null;
353
+ const base = input.base?.trim() || discoverTrunk(cwd);
354
+ return collectReviewInput(base, head, cwd, sandboxRoot);
348
355
  }
349
356
  async function reviewSubmit(input, cfg, opts = {}) {
350
357
  const resolved = cfg ?? await loadConfig3();
351
- const wait = input.wait !== false;
358
+ const wait = input.wait === true;
352
359
  const timeoutMs = waitTimeoutMs(input.timeout_s);
353
360
  assertPinnableSpecialists(input.specialists);
354
361
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
@@ -396,7 +403,13 @@ async function reviewSubmit(input, cfg, opts = {}) {
396
403
  });
397
404
  }
398
405
  if (submitted.status !== 202 && submitted.status !== 200) {
399
- throw new CommandError5(JSON.stringify(submitted.row));
406
+ return {
407
+ ...payloadFromRow({ ...submitted.row, status: "failed" }, {
408
+ jobId,
409
+ error: formatReviewSubmitError(submitted.status, submitted.row)
410
+ }),
411
+ isError: true
412
+ };
400
413
  }
401
414
  if (!jobId) {
402
415
  throw new CommandError5("Review API returned no job_id");
@@ -441,8 +454,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
441
454
  target.repo,
442
455
  target.prNumber,
443
456
  {
444
- timeoutMs: waitTimeoutMs(timeout_s),
457
+ timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
445
458
  afterSha,
459
+ pass: opts.pass,
460
+ afterPass: opts.afterPass,
446
461
  intervalMs: opts.pollIntervalMs
447
462
  }
448
463
  );
@@ -456,7 +471,12 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
456
471
  if (err instanceof PrReviewPollTimeoutError) {
457
472
  const status = err.lastEnvelope?.status ?? "in_progress";
458
473
  return {
459
- summary: `${target.owner}/${target.repo}#${target.prNumber} \xB7 ${status}`,
474
+ summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
475
+ status,
476
+ head_sha: err.lastEnvelope?.head_sha ?? null,
477
+ finding_count: err.lastEnvelope?.finding_count ?? null,
478
+ phase: err.lastEnvelope?.phase ?? null
479
+ }),
460
480
  data: {
461
481
  ...err.lastEnvelope ?? {},
462
482
  status,
@@ -554,9 +574,6 @@ function settingsPatchFromArgs(args) {
554
574
  if (typeof value !== "boolean") {
555
575
  throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
556
576
  }
557
- if (key === "auto_patch_enabled" && value === true) {
558
- throw new CommandError8("auto_patch_enabled cannot be enabled by this tool.", 2, "usage");
559
- }
560
577
  patch[key] = value;
561
578
  }
562
579
  if (Object.keys(patch).length === 0) {
@@ -577,6 +594,105 @@ async function settingsSet(args, cfg) {
577
594
  };
578
595
  }
579
596
 
597
+ // src/tools/stack-adopt.ts
598
+ import { adoptStack, CommandError as CommandError9 } from "mergestorm/client";
599
+ import { z } from "zod";
600
+
601
+ // src/tools/stack-set.ts
602
+ import { setStackPolicy } from "mergestorm/client";
603
+ function triWord(value) {
604
+ return value === null ? "default" : value ? "on" : "off";
605
+ }
606
+ function stackSetPatch(input) {
607
+ const patch = {};
608
+ if (typeof input.auto_land === "boolean") patch.autoEnqueueWhenReady = input.auto_land;
609
+ if (input.auto_review !== void 0) patch.autoReviewOverride = input.auto_review;
610
+ if (input.auto_patch !== void 0) patch.autoPatchOverride = input.auto_patch;
611
+ return patch;
612
+ }
613
+ async function stackSet(stackId, input, cfg) {
614
+ const patch = stackSetPatch(input);
615
+ if (Object.keys(patch).length === 0) {
616
+ return {
617
+ summary: "stack_set needs at least one of auto_land, auto_review, auto_patch",
618
+ isError: true,
619
+ data: {
620
+ error: {
621
+ code: "invalid_input",
622
+ message: "Pass at least one of auto_land, auto_review, auto_patch.",
623
+ stack_id: stackId
624
+ }
625
+ }
626
+ };
627
+ }
628
+ const result = await setStackPolicy(stackId, patch, cfg);
629
+ const parts = [];
630
+ if (typeof input.auto_land === "boolean") {
631
+ parts.push(`Auto land ${input.auto_land ? "on" : "off"}`);
632
+ }
633
+ if (input.auto_review !== void 0) {
634
+ parts.push(`auto-review ${triWord(input.auto_review)}`);
635
+ }
636
+ if (input.auto_patch !== void 0) {
637
+ parts.push(`auto-patch ${triWord(input.auto_patch)}`);
638
+ }
639
+ return {
640
+ summary: `${parts.join(", ")} for stack ${stackId}`,
641
+ data: {
642
+ stack_id: stackId,
643
+ ...typeof input.auto_land === "boolean" ? { auto_land: input.auto_land } : {},
644
+ ...input.auto_review !== void 0 ? { auto_review: input.auto_review } : {},
645
+ ...input.auto_patch !== void 0 ? { auto_patch: input.auto_patch } : {},
646
+ result
647
+ }
648
+ };
649
+ }
650
+
651
+ // src/tools/stack-adopt.ts
652
+ var stackAdoptSchema = {
653
+ owner: z.string().trim().min(1),
654
+ repo: z.string().trim().min(1),
655
+ pr_number: z.number().int().min(1),
656
+ auto_land: z.literal(false).optional(),
657
+ auto_review: z.boolean().nullable().optional(),
658
+ auto_patch: z.literal(false).nullable().optional()
659
+ };
660
+ var inputSchema = z.object(stackAdoptSchema);
661
+ async function stackAdopt(input, cfg) {
662
+ const parsed = inputSchema.safeParse(input);
663
+ if (!parsed.success) {
664
+ const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
665
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
666
+ }
667
+ const { owner, repo, pr_number } = parsed.data;
668
+ try {
669
+ const result = await adoptStack(
670
+ owner,
671
+ repo,
672
+ pr_number,
673
+ cfg,
674
+ stackSetPatch(parsed.data)
675
+ );
676
+ return {
677
+ summary: `Adopted stack for ${owner}/${repo}#${pr_number}`,
678
+ data: { owner, repo, pr_number, result }
679
+ };
680
+ } catch (err) {
681
+ const message = err instanceof Error ? err.message : String(err);
682
+ return {
683
+ summary: message,
684
+ data: {
685
+ error: {
686
+ code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
687
+ message,
688
+ ...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
689
+ }
690
+ },
691
+ isError: true
692
+ };
693
+ }
694
+ }
695
+
580
696
  // src/tools/stack-list.ts
581
697
  import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
582
698
 
@@ -586,7 +702,17 @@ function stackSummary(stack) {
586
702
  const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
587
703
  const states = [...new Set(stack.layers.map((layer) => layer.state))];
588
704
  const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
589
- return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState}`;
705
+ const overrides = [];
706
+ if (typeof stack.autoReviewOverride === "boolean") {
707
+ overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
708
+ }
709
+ if (typeof stack.autoPatchOverride === "boolean") {
710
+ overrides.push(`auto-patch ${stack.autoPatchOverride ? "on" : "off"}`);
711
+ }
712
+ if (stack.cycloneOwnerMatch) {
713
+ overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
714
+ }
715
+ return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}`;
590
716
  }
591
717
 
592
718
  // src/tools/stack-list.ts
@@ -658,11 +784,10 @@ async function stackStatus(stackId, cfg) {
658
784
 
659
785
  // src/tools/whoami.ts
660
786
  import {
661
- CommandError as CommandError9,
787
+ CommandError as CommandError10,
662
788
  apiBase,
663
789
  configPath,
664
790
  getMe as getMe2,
665
- keyDisplay,
666
791
  loadConfig as loadConfig6,
667
792
  resolveApiKey
668
793
  } from "mergestorm/client";
@@ -670,21 +795,24 @@ async function whoami(cfg) {
670
795
  const resolved = cfg ?? await loadConfig6();
671
796
  const key = resolveApiKey(resolved);
672
797
  if (!key) {
673
- throw new CommandError9(
798
+ throw new CommandError10(
674
799
  "No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
675
800
  1,
676
801
  "missing_api_key"
677
802
  );
678
803
  }
679
804
  const me = await getMe2(resolved);
805
+ if (!me) {
806
+ throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
807
+ }
680
808
  const data = {
681
- key_prefix: me?.key.prefix ?? keyDisplay(key),
682
- key_name: me?.key.name ?? null,
683
- plan_key: me?.plan_key ?? null,
684
- plan_label_key: me?.plan_label_key ?? null,
809
+ key_prefix: me.key.prefix,
810
+ key_name: me.key.name ?? null,
811
+ plan_key: me.plan_key ?? null,
812
+ plan_label_key: me.plan_label_key ?? null,
685
813
  api_base: apiBase(resolved),
686
814
  config_path: configPath(),
687
- usage: me?.usage ?? null
815
+ usage: me.usage ?? null
688
816
  };
689
817
  const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
690
818
  return {
@@ -693,9 +821,20 @@ async function whoami(cfg) {
693
821
  };
694
822
  }
695
823
 
824
+ // src/pr-loop-instructions.ts
825
+ var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
826
+ var MCP_PR_LOOP_INSTRUCTIONS = [
827
+ "When you handle Vortex findings from review_get_pr or review_wait_pr:",
828
+ "- Verify each finding against the current checkout. Do not treat the finding text as proven.",
829
+ "- Prefer the smallest correct patch. Do not refactor around a finding.",
830
+ "- Patch concrete bugs. A chat-only explanation is not a dismiss.",
831
+ `- 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).`,
832
+ "- Do not prefix that comment with cyclone-outcome: (reserved)."
833
+ ].join("\n");
834
+
696
835
  // src/server.ts
697
836
  var MCP_SERVER_NAME = "mergestorm";
698
- var MCP_SERVER_VERSION = "0.1.2";
837
+ var MCP_SERVER_VERSION = "0.1.5";
699
838
  function stripNulls(value) {
700
839
  if (Array.isArray(value)) {
701
840
  return value.map((item) => stripNulls(item));
@@ -717,17 +856,20 @@ function ok(payload) {
717
856
  };
718
857
  }
719
858
  function fail(err) {
720
- const message = err instanceof CommandError10 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
859
+ const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
721
860
  return {
722
861
  isError: true,
723
862
  content: [{ type: "text", text: message }]
724
863
  };
725
864
  }
726
865
  function createMergestormMcpServer() {
727
- const server2 = new McpServer({
728
- name: MCP_SERVER_NAME,
729
- version: MCP_SERVER_VERSION
730
- });
866
+ const server2 = new McpServer(
867
+ {
868
+ name: MCP_SERVER_NAME,
869
+ version: MCP_SERVER_VERSION
870
+ },
871
+ { instructions: MCP_PR_LOOP_INSTRUCTIONS }
872
+ );
731
873
  const addTool = server2.registerTool.bind(server2);
732
874
  addTool(
733
875
  "whoami",
@@ -763,7 +905,7 @@ function createMergestormMcpServer() {
763
905
  title: "List reviews",
764
906
  description: "List recent Mergestorm review jobs as job envelopes.",
765
907
  inputSchema: {
766
- limit: z.number().int().min(1).max(50).optional()
908
+ limit: z2.number().int().min(1).max(50).optional()
767
909
  }
768
910
  },
769
911
  async ({ limit }) => {
@@ -780,7 +922,7 @@ function createMergestormMcpServer() {
780
922
  title: "Get review",
781
923
  description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
782
924
  inputSchema: {
783
- job_id: z.string().min(1)
925
+ job_id: z2.string().min(1)
784
926
  }
785
927
  },
786
928
  async ({ job_id }) => {
@@ -795,16 +937,25 @@ function createMergestormMcpServer() {
795
937
  "review_get_pr",
796
938
  {
797
939
  title: "Get GitHub PR Vortex review",
798
- 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.",
940
+ 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).",
799
941
  inputSchema: {
800
- owner: z.string().min(1),
801
- repo: z.string().min(1),
802
- pr_number: z.number().int().min(1)
942
+ owner: z2.string().min(1),
943
+ repo: z2.string().min(1),
944
+ pr_number: z2.number().int().min(1),
945
+ after_sha: z2.string().optional(),
946
+ pass: z2.number().int().min(1).optional(),
947
+ after_pass: z2.number().int().min(1).optional()
803
948
  }
804
949
  },
805
- async ({ owner, repo, pr_number }) => {
950
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
806
951
  try {
807
- return ok(await reviewGetPr(owner, repo, pr_number));
952
+ return ok(
953
+ await reviewGetPr(owner, repo, pr_number, void 0, {
954
+ afterSha: after_sha,
955
+ pass,
956
+ afterPass: after_pass
957
+ })
958
+ );
808
959
  } catch (err) {
809
960
  return fail(err);
810
961
  }
@@ -814,20 +965,20 @@ function createMergestormMcpServer() {
814
965
  "review_submit",
815
966
  {
816
967
  title: "Submit review",
817
- 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.",
968
+ 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.",
818
969
  inputSchema: {
819
- cwd: z.string().optional(),
820
- base: z.string().optional(),
821
- head: z.string().optional(),
822
- diff: z.string().optional(),
823
- router: z.enum(["off", "standard", "max", "manual"]).optional(),
824
- specialists: z.array(z.string().min(1)).max(5).optional(),
825
- context: z.string().optional(),
826
- context_files: z.array(z.string().min(1)).max(10).optional(),
827
- thread: z.string().min(1).max(120).optional(),
828
- idempotency_key: z.string().min(1).max(128).optional(),
829
- wait: z.boolean().optional(),
830
- timeout_s: z.number().positive().max(300).optional()
970
+ cwd: z2.string().optional(),
971
+ base: z2.string().optional(),
972
+ head: z2.string().optional(),
973
+ diff: z2.string().optional(),
974
+ router: z2.enum(["off", "standard", "max", "manual"]).optional(),
975
+ specialists: z2.array(z2.string().min(1)).max(5).optional(),
976
+ context: z2.string().optional(),
977
+ context_files: z2.array(z2.string().min(1)).max(10).optional(),
978
+ thread: z2.string().min(1).max(120).optional(),
979
+ idempotency_key: z2.string().min(1).max(128).optional(),
980
+ wait: z2.boolean().optional(),
981
+ timeout_s: z2.number().positive().max(300).optional()
831
982
  }
832
983
  },
833
984
  async (args) => {
@@ -842,10 +993,10 @@ function createMergestormMcpServer() {
842
993
  "review_wait",
843
994
  {
844
995
  title: "Wait for review",
845
- description: "Poll one Mergestorm review job until it finishes, return in_progress after 300s, or return rate_limited with retry_after_seconds.",
996
+ 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.",
846
997
  inputSchema: {
847
- job_id: z.string().min(1),
848
- timeout_s: z.number().positive().max(300).optional()
998
+ job_id: z2.string().min(1),
999
+ timeout_s: z2.number().positive().max(300).optional()
849
1000
  }
850
1001
  },
851
1002
  async ({ job_id, timeout_s }) => {
@@ -860,16 +1011,18 @@ function createMergestormMcpServer() {
860
1011
  "review_wait_pr",
861
1012
  {
862
1013
  title: "Wait for GitHub PR Vortex review",
863
- 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.",
1014
+ 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.",
864
1015
  inputSchema: {
865
- owner: z.string().min(1),
866
- repo: z.string().min(1),
867
- pr_number: z.number().int().min(1),
868
- after_sha: z.string().optional(),
869
- timeout_s: z.number().positive().max(300).optional()
1016
+ owner: z2.string().min(1),
1017
+ repo: z2.string().min(1),
1018
+ pr_number: z2.number().int().min(1),
1019
+ after_sha: z2.string().optional(),
1020
+ pass: z2.number().int().min(1).optional(),
1021
+ after_pass: z2.number().int().min(1).optional(),
1022
+ timeout_s: z2.number().positive().max(300).optional()
870
1023
  }
871
1024
  },
872
- async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
1025
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
873
1026
  try {
874
1027
  return ok(
875
1028
  await reviewWaitPr(
@@ -877,7 +1030,9 @@ function createMergestormMcpServer() {
877
1030
  repo,
878
1031
  pr_number,
879
1032
  after_sha,
880
- timeout_s
1033
+ timeout_s,
1034
+ void 0,
1035
+ { pass, afterPass: after_pass }
881
1036
  )
882
1037
  );
883
1038
  } catch (err) {
@@ -885,6 +1040,21 @@ function createMergestormMcpServer() {
885
1040
  }
886
1041
  }
887
1042
  );
1043
+ addTool(
1044
+ "stack_adopt",
1045
+ {
1046
+ title: "Adopt PR stack",
1047
+ description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land may only be false; auto_review and auto_patch set per-stack policy, and null clears their overrides. Omitted policy keys follow the account settings. Never changes account settings. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership.",
1048
+ inputSchema: stackAdoptSchema
1049
+ },
1050
+ async (args) => {
1051
+ try {
1052
+ return ok(await stackAdopt(args));
1053
+ } catch (err) {
1054
+ return fail(err);
1055
+ }
1056
+ }
1057
+ );
888
1058
  addTool(
889
1059
  "stack_list",
890
1060
  {
@@ -899,13 +1069,39 @@ function createMergestormMcpServer() {
899
1069
  }
900
1070
  }
901
1071
  );
1072
+ addTool(
1073
+ "stack_set",
1074
+ {
1075
+ title: "Set stack policy",
1076
+ 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.",
1077
+ inputSchema: {
1078
+ stack_id: z2.string().min(1),
1079
+ auto_land: z2.boolean().optional(),
1080
+ auto_review: z2.boolean().nullable().optional(),
1081
+ auto_patch: z2.boolean().nullable().optional()
1082
+ }
1083
+ },
1084
+ async ({ stack_id, auto_land, auto_review, auto_patch }) => {
1085
+ try {
1086
+ return ok(
1087
+ await stackSet(stack_id, {
1088
+ ...auto_land !== void 0 ? { auto_land } : {},
1089
+ ...auto_review !== void 0 ? { auto_review } : {},
1090
+ ...auto_patch !== void 0 ? { auto_patch } : {}
1091
+ })
1092
+ );
1093
+ } catch (err) {
1094
+ return fail(err);
1095
+ }
1096
+ }
1097
+ );
902
1098
  addTool(
903
1099
  "stack_status",
904
1100
  {
905
1101
  title: "Get stack status",
906
1102
  description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
907
1103
  inputSchema: {
908
- stack_id: z.string().min(1)
1104
+ stack_id: z2.string().min(1)
909
1105
  }
910
1106
  },
911
1107
  async ({ stack_id }) => {
@@ -922,7 +1118,7 @@ function createMergestormMcpServer() {
922
1118
  title: "Get merge queue status",
923
1119
  description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
924
1120
  inputSchema: {
925
- stack_id: z.string().min(1).optional()
1121
+ stack_id: z2.string().min(1).optional()
926
1122
  }
927
1123
  },
928
1124
  async ({ stack_id }) => {
@@ -952,15 +1148,9 @@ function createMergestormMcpServer() {
952
1148
  {
953
1149
  title: "Update settings",
954
1150
  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.",
955
- inputSchema: {
956
- auto_review_enabled: z.boolean().optional(),
957
- auto_patch_enabled: z.boolean().optional(),
958
- vortex_show_thinking_traces: z.boolean().optional(),
959
- repo_overview_enabled: z.boolean().optional(),
960
- review_unit_land_prs_enabled: z.boolean().optional(),
961
- cyclone_review_unit_land_prs_enabled: z.boolean().optional(),
962
- vortex_seam_specialist_enabled: z.boolean().optional()
963
- }
1151
+ inputSchema: Object.fromEntries(
1152
+ SETTINGS_WRITABLE_KEYS2.map((key) => [key, z2.boolean().optional()])
1153
+ )
964
1154
  },
965
1155
  async (args) => {
966
1156
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
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.17",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {