mergestorm-mcp 0.1.3 → 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 -92
  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
@@ -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 z2 } from "zod";
9
+ import { CommandError as CommandError11, SETTINGS_WRITABLE_KEYS as SETTINGS_WRITABLE_KEYS2 } 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
@@ -141,9 +143,21 @@ function prReviewTarget(owner, repo, prNumber) {
141
143
  }
142
144
  return { owner: cleanOwner, repo: cleanRepo, prNumber };
143
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
+ }
144
158
  function payloadFromPrReview(owner, repo, prNumber, envelope) {
145
159
  return {
146
- summary: `${owner}/${repo}#${prNumber} \xB7 ${envelope.status}`,
160
+ summary: prReviewSummary(owner, repo, prNumber, envelope),
147
161
  data: { ...envelope }
148
162
  };
149
163
  }
@@ -158,14 +172,15 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
158
172
  }
159
173
  };
160
174
  }
161
- async function reviewGetPr(owner, repo, prNumber, cfg) {
175
+ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
162
176
  const target = prReviewTarget(owner, repo, prNumber);
163
177
  try {
164
178
  const envelope = await getPrVortexReview(
165
179
  target.owner,
166
180
  target.repo,
167
181
  target.prNumber,
168
- cfg
182
+ cfg,
183
+ { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
169
184
  );
170
185
  return payloadFromPrReview(
171
186
  target.owner,
@@ -259,6 +274,8 @@ import {
259
274
  REVIEW_THREAD_SLUG_RE,
260
275
  ReviewPollTimeoutError,
261
276
  collectReviewInput,
277
+ discoverTrunk,
278
+ formatReviewSubmitError,
262
279
  loadConfig as loadConfig3,
263
280
  loadReviewContext,
264
281
  pollReview,
@@ -266,6 +283,7 @@ import {
266
283
  submitReview
267
284
  } from "mergestorm/client";
268
285
  var MCP_WAIT_CEILING_S = 300;
286
+ var MCP_PR_WAIT_DEFAULT_S = 45;
269
287
  var PINNABLE_SPECIALISTS = [
270
288
  "security",
271
289
  "performance",
@@ -290,8 +308,8 @@ function assertPinnableSpecialists(specialists) {
290
308
  function mcpSandboxRoot() {
291
309
  return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
292
310
  }
293
- function waitTimeoutMs(timeout_s) {
294
- 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;
295
313
  if (!Number.isFinite(seconds) || seconds <= 0) {
296
314
  throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
297
315
  }
@@ -306,13 +324,18 @@ async function resolveInput(input) {
306
324
  return {
307
325
  thread,
308
326
  branch,
309
- baseLabel: input.base?.trim() || "main",
327
+ baseLabel: input.base?.trim() || (() => {
328
+ try {
329
+ return discoverTrunk(cwd);
330
+ } catch {
331
+ return "main";
332
+ }
333
+ })(),
310
334
  headLabel: head,
311
335
  diff: input.diff,
312
336
  files: []
313
337
  };
314
338
  }
315
- const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
316
339
  const sandboxRoot = mcpSandboxRoot();
317
340
  try {
318
341
  const [sandboxReal, cwdReal] = await Promise.all([
@@ -327,27 +350,12 @@ async function resolveInput(input) {
327
350
  if (err instanceof CommandError5) throw err;
328
351
  throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
329
352
  }
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;
353
+ const base = input.base?.trim() || discoverTrunk(cwd);
354
+ return collectReviewInput(base, head, cwd, sandboxRoot);
347
355
  }
348
356
  async function reviewSubmit(input, cfg, opts = {}) {
349
357
  const resolved = cfg ?? await loadConfig3();
350
- const wait = input.wait !== false;
358
+ const wait = input.wait === true;
351
359
  const timeoutMs = waitTimeoutMs(input.timeout_s);
352
360
  assertPinnableSpecialists(input.specialists);
353
361
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
@@ -395,7 +403,13 @@ async function reviewSubmit(input, cfg, opts = {}) {
395
403
  });
396
404
  }
397
405
  if (submitted.status !== 202 && submitted.status !== 200) {
398
- 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
+ };
399
413
  }
400
414
  if (!jobId) {
401
415
  throw new CommandError5("Review API returned no job_id");
@@ -440,8 +454,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
440
454
  target.repo,
441
455
  target.prNumber,
442
456
  {
443
- timeoutMs: waitTimeoutMs(timeout_s),
457
+ timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
444
458
  afterSha,
459
+ pass: opts.pass,
460
+ afterPass: opts.afterPass,
445
461
  intervalMs: opts.pollIntervalMs
446
462
  }
447
463
  );
@@ -455,7 +471,12 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
455
471
  if (err instanceof PrReviewPollTimeoutError) {
456
472
  const status = err.lastEnvelope?.status ?? "in_progress";
457
473
  return {
458
- 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
+ }),
459
480
  data: {
460
481
  ...err.lastEnvelope ?? {},
461
482
  status,
@@ -553,9 +574,6 @@ function settingsPatchFromArgs(args) {
553
574
  if (typeof value !== "boolean") {
554
575
  throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
555
576
  }
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
577
  patch[key] = value;
560
578
  }
561
579
  if (Object.keys(patch).length === 0) {
@@ -576,6 +594,105 @@ async function settingsSet(args, cfg) {
576
594
  };
577
595
  }
578
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
+
579
696
  // src/tools/stack-list.ts
580
697
  import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
581
698
 
@@ -585,7 +702,17 @@ function stackSummary(stack) {
585
702
  const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
586
703
  const states = [...new Set(stack.layers.map((layer) => layer.state))];
587
704
  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}`;
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("")}`;
589
716
  }
590
717
 
591
718
  // src/tools/stack-list.ts
@@ -657,11 +784,10 @@ async function stackStatus(stackId, cfg) {
657
784
 
658
785
  // src/tools/whoami.ts
659
786
  import {
660
- CommandError as CommandError9,
787
+ CommandError as CommandError10,
661
788
  apiBase,
662
789
  configPath,
663
790
  getMe as getMe2,
664
- keyDisplay,
665
791
  loadConfig as loadConfig6,
666
792
  resolveApiKey
667
793
  } from "mergestorm/client";
@@ -669,21 +795,24 @@ async function whoami(cfg) {
669
795
  const resolved = cfg ?? await loadConfig6();
670
796
  const key = resolveApiKey(resolved);
671
797
  if (!key) {
672
- throw new CommandError9(
798
+ throw new CommandError10(
673
799
  "No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
674
800
  1,
675
801
  "missing_api_key"
676
802
  );
677
803
  }
678
804
  const me = await getMe2(resolved);
805
+ if (!me) {
806
+ throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
807
+ }
679
808
  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,
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,
684
813
  api_base: apiBase(resolved),
685
814
  config_path: configPath(),
686
- usage: me?.usage ?? null
815
+ usage: me.usage ?? null
687
816
  };
688
817
  const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
689
818
  return {
@@ -692,9 +821,20 @@ async function whoami(cfg) {
692
821
  };
693
822
  }
694
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
+
695
835
  // src/server.ts
696
836
  var MCP_SERVER_NAME = "mergestorm";
697
- var MCP_SERVER_VERSION = "0.1.3";
837
+ var MCP_SERVER_VERSION = "0.1.5";
698
838
  function stripNulls(value) {
699
839
  if (Array.isArray(value)) {
700
840
  return value.map((item) => stripNulls(item));
@@ -716,17 +856,20 @@ function ok(payload) {
716
856
  };
717
857
  }
718
858
  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);
859
+ const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
720
860
  return {
721
861
  isError: true,
722
862
  content: [{ type: "text", text: message }]
723
863
  };
724
864
  }
725
865
  function createMergestormMcpServer() {
726
- const server2 = new McpServer({
727
- name: MCP_SERVER_NAME,
728
- version: MCP_SERVER_VERSION
729
- });
866
+ const server2 = new McpServer(
867
+ {
868
+ name: MCP_SERVER_NAME,
869
+ version: MCP_SERVER_VERSION
870
+ },
871
+ { instructions: MCP_PR_LOOP_INSTRUCTIONS }
872
+ );
730
873
  const addTool = server2.registerTool.bind(server2);
731
874
  addTool(
732
875
  "whoami",
@@ -762,7 +905,7 @@ function createMergestormMcpServer() {
762
905
  title: "List reviews",
763
906
  description: "List recent Mergestorm review jobs as job envelopes.",
764
907
  inputSchema: {
765
- limit: z.number().int().min(1).max(50).optional()
908
+ limit: z2.number().int().min(1).max(50).optional()
766
909
  }
767
910
  },
768
911
  async ({ limit }) => {
@@ -779,7 +922,7 @@ function createMergestormMcpServer() {
779
922
  title: "Get review",
780
923
  description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
781
924
  inputSchema: {
782
- job_id: z.string().min(1)
925
+ job_id: z2.string().min(1)
783
926
  }
784
927
  },
785
928
  async ({ job_id }) => {
@@ -794,16 +937,25 @@ function createMergestormMcpServer() {
794
937
  "review_get_pr",
795
938
  {
796
939
  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.",
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).",
798
941
  inputSchema: {
799
- owner: z.string().min(1),
800
- repo: z.string().min(1),
801
- 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()
802
948
  }
803
949
  },
804
- async ({ owner, repo, pr_number }) => {
950
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
805
951
  try {
806
- 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
+ );
807
959
  } catch (err) {
808
960
  return fail(err);
809
961
  }
@@ -813,20 +965,20 @@ function createMergestormMcpServer() {
813
965
  "review_submit",
814
966
  {
815
967
  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.",
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.",
817
969
  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()
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()
830
982
  }
831
983
  },
832
984
  async (args) => {
@@ -841,10 +993,10 @@ function createMergestormMcpServer() {
841
993
  "review_wait",
842
994
  {
843
995
  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.",
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.",
845
997
  inputSchema: {
846
- job_id: z.string().min(1),
847
- timeout_s: z.number().positive().max(300).optional()
998
+ job_id: z2.string().min(1),
999
+ timeout_s: z2.number().positive().max(300).optional()
848
1000
  }
849
1001
  },
850
1002
  async ({ job_id, timeout_s }) => {
@@ -859,16 +1011,18 @@ function createMergestormMcpServer() {
859
1011
  "review_wait_pr",
860
1012
  {
861
1013
  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.",
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.",
863
1015
  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()
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()
869
1023
  }
870
1024
  },
871
- async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
1025
+ async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
872
1026
  try {
873
1027
  return ok(
874
1028
  await reviewWaitPr(
@@ -876,7 +1030,9 @@ function createMergestormMcpServer() {
876
1030
  repo,
877
1031
  pr_number,
878
1032
  after_sha,
879
- timeout_s
1033
+ timeout_s,
1034
+ void 0,
1035
+ { pass, afterPass: after_pass }
880
1036
  )
881
1037
  );
882
1038
  } catch (err) {
@@ -884,6 +1040,21 @@ function createMergestormMcpServer() {
884
1040
  }
885
1041
  }
886
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
+ );
887
1058
  addTool(
888
1059
  "stack_list",
889
1060
  {
@@ -898,13 +1069,39 @@ function createMergestormMcpServer() {
898
1069
  }
899
1070
  }
900
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
+ );
901
1098
  addTool(
902
1099
  "stack_status",
903
1100
  {
904
1101
  title: "Get stack status",
905
1102
  description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
906
1103
  inputSchema: {
907
- stack_id: z.string().min(1)
1104
+ stack_id: z2.string().min(1)
908
1105
  }
909
1106
  },
910
1107
  async ({ stack_id }) => {
@@ -921,7 +1118,7 @@ function createMergestormMcpServer() {
921
1118
  title: "Get merge queue status",
922
1119
  description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
923
1120
  inputSchema: {
924
- stack_id: z.string().min(1).optional()
1121
+ stack_id: z2.string().min(1).optional()
925
1122
  }
926
1123
  },
927
1124
  async ({ stack_id }) => {
@@ -951,15 +1148,9 @@ function createMergestormMcpServer() {
951
1148
  {
952
1149
  title: "Update settings",
953
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.",
954
- 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()
962
- }
1151
+ inputSchema: Object.fromEntries(
1152
+ SETTINGS_WRITABLE_KEYS2.map((key) => [key, z2.boolean().optional()])
1153
+ )
963
1154
  },
964
1155
  async (args) => {
965
1156
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.1.3",
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": {