mergestorm-mcp 0.1.3 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.js +459 -112
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# mergestorm-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_list`, `stack_status`, `queue_status`, `settings_get`, `settings_set`.
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
|
|
4
4
|
|
|
5
|
-
The stack tools
|
|
5
|
+
The stack tools expose reads plus one scoped policy write:
|
|
6
6
|
|
|
7
|
+
- `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
|
|
7
8
|
- `stack_list` returns the current API key owner's registered stacks.
|
|
9
|
+
- `stack_set({ stack_id, auto_land?, auto_review?, auto_patch? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review` and `auto_patch` pin Vortex auto-review or Cyclone auto-patch for that stack in either direction (`true` or `false`); `null` clears the pin so the stack follows the account setting. Pass at least one key. Account settings are never changed here.
|
|
8
10
|
- `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, or a structured `stack_not_found` error.
|
|
11
|
+
- `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots, or a waiting result with selectors to use for the next call.
|
|
9
12
|
|
|
10
13
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
11
14
|
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
|
|
6
6
|
// src/server.ts
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
-
import { z } from "zod";
|
|
9
|
-
import { CommandError as
|
|
8
|
+
import { z as z3 } from "zod";
|
|
9
|
+
import { CommandError as CommandError11, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
|
|
10
10
|
|
|
11
11
|
// src/tools/credits.ts
|
|
12
12
|
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
@@ -25,8 +25,10 @@ async function credits(cfg) {
|
|
|
25
25
|
}
|
|
26
26
|
const standard = me.usage.standard;
|
|
27
27
|
const remaining = standard.remaining ?? (standard.limit != null ? Math.max(0, standard.limit - standard.used) : null);
|
|
28
|
+
const bonusRemaining = me.usage.bonus?.remaining;
|
|
29
|
+
const bonusSummary = bonusRemaining == null ? "" : ` \xB7 ${bonusRemaining} bonus remaining`;
|
|
28
30
|
return {
|
|
29
|
-
summary: remaining == null ? `${standard.used} used \xB7 unlimited` : `${standard.used} used \xB7 ${remaining} remaining`,
|
|
31
|
+
summary: remaining == null ? `${standard.used} used \xB7 unlimited${bonusSummary}` : `${standard.used} used \xB7 ${remaining} remaining${bonusSummary}`,
|
|
30
32
|
data: {
|
|
31
33
|
usage: me.usage,
|
|
32
34
|
resets_at: me.resets_at ?? null
|
|
@@ -44,20 +46,29 @@ import {
|
|
|
44
46
|
REVIEW_EXIT
|
|
45
47
|
} from "mergestorm/client";
|
|
46
48
|
function entrySummary(entry) {
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
49
|
+
const prNumber = entry.bounceDetail?.prNumber;
|
|
50
|
+
const repo = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
|
|
51
|
+
const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? void 0;
|
|
52
|
+
const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? void 0;
|
|
53
|
+
return [
|
|
54
|
+
entry.state,
|
|
55
|
+
repo,
|
|
56
|
+
`stack ${entry.stackId}`,
|
|
57
|
+
reason,
|
|
58
|
+
sha?.slice(0, 7)
|
|
59
|
+
].filter(Boolean).join(" \xB7 ");
|
|
50
60
|
}
|
|
51
61
|
async function queueStatus(stackId, cfg) {
|
|
52
62
|
try {
|
|
53
63
|
const resolved = cfg ?? await loadConfig2();
|
|
64
|
+
const id = stackId?.trim();
|
|
54
65
|
const { status, body, retryAfterSeconds } = await apiFetch(
|
|
55
66
|
resolved,
|
|
56
|
-
"/api/v1/stacks/queue"
|
|
67
|
+
id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
|
|
57
68
|
);
|
|
58
69
|
if (status === 404) {
|
|
59
70
|
throw new CommandError2(
|
|
60
|
-
"Merge queue
|
|
71
|
+
"Merge queue route returned 404. Deploy the API."
|
|
61
72
|
);
|
|
62
73
|
}
|
|
63
74
|
if (status === 429) {
|
|
@@ -79,11 +90,12 @@ async function queueStatus(stackId, cfg) {
|
|
|
79
90
|
`Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
|
|
80
91
|
);
|
|
81
92
|
}
|
|
82
|
-
const id = stackId?.trim();
|
|
83
93
|
if (id) {
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
const matchingEntries = entries.filter(
|
|
95
|
+
(candidate) => candidate?.stackId?.toLowerCase() === id.toLowerCase()
|
|
96
|
+
);
|
|
97
|
+
if (matchingEntries.length === 0) {
|
|
98
|
+
const message = `Queue entry not found for stack: ${id}`;
|
|
87
99
|
return {
|
|
88
100
|
summary: message,
|
|
89
101
|
data: {
|
|
@@ -97,12 +109,12 @@ async function queueStatus(stackId, cfg) {
|
|
|
97
109
|
};
|
|
98
110
|
}
|
|
99
111
|
return {
|
|
100
|
-
summary: entrySummary(
|
|
101
|
-
data: {
|
|
112
|
+
summary: matchingEntries.map(entrySummary).join("\n"),
|
|
113
|
+
data: { entries: matchingEntries }
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
116
|
return {
|
|
105
|
-
summary: entries.length === 0 ? "No
|
|
117
|
+
summary: entries.length === 0 ? "No merge queue entries (live or bounced)" : entries.map(entrySummary).join("\n"),
|
|
106
118
|
data: { entries }
|
|
107
119
|
};
|
|
108
120
|
} catch (err) {
|
|
@@ -141,9 +153,21 @@ function prReviewTarget(owner, repo, prNumber) {
|
|
|
141
153
|
}
|
|
142
154
|
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
143
155
|
}
|
|
156
|
+
function prReviewSummary(owner, repo, prNumber, envelope) {
|
|
157
|
+
const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
|
|
158
|
+
const sha = envelope.head_sha?.trim();
|
|
159
|
+
if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
|
|
160
|
+
if (typeof envelope.finding_count === "number") {
|
|
161
|
+
bits.push(
|
|
162
|
+
`${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (envelope.phase) bits.push(String(envelope.phase));
|
|
166
|
+
return bits.join(" \xB7 ");
|
|
167
|
+
}
|
|
144
168
|
function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
145
169
|
return {
|
|
146
|
-
summary:
|
|
170
|
+
summary: prReviewSummary(owner, repo, prNumber, envelope),
|
|
147
171
|
data: { ...envelope }
|
|
148
172
|
};
|
|
149
173
|
}
|
|
@@ -158,14 +182,15 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
|
158
182
|
}
|
|
159
183
|
};
|
|
160
184
|
}
|
|
161
|
-
async function reviewGetPr(owner, repo, prNumber, cfg) {
|
|
185
|
+
async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
162
186
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
163
187
|
try {
|
|
164
188
|
const envelope = await getPrVortexReview(
|
|
165
189
|
target.owner,
|
|
166
190
|
target.repo,
|
|
167
191
|
target.prNumber,
|
|
168
|
-
cfg
|
|
192
|
+
cfg,
|
|
193
|
+
{ afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
|
|
169
194
|
);
|
|
170
195
|
return payloadFromPrReview(
|
|
171
196
|
target.owner,
|
|
@@ -259,6 +284,8 @@ import {
|
|
|
259
284
|
REVIEW_THREAD_SLUG_RE,
|
|
260
285
|
ReviewPollTimeoutError,
|
|
261
286
|
collectReviewInput,
|
|
287
|
+
discoverTrunk,
|
|
288
|
+
formatReviewSubmitError,
|
|
262
289
|
loadConfig as loadConfig3,
|
|
263
290
|
loadReviewContext,
|
|
264
291
|
pollReview,
|
|
@@ -266,6 +293,7 @@ import {
|
|
|
266
293
|
submitReview
|
|
267
294
|
} from "mergestorm/client";
|
|
268
295
|
var MCP_WAIT_CEILING_S = 300;
|
|
296
|
+
var MCP_PR_WAIT_DEFAULT_S = 45;
|
|
269
297
|
var PINNABLE_SPECIALISTS = [
|
|
270
298
|
"security",
|
|
271
299
|
"performance",
|
|
@@ -290,8 +318,8 @@ function assertPinnableSpecialists(specialists) {
|
|
|
290
318
|
function mcpSandboxRoot() {
|
|
291
319
|
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
292
320
|
}
|
|
293
|
-
function waitTimeoutMs(timeout_s) {
|
|
294
|
-
const seconds = timeout_s ??
|
|
321
|
+
function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
322
|
+
const seconds = timeout_s ?? defaultSeconds;
|
|
295
323
|
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
296
324
|
throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
|
|
297
325
|
}
|
|
@@ -306,13 +334,18 @@ async function resolveInput(input) {
|
|
|
306
334
|
return {
|
|
307
335
|
thread,
|
|
308
336
|
branch,
|
|
309
|
-
baseLabel: input.base?.trim() ||
|
|
337
|
+
baseLabel: input.base?.trim() || (() => {
|
|
338
|
+
try {
|
|
339
|
+
return discoverTrunk(cwd);
|
|
340
|
+
} catch {
|
|
341
|
+
return "main";
|
|
342
|
+
}
|
|
343
|
+
})(),
|
|
310
344
|
headLabel: head,
|
|
311
345
|
diff: input.diff,
|
|
312
346
|
files: []
|
|
313
347
|
};
|
|
314
348
|
}
|
|
315
|
-
const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
|
|
316
349
|
const sandboxRoot = mcpSandboxRoot();
|
|
317
350
|
try {
|
|
318
351
|
const [sandboxReal, cwdReal] = await Promise.all([
|
|
@@ -327,27 +360,12 @@ async function resolveInput(input) {
|
|
|
327
360
|
if (err instanceof CommandError5) throw err;
|
|
328
361
|
throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
|
|
329
362
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
let yielded = false;
|
|
333
|
-
for (const base of bases) {
|
|
334
|
-
try {
|
|
335
|
-
const candidate = await collectReviewInput(base, head, cwd, sandboxRoot);
|
|
336
|
-
yielded = true;
|
|
337
|
-
if (!collected) collected = candidate;
|
|
338
|
-
} catch (err) {
|
|
339
|
-
if (yielded) continue;
|
|
340
|
-
lastErr = err;
|
|
341
|
-
if (input.base?.trim()) throw err;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
if (yielded) return collected;
|
|
345
|
-
if (lastErr) throw lastErr;
|
|
346
|
-
return null;
|
|
363
|
+
const base = input.base?.trim() || discoverTrunk(cwd);
|
|
364
|
+
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
347
365
|
}
|
|
348
366
|
async function reviewSubmit(input, cfg, opts = {}) {
|
|
349
367
|
const resolved = cfg ?? await loadConfig3();
|
|
350
|
-
const wait = input.wait
|
|
368
|
+
const wait = input.wait === true;
|
|
351
369
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
352
370
|
assertPinnableSpecialists(input.specialists);
|
|
353
371
|
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
@@ -395,7 +413,13 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
395
413
|
});
|
|
396
414
|
}
|
|
397
415
|
if (submitted.status !== 202 && submitted.status !== 200) {
|
|
398
|
-
|
|
416
|
+
return {
|
|
417
|
+
...payloadFromRow({ ...submitted.row, status: "failed" }, {
|
|
418
|
+
jobId,
|
|
419
|
+
error: formatReviewSubmitError(submitted.status, submitted.row)
|
|
420
|
+
}),
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
399
423
|
}
|
|
400
424
|
if (!jobId) {
|
|
401
425
|
throw new CommandError5("Review API returned no job_id");
|
|
@@ -440,8 +464,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
440
464
|
target.repo,
|
|
441
465
|
target.prNumber,
|
|
442
466
|
{
|
|
443
|
-
timeoutMs: waitTimeoutMs(timeout_s),
|
|
467
|
+
timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
|
|
444
468
|
afterSha,
|
|
469
|
+
pass: opts.pass,
|
|
470
|
+
afterPass: opts.afterPass,
|
|
445
471
|
intervalMs: opts.pollIntervalMs
|
|
446
472
|
}
|
|
447
473
|
);
|
|
@@ -455,7 +481,12 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
455
481
|
if (err instanceof PrReviewPollTimeoutError) {
|
|
456
482
|
const status = err.lastEnvelope?.status ?? "in_progress";
|
|
457
483
|
return {
|
|
458
|
-
summary:
|
|
484
|
+
summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
|
|
485
|
+
status,
|
|
486
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
487
|
+
finding_count: err.lastEnvelope?.finding_count ?? null,
|
|
488
|
+
phase: err.lastEnvelope?.phase ?? null
|
|
489
|
+
}),
|
|
459
490
|
data: {
|
|
460
491
|
...err.lastEnvelope ?? {},
|
|
461
492
|
status,
|
|
@@ -533,6 +564,7 @@ async function settingsGet(cfg) {
|
|
|
533
564
|
// src/tools/settings-set.ts
|
|
534
565
|
import {
|
|
535
566
|
CommandError as CommandError8,
|
|
567
|
+
BEARER_SETTINGS,
|
|
536
568
|
SETTINGS_WRITABLE_KEYS,
|
|
537
569
|
patchSettings
|
|
538
570
|
} from "mergestorm/client";
|
|
@@ -550,13 +582,17 @@ function settingsPatchFromArgs(args) {
|
|
|
550
582
|
for (const key of SETTINGS_WRITABLE_KEYS) {
|
|
551
583
|
const value = args[key];
|
|
552
584
|
if (value === void 0) continue;
|
|
585
|
+
const row = BEARER_SETTINGS.find((row2) => row2.key === key);
|
|
586
|
+
if ("kind" in row) {
|
|
587
|
+
const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
|
|
588
|
+
if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
|
|
589
|
+
Object.assign(patch, { [key]: value });
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
553
592
|
if (typeof value !== "boolean") {
|
|
554
593
|
throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
|
|
555
594
|
}
|
|
556
|
-
|
|
557
|
-
throw new CommandError8("auto_patch_enabled cannot be enabled by this tool.", 2, "usage");
|
|
558
|
-
}
|
|
559
|
-
patch[key] = value;
|
|
595
|
+
Object.assign(patch, { [key]: value });
|
|
560
596
|
}
|
|
561
597
|
if (Object.keys(patch).length === 0) {
|
|
562
598
|
throw new CommandError8(
|
|
@@ -576,24 +612,192 @@ async function settingsSet(args, cfg) {
|
|
|
576
612
|
};
|
|
577
613
|
}
|
|
578
614
|
|
|
615
|
+
// src/tools/stack-adopt.ts
|
|
616
|
+
import { adoptStack, CommandError as CommandError9 } from "mergestorm/client";
|
|
617
|
+
import { z } from "zod";
|
|
618
|
+
|
|
619
|
+
// src/tools/stack-set.ts
|
|
620
|
+
import { setStackPolicy } from "mergestorm/client";
|
|
621
|
+
function triWord(value) {
|
|
622
|
+
return value === null ? "default" : value ? "on" : "off";
|
|
623
|
+
}
|
|
624
|
+
function stackSetPatch(input) {
|
|
625
|
+
const patch = {};
|
|
626
|
+
if (typeof input.auto_land === "boolean") patch.autoEnqueueWhenReady = input.auto_land;
|
|
627
|
+
if (input.auto_review !== void 0) patch.autoReviewOverride = input.auto_review;
|
|
628
|
+
if (input.auto_patch !== void 0) patch.autoPatchOverride = input.auto_patch;
|
|
629
|
+
return patch;
|
|
630
|
+
}
|
|
631
|
+
async function stackSet(stackId, input, cfg) {
|
|
632
|
+
const patch = stackSetPatch(input);
|
|
633
|
+
if (Object.keys(patch).length === 0) {
|
|
634
|
+
return {
|
|
635
|
+
summary: "stack_set needs at least one of auto_land, auto_review, auto_patch",
|
|
636
|
+
isError: true,
|
|
637
|
+
data: {
|
|
638
|
+
error: {
|
|
639
|
+
code: "invalid_input",
|
|
640
|
+
message: "Pass at least one of auto_land, auto_review, auto_patch.",
|
|
641
|
+
stack_id: stackId
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const result = await setStackPolicy(stackId, patch, cfg);
|
|
647
|
+
const parts = [];
|
|
648
|
+
if (typeof input.auto_land === "boolean") {
|
|
649
|
+
parts.push(`Auto land ${input.auto_land ? "on" : "off"}`);
|
|
650
|
+
}
|
|
651
|
+
if (input.auto_review !== void 0) {
|
|
652
|
+
parts.push(`auto-review ${triWord(input.auto_review)}`);
|
|
653
|
+
}
|
|
654
|
+
if (input.auto_patch !== void 0) {
|
|
655
|
+
parts.push(`auto-patch ${triWord(input.auto_patch)}`);
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
summary: `${parts.join(", ")} for stack ${stackId}`,
|
|
659
|
+
data: {
|
|
660
|
+
stack_id: stackId,
|
|
661
|
+
...typeof input.auto_land === "boolean" ? { auto_land: input.auto_land } : {},
|
|
662
|
+
...input.auto_review !== void 0 ? { auto_review: input.auto_review } : {},
|
|
663
|
+
...input.auto_patch !== void 0 ? { auto_patch: input.auto_patch } : {},
|
|
664
|
+
result
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// src/tools/stack-adopt.ts
|
|
670
|
+
var stackAdoptSchema = {
|
|
671
|
+
owner: z.string().trim().min(1),
|
|
672
|
+
repo: z.string().trim().min(1),
|
|
673
|
+
pr_number: z.number().int().min(1),
|
|
674
|
+
auto_land: z.boolean().optional(),
|
|
675
|
+
auto_review: z.boolean().nullable().optional(),
|
|
676
|
+
auto_patch: z.boolean().nullable().optional()
|
|
677
|
+
};
|
|
678
|
+
var inputSchema = z.object(stackAdoptSchema);
|
|
679
|
+
async function stackAdopt(input, cfg) {
|
|
680
|
+
const parsed = inputSchema.safeParse(input);
|
|
681
|
+
if (!parsed.success) {
|
|
682
|
+
const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
|
|
683
|
+
return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
|
|
684
|
+
}
|
|
685
|
+
const { owner, repo, pr_number } = parsed.data;
|
|
686
|
+
try {
|
|
687
|
+
const result = await adoptStack(
|
|
688
|
+
owner,
|
|
689
|
+
repo,
|
|
690
|
+
pr_number,
|
|
691
|
+
cfg,
|
|
692
|
+
stackSetPatch(parsed.data)
|
|
693
|
+
);
|
|
694
|
+
return {
|
|
695
|
+
summary: `Adopted stack for ${owner}/${repo}#${pr_number}`,
|
|
696
|
+
data: { owner, repo, pr_number, result }
|
|
697
|
+
};
|
|
698
|
+
} catch (err) {
|
|
699
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
700
|
+
return {
|
|
701
|
+
summary: message,
|
|
702
|
+
data: {
|
|
703
|
+
error: {
|
|
704
|
+
code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
705
|
+
message,
|
|
706
|
+
...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
isError: true
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
579
714
|
// src/tools/stack-list.ts
|
|
580
|
-
import {
|
|
715
|
+
import {
|
|
716
|
+
isCommandErrorCode as isCommandErrorCode4,
|
|
717
|
+
listMergeQueueEntries,
|
|
718
|
+
listStacks
|
|
719
|
+
} from "mergestorm/client";
|
|
581
720
|
|
|
582
721
|
// src/tools/stack-summary.ts
|
|
583
|
-
|
|
722
|
+
import {
|
|
723
|
+
BOUNCE_KIND_LABELS,
|
|
724
|
+
mergeQueueBounceLabel
|
|
725
|
+
} from "mergestorm/client";
|
|
726
|
+
function sameHead(a, b) {
|
|
727
|
+
const left = a?.trim().toLowerCase();
|
|
728
|
+
const right = b?.trim().toLowerCase();
|
|
729
|
+
return !!left && !!right && (left.length === right.length ? left === right : left.startsWith(right) || right.startsWith(left));
|
|
730
|
+
}
|
|
731
|
+
function blockerLabel(stack, entries) {
|
|
732
|
+
if (stack.archivedAt) return null;
|
|
733
|
+
const queue = entries.filter((entry) => entry.stackId === stack.id);
|
|
734
|
+
if (queue.some((entry) => ["queued", "running", "waiting"].includes(entry.state))) return null;
|
|
735
|
+
const open = stack.layers.filter(
|
|
736
|
+
(layer2) => layer2.prNumber > 0 && layer2.state !== "merged" && layer2.state !== "closed"
|
|
737
|
+
);
|
|
738
|
+
const promote = open.filter(
|
|
739
|
+
(layer2) => layer2.prNumber !== stack.unit?.landPrNumber && !stack.unit?.members?.some(
|
|
740
|
+
(member) => member.prNumber === layer2.prNumber && member.promotedHeadSha
|
|
741
|
+
)
|
|
742
|
+
).sort((a, b) => a.position - b.position);
|
|
743
|
+
const layer = promote[0] ?? open.find((layer2) => layer2.prNumber === stack.unit?.landPrNumber) ?? stack.unit?.landPr;
|
|
744
|
+
const bounce = queue.filter((entry) => entry.state === "bounced").sort((a, b) => (Date.parse(b.finishedAt ?? "") || 0) - (Date.parse(a.finishedAt ?? "") || 0))[0];
|
|
745
|
+
const bounceKind = bounce?.bounceDetail?.kind;
|
|
746
|
+
const recoverableBounceSha = (bounceKind === "head_moved" ? bounce?.bounceDetail?.headSha ?? bounce?.verifyHeadSha : bounce?.verifyHeadSha ?? bounce?.bounceDetail?.headSha)?.trim().toLowerCase();
|
|
747
|
+
const currentRecoverableBounce = !!recoverableBounceSha && (bounceKind === "head_moved" || bounceKind === "must_consolidate") && !!layer && layer.state !== "merged" && layer.state !== "closed" && sameHead(layer.headSha, recoverableBounceSha);
|
|
748
|
+
if (layer && layer.state !== "merged" && layer.state !== "closed") {
|
|
749
|
+
if (layer.restackError && layer.state !== "restacking") {
|
|
750
|
+
return layer.restackError.kind === "rebase_conflict" ? "Conflict" : "Restack failed";
|
|
751
|
+
}
|
|
752
|
+
if (layer.state === "conflict") return "Conflict";
|
|
753
|
+
if (layer.draft) return "Draft PR";
|
|
754
|
+
if ((!layer.headSha || layer.mergeableHeadSha === layer.headSha) && (layer.mergeable === false || layer.mergeableState?.toLowerCase() === "dirty")) return "Merge conflicts";
|
|
755
|
+
if (!currentRecoverableBounce && (layer.ciStatus === "failure" || (layer.checks?.failure ?? 0) > 0)) {
|
|
756
|
+
const name = layer.checks?.failingName?.trim();
|
|
757
|
+
return BOUNCE_KIND_LABELS.ci_failure + (name ? ` \u2014 ${name}` : "");
|
|
758
|
+
}
|
|
759
|
+
const tempest = layer.agentRuns?.find(
|
|
760
|
+
(run) => run.agent === "tempest" && ["findings", "failed"].includes(run.status) && sameHead(run.sha, layer.headSha)
|
|
761
|
+
);
|
|
762
|
+
if (tempest) return `Tempest ${tempest.status}`;
|
|
763
|
+
if (layer.vortexStatus === "failed") return "Review failed";
|
|
764
|
+
if (stack.unit?.landPrNumber === layer.prNumber) {
|
|
765
|
+
if (stack.unit.landingBlockReason?.trim()) return stack.unit.landingBlockReason.trim();
|
|
766
|
+
if (stack.unit.tempestLandStatus?.toLowerCase() === "failed") return "Tempest failed";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const kind = bounceKind;
|
|
770
|
+
if (!kind || kind === "head_moved" || kind === "must_consolidate") return null;
|
|
771
|
+
const sha = (bounce.bounceDetail?.headSha ?? bounce.verifyHeadSha)?.trim().toLowerCase();
|
|
772
|
+
const current = sha && layer && layer.prNumber > 0 && layer.state !== "merged" && layer.state !== "closed" && (bounce.bounceDetail?.prNumber == null || layer.prNumber === bounce.bounceDetail.prNumber) && sameHead(layer.headSha, sha);
|
|
773
|
+
return current ? mergeQueueBounceLabel(bounce) : null;
|
|
774
|
+
}
|
|
775
|
+
function stackSummary(stack, entries = []) {
|
|
584
776
|
const count = stack.layers.length;
|
|
585
777
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
586
778
|
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
587
779
|
const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
|
|
588
|
-
|
|
780
|
+
const overrides = [];
|
|
781
|
+
if (typeof stack.autoReviewOverride === "boolean") {
|
|
782
|
+
overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
|
|
783
|
+
}
|
|
784
|
+
if (typeof stack.autoPatchOverride === "boolean") {
|
|
785
|
+
overrides.push(`auto-patch ${stack.autoPatchOverride ? "on" : "off"}`);
|
|
786
|
+
}
|
|
787
|
+
if (stack.cycloneOwnerMatch) {
|
|
788
|
+
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
789
|
+
}
|
|
790
|
+
const blocker = blockerLabel(stack, entries);
|
|
791
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${blocker ? ` \xB7 blocked: ${blocker}` : ""}`;
|
|
589
792
|
}
|
|
590
793
|
|
|
591
794
|
// src/tools/stack-list.ts
|
|
592
795
|
async function stackList(cfg) {
|
|
593
796
|
try {
|
|
594
797
|
const stacks = await listStacks(cfg);
|
|
798
|
+
const entries = await listMergeQueueEntries(cfg);
|
|
595
799
|
return {
|
|
596
|
-
summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack)).join("\n"),
|
|
800
|
+
summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack, entries)).join("\n"),
|
|
597
801
|
data: { stacks }
|
|
598
802
|
};
|
|
599
803
|
} catch (err) {
|
|
@@ -614,8 +818,62 @@ async function stackList(cfg) {
|
|
|
614
818
|
}
|
|
615
819
|
}
|
|
616
820
|
|
|
821
|
+
// src/tools/stack-wait.ts
|
|
822
|
+
import {
|
|
823
|
+
loadConfig as loadConfig6,
|
|
824
|
+
pollStackWatch,
|
|
825
|
+
StackWatchError,
|
|
826
|
+
StackWatchTimeoutError
|
|
827
|
+
} from "mergestorm/client";
|
|
828
|
+
import { z as z2 } from "zod";
|
|
829
|
+
var stackWaitSchema = {
|
|
830
|
+
stack_id: z2.string().trim().min(1),
|
|
831
|
+
timeout_s: z2.number().finite().min(0).max(45).default(45),
|
|
832
|
+
enrolled_head_sha: z2.string().nullable().optional(),
|
|
833
|
+
after_finished_at: z2.string().nullable().optional(),
|
|
834
|
+
bounce_id: z2.string().nullable().optional()
|
|
835
|
+
};
|
|
836
|
+
async function stackWait(input, cfg, deps = {}) {
|
|
837
|
+
const args = z2.object(stackWaitSchema).parse(input);
|
|
838
|
+
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
839
|
+
stackId: args.stack_id,
|
|
840
|
+
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
841
|
+
...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
|
|
842
|
+
...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
|
|
843
|
+
} : void 0;
|
|
844
|
+
let envelope;
|
|
845
|
+
try {
|
|
846
|
+
envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
|
|
847
|
+
timeoutMs: args.timeout_s * 1e3,
|
|
848
|
+
cursor,
|
|
849
|
+
signal: deps.signal
|
|
850
|
+
});
|
|
851
|
+
} catch (err) {
|
|
852
|
+
if (err instanceof StackWatchError) {
|
|
853
|
+
return {
|
|
854
|
+
summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}`,
|
|
855
|
+
data: {
|
|
856
|
+
...err.lastEnvelope,
|
|
857
|
+
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
858
|
+
},
|
|
859
|
+
isError: true
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
863
|
+
envelope = { ...err.lastEnvelope, status: "waiting" };
|
|
864
|
+
}
|
|
865
|
+
return {
|
|
866
|
+
summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${envelope.blocker ? ` \xB7 ${envelope.blocker}` : ""}`,
|
|
867
|
+
data: envelope
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
617
871
|
// src/tools/stack-status.ts
|
|
618
|
-
import {
|
|
872
|
+
import {
|
|
873
|
+
getEnrichedStack,
|
|
874
|
+
listMergeQueueEntries as listMergeQueueEntries2,
|
|
875
|
+
isCommandErrorCode as isCommandErrorCode5
|
|
876
|
+
} from "mergestorm/client";
|
|
619
877
|
async function stackStatus(stackId, cfg) {
|
|
620
878
|
try {
|
|
621
879
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
@@ -633,8 +891,9 @@ async function stackStatus(stackId, cfg) {
|
|
|
633
891
|
isError: true
|
|
634
892
|
};
|
|
635
893
|
}
|
|
894
|
+
const entries = await listMergeQueueEntries2(cfg);
|
|
636
895
|
return {
|
|
637
|
-
summary: stackSummary(stack),
|
|
896
|
+
summary: stackSummary(stack, entries),
|
|
638
897
|
data: { stack }
|
|
639
898
|
};
|
|
640
899
|
} catch (err) {
|
|
@@ -657,33 +916,35 @@ async function stackStatus(stackId, cfg) {
|
|
|
657
916
|
|
|
658
917
|
// src/tools/whoami.ts
|
|
659
918
|
import {
|
|
660
|
-
CommandError as
|
|
919
|
+
CommandError as CommandError10,
|
|
661
920
|
apiBase,
|
|
662
921
|
configPath,
|
|
663
922
|
getMe as getMe2,
|
|
664
|
-
|
|
665
|
-
loadConfig as loadConfig6,
|
|
923
|
+
loadConfig as loadConfig7,
|
|
666
924
|
resolveApiKey
|
|
667
925
|
} from "mergestorm/client";
|
|
668
926
|
async function whoami(cfg) {
|
|
669
|
-
const resolved = cfg ?? await
|
|
927
|
+
const resolved = cfg ?? await loadConfig7();
|
|
670
928
|
const key = resolveApiKey(resolved);
|
|
671
929
|
if (!key) {
|
|
672
|
-
throw new
|
|
930
|
+
throw new CommandError10(
|
|
673
931
|
"No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
|
|
674
932
|
1,
|
|
675
933
|
"missing_api_key"
|
|
676
934
|
);
|
|
677
935
|
}
|
|
678
936
|
const me = await getMe2(resolved);
|
|
937
|
+
if (!me) {
|
|
938
|
+
throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
|
|
939
|
+
}
|
|
679
940
|
const data = {
|
|
680
|
-
key_prefix: me
|
|
681
|
-
key_name: me
|
|
682
|
-
plan_key: me
|
|
683
|
-
plan_label_key: me
|
|
941
|
+
key_prefix: me.key.prefix,
|
|
942
|
+
key_name: me.key.name ?? null,
|
|
943
|
+
plan_key: me.plan_key ?? null,
|
|
944
|
+
plan_label_key: me.plan_label_key ?? null,
|
|
684
945
|
api_base: apiBase(resolved),
|
|
685
946
|
config_path: configPath(),
|
|
686
|
-
usage: me
|
|
947
|
+
usage: me.usage ?? null
|
|
687
948
|
};
|
|
688
949
|
const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
|
|
689
950
|
return {
|
|
@@ -692,9 +953,20 @@ async function whoami(cfg) {
|
|
|
692
953
|
};
|
|
693
954
|
}
|
|
694
955
|
|
|
956
|
+
// src/pr-loop-instructions.ts
|
|
957
|
+
var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
|
|
958
|
+
var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
959
|
+
"When you handle Vortex findings from review_get_pr or review_wait_pr:",
|
|
960
|
+
"- Verify each finding against the current checkout. Do not treat the finding text as proven.",
|
|
961
|
+
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
962
|
+
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
963
|
+
`- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line must be: ${MCP_FINDING_DISMISS_MARKER}. Then one line per skipped finding (path, severity, why).`,
|
|
964
|
+
"- Do not prefix that comment with cyclone-outcome: (reserved)."
|
|
965
|
+
].join("\n");
|
|
966
|
+
|
|
695
967
|
// src/server.ts
|
|
696
968
|
var MCP_SERVER_NAME = "mergestorm";
|
|
697
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
969
|
+
var MCP_SERVER_VERSION = "0.1.6";
|
|
698
970
|
function stripNulls(value) {
|
|
699
971
|
if (Array.isArray(value)) {
|
|
700
972
|
return value.map((item) => stripNulls(item));
|
|
@@ -716,17 +988,20 @@ function ok(payload) {
|
|
|
716
988
|
};
|
|
717
989
|
}
|
|
718
990
|
function fail(err) {
|
|
719
|
-
const message = err instanceof
|
|
991
|
+
const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
|
|
720
992
|
return {
|
|
721
993
|
isError: true,
|
|
722
994
|
content: [{ type: "text", text: message }]
|
|
723
995
|
};
|
|
724
996
|
}
|
|
725
997
|
function createMergestormMcpServer() {
|
|
726
|
-
const server2 = new McpServer(
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
998
|
+
const server2 = new McpServer(
|
|
999
|
+
{
|
|
1000
|
+
name: MCP_SERVER_NAME,
|
|
1001
|
+
version: MCP_SERVER_VERSION
|
|
1002
|
+
},
|
|
1003
|
+
{ instructions: MCP_PR_LOOP_INSTRUCTIONS }
|
|
1004
|
+
);
|
|
730
1005
|
const addTool = server2.registerTool.bind(server2);
|
|
731
1006
|
addTool(
|
|
732
1007
|
"whoami",
|
|
@@ -762,7 +1037,7 @@ function createMergestormMcpServer() {
|
|
|
762
1037
|
title: "List reviews",
|
|
763
1038
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
764
1039
|
inputSchema: {
|
|
765
|
-
limit:
|
|
1040
|
+
limit: z3.number().int().min(1).max(50).optional()
|
|
766
1041
|
}
|
|
767
1042
|
},
|
|
768
1043
|
async ({ limit }) => {
|
|
@@ -779,7 +1054,7 @@ function createMergestormMcpServer() {
|
|
|
779
1054
|
title: "Get review",
|
|
780
1055
|
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
781
1056
|
inputSchema: {
|
|
782
|
-
job_id:
|
|
1057
|
+
job_id: z3.string().min(1)
|
|
783
1058
|
}
|
|
784
1059
|
},
|
|
785
1060
|
async ({ job_id }) => {
|
|
@@ -794,16 +1069,25 @@ function createMergestormMcpServer() {
|
|
|
794
1069
|
"review_get_pr",
|
|
795
1070
|
{
|
|
796
1071
|
title: "Get GitHub PR Vortex review",
|
|
797
|
-
description: "Fetch the latest GitHub PR Vortex pass from the DB-only endpoint. This tool is read-only. On rate_limited, wait retry_after_seconds before trying again.",
|
|
1072
|
+
description: "Fetch the latest GitHub PR Vortex pass from the DB-only endpoint. Review identity is SHA + pass: the envelope carries pass (1 for the first review on a head; a re-review on the same head is the next pass). Pass after_sha to scope to a head, pass to read one exact attempt, or after_pass to read only a later attempt on that head. This tool is read-only. On rate_limited, wait retry_after_seconds before trying again. Verify each finding against the current code; prefer the smallest correct patch; a chat-only skip is not a dismiss (post a PR comment starting with mergestorm-loop: dismiss).",
|
|
798
1073
|
inputSchema: {
|
|
799
|
-
owner:
|
|
800
|
-
repo:
|
|
801
|
-
pr_number:
|
|
1074
|
+
owner: z3.string().min(1),
|
|
1075
|
+
repo: z3.string().min(1),
|
|
1076
|
+
pr_number: z3.number().int().min(1),
|
|
1077
|
+
after_sha: z3.string().optional(),
|
|
1078
|
+
pass: z3.number().int().min(1).optional(),
|
|
1079
|
+
after_pass: z3.number().int().min(1).optional()
|
|
802
1080
|
}
|
|
803
1081
|
},
|
|
804
|
-
async ({ owner, repo, pr_number }) => {
|
|
1082
|
+
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
805
1083
|
try {
|
|
806
|
-
return ok(
|
|
1084
|
+
return ok(
|
|
1085
|
+
await reviewGetPr(owner, repo, pr_number, void 0, {
|
|
1086
|
+
afterSha: after_sha,
|
|
1087
|
+
pass,
|
|
1088
|
+
afterPass: after_pass
|
|
1089
|
+
})
|
|
1090
|
+
);
|
|
807
1091
|
} catch (err) {
|
|
808
1092
|
return fail(err);
|
|
809
1093
|
}
|
|
@@ -813,20 +1097,20 @@ function createMergestormMcpServer() {
|
|
|
813
1097
|
"review_submit",
|
|
814
1098
|
{
|
|
815
1099
|
title: "Submit review",
|
|
816
|
-
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job.
|
|
1100
|
+
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Does not wait by default; returns job_id immediately after submission. Explicit wait: true polls one 45s slice by default. Record job_id and loop review_wait until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting. specialists may be security, performance, architecture, tests, data, api, frontend. governance and seam cannot be requested.",
|
|
817
1101
|
inputSchema: {
|
|
818
|
-
cwd:
|
|
819
|
-
base:
|
|
820
|
-
head:
|
|
821
|
-
diff:
|
|
822
|
-
router:
|
|
823
|
-
specialists:
|
|
824
|
-
context:
|
|
825
|
-
context_files:
|
|
826
|
-
thread:
|
|
827
|
-
idempotency_key:
|
|
828
|
-
wait:
|
|
829
|
-
timeout_s:
|
|
1102
|
+
cwd: z3.string().optional(),
|
|
1103
|
+
base: z3.string().optional(),
|
|
1104
|
+
head: z3.string().optional(),
|
|
1105
|
+
diff: z3.string().optional(),
|
|
1106
|
+
router: z3.enum(["off", "standard", "max", "manual"]).optional(),
|
|
1107
|
+
specialists: z3.array(z3.string().min(1)).max(5).optional(),
|
|
1108
|
+
context: z3.string().optional(),
|
|
1109
|
+
context_files: z3.array(z3.string().min(1)).max(10).optional(),
|
|
1110
|
+
thread: z3.string().min(1).max(120).optional(),
|
|
1111
|
+
idempotency_key: z3.string().min(1).max(128).optional(),
|
|
1112
|
+
wait: z3.boolean().optional(),
|
|
1113
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
830
1114
|
}
|
|
831
1115
|
},
|
|
832
1116
|
async (args) => {
|
|
@@ -841,10 +1125,10 @@ function createMergestormMcpServer() {
|
|
|
841
1125
|
"review_wait",
|
|
842
1126
|
{
|
|
843
1127
|
title: "Wait for review",
|
|
844
|
-
description: "Poll one Mergestorm review job
|
|
1128
|
+
description: "Poll one Mergestorm review job for one 45s slice by default (max 300s). On timeout, returns in_progress; call again with the same job_id until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again.",
|
|
845
1129
|
inputSchema: {
|
|
846
|
-
job_id:
|
|
847
|
-
timeout_s:
|
|
1130
|
+
job_id: z3.string().min(1),
|
|
1131
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
848
1132
|
}
|
|
849
1133
|
},
|
|
850
1134
|
async ({ job_id, timeout_s }) => {
|
|
@@ -859,16 +1143,18 @@ function createMergestormMcpServer() {
|
|
|
859
1143
|
"review_wait_pr",
|
|
860
1144
|
{
|
|
861
1145
|
title: "Wait for GitHub PR Vortex review",
|
|
862
|
-
description: "Poll the DB-only GitHub PR Vortex pass
|
|
1146
|
+
description: "Poll the DB-only GitHub PR Vortex pass for up to 45s (pass timeout_s to override, max 300). Review identity is SHA + pass. Pass after_sha for the head you pushed. When you already hold an envelope for that same head, also pass after_pass set to its pass so the wait cannot return that attempt again; keep after_pass unchanged across timeout retries and never replace it with the pass of an in-progress envelope. Omit after_pass for a head you have no envelope for (its first pass is 1). This tool is read-only. On timeout, returns in_progress so you can call again. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
|
|
863
1147
|
inputSchema: {
|
|
864
|
-
owner:
|
|
865
|
-
repo:
|
|
866
|
-
pr_number:
|
|
867
|
-
after_sha:
|
|
868
|
-
|
|
1148
|
+
owner: z3.string().min(1),
|
|
1149
|
+
repo: z3.string().min(1),
|
|
1150
|
+
pr_number: z3.number().int().min(1),
|
|
1151
|
+
after_sha: z3.string().optional(),
|
|
1152
|
+
pass: z3.number().int().min(1).optional(),
|
|
1153
|
+
after_pass: z3.number().int().min(1).optional(),
|
|
1154
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
869
1155
|
}
|
|
870
1156
|
},
|
|
871
|
-
async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
|
|
1157
|
+
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
872
1158
|
try {
|
|
873
1159
|
return ok(
|
|
874
1160
|
await reviewWaitPr(
|
|
@@ -876,7 +1162,9 @@ function createMergestormMcpServer() {
|
|
|
876
1162
|
repo,
|
|
877
1163
|
pr_number,
|
|
878
1164
|
after_sha,
|
|
879
|
-
timeout_s
|
|
1165
|
+
timeout_s,
|
|
1166
|
+
void 0,
|
|
1167
|
+
{ pass, afterPass: after_pass }
|
|
880
1168
|
)
|
|
881
1169
|
);
|
|
882
1170
|
} catch (err) {
|
|
@@ -884,6 +1172,21 @@ function createMergestormMcpServer() {
|
|
|
884
1172
|
}
|
|
885
1173
|
}
|
|
886
1174
|
);
|
|
1175
|
+
addTool(
|
|
1176
|
+
"stack_adopt",
|
|
1177
|
+
{
|
|
1178
|
+
title: "Adopt PR stack",
|
|
1179
|
+
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption requires a Cyclone GitHub App install; without it the API returns cyclone_not_connected \u2014 do not retry, tell the human. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership.",
|
|
1180
|
+
inputSchema: stackAdoptSchema
|
|
1181
|
+
},
|
|
1182
|
+
async (args) => {
|
|
1183
|
+
try {
|
|
1184
|
+
return ok(await stackAdopt(args));
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
return fail(err);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
);
|
|
887
1190
|
addTool(
|
|
888
1191
|
"stack_list",
|
|
889
1192
|
{
|
|
@@ -898,13 +1201,39 @@ function createMergestormMcpServer() {
|
|
|
898
1201
|
}
|
|
899
1202
|
}
|
|
900
1203
|
);
|
|
1204
|
+
addTool(
|
|
1205
|
+
"stack_set",
|
|
1206
|
+
{
|
|
1207
|
+
title: "Set stack policy",
|
|
1208
|
+
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review and auto_patch pin Vortex auto-review or Cyclone auto-patch for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings.",
|
|
1209
|
+
inputSchema: {
|
|
1210
|
+
stack_id: z3.string().min(1),
|
|
1211
|
+
auto_land: z3.boolean().optional(),
|
|
1212
|
+
auto_review: z3.boolean().nullable().optional(),
|
|
1213
|
+
auto_patch: z3.boolean().nullable().optional()
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
1217
|
+
try {
|
|
1218
|
+
return ok(
|
|
1219
|
+
await stackSet(stack_id, {
|
|
1220
|
+
...auto_land !== void 0 ? { auto_land } : {},
|
|
1221
|
+
...auto_review !== void 0 ? { auto_review } : {},
|
|
1222
|
+
...auto_patch !== void 0 ? { auto_patch } : {}
|
|
1223
|
+
})
|
|
1224
|
+
);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
return fail(err);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
901
1230
|
addTool(
|
|
902
1231
|
"stack_status",
|
|
903
1232
|
{
|
|
904
1233
|
title: "Get stack status",
|
|
905
1234
|
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
906
1235
|
inputSchema: {
|
|
907
|
-
stack_id:
|
|
1236
|
+
stack_id: z3.string().min(1)
|
|
908
1237
|
}
|
|
909
1238
|
},
|
|
910
1239
|
async ({ stack_id }) => {
|
|
@@ -915,13 +1244,29 @@ function createMergestormMcpServer() {
|
|
|
915
1244
|
}
|
|
916
1245
|
}
|
|
917
1246
|
);
|
|
1247
|
+
addTool(
|
|
1248
|
+
"stack_wait",
|
|
1249
|
+
{
|
|
1250
|
+
title: "Wait for stack attention",
|
|
1251
|
+
description: "Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout returns waiting. This tool is read-only. Do not pass 300; hosts drop long MCP calls.",
|
|
1252
|
+
inputSchema: stackWaitSchema
|
|
1253
|
+
},
|
|
1254
|
+
async (args, extra) => {
|
|
1255
|
+
try {
|
|
1256
|
+
const payload = await stackWait(args, void 0, { signal: extra.signal });
|
|
1257
|
+
return { ...ok(payload), structuredContent: payload.data };
|
|
1258
|
+
} catch (err) {
|
|
1259
|
+
return fail(err);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
);
|
|
918
1263
|
addTool(
|
|
919
1264
|
"queue_status",
|
|
920
1265
|
{
|
|
921
1266
|
title: "Get merge queue status",
|
|
922
|
-
description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
|
|
1267
|
+
description: "List live merge queue entries (position >= 1) plus the newest bounced entries (position 0) with bounceDetail for the current user, optionally filtered by stack. This tool is read-only.",
|
|
923
1268
|
inputSchema: {
|
|
924
|
-
stack_id:
|
|
1269
|
+
stack_id: z3.string().min(1).optional()
|
|
925
1270
|
}
|
|
926
1271
|
},
|
|
927
1272
|
async ({ stack_id }) => {
|
|
@@ -950,15 +1295,17 @@ function createMergestormMcpServer() {
|
|
|
950
1295
|
"settings_set",
|
|
951
1296
|
{
|
|
952
1297
|
title: "Update settings",
|
|
953
|
-
description: "Update writable Bearer /api/v1/settings
|
|
1298
|
+
description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. cyclone_connected and github_connected are read-only and cannot be set.",
|
|
954
1299
|
inputSchema: {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
1300
|
+
...Object.fromEntries(
|
|
1301
|
+
BEARER_SETTINGS2.map((row) => [
|
|
1302
|
+
row.key,
|
|
1303
|
+
("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : z3.enum(row.values) : z3.boolean()).optional()
|
|
1304
|
+
])
|
|
1305
|
+
),
|
|
1306
|
+
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
1307
|
+
cyclone_connected: z3.unknown().optional(),
|
|
1308
|
+
github_connected: z3.unknown().optional()
|
|
962
1309
|
}
|
|
963
1310
|
},
|
|
964
1311
|
async (args) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
|
-
"mergestorm": "
|
|
32
|
+
"mergestorm": "0.3.19",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|