mergestorm-mcp 0.1.5 → 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 +3 -2
- package/dist/index.js +223 -67
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# mergestorm-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `queue_status`, `settings_get`, `settings_set`.
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
|
|
4
4
|
|
|
5
5
|
The stack tools expose reads plus one scoped policy write:
|
|
6
6
|
|
|
7
|
-
- `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy.
|
|
7
|
+
- `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
|
|
8
8
|
- `stack_list` returns the current API key owner's registered stacks.
|
|
9
9
|
- `stack_set({ stack_id, auto_land?, auto_review?, auto_patch? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review` and `auto_patch` pin Vortex auto-review or Cyclone auto-patch for that stack in either direction (`true` or `false`); `null` clears the pin so the stack follows the account setting. Pass at least one key. Account settings are never changed here.
|
|
10
10
|
- `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, or a structured `stack_not_found` error.
|
|
11
|
+
- `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots, or a waiting result with selectors to use for the next call.
|
|
11
12
|
|
|
12
13
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
13
14
|
|
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
5
5
|
|
|
6
6
|
// src/server.ts
|
|
7
7
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
-
import { z as
|
|
9
|
-
import { CommandError as CommandError11,
|
|
8
|
+
import { z as z3 } from "zod";
|
|
9
|
+
import { CommandError as CommandError11, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
|
|
10
10
|
|
|
11
11
|
// src/tools/credits.ts
|
|
12
12
|
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
@@ -46,20 +46,29 @@ import {
|
|
|
46
46
|
REVIEW_EXIT
|
|
47
47
|
} from "mergestorm/client";
|
|
48
48
|
function entrySummary(entry) {
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
49
|
+
const prNumber = entry.bounceDetail?.prNumber;
|
|
50
|
+
const repo = `${entry.owner}/${entry.repo}${prNumber ? `#${prNumber}` : ""}`;
|
|
51
|
+
const reason = entry.bounceDetail?.kind ?? entry.bounceReason ?? entry.waitReason ?? void 0;
|
|
52
|
+
const sha = entry.bounceDetail?.headSha ?? entry.verifyHeadSha ?? void 0;
|
|
53
|
+
return [
|
|
54
|
+
entry.state,
|
|
55
|
+
repo,
|
|
56
|
+
`stack ${entry.stackId}`,
|
|
57
|
+
reason,
|
|
58
|
+
sha?.slice(0, 7)
|
|
59
|
+
].filter(Boolean).join(" \xB7 ");
|
|
52
60
|
}
|
|
53
61
|
async function queueStatus(stackId, cfg) {
|
|
54
62
|
try {
|
|
55
63
|
const resolved = cfg ?? await loadConfig2();
|
|
64
|
+
const id = stackId?.trim();
|
|
56
65
|
const { status, body, retryAfterSeconds } = await apiFetch(
|
|
57
66
|
resolved,
|
|
58
|
-
"/api/v1/stacks/queue"
|
|
67
|
+
id ? `/api/v1/stacks/queue?stackId=${encodeURIComponent(id)}` : "/api/v1/stacks/queue"
|
|
59
68
|
);
|
|
60
69
|
if (status === 404) {
|
|
61
70
|
throw new CommandError2(
|
|
62
|
-
"Merge queue
|
|
71
|
+
"Merge queue route returned 404. Deploy the API."
|
|
63
72
|
);
|
|
64
73
|
}
|
|
65
74
|
if (status === 429) {
|
|
@@ -81,11 +90,12 @@ async function queueStatus(stackId, cfg) {
|
|
|
81
90
|
`Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
|
|
82
91
|
);
|
|
83
92
|
}
|
|
84
|
-
const id = stackId?.trim();
|
|
85
93
|
if (id) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
const matchingEntries = entries.filter(
|
|
95
|
+
(candidate) => candidate?.stackId?.toLowerCase() === id.toLowerCase()
|
|
96
|
+
);
|
|
97
|
+
if (matchingEntries.length === 0) {
|
|
98
|
+
const message = `Queue entry not found for stack: ${id}`;
|
|
89
99
|
return {
|
|
90
100
|
summary: message,
|
|
91
101
|
data: {
|
|
@@ -99,12 +109,12 @@ async function queueStatus(stackId, cfg) {
|
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
111
|
return {
|
|
102
|
-
summary: entrySummary(
|
|
103
|
-
data: {
|
|
112
|
+
summary: matchingEntries.map(entrySummary).join("\n"),
|
|
113
|
+
data: { entries: matchingEntries }
|
|
104
114
|
};
|
|
105
115
|
}
|
|
106
116
|
return {
|
|
107
|
-
summary: entries.length === 0 ? "No
|
|
117
|
+
summary: entries.length === 0 ? "No merge queue entries (live or bounced)" : entries.map(entrySummary).join("\n"),
|
|
108
118
|
data: { entries }
|
|
109
119
|
};
|
|
110
120
|
} catch (err) {
|
|
@@ -554,6 +564,7 @@ async function settingsGet(cfg) {
|
|
|
554
564
|
// src/tools/settings-set.ts
|
|
555
565
|
import {
|
|
556
566
|
CommandError as CommandError8,
|
|
567
|
+
BEARER_SETTINGS,
|
|
557
568
|
SETTINGS_WRITABLE_KEYS,
|
|
558
569
|
patchSettings
|
|
559
570
|
} from "mergestorm/client";
|
|
@@ -571,10 +582,17 @@ function settingsPatchFromArgs(args) {
|
|
|
571
582
|
for (const key of SETTINGS_WRITABLE_KEYS) {
|
|
572
583
|
const value = args[key];
|
|
573
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
|
+
}
|
|
574
592
|
if (typeof value !== "boolean") {
|
|
575
593
|
throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
|
|
576
594
|
}
|
|
577
|
-
patch[key]
|
|
595
|
+
Object.assign(patch, { [key]: value });
|
|
578
596
|
}
|
|
579
597
|
if (Object.keys(patch).length === 0) {
|
|
580
598
|
throw new CommandError8(
|
|
@@ -653,9 +671,9 @@ var stackAdoptSchema = {
|
|
|
653
671
|
owner: z.string().trim().min(1),
|
|
654
672
|
repo: z.string().trim().min(1),
|
|
655
673
|
pr_number: z.number().int().min(1),
|
|
656
|
-
auto_land: z.
|
|
674
|
+
auto_land: z.boolean().optional(),
|
|
657
675
|
auto_review: z.boolean().nullable().optional(),
|
|
658
|
-
auto_patch: z.
|
|
676
|
+
auto_patch: z.boolean().nullable().optional()
|
|
659
677
|
};
|
|
660
678
|
var inputSchema = z.object(stackAdoptSchema);
|
|
661
679
|
async function stackAdopt(input, cfg) {
|
|
@@ -694,10 +712,67 @@ async function stackAdopt(input, cfg) {
|
|
|
694
712
|
}
|
|
695
713
|
|
|
696
714
|
// src/tools/stack-list.ts
|
|
697
|
-
import {
|
|
715
|
+
import {
|
|
716
|
+
isCommandErrorCode as isCommandErrorCode4,
|
|
717
|
+
listMergeQueueEntries,
|
|
718
|
+
listStacks
|
|
719
|
+
} from "mergestorm/client";
|
|
698
720
|
|
|
699
721
|
// src/tools/stack-summary.ts
|
|
700
|
-
|
|
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 = []) {
|
|
701
776
|
const count = stack.layers.length;
|
|
702
777
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
703
778
|
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
@@ -712,15 +787,17 @@ function stackSummary(stack) {
|
|
|
712
787
|
if (stack.cycloneOwnerMatch) {
|
|
713
788
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
714
789
|
}
|
|
715
|
-
|
|
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}` : ""}`;
|
|
716
792
|
}
|
|
717
793
|
|
|
718
794
|
// src/tools/stack-list.ts
|
|
719
795
|
async function stackList(cfg) {
|
|
720
796
|
try {
|
|
721
797
|
const stacks = await listStacks(cfg);
|
|
798
|
+
const entries = await listMergeQueueEntries(cfg);
|
|
722
799
|
return {
|
|
723
|
-
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"),
|
|
724
801
|
data: { stacks }
|
|
725
802
|
};
|
|
726
803
|
} catch (err) {
|
|
@@ -741,8 +818,62 @@ async function stackList(cfg) {
|
|
|
741
818
|
}
|
|
742
819
|
}
|
|
743
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
|
+
|
|
744
871
|
// src/tools/stack-status.ts
|
|
745
|
-
import {
|
|
872
|
+
import {
|
|
873
|
+
getEnrichedStack,
|
|
874
|
+
listMergeQueueEntries as listMergeQueueEntries2,
|
|
875
|
+
isCommandErrorCode as isCommandErrorCode5
|
|
876
|
+
} from "mergestorm/client";
|
|
746
877
|
async function stackStatus(stackId, cfg) {
|
|
747
878
|
try {
|
|
748
879
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
@@ -760,8 +891,9 @@ async function stackStatus(stackId, cfg) {
|
|
|
760
891
|
isError: true
|
|
761
892
|
};
|
|
762
893
|
}
|
|
894
|
+
const entries = await listMergeQueueEntries2(cfg);
|
|
763
895
|
return {
|
|
764
|
-
summary: stackSummary(stack),
|
|
896
|
+
summary: stackSummary(stack, entries),
|
|
765
897
|
data: { stack }
|
|
766
898
|
};
|
|
767
899
|
} catch (err) {
|
|
@@ -788,11 +920,11 @@ import {
|
|
|
788
920
|
apiBase,
|
|
789
921
|
configPath,
|
|
790
922
|
getMe as getMe2,
|
|
791
|
-
loadConfig as
|
|
923
|
+
loadConfig as loadConfig7,
|
|
792
924
|
resolveApiKey
|
|
793
925
|
} from "mergestorm/client";
|
|
794
926
|
async function whoami(cfg) {
|
|
795
|
-
const resolved = cfg ?? await
|
|
927
|
+
const resolved = cfg ?? await loadConfig7();
|
|
796
928
|
const key = resolveApiKey(resolved);
|
|
797
929
|
if (!key) {
|
|
798
930
|
throw new CommandError10(
|
|
@@ -834,7 +966,7 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
834
966
|
|
|
835
967
|
// src/server.ts
|
|
836
968
|
var MCP_SERVER_NAME = "mergestorm";
|
|
837
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
969
|
+
var MCP_SERVER_VERSION = "0.1.6";
|
|
838
970
|
function stripNulls(value) {
|
|
839
971
|
if (Array.isArray(value)) {
|
|
840
972
|
return value.map((item) => stripNulls(item));
|
|
@@ -905,7 +1037,7 @@ function createMergestormMcpServer() {
|
|
|
905
1037
|
title: "List reviews",
|
|
906
1038
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
907
1039
|
inputSchema: {
|
|
908
|
-
limit:
|
|
1040
|
+
limit: z3.number().int().min(1).max(50).optional()
|
|
909
1041
|
}
|
|
910
1042
|
},
|
|
911
1043
|
async ({ limit }) => {
|
|
@@ -922,7 +1054,7 @@ function createMergestormMcpServer() {
|
|
|
922
1054
|
title: "Get review",
|
|
923
1055
|
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
924
1056
|
inputSchema: {
|
|
925
|
-
job_id:
|
|
1057
|
+
job_id: z3.string().min(1)
|
|
926
1058
|
}
|
|
927
1059
|
},
|
|
928
1060
|
async ({ job_id }) => {
|
|
@@ -939,12 +1071,12 @@ function createMergestormMcpServer() {
|
|
|
939
1071
|
title: "Get GitHub PR Vortex review",
|
|
940
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).",
|
|
941
1073
|
inputSchema: {
|
|
942
|
-
owner:
|
|
943
|
-
repo:
|
|
944
|
-
pr_number:
|
|
945
|
-
after_sha:
|
|
946
|
-
pass:
|
|
947
|
-
after_pass:
|
|
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()
|
|
948
1080
|
}
|
|
949
1081
|
},
|
|
950
1082
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
@@ -967,18 +1099,18 @@ function createMergestormMcpServer() {
|
|
|
967
1099
|
title: "Submit review",
|
|
968
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.",
|
|
969
1101
|
inputSchema: {
|
|
970
|
-
cwd:
|
|
971
|
-
base:
|
|
972
|
-
head:
|
|
973
|
-
diff:
|
|
974
|
-
router:
|
|
975
|
-
specialists:
|
|
976
|
-
context:
|
|
977
|
-
context_files:
|
|
978
|
-
thread:
|
|
979
|
-
idempotency_key:
|
|
980
|
-
wait:
|
|
981
|
-
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()
|
|
982
1114
|
}
|
|
983
1115
|
},
|
|
984
1116
|
async (args) => {
|
|
@@ -995,8 +1127,8 @@ function createMergestormMcpServer() {
|
|
|
995
1127
|
title: "Wait for review",
|
|
996
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.",
|
|
997
1129
|
inputSchema: {
|
|
998
|
-
job_id:
|
|
999
|
-
timeout_s:
|
|
1130
|
+
job_id: z3.string().min(1),
|
|
1131
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
1000
1132
|
}
|
|
1001
1133
|
},
|
|
1002
1134
|
async ({ job_id, timeout_s }) => {
|
|
@@ -1013,13 +1145,13 @@ function createMergestormMcpServer() {
|
|
|
1013
1145
|
title: "Wait for GitHub PR Vortex review",
|
|
1014
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.",
|
|
1015
1147
|
inputSchema: {
|
|
1016
|
-
owner:
|
|
1017
|
-
repo:
|
|
1018
|
-
pr_number:
|
|
1019
|
-
after_sha:
|
|
1020
|
-
pass:
|
|
1021
|
-
after_pass:
|
|
1022
|
-
timeout_s:
|
|
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()
|
|
1023
1155
|
}
|
|
1024
1156
|
},
|
|
1025
1157
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
@@ -1044,7 +1176,7 @@ function createMergestormMcpServer() {
|
|
|
1044
1176
|
"stack_adopt",
|
|
1045
1177
|
{
|
|
1046
1178
|
title: "Adopt PR stack",
|
|
1047
|
-
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land
|
|
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.",
|
|
1048
1180
|
inputSchema: stackAdoptSchema
|
|
1049
1181
|
},
|
|
1050
1182
|
async (args) => {
|
|
@@ -1075,10 +1207,10 @@ function createMergestormMcpServer() {
|
|
|
1075
1207
|
title: "Set stack policy",
|
|
1076
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.",
|
|
1077
1209
|
inputSchema: {
|
|
1078
|
-
stack_id:
|
|
1079
|
-
auto_land:
|
|
1080
|
-
auto_review:
|
|
1081
|
-
auto_patch:
|
|
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()
|
|
1082
1214
|
}
|
|
1083
1215
|
},
|
|
1084
1216
|
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
@@ -1101,7 +1233,7 @@ function createMergestormMcpServer() {
|
|
|
1101
1233
|
title: "Get stack status",
|
|
1102
1234
|
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
1103
1235
|
inputSchema: {
|
|
1104
|
-
stack_id:
|
|
1236
|
+
stack_id: z3.string().min(1)
|
|
1105
1237
|
}
|
|
1106
1238
|
},
|
|
1107
1239
|
async ({ stack_id }) => {
|
|
@@ -1112,13 +1244,29 @@ function createMergestormMcpServer() {
|
|
|
1112
1244
|
}
|
|
1113
1245
|
}
|
|
1114
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
|
+
);
|
|
1115
1263
|
addTool(
|
|
1116
1264
|
"queue_status",
|
|
1117
1265
|
{
|
|
1118
1266
|
title: "Get merge queue status",
|
|
1119
|
-
description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
|
|
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.",
|
|
1120
1268
|
inputSchema: {
|
|
1121
|
-
stack_id:
|
|
1269
|
+
stack_id: z3.string().min(1).optional()
|
|
1122
1270
|
}
|
|
1123
1271
|
},
|
|
1124
1272
|
async ({ stack_id }) => {
|
|
@@ -1147,10 +1295,18 @@ function createMergestormMcpServer() {
|
|
|
1147
1295
|
"settings_set",
|
|
1148
1296
|
{
|
|
1149
1297
|
title: "Update settings",
|
|
1150
|
-
description: "Update writable Bearer /api/v1/settings
|
|
1151
|
-
inputSchema:
|
|
1152
|
-
|
|
1153
|
-
|
|
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.",
|
|
1299
|
+
inputSchema: {
|
|
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()
|
|
1309
|
+
}
|
|
1154
1310
|
},
|
|
1155
1311
|
async (args) => {
|
|
1156
1312
|
try {
|
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": {
|