mergestorm-mcp 0.1.2 → 0.1.5
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 +4 -2
- package/dist/index.js +283 -93
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
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`, `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. `auto_patch` defaults to `false` and accepts only `false` or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
|
|
7
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.
|
|
9
11
|
|
|
10
12
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
|
|
4
3
|
// src/index.ts
|
|
5
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
5
|
|
|
7
6
|
// src/server.ts
|
|
8
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
-
import { z } from "zod";
|
|
10
|
-
import { CommandError as
|
|
8
|
+
import { z as z2 } from "zod";
|
|
9
|
+
import { CommandError as CommandError11, SETTINGS_WRITABLE_KEYS as SETTINGS_WRITABLE_KEYS2 } from "mergestorm/client";
|
|
11
10
|
|
|
12
11
|
// src/tools/credits.ts
|
|
13
12
|
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
@@ -26,8 +25,10 @@ async function credits(cfg) {
|
|
|
26
25
|
}
|
|
27
26
|
const standard = me.usage.standard;
|
|
28
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`;
|
|
29
30
|
return {
|
|
30
|
-
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}`,
|
|
31
32
|
data: {
|
|
32
33
|
usage: me.usage,
|
|
33
34
|
resets_at: me.resets_at ?? null
|
|
@@ -142,9 +143,21 @@ function prReviewTarget(owner, repo, prNumber) {
|
|
|
142
143
|
}
|
|
143
144
|
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
144
145
|
}
|
|
146
|
+
function prReviewSummary(owner, repo, prNumber, envelope) {
|
|
147
|
+
const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
|
|
148
|
+
const sha = envelope.head_sha?.trim();
|
|
149
|
+
if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
|
|
150
|
+
if (typeof envelope.finding_count === "number") {
|
|
151
|
+
bits.push(
|
|
152
|
+
`${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if (envelope.phase) bits.push(String(envelope.phase));
|
|
156
|
+
return bits.join(" \xB7 ");
|
|
157
|
+
}
|
|
145
158
|
function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
146
159
|
return {
|
|
147
|
-
summary:
|
|
160
|
+
summary: prReviewSummary(owner, repo, prNumber, envelope),
|
|
148
161
|
data: { ...envelope }
|
|
149
162
|
};
|
|
150
163
|
}
|
|
@@ -159,14 +172,15 @@ function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
|
159
172
|
}
|
|
160
173
|
};
|
|
161
174
|
}
|
|
162
|
-
async function reviewGetPr(owner, repo, prNumber, cfg) {
|
|
175
|
+
async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
163
176
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
164
177
|
try {
|
|
165
178
|
const envelope = await getPrVortexReview(
|
|
166
179
|
target.owner,
|
|
167
180
|
target.repo,
|
|
168
181
|
target.prNumber,
|
|
169
|
-
cfg
|
|
182
|
+
cfg,
|
|
183
|
+
{ afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
|
|
170
184
|
);
|
|
171
185
|
return payloadFromPrReview(
|
|
172
186
|
target.owner,
|
|
@@ -260,6 +274,8 @@ import {
|
|
|
260
274
|
REVIEW_THREAD_SLUG_RE,
|
|
261
275
|
ReviewPollTimeoutError,
|
|
262
276
|
collectReviewInput,
|
|
277
|
+
discoverTrunk,
|
|
278
|
+
formatReviewSubmitError,
|
|
263
279
|
loadConfig as loadConfig3,
|
|
264
280
|
loadReviewContext,
|
|
265
281
|
pollReview,
|
|
@@ -267,6 +283,7 @@ import {
|
|
|
267
283
|
submitReview
|
|
268
284
|
} from "mergestorm/client";
|
|
269
285
|
var MCP_WAIT_CEILING_S = 300;
|
|
286
|
+
var MCP_PR_WAIT_DEFAULT_S = 45;
|
|
270
287
|
var PINNABLE_SPECIALISTS = [
|
|
271
288
|
"security",
|
|
272
289
|
"performance",
|
|
@@ -291,8 +308,8 @@ function assertPinnableSpecialists(specialists) {
|
|
|
291
308
|
function mcpSandboxRoot() {
|
|
292
309
|
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
293
310
|
}
|
|
294
|
-
function waitTimeoutMs(timeout_s) {
|
|
295
|
-
const seconds = timeout_s ??
|
|
311
|
+
function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
312
|
+
const seconds = timeout_s ?? defaultSeconds;
|
|
296
313
|
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
297
314
|
throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
|
|
298
315
|
}
|
|
@@ -307,13 +324,18 @@ async function resolveInput(input) {
|
|
|
307
324
|
return {
|
|
308
325
|
thread,
|
|
309
326
|
branch,
|
|
310
|
-
baseLabel: input.base?.trim() ||
|
|
327
|
+
baseLabel: input.base?.trim() || (() => {
|
|
328
|
+
try {
|
|
329
|
+
return discoverTrunk(cwd);
|
|
330
|
+
} catch {
|
|
331
|
+
return "main";
|
|
332
|
+
}
|
|
333
|
+
})(),
|
|
311
334
|
headLabel: head,
|
|
312
335
|
diff: input.diff,
|
|
313
336
|
files: []
|
|
314
337
|
};
|
|
315
338
|
}
|
|
316
|
-
const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
|
|
317
339
|
const sandboxRoot = mcpSandboxRoot();
|
|
318
340
|
try {
|
|
319
341
|
const [sandboxReal, cwdReal] = await Promise.all([
|
|
@@ -328,27 +350,12 @@ async function resolveInput(input) {
|
|
|
328
350
|
if (err instanceof CommandError5) throw err;
|
|
329
351
|
throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
|
|
330
352
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
let yielded = false;
|
|
334
|
-
for (const base of bases) {
|
|
335
|
-
try {
|
|
336
|
-
const candidate = await collectReviewInput(base, head, cwd, sandboxRoot);
|
|
337
|
-
yielded = true;
|
|
338
|
-
if (!collected) collected = candidate;
|
|
339
|
-
} catch (err) {
|
|
340
|
-
if (yielded) continue;
|
|
341
|
-
lastErr = err;
|
|
342
|
-
if (input.base?.trim()) throw err;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
if (yielded) return collected;
|
|
346
|
-
if (lastErr) throw lastErr;
|
|
347
|
-
return null;
|
|
353
|
+
const base = input.base?.trim() || discoverTrunk(cwd);
|
|
354
|
+
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
348
355
|
}
|
|
349
356
|
async function reviewSubmit(input, cfg, opts = {}) {
|
|
350
357
|
const resolved = cfg ?? await loadConfig3();
|
|
351
|
-
const wait = input.wait
|
|
358
|
+
const wait = input.wait === true;
|
|
352
359
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
353
360
|
assertPinnableSpecialists(input.specialists);
|
|
354
361
|
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
@@ -396,7 +403,13 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
396
403
|
});
|
|
397
404
|
}
|
|
398
405
|
if (submitted.status !== 202 && submitted.status !== 200) {
|
|
399
|
-
|
|
406
|
+
return {
|
|
407
|
+
...payloadFromRow({ ...submitted.row, status: "failed" }, {
|
|
408
|
+
jobId,
|
|
409
|
+
error: formatReviewSubmitError(submitted.status, submitted.row)
|
|
410
|
+
}),
|
|
411
|
+
isError: true
|
|
412
|
+
};
|
|
400
413
|
}
|
|
401
414
|
if (!jobId) {
|
|
402
415
|
throw new CommandError5("Review API returned no job_id");
|
|
@@ -441,8 +454,10 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
441
454
|
target.repo,
|
|
442
455
|
target.prNumber,
|
|
443
456
|
{
|
|
444
|
-
timeoutMs: waitTimeoutMs(timeout_s),
|
|
457
|
+
timeoutMs: waitTimeoutMs(timeout_s, MCP_PR_WAIT_DEFAULT_S),
|
|
445
458
|
afterSha,
|
|
459
|
+
pass: opts.pass,
|
|
460
|
+
afterPass: opts.afterPass,
|
|
446
461
|
intervalMs: opts.pollIntervalMs
|
|
447
462
|
}
|
|
448
463
|
);
|
|
@@ -456,7 +471,12 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
456
471
|
if (err instanceof PrReviewPollTimeoutError) {
|
|
457
472
|
const status = err.lastEnvelope?.status ?? "in_progress";
|
|
458
473
|
return {
|
|
459
|
-
summary:
|
|
474
|
+
summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
|
|
475
|
+
status,
|
|
476
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
477
|
+
finding_count: err.lastEnvelope?.finding_count ?? null,
|
|
478
|
+
phase: err.lastEnvelope?.phase ?? null
|
|
479
|
+
}),
|
|
460
480
|
data: {
|
|
461
481
|
...err.lastEnvelope ?? {},
|
|
462
482
|
status,
|
|
@@ -554,9 +574,6 @@ function settingsPatchFromArgs(args) {
|
|
|
554
574
|
if (typeof value !== "boolean") {
|
|
555
575
|
throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
|
|
556
576
|
}
|
|
557
|
-
if (key === "auto_patch_enabled" && value === true) {
|
|
558
|
-
throw new CommandError8("auto_patch_enabled cannot be enabled by this tool.", 2, "usage");
|
|
559
|
-
}
|
|
560
577
|
patch[key] = value;
|
|
561
578
|
}
|
|
562
579
|
if (Object.keys(patch).length === 0) {
|
|
@@ -577,6 +594,105 @@ async function settingsSet(args, cfg) {
|
|
|
577
594
|
};
|
|
578
595
|
}
|
|
579
596
|
|
|
597
|
+
// src/tools/stack-adopt.ts
|
|
598
|
+
import { adoptStack, CommandError as CommandError9 } from "mergestorm/client";
|
|
599
|
+
import { z } from "zod";
|
|
600
|
+
|
|
601
|
+
// src/tools/stack-set.ts
|
|
602
|
+
import { setStackPolicy } from "mergestorm/client";
|
|
603
|
+
function triWord(value) {
|
|
604
|
+
return value === null ? "default" : value ? "on" : "off";
|
|
605
|
+
}
|
|
606
|
+
function stackSetPatch(input) {
|
|
607
|
+
const patch = {};
|
|
608
|
+
if (typeof input.auto_land === "boolean") patch.autoEnqueueWhenReady = input.auto_land;
|
|
609
|
+
if (input.auto_review !== void 0) patch.autoReviewOverride = input.auto_review;
|
|
610
|
+
if (input.auto_patch !== void 0) patch.autoPatchOverride = input.auto_patch;
|
|
611
|
+
return patch;
|
|
612
|
+
}
|
|
613
|
+
async function stackSet(stackId, input, cfg) {
|
|
614
|
+
const patch = stackSetPatch(input);
|
|
615
|
+
if (Object.keys(patch).length === 0) {
|
|
616
|
+
return {
|
|
617
|
+
summary: "stack_set needs at least one of auto_land, auto_review, auto_patch",
|
|
618
|
+
isError: true,
|
|
619
|
+
data: {
|
|
620
|
+
error: {
|
|
621
|
+
code: "invalid_input",
|
|
622
|
+
message: "Pass at least one of auto_land, auto_review, auto_patch.",
|
|
623
|
+
stack_id: stackId
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
const result = await setStackPolicy(stackId, patch, cfg);
|
|
629
|
+
const parts = [];
|
|
630
|
+
if (typeof input.auto_land === "boolean") {
|
|
631
|
+
parts.push(`Auto land ${input.auto_land ? "on" : "off"}`);
|
|
632
|
+
}
|
|
633
|
+
if (input.auto_review !== void 0) {
|
|
634
|
+
parts.push(`auto-review ${triWord(input.auto_review)}`);
|
|
635
|
+
}
|
|
636
|
+
if (input.auto_patch !== void 0) {
|
|
637
|
+
parts.push(`auto-patch ${triWord(input.auto_patch)}`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
summary: `${parts.join(", ")} for stack ${stackId}`,
|
|
641
|
+
data: {
|
|
642
|
+
stack_id: stackId,
|
|
643
|
+
...typeof input.auto_land === "boolean" ? { auto_land: input.auto_land } : {},
|
|
644
|
+
...input.auto_review !== void 0 ? { auto_review: input.auto_review } : {},
|
|
645
|
+
...input.auto_patch !== void 0 ? { auto_patch: input.auto_patch } : {},
|
|
646
|
+
result
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/tools/stack-adopt.ts
|
|
652
|
+
var stackAdoptSchema = {
|
|
653
|
+
owner: z.string().trim().min(1),
|
|
654
|
+
repo: z.string().trim().min(1),
|
|
655
|
+
pr_number: z.number().int().min(1),
|
|
656
|
+
auto_land: z.literal(false).optional(),
|
|
657
|
+
auto_review: z.boolean().nullable().optional(),
|
|
658
|
+
auto_patch: z.literal(false).nullable().optional()
|
|
659
|
+
};
|
|
660
|
+
var inputSchema = z.object(stackAdoptSchema);
|
|
661
|
+
async function stackAdopt(input, cfg) {
|
|
662
|
+
const parsed = inputSchema.safeParse(input);
|
|
663
|
+
if (!parsed.success) {
|
|
664
|
+
const message = "stack_adopt requires owner, repo, a positive pr_number, and valid policy values";
|
|
665
|
+
return { summary: message, data: { error: { code: "invalid_input", message } }, isError: true };
|
|
666
|
+
}
|
|
667
|
+
const { owner, repo, pr_number } = parsed.data;
|
|
668
|
+
try {
|
|
669
|
+
const result = await adoptStack(
|
|
670
|
+
owner,
|
|
671
|
+
repo,
|
|
672
|
+
pr_number,
|
|
673
|
+
cfg,
|
|
674
|
+
stackSetPatch(parsed.data)
|
|
675
|
+
);
|
|
676
|
+
return {
|
|
677
|
+
summary: `Adopted stack for ${owner}/${repo}#${pr_number}`,
|
|
678
|
+
data: { owner, repo, pr_number, result }
|
|
679
|
+
};
|
|
680
|
+
} catch (err) {
|
|
681
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
682
|
+
return {
|
|
683
|
+
summary: message,
|
|
684
|
+
data: {
|
|
685
|
+
error: {
|
|
686
|
+
code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
687
|
+
message,
|
|
688
|
+
...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
isError: true
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
580
696
|
// src/tools/stack-list.ts
|
|
581
697
|
import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
|
|
582
698
|
|
|
@@ -586,7 +702,17 @@ function stackSummary(stack) {
|
|
|
586
702
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
587
703
|
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
588
704
|
const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
|
|
589
|
-
|
|
705
|
+
const overrides = [];
|
|
706
|
+
if (typeof stack.autoReviewOverride === "boolean") {
|
|
707
|
+
overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
|
|
708
|
+
}
|
|
709
|
+
if (typeof stack.autoPatchOverride === "boolean") {
|
|
710
|
+
overrides.push(`auto-patch ${stack.autoPatchOverride ? "on" : "off"}`);
|
|
711
|
+
}
|
|
712
|
+
if (stack.cycloneOwnerMatch) {
|
|
713
|
+
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
714
|
+
}
|
|
715
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}`;
|
|
590
716
|
}
|
|
591
717
|
|
|
592
718
|
// src/tools/stack-list.ts
|
|
@@ -658,11 +784,10 @@ async function stackStatus(stackId, cfg) {
|
|
|
658
784
|
|
|
659
785
|
// src/tools/whoami.ts
|
|
660
786
|
import {
|
|
661
|
-
CommandError as
|
|
787
|
+
CommandError as CommandError10,
|
|
662
788
|
apiBase,
|
|
663
789
|
configPath,
|
|
664
790
|
getMe as getMe2,
|
|
665
|
-
keyDisplay,
|
|
666
791
|
loadConfig as loadConfig6,
|
|
667
792
|
resolveApiKey
|
|
668
793
|
} from "mergestorm/client";
|
|
@@ -670,21 +795,24 @@ async function whoami(cfg) {
|
|
|
670
795
|
const resolved = cfg ?? await loadConfig6();
|
|
671
796
|
const key = resolveApiKey(resolved);
|
|
672
797
|
if (!key) {
|
|
673
|
-
throw new
|
|
798
|
+
throw new CommandError10(
|
|
674
799
|
"No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
|
|
675
800
|
1,
|
|
676
801
|
"missing_api_key"
|
|
677
802
|
);
|
|
678
803
|
}
|
|
679
804
|
const me = await getMe2(resolved);
|
|
805
|
+
if (!me) {
|
|
806
|
+
throw new CommandError10("Live account details unavailable. Could not verify account with /me.");
|
|
807
|
+
}
|
|
680
808
|
const data = {
|
|
681
|
-
key_prefix: me
|
|
682
|
-
key_name: me
|
|
683
|
-
plan_key: me
|
|
684
|
-
plan_label_key: me
|
|
809
|
+
key_prefix: me.key.prefix,
|
|
810
|
+
key_name: me.key.name ?? null,
|
|
811
|
+
plan_key: me.plan_key ?? null,
|
|
812
|
+
plan_label_key: me.plan_label_key ?? null,
|
|
685
813
|
api_base: apiBase(resolved),
|
|
686
814
|
config_path: configPath(),
|
|
687
|
-
usage: me
|
|
815
|
+
usage: me.usage ?? null
|
|
688
816
|
};
|
|
689
817
|
const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
|
|
690
818
|
return {
|
|
@@ -693,9 +821,20 @@ async function whoami(cfg) {
|
|
|
693
821
|
};
|
|
694
822
|
}
|
|
695
823
|
|
|
824
|
+
// src/pr-loop-instructions.ts
|
|
825
|
+
var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
|
|
826
|
+
var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
827
|
+
"When you handle Vortex findings from review_get_pr or review_wait_pr:",
|
|
828
|
+
"- Verify each finding against the current checkout. Do not treat the finding text as proven.",
|
|
829
|
+
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
830
|
+
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
831
|
+
`- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line must be: ${MCP_FINDING_DISMISS_MARKER}. Then one line per skipped finding (path, severity, why).`,
|
|
832
|
+
"- Do not prefix that comment with cyclone-outcome: (reserved)."
|
|
833
|
+
].join("\n");
|
|
834
|
+
|
|
696
835
|
// src/server.ts
|
|
697
836
|
var MCP_SERVER_NAME = "mergestorm";
|
|
698
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
837
|
+
var MCP_SERVER_VERSION = "0.1.5";
|
|
699
838
|
function stripNulls(value) {
|
|
700
839
|
if (Array.isArray(value)) {
|
|
701
840
|
return value.map((item) => stripNulls(item));
|
|
@@ -717,17 +856,20 @@ function ok(payload) {
|
|
|
717
856
|
};
|
|
718
857
|
}
|
|
719
858
|
function fail(err) {
|
|
720
|
-
const message = err instanceof
|
|
859
|
+
const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
|
|
721
860
|
return {
|
|
722
861
|
isError: true,
|
|
723
862
|
content: [{ type: "text", text: message }]
|
|
724
863
|
};
|
|
725
864
|
}
|
|
726
865
|
function createMergestormMcpServer() {
|
|
727
|
-
const server2 = new McpServer(
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
866
|
+
const server2 = new McpServer(
|
|
867
|
+
{
|
|
868
|
+
name: MCP_SERVER_NAME,
|
|
869
|
+
version: MCP_SERVER_VERSION
|
|
870
|
+
},
|
|
871
|
+
{ instructions: MCP_PR_LOOP_INSTRUCTIONS }
|
|
872
|
+
);
|
|
731
873
|
const addTool = server2.registerTool.bind(server2);
|
|
732
874
|
addTool(
|
|
733
875
|
"whoami",
|
|
@@ -763,7 +905,7 @@ function createMergestormMcpServer() {
|
|
|
763
905
|
title: "List reviews",
|
|
764
906
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
765
907
|
inputSchema: {
|
|
766
|
-
limit:
|
|
908
|
+
limit: z2.number().int().min(1).max(50).optional()
|
|
767
909
|
}
|
|
768
910
|
},
|
|
769
911
|
async ({ limit }) => {
|
|
@@ -780,7 +922,7 @@ function createMergestormMcpServer() {
|
|
|
780
922
|
title: "Get review",
|
|
781
923
|
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
782
924
|
inputSchema: {
|
|
783
|
-
job_id:
|
|
925
|
+
job_id: z2.string().min(1)
|
|
784
926
|
}
|
|
785
927
|
},
|
|
786
928
|
async ({ job_id }) => {
|
|
@@ -795,16 +937,25 @@ function createMergestormMcpServer() {
|
|
|
795
937
|
"review_get_pr",
|
|
796
938
|
{
|
|
797
939
|
title: "Get GitHub PR Vortex review",
|
|
798
|
-
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.",
|
|
940
|
+
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).",
|
|
799
941
|
inputSchema: {
|
|
800
|
-
owner:
|
|
801
|
-
repo:
|
|
802
|
-
pr_number:
|
|
942
|
+
owner: z2.string().min(1),
|
|
943
|
+
repo: z2.string().min(1),
|
|
944
|
+
pr_number: z2.number().int().min(1),
|
|
945
|
+
after_sha: z2.string().optional(),
|
|
946
|
+
pass: z2.number().int().min(1).optional(),
|
|
947
|
+
after_pass: z2.number().int().min(1).optional()
|
|
803
948
|
}
|
|
804
949
|
},
|
|
805
|
-
async ({ owner, repo, pr_number }) => {
|
|
950
|
+
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
806
951
|
try {
|
|
807
|
-
return ok(
|
|
952
|
+
return ok(
|
|
953
|
+
await reviewGetPr(owner, repo, pr_number, void 0, {
|
|
954
|
+
afterSha: after_sha,
|
|
955
|
+
pass,
|
|
956
|
+
afterPass: after_pass
|
|
957
|
+
})
|
|
958
|
+
);
|
|
808
959
|
} catch (err) {
|
|
809
960
|
return fail(err);
|
|
810
961
|
}
|
|
@@ -814,20 +965,20 @@ function createMergestormMcpServer() {
|
|
|
814
965
|
"review_submit",
|
|
815
966
|
{
|
|
816
967
|
title: "Submit review",
|
|
817
|
-
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job.
|
|
968
|
+
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.",
|
|
818
969
|
inputSchema: {
|
|
819
|
-
cwd:
|
|
820
|
-
base:
|
|
821
|
-
head:
|
|
822
|
-
diff:
|
|
823
|
-
router:
|
|
824
|
-
specialists:
|
|
825
|
-
context:
|
|
826
|
-
context_files:
|
|
827
|
-
thread:
|
|
828
|
-
idempotency_key:
|
|
829
|
-
wait:
|
|
830
|
-
timeout_s:
|
|
970
|
+
cwd: z2.string().optional(),
|
|
971
|
+
base: z2.string().optional(),
|
|
972
|
+
head: z2.string().optional(),
|
|
973
|
+
diff: z2.string().optional(),
|
|
974
|
+
router: z2.enum(["off", "standard", "max", "manual"]).optional(),
|
|
975
|
+
specialists: z2.array(z2.string().min(1)).max(5).optional(),
|
|
976
|
+
context: z2.string().optional(),
|
|
977
|
+
context_files: z2.array(z2.string().min(1)).max(10).optional(),
|
|
978
|
+
thread: z2.string().min(1).max(120).optional(),
|
|
979
|
+
idempotency_key: z2.string().min(1).max(128).optional(),
|
|
980
|
+
wait: z2.boolean().optional(),
|
|
981
|
+
timeout_s: z2.number().positive().max(300).optional()
|
|
831
982
|
}
|
|
832
983
|
},
|
|
833
984
|
async (args) => {
|
|
@@ -842,10 +993,10 @@ function createMergestormMcpServer() {
|
|
|
842
993
|
"review_wait",
|
|
843
994
|
{
|
|
844
995
|
title: "Wait for review",
|
|
845
|
-
description: "Poll one Mergestorm review job
|
|
996
|
+
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.",
|
|
846
997
|
inputSchema: {
|
|
847
|
-
job_id:
|
|
848
|
-
timeout_s:
|
|
998
|
+
job_id: z2.string().min(1),
|
|
999
|
+
timeout_s: z2.number().positive().max(300).optional()
|
|
849
1000
|
}
|
|
850
1001
|
},
|
|
851
1002
|
async ({ job_id, timeout_s }) => {
|
|
@@ -860,16 +1011,18 @@ function createMergestormMcpServer() {
|
|
|
860
1011
|
"review_wait_pr",
|
|
861
1012
|
{
|
|
862
1013
|
title: "Wait for GitHub PR Vortex review",
|
|
863
|
-
description: "Poll the DB-only GitHub PR Vortex pass
|
|
1014
|
+
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.",
|
|
864
1015
|
inputSchema: {
|
|
865
|
-
owner:
|
|
866
|
-
repo:
|
|
867
|
-
pr_number:
|
|
868
|
-
after_sha:
|
|
869
|
-
|
|
1016
|
+
owner: z2.string().min(1),
|
|
1017
|
+
repo: z2.string().min(1),
|
|
1018
|
+
pr_number: z2.number().int().min(1),
|
|
1019
|
+
after_sha: z2.string().optional(),
|
|
1020
|
+
pass: z2.number().int().min(1).optional(),
|
|
1021
|
+
after_pass: z2.number().int().min(1).optional(),
|
|
1022
|
+
timeout_s: z2.number().positive().max(300).optional()
|
|
870
1023
|
}
|
|
871
1024
|
},
|
|
872
|
-
async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
|
|
1025
|
+
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
873
1026
|
try {
|
|
874
1027
|
return ok(
|
|
875
1028
|
await reviewWaitPr(
|
|
@@ -877,7 +1030,9 @@ function createMergestormMcpServer() {
|
|
|
877
1030
|
repo,
|
|
878
1031
|
pr_number,
|
|
879
1032
|
after_sha,
|
|
880
|
-
timeout_s
|
|
1033
|
+
timeout_s,
|
|
1034
|
+
void 0,
|
|
1035
|
+
{ pass, afterPass: after_pass }
|
|
881
1036
|
)
|
|
882
1037
|
);
|
|
883
1038
|
} catch (err) {
|
|
@@ -885,6 +1040,21 @@ function createMergestormMcpServer() {
|
|
|
885
1040
|
}
|
|
886
1041
|
}
|
|
887
1042
|
);
|
|
1043
|
+
addTool(
|
|
1044
|
+
"stack_adopt",
|
|
1045
|
+
{
|
|
1046
|
+
title: "Adopt PR stack",
|
|
1047
|
+
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land may only be false; auto_review and auto_patch set per-stack policy, and null clears their overrides. Omitted policy keys follow the account settings. Never changes account settings. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership.",
|
|
1048
|
+
inputSchema: stackAdoptSchema
|
|
1049
|
+
},
|
|
1050
|
+
async (args) => {
|
|
1051
|
+
try {
|
|
1052
|
+
return ok(await stackAdopt(args));
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
return fail(err);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
);
|
|
888
1058
|
addTool(
|
|
889
1059
|
"stack_list",
|
|
890
1060
|
{
|
|
@@ -899,13 +1069,39 @@ function createMergestormMcpServer() {
|
|
|
899
1069
|
}
|
|
900
1070
|
}
|
|
901
1071
|
);
|
|
1072
|
+
addTool(
|
|
1073
|
+
"stack_set",
|
|
1074
|
+
{
|
|
1075
|
+
title: "Set stack policy",
|
|
1076
|
+
description: "Set per-stack automation on one owned Mergestorm stack: auto_land arms or disarms Auto land; auto_review and auto_patch pin Vortex auto-review or Cyclone auto-patch for this stack only (true or false), and null clears the pin so the stack follows the account setting again. Pass at least one key. Never changes account settings.",
|
|
1077
|
+
inputSchema: {
|
|
1078
|
+
stack_id: z2.string().min(1),
|
|
1079
|
+
auto_land: z2.boolean().optional(),
|
|
1080
|
+
auto_review: z2.boolean().nullable().optional(),
|
|
1081
|
+
auto_patch: z2.boolean().nullable().optional()
|
|
1082
|
+
}
|
|
1083
|
+
},
|
|
1084
|
+
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
1085
|
+
try {
|
|
1086
|
+
return ok(
|
|
1087
|
+
await stackSet(stack_id, {
|
|
1088
|
+
...auto_land !== void 0 ? { auto_land } : {},
|
|
1089
|
+
...auto_review !== void 0 ? { auto_review } : {},
|
|
1090
|
+
...auto_patch !== void 0 ? { auto_patch } : {}
|
|
1091
|
+
})
|
|
1092
|
+
);
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
return fail(err);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
);
|
|
902
1098
|
addTool(
|
|
903
1099
|
"stack_status",
|
|
904
1100
|
{
|
|
905
1101
|
title: "Get stack status",
|
|
906
1102
|
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
907
1103
|
inputSchema: {
|
|
908
|
-
stack_id:
|
|
1104
|
+
stack_id: z2.string().min(1)
|
|
909
1105
|
}
|
|
910
1106
|
},
|
|
911
1107
|
async ({ stack_id }) => {
|
|
@@ -922,7 +1118,7 @@ function createMergestormMcpServer() {
|
|
|
922
1118
|
title: "Get merge queue status",
|
|
923
1119
|
description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
|
|
924
1120
|
inputSchema: {
|
|
925
|
-
stack_id:
|
|
1121
|
+
stack_id: z2.string().min(1).optional()
|
|
926
1122
|
}
|
|
927
1123
|
},
|
|
928
1124
|
async ({ stack_id }) => {
|
|
@@ -952,15 +1148,9 @@ function createMergestormMcpServer() {
|
|
|
952
1148
|
{
|
|
953
1149
|
title: "Update settings",
|
|
954
1150
|
description: "Update writable Bearer /api/v1/settings toggles and return the stored result. At least one key is required. cyclone_connected and github_connected are read-only and cannot be set.",
|
|
955
|
-
inputSchema:
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
vortex_show_thinking_traces: z.boolean().optional(),
|
|
959
|
-
repo_overview_enabled: z.boolean().optional(),
|
|
960
|
-
review_unit_land_prs_enabled: z.boolean().optional(),
|
|
961
|
-
cyclone_review_unit_land_prs_enabled: z.boolean().optional(),
|
|
962
|
-
vortex_seam_specialist_enabled: z.boolean().optional()
|
|
963
|
-
}
|
|
1151
|
+
inputSchema: Object.fromEntries(
|
|
1152
|
+
SETTINGS_WRITABLE_KEYS2.map((key) => [key, z2.boolean().optional()])
|
|
1153
|
+
)
|
|
964
1154
|
},
|
|
965
1155
|
async (args) => {
|
|
966
1156
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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.17",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|