mergestorm-mcp 0.1.5 → 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 +3 -2
  2. package/dist/index.js +223 -67
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,13 +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_adopt`, `stack_list`, `stack_set`, `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
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
+ - `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.
8
8
  - `stack_list` returns the current API key owner's registered stacks.
9
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.
10
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.
11
12
 
12
13
  Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
13
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 as z2 } from "zod";
9
- import { CommandError as CommandError11, SETTINGS_WRITABLE_KEYS as SETTINGS_WRITABLE_KEYS2 } 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";
@@ -46,20 +46,29 @@ import {
46
46
  REVIEW_EXIT
47
47
  } from "mergestorm/client";
48
48
  function entrySummary(entry) {
49
- const repo = entry.owner && entry.repo ? `${entry.owner}/${entry.repo}` : "unknown repo";
50
- const position = entry.position === void 0 ? "queue" : `#${entry.position}`;
51
- 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 ");
52
60
  }
53
61
  async function queueStatus(stackId, cfg) {
54
62
  try {
55
63
  const resolved = cfg ?? await loadConfig2();
64
+ const id = stackId?.trim();
56
65
  const { status, body, retryAfterSeconds } = await apiFetch(
57
66
  resolved,
58
- "/api/v1/stacks/queue"
67
+ id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
59
68
  );
60
69
  if (status === 404) {
61
70
  throw new CommandError2(
62
- "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."
63
72
  );
64
73
  }
65
74
  if (status === 429) {
@@ -81,11 +90,12 @@ async function queueStatus(stackId, cfg) {
81
90
  `Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
82
91
  );
83
92
  }
84
- const id = stackId?.trim();
85
93
  if (id) {
86
- const entry = entries.find((candidate) => candidate?.stackId === id);
87
- if (!entry) {
88
- 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}`;
89
99
  return {
90
100
  summary: message,
91
101
  data: {
@@ -99,12 +109,12 @@ async function queueStatus(stackId, cfg) {
99
109
  };
100
110
  }
101
111
  return {
102
- summary: entrySummary(entry),
103
- data: { entry }
112
+ summary: matchingEntries.map(entrySummary).join("\n"),
113
+ data: { entries: matchingEntries }
104
114
  };
105
115
  }
106
116
  return {
107
- 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"),
108
118
  data: { entries }
109
119
  };
110
120
  } catch (err) {
@@ -554,6 +564,7 @@ async function settingsGet(cfg) {
554
564
  // src/tools/settings-set.ts
555
565
  import {
556
566
  CommandError as CommandError8,
567
+ BEARER_SETTINGS,
557
568
  SETTINGS_WRITABLE_KEYS,
558
569
  patchSettings
559
570
  } from "mergestorm/client";
@@ -571,10 +582,17 @@ function settingsPatchFromArgs(args) {
571
582
  for (const key of SETTINGS_WRITABLE_KEYS) {
572
583
  const value = args[key];
573
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
+ }
574
592
  if (typeof value !== "boolean") {
575
593
  throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
576
594
  }
577
- patch[key] = value;
595
+ Object.assign(patch, { [key]: value });
578
596
  }
579
597
  if (Object.keys(patch).length === 0) {
580
598
  throw new CommandError8(
@@ -653,9 +671,9 @@ var stackAdoptSchema = {
653
671
  owner: z.string().trim().min(1),
654
672
  repo: z.string().trim().min(1),
655
673
  pr_number: z.number().int().min(1),
656
- auto_land: z.literal(false).optional(),
674
+ auto_land: z.boolean().optional(),
657
675
  auto_review: z.boolean().nullable().optional(),
658
- auto_patch: z.literal(false).nullable().optional()
676
+ auto_patch: z.boolean().nullable().optional()
659
677
  };
660
678
  var inputSchema = z.object(stackAdoptSchema);
661
679
  async function stackAdopt(input, cfg) {
@@ -694,10 +712,67 @@ async function stackAdopt(input, cfg) {
694
712
  }
695
713
 
696
714
  // src/tools/stack-list.ts
697
- import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
715
+ import {
716
+ isCommandErrorCode as isCommandErrorCode4,
717
+ listMergeQueueEntries,
718
+ listStacks
719
+ } from "mergestorm/client";
698
720
 
699
721
  // src/tools/stack-summary.ts
700
- 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 = []) {
701
776
  const count = stack.layers.length;
702
777
  const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
703
778
  const states = [...new Set(stack.layers.map((layer) => layer.state))];
@@ -712,15 +787,17 @@ function stackSummary(stack) {
712
787
  if (stack.cycloneOwnerMatch) {
713
788
  overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
714
789
  }
715
- return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}`;
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}` : ""}`;
716
792
  }
717
793
 
718
794
  // src/tools/stack-list.ts
719
795
  async function stackList(cfg) {
720
796
  try {
721
797
  const stacks = await listStacks(cfg);
798
+ const entries = await listMergeQueueEntries(cfg);
722
799
  return {
723
- 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"),
724
801
  data: { stacks }
725
802
  };
726
803
  } catch (err) {
@@ -741,8 +818,62 @@ async function stackList(cfg) {
741
818
  }
742
819
  }
743
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
+
744
871
  // src/tools/stack-status.ts
745
- import { getEnrichedStack, isCommandErrorCode as isCommandErrorCode5 } from "mergestorm/client";
872
+ import {
873
+ getEnrichedStack,
874
+ listMergeQueueEntries as listMergeQueueEntries2,
875
+ isCommandErrorCode as isCommandErrorCode5
876
+ } from "mergestorm/client";
746
877
  async function stackStatus(stackId, cfg) {
747
878
  try {
748
879
  const stack = await getEnrichedStack(stackId, cfg);
@@ -760,8 +891,9 @@ async function stackStatus(stackId, cfg) {
760
891
  isError: true
761
892
  };
762
893
  }
894
+ const entries = await listMergeQueueEntries2(cfg);
763
895
  return {
764
- summary: stackSummary(stack),
896
+ summary: stackSummary(stack, entries),
765
897
  data: { stack }
766
898
  };
767
899
  } catch (err) {
@@ -788,11 +920,11 @@ import {
788
920
  apiBase,
789
921
  configPath,
790
922
  getMe as getMe2,
791
- loadConfig as loadConfig6,
923
+ loadConfig as loadConfig7,
792
924
  resolveApiKey
793
925
  } from "mergestorm/client";
794
926
  async function whoami(cfg) {
795
- const resolved = cfg ?? await loadConfig6();
927
+ const resolved = cfg ?? await loadConfig7();
796
928
  const key = resolveApiKey(resolved);
797
929
  if (!key) {
798
930
  throw new CommandError10(
@@ -834,7 +966,7 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
834
966
 
835
967
  // src/server.ts
836
968
  var MCP_SERVER_NAME = "mergestorm";
837
- var MCP_SERVER_VERSION = "0.1.5";
969
+ var MCP_SERVER_VERSION = "0.1.6";
838
970
  function stripNulls(value) {
839
971
  if (Array.isArray(value)) {
840
972
  return value.map((item) => stripNulls(item));
@@ -905,7 +1037,7 @@ function createMergestormMcpServer() {
905
1037
  title: "List reviews",
906
1038
  description: "List recent Mergestorm review jobs as job envelopes.",
907
1039
  inputSchema: {
908
- limit: z2.number().int().min(1).max(50).optional()
1040
+ limit: z3.number().int().min(1).max(50).optional()
909
1041
  }
910
1042
  },
911
1043
  async ({ limit }) => {
@@ -922,7 +1054,7 @@ function createMergestormMcpServer() {
922
1054
  title: "Get review",
923
1055
  description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
924
1056
  inputSchema: {
925
- job_id: z2.string().min(1)
1057
+ job_id: z3.string().min(1)
926
1058
  }
927
1059
  },
928
1060
  async ({ job_id }) => {
@@ -939,12 +1071,12 @@ function createMergestormMcpServer() {
939
1071
  title: "Get GitHub PR Vortex review",
940
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).",
941
1073
  inputSchema: {
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()
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()
948
1080
  }
949
1081
  },
950
1082
  async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
@@ -967,18 +1099,18 @@ function createMergestormMcpServer() {
967
1099
  title: "Submit review",
968
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.",
969
1101
  inputSchema: {
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()
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()
982
1114
  }
983
1115
  },
984
1116
  async (args) => {
@@ -995,8 +1127,8 @@ function createMergestormMcpServer() {
995
1127
  title: "Wait for review",
996
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.",
997
1129
  inputSchema: {
998
- job_id: z2.string().min(1),
999
- timeout_s: z2.number().positive().max(300).optional()
1130
+ job_id: z3.string().min(1),
1131
+ timeout_s: z3.number().positive().max(300).optional()
1000
1132
  }
1001
1133
  },
1002
1134
  async ({ job_id, timeout_s }) => {
@@ -1013,13 +1145,13 @@ function createMergestormMcpServer() {
1013
1145
  title: "Wait for GitHub PR Vortex review",
1014
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.",
1015
1147
  inputSchema: {
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()
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()
1023
1155
  }
1024
1156
  },
1025
1157
  async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
@@ -1044,7 +1176,7 @@ function createMergestormMcpServer() {
1044
1176
  "stack_adopt",
1045
1177
  {
1046
1178
  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.",
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.",
1048
1180
  inputSchema: stackAdoptSchema
1049
1181
  },
1050
1182
  async (args) => {
@@ -1075,10 +1207,10 @@ function createMergestormMcpServer() {
1075
1207
  title: "Set stack policy",
1076
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.",
1077
1209
  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()
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()
1082
1214
  }
1083
1215
  },
1084
1216
  async ({ stack_id, auto_land, auto_review, auto_patch }) => {
@@ -1101,7 +1233,7 @@ function createMergestormMcpServer() {
1101
1233
  title: "Get stack status",
1102
1234
  description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
1103
1235
  inputSchema: {
1104
- stack_id: z2.string().min(1)
1236
+ stack_id: z3.string().min(1)
1105
1237
  }
1106
1238
  },
1107
1239
  async ({ stack_id }) => {
@@ -1112,13 +1244,29 @@ function createMergestormMcpServer() {
1112
1244
  }
1113
1245
  }
1114
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
+ );
1115
1263
  addTool(
1116
1264
  "queue_status",
1117
1265
  {
1118
1266
  title: "Get merge queue status",
1119
- 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.",
1120
1268
  inputSchema: {
1121
- stack_id: z2.string().min(1).optional()
1269
+ stack_id: z3.string().min(1).optional()
1122
1270
  }
1123
1271
  },
1124
1272
  async ({ stack_id }) => {
@@ -1147,10 +1295,18 @@ function createMergestormMcpServer() {
1147
1295
  "settings_set",
1148
1296
  {
1149
1297
  title: "Update settings",
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.",
1151
- inputSchema: Object.fromEntries(
1152
- SETTINGS_WRITABLE_KEYS2.map((key) => [key, z2.boolean().optional()])
1153
- )
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.",
1299
+ inputSchema: {
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()
1309
+ }
1154
1310
  },
1155
1311
  async (args) => {
1156
1312
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.1.5",
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.17",
32
+ "mergestorm": "0.3.19",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {