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.
- package/README.md +7 -3
- package/dist/index.js +323 -130
- 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
|
|
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
|
|
9
|
-
import { CommandError as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
196
|
-
timeout_s:
|
|
197
|
-
enrolled_head_sha:
|
|
198
|
-
after_finished_at:
|
|
199
|
-
bounce_id:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
498
|
+
throw new CommandError4("cwd must be inside the configured sandbox root", 2, "usage");
|
|
450
499
|
}
|
|
451
500
|
} catch (err) {
|
|
452
|
-
if (err instanceof
|
|
453
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
648
|
+
const target2 = prReviewTarget(owner, repo, prNumber);
|
|
600
649
|
try {
|
|
601
650
|
const envelope = await getPrVortexReview(
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
|
677
|
+
const target2 = prReviewTarget(owner, repo, prNumber);
|
|
629
678
|
const resolved = cfg ?? await loadConfig5();
|
|
630
679
|
if (timeout_s === 0) {
|
|
631
|
-
return reviewGetPr(
|
|
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
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
853
|
-
import { z as
|
|
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:
|
|
922
|
-
repo:
|
|
923
|
-
pr_number:
|
|
924
|
-
auto_land:
|
|
925
|
-
auto_review:
|
|
926
|
-
auto_patch:
|
|
927
|
-
auto_resolve_conflicts:
|
|
928
|
-
auto_fix_ci:
|
|
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
|
|
979
|
+
var inputSchema2 = z3.object(stackAdoptSchema);
|
|
931
980
|
async function stackAdopt(input, cfg) {
|
|
932
|
-
const parsed =
|
|
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
|
|
1011
|
+
code: err instanceof CommandError10 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
963
1012
|
message,
|
|
964
|
-
...err instanceof
|
|
965
|
-
...err instanceof
|
|
966
|
-
...err instanceof
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
1301
|
-
base:
|
|
1302
|
-
head:
|
|
1303
|
-
diff:
|
|
1304
|
-
router:
|
|
1305
|
-
specialists:
|
|
1306
|
-
context:
|
|
1307
|
-
context_files:
|
|
1308
|
-
thread:
|
|
1309
|
-
idempotency_key:
|
|
1310
|
-
wait:
|
|
1311
|
-
timeout_s:
|
|
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:
|
|
1330
|
-
timeout_s:
|
|
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:
|
|
1349
|
-
repo:
|
|
1350
|
-
pr_number:
|
|
1351
|
-
after_sha:
|
|
1352
|
-
pass:
|
|
1353
|
-
after_pass:
|
|
1354
|
-
timeout_s:
|
|
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:
|
|
1383
|
-
repo:
|
|
1384
|
-
pr_number:
|
|
1385
|
-
head_sha:
|
|
1386
|
-
review_id:
|
|
1387
|
-
finding_ids:
|
|
1388
|
-
scope:
|
|
1389
|
-
reason:
|
|
1390
|
-
evidence_url:
|
|
1391
|
-
preview:
|
|
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:
|
|
1441
|
-
auto_land:
|
|
1442
|
-
auto_review:
|
|
1443
|
-
auto_patch:
|
|
1444
|
-
auto_resolve_conflicts:
|
|
1445
|
-
auto_fix_ci:
|
|
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:
|
|
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:
|
|
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" ?
|
|
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:
|
|
1547
|
-
github_connected:
|
|
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.
|
|
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.
|
|
32
|
+
"mergestorm": "0.3.54",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|