mergestorm-mcp 0.2.19 → 0.2.21

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 +7 -3
  2. package/dist/index.js +323 -130
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # mergestorm-mcp
2
2
 
3
- Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `review_dismiss`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
3
+ Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `review_dismiss`, `issue_create`, `stack_adopt`, `stack_list`, `stack_publish`, `stack_publish_status`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
4
4
 
5
5
  Every tool declares MCP `annotations` for read-only access, destructive changes, idempotence, and external systems. Reads are read-only; submission, adoption, dismissal, and policy changes are writes. Tools that can change existing policy, topology, or review gates are marked potentially destructive even when some inputs only preview or read. Annotations describe effects; the client still controls approval and authorization.
6
6
 
@@ -16,10 +16,14 @@ Every tool declares MCP `annotations` for read-only access, destructive changes,
16
16
  - **Gates.** `gate.seam.cleared` is true only when every finding of that integration review is dismissed and it is still the member's current seam verdict. `gate.vortex.cleared` is true once every Vortex Blocker (error finding) at the live head is dismissed (every Core review merged into that head) and no Vortex review of the head is running or incomplete; with a warning still open the head reads `comment`, which Auto land accepts; otherwise `gate.vortex.reason` and `message` say what still holds it: that head's Vortex verdict becomes approve for Auto land, the stack watch, the dashboard and Cyclone. A partial dismissal clears nothing. CI and Auto land policy are unchanged (`gate.other_gates: "unchanged"`). For a stack PR the result carries `watch` with the next `stack_wait` call.
17
17
  - **No resurfacing.** Vortex does not raise a dismissed finding again on the same diff: the same head, or a restack whose diff is unchanged.
18
18
 
19
- The stack tools expose reads plus one scoped policy write:
19
+ `issue_create({ title, body?, labels?, owner?, repo?, cwd? })` opens a GitHub issue through Mergestorm with no GitHub credentials: the Surge App creates it and names the caller in the body. It defaults to the origin repository of `cwd`. Labels must already exist on the repository. The same title again returns the open issue it already made (`created: false`), so a retry does not duplicate it. Put `Fixes #<number>` in the commit or pull request body.
20
+
21
+ The stack tools expose reads, one scoped policy write, and publication of your own branch:
20
22
 
21
23
  - `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch?, auto_resolve_conflicts?, auto_fix_ci? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review`, `auto_patch`, `auto_resolve_conflicts`, and `auto_fix_ci` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership. Adoption needs the Mergestorm Surge GitHub App on the repository (Cyclone is the auto-patch App). A refusal keeps the code `cyclone_not_connected` or `cyclone_not_installed`; its message, and `error.reason` starting with `surge_` when Surge is the one to install, say which App is missing.
22
24
  - `stack_list` returns the current API key owner's registered stacks.
25
+ - `stack_publish({ cwd?, owner?, repo?, branch?, base?, title?, body?, replace_head?, auto_land? })` publishes a local branch with no GitHub credentials. It reads the branch's commits from the repository at `cwd` and sends them to Mergestorm, which writes them and the branch on GitHub as the Surge App, opens a ready pull request, and registers the stack with Auto patch pinned off, Auto land on for a new stack, and Auto review left to the account. It never runs `gh`, `git push` or `git fetch`. Call it again for every later commit to that branch, and after a lost response: it resumes and does not duplicate the branch, commits, pull request or stack. It refuses with `head_changed` when the branch on GitHub holds work the local branch does not; `replace_head` names the exact GitHub head to replace. Limits: 20 commits, 500 changed paths, 2 MiB a file, 8 MiB in total; no submodules or merge commits. The result carries the verified head commit and tree, the pull request, the stack and its `watch`.
26
+ - `stack_publish_status({ cwd?, owner?, repo?, branch? })` reads how far a publication got (`absent`, `pushed`, `opened`, `registered`) with the head on GitHub, the pull request and the stack's Auto patch and Auto land state. It changes nothing.
23
27
  - `stack_set({ stack_id, auto_land?, auto_review?, auto_patch?, auto_resolve_conflicts?, auto_fix_ci? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review`, `auto_patch`, `auto_resolve_conflicts`, and `auto_fix_ci` pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI 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.
24
28
  - `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, plus `attention`, `issues`, and `currentCandidate` from the same blocker rules as `stack_wait`, read against that stack's own merge queue. It returns a structured `stack_not_found` or `rate_limited` error.
25
29
  - `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`. While Cyclone or Vortex is still working on the blocked PR, `stack_wait` and `stack_status` return `in_progress` instead of `attention`, with `busy[]` naming the PR, blocker, and agent, and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and busy flags. A nonempty `busy[]` means wait. A queued Vortex run holds for at most 15 minutes, `seam_pending` alone does not hold, and reviewing or patching holds until the run finishes or its lease or check run is cleared. A blocker no agent run clears (a merge conflict vs the live parent, a restack `Conflict` or `Restack failed`, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that `in_progress`, in `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"` and `waitingOn` listing the busy agents; act when a snapshot after the agents finish returns `attention`, re-reading the blocker then. For the three stuck-seam blockers, `repair` has kind `seam_review_stuck`: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. `repair` is included when another concrete repair hint is available. Blockers the agents can change keep `blocker` and `actAfter` null. If a hold looks stuck, refresh `stack_status` and tell the human.
@@ -36,7 +40,7 @@ Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
36
40
 
37
41
  ## Client setup
38
42
 
39
- Requires Node.js 22+ and a Mergestorm API key. Run `mg login` on the machine running the MCP server, or supply `MERGESTORM_API_KEY` through the host's environment. The stdio server uses that credential; it does not provide an OAuth endpoint. Remote hosts need their own configuration and access to any reviewed checkout.
43
+ Requires Node.js 22+ and a Mergestorm API key. Run `mg login` on the machine running the MCP server, or supply `MERGESTORM_API_KEY` through the host's environment. The stdio server uses that credential; it does not provide an OAuth endpoint. An agent on that machine can sign in without a browser: `mg login --start` prints a link and a code and exits, the account owner approves the link on any device, and `mg login --finish` stores the key. Remote hosts need their own configuration and access to any reviewed checkout.
40
44
 
41
45
  ### Codex
42
46
 
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 z3 } from "zod";
9
- import { CommandError as CommandError11, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
8
+ import { z as z5 } from "zod";
9
+ import { CommandError as CommandError13, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
10
10
 
11
11
  // src/brand-icons.ts
12
12
  var MARK_RECTS = [
@@ -75,10 +75,59 @@ async function credits(cfg) {
75
75
  };
76
76
  }
77
77
 
78
+ // src/tools/issue-create.ts
79
+ import { CommandError as CommandError2, openStackIssue, parseGithubOriginRepo } from "mergestorm/client";
80
+ import { z } from "zod";
81
+ var issueCreateSchema = {
82
+ title: z.string().trim().min(1).max(256),
83
+ body: z.string().max(6e4).optional(),
84
+ labels: z.array(z.string().trim().min(1).max(50)).max(10).optional(),
85
+ owner: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
86
+ repo: z.string().trim().regex(/^[A-Za-z0-9_.-]{1,100}$/).optional(),
87
+ cwd: z.string().trim().min(1).optional()
88
+ };
89
+ var inputSchema = z.object(issueCreateSchema);
90
+ async function issueCreate(input, cfg, deps = {}) {
91
+ const parsed = inputSchema.safeParse(input);
92
+ if (!parsed.success || Boolean(parsed.data.owner) !== Boolean(parsed.data.repo)) {
93
+ const message = "issue_create needs a one-line title of at most 256 characters, and owner and repo together";
94
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
95
+ }
96
+ const { title, body, labels, owner, repo, cwd } = parsed.data;
97
+ try {
98
+ const target2 = owner && repo ? { owner, repo } : (deps.parseGithubOriginRepo ?? parseGithubOriginRepo)(cwd);
99
+ if (!target2) {
100
+ const message = "Could not read a GitHub owner/repo from origin. Pass owner and repo.";
101
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
102
+ }
103
+ const issue = await (deps.openStackIssue ?? openStackIssue)(
104
+ { owner: target2.owner, repo: target2.repo, title, body: body ?? "", labels: labels ?? [] },
105
+ cfg
106
+ );
107
+ return {
108
+ summary: `${issue.created ? "Opened" : "Already open:"} ${target2.owner}/${target2.repo}#${issue.number} ${issue.url}. Put "Fixes #${issue.number}" in the commit or pull request body.`,
109
+ data: { owner: target2.owner, repo: target2.repo, ...issue }
110
+ };
111
+ } catch (err) {
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ return {
114
+ summary: message,
115
+ data: {
116
+ error: {
117
+ code: err instanceof CommandError2 ? err.code ?? "issue_create_failed" : "issue_create_failed",
118
+ message,
119
+ ...err instanceof CommandError2 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
120
+ }
121
+ },
122
+ isError: true
123
+ };
124
+ }
125
+ }
126
+
78
127
  // src/tools/queue-status.ts
79
128
  import {
80
129
  apiFetch,
81
- CommandError as CommandError2,
130
+ CommandError as CommandError3,
82
131
  isCommandErrorCode,
83
132
  loadConfig as loadConfig2,
84
133
  rateLimitedMessage,
@@ -106,12 +155,12 @@ async function queueStatus(stackId, cfg) {
106
155
  id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
107
156
  );
108
157
  if (status === 404) {
109
- throw new CommandError2(
158
+ throw new CommandError3(
110
159
  "Merge queue route returned 404. Deploy the API."
111
160
  );
112
161
  }
113
162
  if (status === 429) {
114
- throw new CommandError2(
163
+ throw new CommandError3(
115
164
  rateLimitedMessage(retryAfterSeconds),
116
165
  REVIEW_EXIT.rate_limited,
117
166
  "rate_limited",
@@ -119,13 +168,13 @@ async function queueStatus(stackId, cfg) {
119
168
  );
120
169
  }
121
170
  if (status !== 200) {
122
- throw new CommandError2(
171
+ throw new CommandError3(
123
172
  `Failed to get queue status (HTTP ${status}): ${JSON.stringify(body)}`
124
173
  );
125
174
  }
126
175
  const entries = body.entries;
127
176
  if (!Array.isArray(entries)) {
128
- throw new CommandError2(
177
+ throw new CommandError3(
129
178
  `Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
130
179
  );
131
180
  }
@@ -190,13 +239,13 @@ import {
190
239
  stackWatchBackgroundCommand,
191
240
  stackWatchObligation
192
241
  } from "mergestorm/client";
193
- import { z } from "zod";
242
+ import { z as z2 } from "zod";
194
243
  var stackWaitSchema = {
195
- stack_id: z.string().trim().min(1),
196
- timeout_s: z.number().finite().min(0).max(45).default(45),
197
- enrolled_head_sha: z.string().nullable().optional(),
198
- after_finished_at: z.string().nullable().optional(),
199
- bounce_id: z.string().nullable().optional()
244
+ stack_id: z2.string().trim().min(1),
245
+ timeout_s: z2.number().finite().min(0).max(45).default(45),
246
+ enrolled_head_sha: z2.string().nullable().optional(),
247
+ after_finished_at: z2.string().nullable().optional(),
248
+ bounce_id: z2.string().nullable().optional()
200
249
  };
201
250
  function repairSummary(repair) {
202
251
  if (!repair) return "";
@@ -233,7 +282,7 @@ function stackWaitStatusLine(envelope) {
233
282
  return `${base} \xB7 watch not finished: only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId)} as a background command and end your turn; otherwise call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
234
283
  }
235
284
  async function stackWait(input, cfg, deps = {}) {
236
- const args = z.object(stackWaitSchema).parse(input);
285
+ const args = z2.object(stackWaitSchema).parse(input);
237
286
  const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
238
287
  stackId: args.stack_id,
239
288
  enrolledHeadSha: args.enrolled_head_sha ?? null,
@@ -346,7 +395,7 @@ import { createHash } from "node:crypto";
346
395
  import { realpath } from "node:fs/promises";
347
396
  import { relative, resolve, sep } from "node:path";
348
397
  import {
349
- CommandError as CommandError3,
398
+ CommandError as CommandError4,
350
399
  REVIEW_THREAD_SLUG_RE,
351
400
  ReviewPollTimeoutError,
352
401
  collectReviewInput,
@@ -401,7 +450,7 @@ function assertPinnableSpecialists(specialists) {
401
450
  (id) => !PINNABLE_SPECIALISTS.includes(id)
402
451
  );
403
452
  if (unknown.length === 0) return;
404
- throw new CommandError3(
453
+ throw new CommandError4(
405
454
  `Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
406
455
  2,
407
456
  "usage"
@@ -413,7 +462,7 @@ function mcpSandboxRoot() {
413
462
  function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
414
463
  const seconds = timeout_s ?? defaultSeconds;
415
464
  if (!Number.isFinite(seconds) || seconds <= 0) {
416
- throw new CommandError3("timeout_s must be a positive number of seconds", 2, "usage");
465
+ throw new CommandError4("timeout_s must be a positive number of seconds", 2, "usage");
417
466
  }
418
467
  return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
419
468
  }
@@ -446,11 +495,11 @@ async function resolveInput(input) {
446
495
  ]);
447
496
  const relToSandbox = relative(sandboxReal, cwdReal);
448
497
  if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
449
- throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
498
+ throw new CommandError4("cwd must be inside the configured sandbox root", 2, "usage");
450
499
  }
451
500
  } catch (err) {
452
- if (err instanceof CommandError3) throw err;
453
- throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
501
+ if (err instanceof CommandError4) throw err;
502
+ throw new CommandError4("cwd must be inside the configured sandbox root", 2, "usage");
454
503
  }
455
504
  const base = input.base?.trim() || discoverTrunk(cwd);
456
505
  return collectReviewInput(base, head, cwd, sandboxRoot);
@@ -461,10 +510,10 @@ async function reviewSubmit(input, cfg, opts = {}) {
461
510
  const timeoutMs = waitTimeoutMs(input.timeout_s);
462
511
  assertPinnableSpecialists(input.specialists);
463
512
  if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
464
- throw new CommandError3("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
513
+ throw new CommandError4("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
465
514
  }
466
515
  if (input.context === "-") {
467
- throw new CommandError3(
516
+ throw new CommandError4(
468
517
  'context "-" (stdin) is not available over MCP; pass the text directly',
469
518
  2,
470
519
  "usage"
@@ -514,7 +563,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
514
563
  };
515
564
  }
516
565
  if (!jobId) {
517
- throw new CommandError3("Review API returned no job_id");
566
+ throw new CommandError4("Review API returned no job_id");
518
567
  }
519
568
  if (!wait) {
520
569
  return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
@@ -550,7 +599,7 @@ import {
550
599
 
551
600
  // src/tools/review-get-pr.ts
552
601
  import {
553
- CommandError as CommandError4,
602
+ CommandError as CommandError5,
554
603
  getPrVortexReview,
555
604
  isCommandErrorCode as isCommandErrorCode3
556
605
  } from "mergestorm/client";
@@ -558,7 +607,7 @@ function prReviewTarget(owner, repo, prNumber) {
558
607
  const cleanOwner = owner.trim();
559
608
  const cleanRepo = repo.trim();
560
609
  if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
561
- throw new CommandError4(
610
+ throw new CommandError5(
562
611
  "owner, repo, and a positive pr_number are required",
563
612
  2,
564
613
  "usage"
@@ -596,27 +645,27 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
596
645
  };
597
646
  }
598
647
  async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
599
- const target = prReviewTarget(owner, repo, prNumber);
648
+ const target2 = prReviewTarget(owner, repo, prNumber);
600
649
  try {
601
650
  const envelope = await getPrVortexReview(
602
- target.owner,
603
- target.repo,
604
- target.prNumber,
651
+ target2.owner,
652
+ target2.repo,
653
+ target2.prNumber,
605
654
  cfg,
606
655
  { afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
607
656
  );
608
657
  return payloadFromPrReview(
609
- target.owner,
610
- target.repo,
611
- target.prNumber,
658
+ target2.owner,
659
+ target2.repo,
660
+ target2.prNumber,
612
661
  envelope
613
662
  );
614
663
  } catch (err) {
615
664
  const rateLimited = payloadFromPrRateLimit(
616
665
  err,
617
- target.owner,
618
- target.repo,
619
- target.prNumber
666
+ target2.owner,
667
+ target2.repo,
668
+ target2.prNumber
620
669
  );
621
670
  if (rateLimited) return rateLimited;
622
671
  throw err;
@@ -625,10 +674,10 @@ async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
625
674
 
626
675
  // src/tools/review-wait-pr.ts
627
676
  async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
628
- const target = prReviewTarget(owner, repo, prNumber);
677
+ const target2 = prReviewTarget(owner, repo, prNumber);
629
678
  const resolved = cfg ?? await loadConfig5();
630
679
  if (timeout_s === 0) {
631
- return reviewGetPr(target.owner, target.repo, target.prNumber, resolved, {
680
+ return reviewGetPr(target2.owner, target2.repo, target2.prNumber, resolved, {
632
681
  afterSha,
633
682
  pass: opts.pass,
634
683
  afterPass: opts.afterPass
@@ -637,9 +686,9 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
637
686
  try {
638
687
  const envelope = await pollPrVortexReview(
639
688
  resolved,
640
- target.owner,
641
- target.repo,
642
- target.prNumber,
689
+ target2.owner,
690
+ target2.repo,
691
+ target2.prNumber,
643
692
  {
644
693
  timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
645
694
  afterSha,
@@ -649,9 +698,9 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
649
698
  }
650
699
  );
651
700
  return payloadFromPrReview(
652
- target.owner,
653
- target.repo,
654
- target.prNumber,
701
+ target2.owner,
702
+ target2.repo,
703
+ target2.prNumber,
655
704
  envelope
656
705
  );
657
706
  } catch (err) {
@@ -663,14 +712,14 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
663
712
  observedSha && expectedSha.length >= 7 && (observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha))
664
713
  );
665
714
  const findingCount = headShaMatches ? err.lastEnvelope?.finding_count ?? null : null;
666
- const summary = prReviewSummary(target.owner, target.repo, target.prNumber, {
715
+ const summary = prReviewSummary(target2.owner, target2.repo, target2.prNumber, {
667
716
  status,
668
717
  head_sha: err.lastEnvelope?.head_sha ?? null,
669
718
  finding_count: findingCount,
670
719
  phase: err.lastEnvelope?.phase ?? null
671
720
  });
672
721
  return {
673
- summary: err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target.owner, target.repo, target.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`,
722
+ summary: err.reviewSeen ? summary : `${summary} \xB7 ${noPrReviewYetMessage(target2.owner, target2.repo, target2.prNumber, { afterSha, pass: opts.pass, afterPass: opts.afterPass })}`,
674
723
  data: {
675
724
  status,
676
725
  head_sha: err.lastEnvelope?.head_sha ?? null,
@@ -683,9 +732,9 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
683
732
  }
684
733
  const rateLimited = payloadFromPrRateLimit(
685
734
  err,
686
- target.owner,
687
- target.repo,
688
- target.prNumber
735
+ target2.owner,
736
+ target2.repo,
737
+ target2.prNumber
689
738
  );
690
739
  if (rateLimited) return rateLimited;
691
740
  throw err;
@@ -694,7 +743,7 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
694
743
 
695
744
  // src/tools/review-wait.ts
696
745
  import {
697
- CommandError as CommandError6,
746
+ CommandError as CommandError7,
698
747
  ReviewPollTimeoutError as ReviewPollTimeoutError2,
699
748
  loadConfig as loadConfig6,
700
749
  pollReview as pollReview2
@@ -702,7 +751,7 @@ import {
702
751
 
703
752
  // src/tools/review-get.ts
704
753
  import {
705
- CommandError as CommandError5,
754
+ CommandError as CommandError6,
706
755
  getReview,
707
756
  toReviewJobEnvelope as toReviewJobEnvelope3
708
757
  } from "mergestorm/client";
@@ -716,7 +765,7 @@ async function reviewGet(jobId, cfg) {
716
765
  throw err;
717
766
  }
718
767
  if (!row || typeof row !== "object" || typeof row.status !== "string") {
719
- throw new CommandError5(`Unexpected response for review ${jobId}.`);
768
+ throw new CommandError6(`Unexpected response for review ${jobId}.`);
720
769
  }
721
770
  const envelope = toReviewJobEnvelope3(row, { jobId });
722
771
  const bits = [envelope.status];
@@ -732,7 +781,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
732
781
  const resolved = cfg ?? await loadConfig6();
733
782
  const id = jobId.trim();
734
783
  if (!id) {
735
- throw new CommandError6("job_id is required", 2, "usage");
784
+ throw new CommandError7("job_id is required", 2, "usage");
736
785
  }
737
786
  if (timeout_s === 0) return reviewGet(id, resolved);
738
787
  try {
@@ -757,7 +806,7 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
757
806
 
758
807
  // src/tools/settings-get.ts
759
808
  import {
760
- CommandError as CommandError7,
809
+ CommandError as CommandError8,
761
810
  getSettings
762
811
  } from "mergestorm/client";
763
812
  function settingsSummary(settings) {
@@ -775,7 +824,7 @@ function settingsSummary(settings) {
775
824
  async function settingsGet(cfg) {
776
825
  const settings = await getSettings(cfg);
777
826
  if (!settings) {
778
- throw new CommandError7(
827
+ throw new CommandError8(
779
828
  "Settings are not available (offline or API too old). Try again or open https://mergestorm.ai/settings"
780
829
  );
781
830
  }
@@ -787,7 +836,7 @@ async function settingsGet(cfg) {
787
836
 
788
837
  // src/tools/settings-set.ts
789
838
  import {
790
- CommandError as CommandError8,
839
+ CommandError as CommandError9,
791
840
  BEARER_SETTINGS,
792
841
  SETTINGS_WRITABLE_KEYS,
793
842
  patchSettings
@@ -799,7 +848,7 @@ var SETTINGS_READ_ONLY_KEYS = [
799
848
  function settingsPatchFromArgs(args) {
800
849
  for (const key of SETTINGS_READ_ONLY_KEYS) {
801
850
  if (args[key] !== void 0) {
802
- throw new CommandError8(`${key} is read-only and cannot be set.`, 2, "usage");
851
+ throw new CommandError9(`${key} is read-only and cannot be set.`, 2, "usage");
803
852
  }
804
853
  }
805
854
  const patch = {};
@@ -810,7 +859,7 @@ function settingsPatchFromArgs(args) {
810
859
  if ("kind" in row && (row.kind === "seconds" || row.kind === "count")) {
811
860
  if (typeof value !== "number" || !Number.isInteger(value) || value < row.min || value > row.max) {
812
861
  const unit = row.kind === "seconds" ? " of seconds" : "";
813
- throw new CommandError8(
862
+ throw new CommandError9(
814
863
  `${key} takes a whole number${unit} from ${row.min} through ${row.max}.`,
815
864
  2,
816
865
  "usage"
@@ -821,17 +870,17 @@ function settingsPatchFromArgs(args) {
821
870
  }
822
871
  if ("kind" in row) {
823
872
  const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
824
- if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
873
+ if (!valid) throw new CommandError9(`Invalid value for ${key}.`, 2, "usage");
825
874
  Object.assign(patch, { [key]: value });
826
875
  continue;
827
876
  }
828
877
  if (typeof value !== "boolean") {
829
- throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
878
+ throw new CommandError9(`${key} takes a boolean.`, 2, "usage");
830
879
  }
831
880
  Object.assign(patch, { [key]: value });
832
881
  }
833
882
  if (Object.keys(patch).length === 0) {
834
- throw new CommandError8(
883
+ throw new CommandError9(
835
884
  `At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`,
836
885
  2,
837
886
  "usage"
@@ -849,8 +898,8 @@ async function settingsSet(args, cfg) {
849
898
  }
850
899
 
851
900
  // src/tools/stack-adopt.ts
852
- import { adoptStack, CommandError as CommandError9, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
853
- import { z as z2 } from "zod";
901
+ import { adoptStack, CommandError as CommandError10, stackWatchObligation as stackWatchObligation4 } from "mergestorm/client";
902
+ import { z as z3 } from "zod";
854
903
 
855
904
  // src/tools/stack-set.ts
856
905
  import { setStackPolicy, stackWatchObligation as stackWatchObligation3 } from "mergestorm/client";
@@ -918,18 +967,18 @@ async function stackSet(stackId, input, cfg) {
918
967
 
919
968
  // src/tools/stack-adopt.ts
920
969
  var stackAdoptSchema = {
921
- owner: z2.string().trim().min(1),
922
- repo: z2.string().trim().min(1),
923
- pr_number: z2.number().int().min(1),
924
- auto_land: z2.boolean().optional(),
925
- auto_review: z2.boolean().nullable().optional(),
926
- auto_patch: z2.boolean().nullable().optional(),
927
- auto_resolve_conflicts: z2.boolean().nullable().optional(),
928
- auto_fix_ci: z2.boolean().nullable().optional()
970
+ owner: z3.string().trim().min(1),
971
+ repo: z3.string().trim().min(1),
972
+ pr_number: z3.number().int().min(1),
973
+ auto_land: z3.boolean().optional(),
974
+ auto_review: z3.boolean().nullable().optional(),
975
+ auto_patch: z3.boolean().nullable().optional(),
976
+ auto_resolve_conflicts: z3.boolean().nullable().optional(),
977
+ auto_fix_ci: z3.boolean().nullable().optional()
929
978
  };
930
- var inputSchema = z2.object(stackAdoptSchema);
979
+ var inputSchema2 = z3.object(stackAdoptSchema);
931
980
  async function stackAdopt(input, cfg) {
932
- const parsed = inputSchema.safeParse(input);
981
+ const parsed = inputSchema2.safeParse(input);
933
982
  if (!parsed.success) {
934
983
  const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
935
984
  return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
@@ -959,11 +1008,11 @@ async function stackAdopt(input, cfg) {
959
1008
  summary: message,
960
1009
  data: {
961
1010
  error: {
962
- code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
1011
+ code: err instanceof CommandError10 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
963
1012
  message,
964
- ...err instanceof CommandError9 ? { exit_code: err.exitCode } : {},
965
- ...err instanceof CommandError9 && err.reason !== void 0 ? { reason: err.reason } : {},
966
- ...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1013
+ ...err instanceof CommandError10 ? { exit_code: err.exitCode } : {},
1014
+ ...err instanceof CommandError10 && err.reason !== void 0 ? { reason: err.reason } : {},
1015
+ ...err instanceof CommandError10 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
967
1016
  }
968
1017
  },
969
1018
  isError: true
@@ -1032,6 +1081,102 @@ async function stackList(cfg) {
1032
1081
  }
1033
1082
  }
1034
1083
 
1084
+ // src/tools/stack-publish.ts
1085
+ import {
1086
+ CommandError as CommandError11,
1087
+ publishWorkingBranch,
1088
+ stackWatchObligation as stackWatchObligation5,
1089
+ workingBranchPublication
1090
+ } from "mergestorm/client";
1091
+ import { z as z4 } from "zod";
1092
+ var target = {
1093
+ cwd: z4.string().trim().min(1).optional(),
1094
+ owner: z4.string().trim().min(1).optional(),
1095
+ repo: z4.string().trim().min(1).optional(),
1096
+ branch: z4.string().trim().min(1).optional()
1097
+ };
1098
+ var stackPublishSchema = {
1099
+ ...target,
1100
+ base: z4.string().trim().min(1).optional(),
1101
+ title: z4.string().trim().min(1).max(256).optional(),
1102
+ body: z4.string().max(6e4).optional(),
1103
+ replace_head: z4.string().regex(/^[0-9a-f]{40}$/).optional(),
1104
+ auto_land: z4.boolean().optional()
1105
+ };
1106
+ var stackPublishStatusSchema = target;
1107
+ var publishInput = z4.object(stackPublishSchema);
1108
+ var statusInput = z4.object(stackPublishStatusSchema);
1109
+ function failure(err, fallback) {
1110
+ const message = err instanceof Error ? err.message : String(err);
1111
+ return {
1112
+ summary: message,
1113
+ data: {
1114
+ error: {
1115
+ code: err instanceof CommandError11 ? err.code ?? fallback : fallback,
1116
+ message,
1117
+ ...err instanceof CommandError11 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1118
+ }
1119
+ },
1120
+ isError: true
1121
+ };
1122
+ }
1123
+ function targetOf(input, cfg) {
1124
+ return {
1125
+ ...input.cwd ? { cwd: input.cwd } : {},
1126
+ ...input.owner ? { owner: input.owner } : {},
1127
+ ...input.repo ? { repo: input.repo } : {},
1128
+ ...input.branch ? { branch: input.branch } : {},
1129
+ ...cfg ? { cfg } : {}
1130
+ };
1131
+ }
1132
+ async function stackPublish(input, cfg, deps) {
1133
+ const parsed = publishInput.safeParse(input);
1134
+ if (!parsed.success || Boolean(parsed.data.owner) !== Boolean(parsed.data.repo)) {
1135
+ const message = "stack_publish takes owner and repo together, and replace_head as a full 40-character commit SHA";
1136
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
1137
+ }
1138
+ try {
1139
+ const result = await publishWorkingBranch(
1140
+ {
1141
+ ...targetOf(parsed.data, cfg),
1142
+ ...parsed.data.base ? { base: parsed.data.base } : {},
1143
+ ...parsed.data.title ? { title: parsed.data.title } : {},
1144
+ ...parsed.data.body !== void 0 ? { body: parsed.data.body } : {},
1145
+ ...parsed.data.replace_head ? { replaceHead: parsed.data.replace_head } : {},
1146
+ ...typeof parsed.data.auto_land === "boolean" ? { autoLand: parsed.data.auto_land } : {}
1147
+ },
1148
+ deps
1149
+ );
1150
+ const watch = stackWatchObligation5({ stackId: result.stack.id, terminal: null, unread: true, freshCursor: true });
1151
+ const moved = result.branchCreated ? "created" : result.branchUpdated ? "updated" : "already current";
1152
+ return {
1153
+ summary: withWatchText(
1154
+ watch,
1155
+ `Published ${result.owner}/${result.repo} ${result.branch} (${moved}) at ${result.head.sha}, PR #${result.pr.number}, stack ${result.stack.id} with Auto patch off`
1156
+ ),
1157
+ data: { result, watch }
1158
+ };
1159
+ } catch (err) {
1160
+ return failure(err, "stack_publish_failed");
1161
+ }
1162
+ }
1163
+ async function stackPublishStatus(input, cfg, deps) {
1164
+ const parsed = statusInput.safeParse(input);
1165
+ if (!parsed.success || Boolean(parsed.data.owner) !== Boolean(parsed.data.repo)) {
1166
+ const message = "stack_publish_status takes owner and repo together";
1167
+ return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
1168
+ }
1169
+ try {
1170
+ const status = await workingBranchPublication(targetOf(parsed.data, cfg), deps);
1171
+ return {
1172
+ summary: `${status.owner}/${status.repo} ${status.branch}: ${status.state}${status.head ? ` at ${status.head.sha}` : ""}${status.pr ? `, PR #${status.pr.number}` : ""}${status.stack ? `, stack ${status.stack.id}` : ""}`,
1173
+ data: { status }
1174
+ };
1175
+ } catch (err) {
1176
+ return failure(err, "stack_publish_status_failed");
1177
+ }
1178
+ }
1179
+
1035
1180
  // src/tools/stack-status.ts
1036
1181
  import {
1037
1182
  getEnrichedStack,
@@ -1039,14 +1184,14 @@ import {
1039
1184
  isCommandErrorCode as isCommandErrorCode5,
1040
1185
  stackBlockers as stackBlockers2,
1041
1186
  stackTerminalReason,
1042
- stackWatchObligation as stackWatchObligation5
1187
+ stackWatchObligation as stackWatchObligation6
1043
1188
  } from "mergestorm/client";
1044
1189
  async function stackStatus(stackId, cfg) {
1045
1190
  try {
1046
1191
  const stack = await getEnrichedStack(stackId, cfg);
1047
1192
  if (!stack) {
1048
1193
  const message = `Stack not found or not owned by the current user: ${stackId}`;
1049
- const watch2 = stackWatchObligation5({ stackId, terminal: "not_found" });
1194
+ const watch2 = stackWatchObligation6({ stackId, terminal: "not_found" });
1050
1195
  return {
1051
1196
  summary: withWatchText(watch2, message),
1052
1197
  data: {
@@ -1062,7 +1207,7 @@ async function stackStatus(stackId, cfg) {
1062
1207
  }
1063
1208
  const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
1064
1209
  const { attention, held, issues, currentCandidate, busy, agents, repair, landGatePending, queueWait } = stackBlockers2(stack, entries);
1065
- const watch = stackWatchObligation5({
1210
+ const watch = stackWatchObligation6({
1066
1211
  stackId: stack.id,
1067
1212
  terminal: stackTerminalReason(stack),
1068
1213
  cursor: { enrolledHeadSha: currentCandidate?.headSha ?? null },
@@ -1075,7 +1220,7 @@ async function stackStatus(stackId, cfg) {
1075
1220
  };
1076
1221
  } catch (err) {
1077
1222
  if (isCommandErrorCode5(err, "rate_limited")) {
1078
- const watch = stackWatchObligation5({
1223
+ const watch = stackWatchObligation6({
1079
1224
  stackId,
1080
1225
  terminal: null,
1081
1226
  status: "rate_limited",
@@ -1101,7 +1246,7 @@ async function stackStatus(stackId, cfg) {
1101
1246
 
1102
1247
  // src/tools/whoami.ts
1103
1248
  import {
1104
- CommandError as CommandError10,
1249
+ CommandError as CommandError12,
1105
1250
  apiBase,
1106
1251
  configPath,
1107
1252
  getMe as getMe2,
@@ -1112,15 +1257,15 @@ async function whoami(cfg, serverVersion) {
1112
1257
  const resolved = cfg ?? await loadConfig7();
1113
1258
  const key = resolveApiKey(resolved);
1114
1259
  if (!key) {
1115
- throw new CommandError10(
1116
- "No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
1260
+ throw new CommandError12(
1261
+ "No API key. Run `mergestorm login` (agents: `mergestorm login --start`, then `--finish`) or set MERGESTORM_API_KEY.",
1117
1262
  1,
1118
1263
  "missing_api_key"
1119
1264
  );
1120
1265
  }
1121
1266
  const me = await getMe2(resolved);
1122
1267
  if (!me) {
1123
- throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
1268
+ throw new CommandError12("Live account details unavailable. Could not verify account with /me.");
1124
1269
  }
1125
1270
  const data = {
1126
1271
  key_prefix: me.key.prefix,
@@ -1195,7 +1340,7 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
1195
1340
 
1196
1341
  // src/server.ts
1197
1342
  var MCP_SERVER_NAME = "mergestorm";
1198
- var MCP_SERVER_VERSION = "0.2.19";
1343
+ var MCP_SERVER_VERSION = "0.2.21";
1199
1344
  var MCP_SERVER_INSTRUCTIONS = [
1200
1345
  MCP_STACK_DONE_INSTRUCTIONS,
1201
1346
  MCP_PR_LOOP_INSTRUCTIONS,
@@ -1214,14 +1359,14 @@ function ok(payload) {
1214
1359
  };
1215
1360
  }
1216
1361
  function fail(err) {
1217
- const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1362
+ const message = err instanceof CommandError13 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1218
1363
  return ok({
1219
1364
  isError: true,
1220
1365
  summary: message,
1221
1366
  data: {
1222
1367
  error: {
1223
1368
  message: err instanceof Error ? err.message : String(err),
1224
- ...err instanceof CommandError11 ? {
1369
+ ...err instanceof CommandError13 ? {
1225
1370
  exit_code: err.exitCode,
1226
1371
  ...err.code !== void 0 ? { code: err.code } : {},
1227
1372
  ...err.reason !== void 0 ? { reason: err.reason } : {},
@@ -1279,7 +1424,7 @@ function createMergestormMcpServer() {
1279
1424
  title: "List reviews",
1280
1425
  description: "List recent Mergestorm review jobs as job envelopes.",
1281
1426
  inputSchema: {
1282
- limit: z3.number().int().min(1).max(50).optional()
1427
+ limit: z5.number().int().min(1).max(50).optional()
1283
1428
  },
1284
1429
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1285
1430
  },
@@ -1297,18 +1442,18 @@ function createMergestormMcpServer() {
1297
1442
  title: "Submit review",
1298
1443
  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.",
1299
1444
  inputSchema: {
1300
- cwd: z3.string().optional(),
1301
- base: z3.string().optional(),
1302
- head: z3.string().optional(),
1303
- diff: z3.string().optional(),
1304
- router: z3.enum(["off", "standard", "max", "manual"]).optional(),
1305
- specialists: z3.array(z3.string().min(1)).max(5).optional(),
1306
- context: z3.string().optional(),
1307
- context_files: z3.array(z3.string().min(1)).max(10).optional(),
1308
- thread: z3.string().min(1).max(120).optional(),
1309
- idempotency_key: z3.string().min(1).max(128).optional(),
1310
- wait: z3.boolean().optional(),
1311
- timeout_s: z3.number().positive().max(300).optional()
1445
+ cwd: z5.string().optional(),
1446
+ base: z5.string().optional(),
1447
+ head: z5.string().optional(),
1448
+ diff: z5.string().optional(),
1449
+ router: z5.enum(["off", "standard", "max", "manual"]).optional(),
1450
+ specialists: z5.array(z5.string().min(1)).max(5).optional(),
1451
+ context: z5.string().optional(),
1452
+ context_files: z5.array(z5.string().min(1)).max(10).optional(),
1453
+ thread: z5.string().min(1).max(120).optional(),
1454
+ idempotency_key: z5.string().min(1).max(128).optional(),
1455
+ wait: z5.boolean().optional(),
1456
+ timeout_s: z5.number().positive().max(300).optional()
1312
1457
  },
1313
1458
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1314
1459
  },
@@ -1326,8 +1471,8 @@ function createMergestormMcpServer() {
1326
1471
  title: "Wait for review",
1327
1472
  description: "Read one local Mergestorm review job by the job_id that review_submit returned, as a job envelope. timeout_s: 0 reads it once without polling; otherwise it polls 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.",
1328
1473
  inputSchema: {
1329
- job_id: z3.string().min(1),
1330
- timeout_s: z3.number().min(0).max(300).optional()
1474
+ job_id: z5.string().min(1),
1475
+ timeout_s: z5.number().min(0).max(300).optional()
1331
1476
  },
1332
1477
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1333
1478
  },
@@ -1345,13 +1490,13 @@ function createMergestormMcpServer() {
1345
1490
  title: "Wait for GitHub PR Vortex review",
1346
1491
  description: "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again, or none when no review exists for that PR and head yet (call once more; if it is none again, Vortex is not reviewing that head: comment @mergestorm-vortex review on the PR, run mg review, or push a new commit). Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. 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.",
1347
1492
  inputSchema: {
1348
- owner: z3.string().min(1),
1349
- repo: z3.string().min(1),
1350
- pr_number: z3.number().int().min(1),
1351
- after_sha: z3.string().optional(),
1352
- pass: z3.number().int().min(1).optional(),
1353
- after_pass: z3.number().int().min(1).optional(),
1354
- timeout_s: z3.number().min(0).max(300).optional()
1493
+ owner: z5.string().min(1),
1494
+ repo: z5.string().min(1),
1495
+ pr_number: z5.number().int().min(1),
1496
+ after_sha: z5.string().optional(),
1497
+ pass: z5.number().int().min(1).optional(),
1498
+ after_pass: z5.number().int().min(1).optional(),
1499
+ timeout_s: z5.number().min(0).max(300).optional()
1355
1500
  },
1356
1501
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1357
1502
  },
@@ -1379,16 +1524,16 @@ function createMergestormMcpServer() {
1379
1524
  title: "Dismiss Vortex findings",
1380
1525
  description: `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed; the Vortex gate at the head clears only once every Vortex Blocker (error finding) at that head is dismissed and no Vortex review of it is running (gate.vortex.cleared, else gate.vortex.message); CI and Auto land policy are unchanged. Vortex does not raise a dismissed finding again on the same diff. Never dismiss a finding you did not check.` + STACK_WATCH_RESULT_NOTE,
1381
1526
  inputSchema: {
1382
- owner: z3.string().min(1),
1383
- repo: z3.string().min(1),
1384
- pr_number: z3.number().int().min(1),
1385
- head_sha: z3.string().regex(/^[0-9a-fA-F]{40}$/),
1386
- review_id: z3.number().int().min(1),
1387
- finding_ids: z3.array(z3.string().min(1)).max(50).optional(),
1388
- scope: z3.enum(["findings", "review"]).optional(),
1389
- reason: z3.string().optional(),
1390
- evidence_url: z3.string().url().optional(),
1391
- preview: z3.boolean().optional()
1527
+ owner: z5.string().min(1),
1528
+ repo: z5.string().min(1),
1529
+ pr_number: z5.number().int().min(1),
1530
+ head_sha: z5.string().regex(/^[0-9a-fA-F]{40}$/),
1531
+ review_id: z5.number().int().min(1),
1532
+ finding_ids: z5.array(z5.string().min(1)).max(50).optional(),
1533
+ scope: z5.enum(["findings", "review"]).optional(),
1534
+ reason: z5.string().optional(),
1535
+ evidence_url: z5.string().url().optional(),
1536
+ preview: z5.boolean().optional()
1392
1537
  },
1393
1538
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1394
1539
  },
@@ -1400,6 +1545,22 @@ function createMergestormMcpServer() {
1400
1545
  }
1401
1546
  }
1402
1547
  );
1548
+ addTool(
1549
+ "issue_create",
1550
+ {
1551
+ title: "Open GitHub issue",
1552
+ description: "Open a GitHub issue through Mergestorm with no GitHub credentials: the Surge App creates it and names you in the body. Use it for the issue a pull request must have before it is opened, then put Fixes #<number> in the commit or pull request body. Defaults to the origin repository of cwd; pass owner and repo together to name another. labels must already exist on the repository; an unknown one is refused with unknown_label and nothing is created. Calling it again with the same title returns the open issue it already made (created: false) when that issue is among the newest 100 open issues of the repository; an older one, or two calls at the same time, can still create a duplicate, so check the returned number before retrying after a lost response. Needs write access to the repository through your linked GitHub account and Surge installed on it.",
1553
+ inputSchema: issueCreateSchema,
1554
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
1555
+ },
1556
+ async (args) => {
1557
+ try {
1558
+ return ok(await issueCreate(args));
1559
+ } catch (err) {
1560
+ return fail(err);
1561
+ }
1562
+ }
1563
+ );
1403
1564
  addTool(
1404
1565
  "stack_adopt",
1405
1566
  {
@@ -1431,18 +1592,50 @@ function createMergestormMcpServer() {
1431
1592
  }
1432
1593
  }
1433
1594
  );
1595
+ addTool(
1596
+ "stack_publish",
1597
+ {
1598
+ title: "Publish branch through Mergestorm",
1599
+ description: "Publish a local branch without any GitHub credentials. Reads the branch's commits from the local repository at cwd (never runs gh, git push or git fetch) and sends them to Mergestorm, which writes the exact commits and the branch on GitHub as the Surge App, opens a ready pull request when the branch has none, and registers the stack with Auto patch pinned off, Auto land on for a new stack, and Auto review left to the account. These are your own changes; Cyclone does not write or patch them. Use it for the first publication and for every later commit to that branch. Defaults: owner and repo from origin, the checked-out branch, the repository's trunk as base, title and body from the tip commit. Mergestorm verifies each commit's Git tree before it moves the branch and returns the verified head and tree. It refuses with head_changed when the branch on GitHub holds work your local branch does not, base_changed when the base commit left the base branch, protected_branch for trunk and Mergestorm-owned branches, and payload_too_large beyond 20 commits, 500 changed paths, 2 MiB a file or 8 MiB in total; submodules and merge commits are not carried. replace_head replaces that exact GitHub head with the local branch and discards its commits. Safe to call again after a lost response or a failure: it resumes and never duplicates the branch, commits, pull request or stack." + STACK_WATCH_RESULT_NOTE,
1600
+ inputSchema: stackPublishSchema,
1601
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1602
+ },
1603
+ async (args) => {
1604
+ try {
1605
+ return ok(await stackPublish(args));
1606
+ } catch (err) {
1607
+ return fail(err);
1608
+ }
1609
+ }
1610
+ );
1611
+ addTool(
1612
+ "stack_publish_status",
1613
+ {
1614
+ title: "Read branch publication",
1615
+ description: "Read how far a branch published with stack_publish got: absent, pushed (branch on GitHub, no pull request), opened (pull request open, stack not registered) or registered, with the head commit and tree on GitHub, the pull request and the stack's Auto patch and Auto land state. Changes nothing. Call it after a lost stack_publish response, then call stack_publish again to resume.",
1616
+ inputSchema: stackPublishStatusSchema,
1617
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1618
+ },
1619
+ async (args) => {
1620
+ try {
1621
+ return ok(await stackPublishStatus(args));
1622
+ } catch (err) {
1623
+ return fail(err);
1624
+ }
1625
+ }
1626
+ );
1434
1627
  addTool(
1435
1628
  "stack_set",
1436
1629
  {
1437
1630
  title: "Set stack policy",
1438
1631
  description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review, auto_patch, auto_resolve_conflicts, and auto_fix_ci pin Vortex auto-review, Cyclone auto-patch, Auto-resolve merge conflicts, or Auto-fix failing CI 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." + STACK_WATCH_RESULT_NOTE,
1439
1632
  inputSchema: {
1440
- stack_id: z3.string().min(1),
1441
- auto_land: z3.boolean().optional(),
1442
- auto_review: z3.boolean().nullable().optional(),
1443
- auto_patch: z3.boolean().nullable().optional(),
1444
- auto_resolve_conflicts: z3.boolean().nullable().optional(),
1445
- auto_fix_ci: z3.boolean().nullable().optional()
1633
+ stack_id: z5.string().min(1),
1634
+ auto_land: z5.boolean().optional(),
1635
+ auto_review: z5.boolean().nullable().optional(),
1636
+ auto_patch: z5.boolean().nullable().optional(),
1637
+ auto_resolve_conflicts: z5.boolean().nullable().optional(),
1638
+ auto_fix_ci: z5.boolean().nullable().optional()
1446
1639
  },
1447
1640
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1448
1641
  },
@@ -1468,7 +1661,7 @@ function createMergestormMcpServer() {
1468
1661
  title: "Get stack status",
1469
1662
  description: `Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix. repair names the concrete fix for attention; landGatePending is a wait, not attention. held names a blocker no agent run clears while Vortex or Cyclone is still busy on that PR (actAfter agents_idle, waitingOn); repair then describes the fix to plan, not to apply yet. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_WATCH_RESULT_NOTE,
1470
1663
  inputSchema: {
1471
- stack_id: z3.string().min(1)
1664
+ stack_id: z5.string().min(1)
1472
1665
  },
1473
1666
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1474
1667
  },
@@ -1503,7 +1696,7 @@ function createMergestormMcpServer() {
1503
1696
  title: "Get merge queue status",
1504
1697
  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.",
1505
1698
  inputSchema: {
1506
- stack_id: z3.string().min(1).optional()
1699
+ stack_id: z5.string().min(1).optional()
1507
1700
  },
1508
1701
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1509
1702
  },
@@ -1539,12 +1732,12 @@ function createMergestormMcpServer() {
1539
1732
  ...Object.fromEntries(
1540
1733
  BEARER_SETTINGS2.map((row) => [
1541
1734
  row.key,
1542
- ("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : row.kind === "seconds" || row.kind === "count" ? z3.number().int().min(row.min).max(row.max) : z3.enum(row.values) : z3.boolean()).optional()
1735
+ ("kind" in row ? row.kind === "logins" ? z5.array(z5.string()) : row.kind === "seconds" || row.kind === "count" ? z5.number().int().min(row.min).max(row.max) : z5.enum(row.values) : z5.boolean()).optional()
1543
1736
  ])
1544
1737
  ),
1545
1738
  // Preserve these args so the handler rejects them even alongside a writable key.
1546
- cyclone_connected: z3.unknown().optional(),
1547
- github_connected: z3.unknown().optional()
1739
+ cyclone_connected: z5.unknown().optional(),
1740
+ github_connected: z5.unknown().optional()
1548
1741
  },
1549
1742
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1550
1743
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.2.19",
3
+ "version": "0.2.21",
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.49",
32
+ "mergestorm": "0.3.54",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {