mergestorm-mcp 0.1.5 → 0.1.7

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 +198 -72
  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. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`.
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) {
@@ -469,17 +479,26 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
469
479
  );
470
480
  } catch (err) {
471
481
  if (err instanceof PrReviewPollTimeoutError) {
472
- const status = err.lastEnvelope?.status ?? "in_progress";
482
+ const status = "in_progress";
483
+ const expectedSha = afterSha?.trim().toLowerCase() ?? "";
484
+ const observedSha = err.lastEnvelope?.head_sha?.trim().toLowerCase() ?? "";
485
+ const headShaMatches = Boolean(
486
+ observedSha && expectedSha.length >= 7 && (observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha))
487
+ );
488
+ const findingCount = headShaMatches ? err.lastEnvelope?.finding_count ?? null : null;
473
489
  return {
474
490
  summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
475
491
  status,
476
492
  head_sha: err.lastEnvelope?.head_sha ?? null,
477
- finding_count: err.lastEnvelope?.finding_count ?? null,
493
+ finding_count: findingCount,
478
494
  phase: err.lastEnvelope?.phase ?? null
479
495
  }),
480
496
  data: {
481
- ...err.lastEnvelope ?? {},
482
497
  status,
498
+ head_sha: err.lastEnvelope?.head_sha ?? null,
499
+ pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
500
+ finding_count: findingCount,
501
+ phase: err.lastEnvelope?.phase ?? null,
483
502
  error: err.message
484
503
  }
485
504
  };
@@ -554,6 +573,7 @@ async function settingsGet(cfg) {
554
573
  // src/tools/settings-set.ts
555
574
  import {
556
575
  CommandError as CommandError8,
576
+ BEARER_SETTINGS,
557
577
  SETTINGS_WRITABLE_KEYS,
558
578
  patchSettings
559
579
  } from "mergestorm/client";
@@ -571,10 +591,17 @@ function settingsPatchFromArgs(args) {
571
591
  for (const key of SETTINGS_WRITABLE_KEYS) {
572
592
  const value = args[key];
573
593
  if (value === void 0) continue;
594
+ const row = BEARER_SETTINGS.find((row2) => row2.key === key);
595
+ if ("kind" in row) {
596
+ const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
597
+ if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
598
+ Object.assign(patch, { [key]: value });
599
+ continue;
600
+ }
574
601
  if (typeof value !== "boolean") {
575
602
  throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
576
603
  }
577
- patch[key] = value;
604
+ Object.assign(patch, { [key]: value });
578
605
  }
579
606
  if (Object.keys(patch).length === 0) {
580
607
  throw new CommandError8(
@@ -653,9 +680,9 @@ var stackAdoptSchema = {
653
680
  owner: z.string().trim().min(1),
654
681
  repo: z.string().trim().min(1),
655
682
  pr_number: z.number().int().min(1),
656
- auto_land: z.literal(false).optional(),
683
+ auto_land: z.boolean().optional(),
657
684
  auto_review: z.boolean().nullable().optional(),
658
- auto_patch: z.literal(false).nullable().optional()
685
+ auto_patch: z.boolean().nullable().optional()
659
686
  };
660
687
  var inputSchema = z.object(stackAdoptSchema);
661
688
  async function stackAdopt(input, cfg) {
@@ -694,14 +721,19 @@ async function stackAdopt(input, cfg) {
694
721
  }
695
722
 
696
723
  // src/tools/stack-list.ts
697
- import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
724
+ import {
725
+ isCommandErrorCode as isCommandErrorCode4,
726
+ listMergeQueueEntries,
727
+ listStacks
728
+ } from "mergestorm/client";
698
729
 
699
730
  // src/tools/stack-summary.ts
700
- function stackSummary(stack) {
731
+ import { stackBlockers, stackBlockersSummary } from "mergestorm/client";
732
+ function stackSummary(stack, entries = []) {
701
733
  const count = stack.layers.length;
702
734
  const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
703
735
  const states = [...new Set(stack.layers.map((layer) => layer.state))];
704
- const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
736
+ const currentState = states.length > 0 ? `restack: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
705
737
  const overrides = [];
706
738
  if (typeof stack.autoReviewOverride === "boolean") {
707
739
  overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
@@ -712,15 +744,17 @@ function stackSummary(stack) {
712
744
  if (stack.cycloneOwnerMatch) {
713
745
  overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
714
746
  }
715
- return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}`;
747
+ const { attention, issues } = stackBlockers(stack, entries);
748
+ return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${stackBlockersSummary(attention, issues)}`;
716
749
  }
717
750
 
718
751
  // src/tools/stack-list.ts
719
752
  async function stackList(cfg) {
720
753
  try {
721
754
  const stacks = await listStacks(cfg);
755
+ const entries = await listMergeQueueEntries(cfg);
722
756
  return {
723
- summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack)).join("\n"),
757
+ summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack, entries)).join("\n"),
724
758
  data: { stacks }
725
759
  };
726
760
  } catch (err) {
@@ -741,8 +775,63 @@ async function stackList(cfg) {
741
775
  }
742
776
  }
743
777
 
778
+ // src/tools/stack-wait.ts
779
+ import {
780
+ loadConfig as loadConfig6,
781
+ stackBlockersSummary as stackBlockersSummary2,
782
+ pollStackWatch,
783
+ StackWatchError,
784
+ StackWatchTimeoutError
785
+ } from "mergestorm/client";
786
+ import { z as z2 } from "zod";
787
+ var stackWaitSchema = {
788
+ stack_id: z2.string().trim().min(1),
789
+ timeout_s: z2.number().finite().min(0).max(45).default(45),
790
+ enrolled_head_sha: z2.string().nullable().optional(),
791
+ after_finished_at: z2.string().nullable().optional(),
792
+ bounce_id: z2.string().nullable().optional()
793
+ };
794
+ async function stackWait(input, cfg, deps = {}) {
795
+ const args = z2.object(stackWaitSchema).parse(input);
796
+ const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
797
+ stackId: args.stack_id,
798
+ enrolledHeadSha: args.enrolled_head_sha ?? null,
799
+ ...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
800
+ ...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
801
+ } : void 0;
802
+ let envelope;
803
+ try {
804
+ envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
805
+ timeoutMs: args.timeout_s * 1e3,
806
+ cursor,
807
+ signal: deps.signal
808
+ });
809
+ } catch (err) {
810
+ if (err instanceof StackWatchError) {
811
+ return {
812
+ summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}${stackBlockersSummary2(err.lastEnvelope.blocker && err.lastEnvelope.prNumber != null ? { prNumber: err.lastEnvelope.prNumber, blocker: err.lastEnvelope.blocker } : null, err.lastEnvelope.issues)}${err.lastEnvelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
813
+ data: {
814
+ ...err.lastEnvelope,
815
+ ...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
816
+ },
817
+ isError: true
818
+ };
819
+ }
820
+ if (!(err instanceof StackWatchTimeoutError)) throw err;
821
+ envelope = err.lastEnvelope;
822
+ }
823
+ return {
824
+ summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary2(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
825
+ data: envelope
826
+ };
827
+ }
828
+
744
829
  // src/tools/stack-status.ts
745
- import { getEnrichedStack, isCommandErrorCode as isCommandErrorCode5 } from "mergestorm/client";
830
+ import {
831
+ getEnrichedStack,
832
+ listMergeQueueEntries as listMergeQueueEntries2,
833
+ isCommandErrorCode as isCommandErrorCode5
834
+ } from "mergestorm/client";
746
835
  async function stackStatus(stackId, cfg) {
747
836
  try {
748
837
  const stack = await getEnrichedStack(stackId, cfg);
@@ -760,8 +849,9 @@ async function stackStatus(stackId, cfg) {
760
849
  isError: true
761
850
  };
762
851
  }
852
+ const entries = await listMergeQueueEntries2(cfg);
763
853
  return {
764
- summary: stackSummary(stack),
854
+ summary: stackSummary(stack, entries),
765
855
  data: { stack }
766
856
  };
767
857
  } catch (err) {
@@ -788,11 +878,11 @@ import {
788
878
  apiBase,
789
879
  configPath,
790
880
  getMe as getMe2,
791
- loadConfig as loadConfig6,
881
+ loadConfig as loadConfig7,
792
882
  resolveApiKey
793
883
  } from "mergestorm/client";
794
884
  async function whoami(cfg) {
795
- const resolved = cfg ?? await loadConfig6();
885
+ const resolved = cfg ?? await loadConfig7();
796
886
  const key = resolveApiKey(resolved);
797
887
  if (!key) {
798
888
  throw new CommandError10(
@@ -831,10 +921,20 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
831
921
  `- 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
922
  "- Do not prefix that comment with cyclone-outcome: (reserved)."
833
923
  ].join("\n");
924
+ var MCP_STACK_WATCH_INSTRUCTIONS = [
925
+ "Stack-watch facts (mergestorm.stack_watch/v1):",
926
+ "- waiting with nonempty issues[] is not idle; in_progress can also carry issues[].",
927
+ "- assessment unavailable does not mean issue-free. timeout_s: 0 reads one enrich and one queue snapshot.",
928
+ "- Restack clean does not mean mergeable or CI-green.",
929
+ "- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
930
+ "- verifyHeadSha is a queue verification SHA, not the live PR head.",
931
+ "- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
932
+ "- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
933
+ ].join("\n");
834
934
 
835
935
  // src/server.ts
836
936
  var MCP_SERVER_NAME = "mergestorm";
837
- var MCP_SERVER_VERSION = "0.1.5";
937
+ var MCP_SERVER_VERSION = "0.1.7";
838
938
  function stripNulls(value) {
839
939
  if (Array.isArray(value)) {
840
940
  return value.map((item) => stripNulls(item));
@@ -868,7 +968,9 @@ function createMergestormMcpServer() {
868
968
  name: MCP_SERVER_NAME,
869
969
  version: MCP_SERVER_VERSION
870
970
  },
871
- { instructions: MCP_PR_LOOP_INSTRUCTIONS }
971
+ { instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
972
+
973
+ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
872
974
  );
873
975
  const addTool = server2.registerTool.bind(server2);
874
976
  addTool(
@@ -905,7 +1007,7 @@ function createMergestormMcpServer() {
905
1007
  title: "List reviews",
906
1008
  description: "List recent Mergestorm review jobs as job envelopes.",
907
1009
  inputSchema: {
908
- limit: z2.number().int().min(1).max(50).optional()
1010
+ limit: z3.number().int().min(1).max(50).optional()
909
1011
  }
910
1012
  },
911
1013
  async ({ limit }) => {
@@ -922,7 +1024,7 @@ function createMergestormMcpServer() {
922
1024
  title: "Get review",
923
1025
  description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
924
1026
  inputSchema: {
925
- job_id: z2.string().min(1)
1027
+ job_id: z3.string().min(1)
926
1028
  }
927
1029
  },
928
1030
  async ({ job_id }) => {
@@ -939,12 +1041,12 @@ function createMergestormMcpServer() {
939
1041
  title: "Get GitHub PR Vortex review",
940
1042
  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
1043
  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()
1044
+ owner: z3.string().min(1),
1045
+ repo: z3.string().min(1),
1046
+ pr_number: z3.number().int().min(1),
1047
+ after_sha: z3.string().optional(),
1048
+ pass: z3.number().int().min(1).optional(),
1049
+ after_pass: z3.number().int().min(1).optional()
948
1050
  }
949
1051
  },
950
1052
  async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
@@ -967,18 +1069,18 @@ function createMergestormMcpServer() {
967
1069
  title: "Submit review",
968
1070
  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
1071
  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()
1072
+ cwd: z3.string().optional(),
1073
+ base: z3.string().optional(),
1074
+ head: z3.string().optional(),
1075
+ diff: z3.string().optional(),
1076
+ router: z3.enum(["off", "standard", "max", "manual"]).optional(),
1077
+ specialists: z3.array(z3.string().min(1)).max(5).optional(),
1078
+ context: z3.string().optional(),
1079
+ context_files: z3.array(z3.string().min(1)).max(10).optional(),
1080
+ thread: z3.string().min(1).max(120).optional(),
1081
+ idempotency_key: z3.string().min(1).max(128).optional(),
1082
+ wait: z3.boolean().optional(),
1083
+ timeout_s: z3.number().positive().max(300).optional()
982
1084
  }
983
1085
  },
984
1086
  async (args) => {
@@ -995,8 +1097,8 @@ function createMergestormMcpServer() {
995
1097
  title: "Wait for review",
996
1098
  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
1099
  inputSchema: {
998
- job_id: z2.string().min(1),
999
- timeout_s: z2.number().positive().max(300).optional()
1100
+ job_id: z3.string().min(1),
1101
+ timeout_s: z3.number().positive().max(300).optional()
1000
1102
  }
1001
1103
  },
1002
1104
  async ({ job_id, timeout_s }) => {
@@ -1013,13 +1115,13 @@ function createMergestormMcpServer() {
1013
1115
  title: "Wait for GitHub PR Vortex review",
1014
1116
  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
1117
  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()
1118
+ owner: z3.string().min(1),
1119
+ repo: z3.string().min(1),
1120
+ pr_number: z3.number().int().min(1),
1121
+ after_sha: z3.string().optional(),
1122
+ pass: z3.number().int().min(1).optional(),
1123
+ after_pass: z3.number().int().min(1).optional(),
1124
+ timeout_s: z3.number().positive().max(300).optional()
1023
1125
  }
1024
1126
  },
1025
1127
  async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
@@ -1044,7 +1146,7 @@ function createMergestormMcpServer() {
1044
1146
  "stack_adopt",
1045
1147
  {
1046
1148
  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.",
1149
+ 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
1150
  inputSchema: stackAdoptSchema
1049
1151
  },
1050
1152
  async (args) => {
@@ -1075,10 +1177,10 @@ function createMergestormMcpServer() {
1075
1177
  title: "Set stack policy",
1076
1178
  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
1179
  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()
1180
+ stack_id: z3.string().min(1),
1181
+ auto_land: z3.boolean().optional(),
1182
+ auto_review: z3.boolean().nullable().optional(),
1183
+ auto_patch: z3.boolean().nullable().optional()
1082
1184
  }
1083
1185
  },
1084
1186
  async ({ stack_id, auto_land, auto_review, auto_patch }) => {
@@ -1101,7 +1203,7 @@ function createMergestormMcpServer() {
1101
1203
  title: "Get stack status",
1102
1204
  description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
1103
1205
  inputSchema: {
1104
- stack_id: z2.string().min(1)
1206
+ stack_id: z3.string().min(1)
1105
1207
  }
1106
1208
  },
1107
1209
  async ({ stack_id }) => {
@@ -1112,13 +1214,29 @@ function createMergestormMcpServer() {
1112
1214
  }
1113
1215
  }
1114
1216
  );
1217
+ addTool(
1218
+ "stack_wait",
1219
+ {
1220
+ title: "Wait for stack attention",
1221
+ description: "Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure. A timeout of failed means no assessment was produced. This tool is read-only.",
1222
+ inputSchema: stackWaitSchema
1223
+ },
1224
+ async (args, extra) => {
1225
+ try {
1226
+ const payload = await stackWait(args, void 0, { signal: extra.signal });
1227
+ return { ...ok(payload), structuredContent: payload.data };
1228
+ } catch (err) {
1229
+ return fail(err);
1230
+ }
1231
+ }
1232
+ );
1115
1233
  addTool(
1116
1234
  "queue_status",
1117
1235
  {
1118
1236
  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.",
1237
+ 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
1238
  inputSchema: {
1121
- stack_id: z2.string().min(1).optional()
1239
+ stack_id: z3.string().min(1).optional()
1122
1240
  }
1123
1241
  },
1124
1242
  async ({ stack_id }) => {
@@ -1147,10 +1265,18 @@ function createMergestormMcpServer() {
1147
1265
  "settings_set",
1148
1266
  {
1149
1267
  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
- )
1268
+ 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.",
1269
+ inputSchema: {
1270
+ ...Object.fromEntries(
1271
+ BEARER_SETTINGS2.map((row) => [
1272
+ row.key,
1273
+ ("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : z3.enum(row.values) : z3.boolean()).optional()
1274
+ ])
1275
+ ),
1276
+ // Preserve these args so the handler rejects them even alongside a writable key.
1277
+ cyclone_connected: z3.unknown().optional(),
1278
+ github_connected: z3.unknown().optional()
1279
+ }
1154
1280
  },
1155
1281
  async (args) => {
1156
1282
  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.7",
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.20",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {