mergestorm-mcp 0.1.5 → 0.1.7
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 +198 -72
- 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. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`.
|
|
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) {
|
|
@@ -469,17 +479,26 @@ async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opt
|
|
|
469
479
|
);
|
|
470
480
|
} catch (err) {
|
|
471
481
|
if (err instanceof PrReviewPollTimeoutError) {
|
|
472
|
-
const status =
|
|
482
|
+
const status = "in_progress";
|
|
483
|
+
const expectedSha = afterSha?.trim().toLowerCase() ?? "";
|
|
484
|
+
const observedSha = err.lastEnvelope?.head_sha?.trim().toLowerCase() ?? "";
|
|
485
|
+
const headShaMatches = Boolean(
|
|
486
|
+
observedSha && expectedSha.length >= 7 && (observedSha.startsWith(expectedSha) || expectedSha.startsWith(observedSha))
|
|
487
|
+
);
|
|
488
|
+
const findingCount = headShaMatches ? err.lastEnvelope?.finding_count ?? null : null;
|
|
473
489
|
return {
|
|
474
490
|
summary: prReviewSummary(target.owner, target.repo, target.prNumber, {
|
|
475
491
|
status,
|
|
476
492
|
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
477
|
-
finding_count:
|
|
493
|
+
finding_count: findingCount,
|
|
478
494
|
phase: err.lastEnvelope?.phase ?? null
|
|
479
495
|
}),
|
|
480
496
|
data: {
|
|
481
|
-
...err.lastEnvelope ?? {},
|
|
482
497
|
status,
|
|
498
|
+
head_sha: err.lastEnvelope?.head_sha ?? null,
|
|
499
|
+
pass: headShaMatches ? err.lastEnvelope?.pass ?? null : null,
|
|
500
|
+
finding_count: findingCount,
|
|
501
|
+
phase: err.lastEnvelope?.phase ?? null,
|
|
483
502
|
error: err.message
|
|
484
503
|
}
|
|
485
504
|
};
|
|
@@ -554,6 +573,7 @@ async function settingsGet(cfg) {
|
|
|
554
573
|
// src/tools/settings-set.ts
|
|
555
574
|
import {
|
|
556
575
|
CommandError as CommandError8,
|
|
576
|
+
BEARER_SETTINGS,
|
|
557
577
|
SETTINGS_WRITABLE_KEYS,
|
|
558
578
|
patchSettings
|
|
559
579
|
} from "mergestorm/client";
|
|
@@ -571,10 +591,17 @@ function settingsPatchFromArgs(args) {
|
|
|
571
591
|
for (const key of SETTINGS_WRITABLE_KEYS) {
|
|
572
592
|
const value = args[key];
|
|
573
593
|
if (value === void 0) continue;
|
|
594
|
+
const row = BEARER_SETTINGS.find((row2) => row2.key === key);
|
|
595
|
+
if ("kind" in row) {
|
|
596
|
+
const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
|
|
597
|
+
if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
|
|
598
|
+
Object.assign(patch, { [key]: value });
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
574
601
|
if (typeof value !== "boolean") {
|
|
575
602
|
throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
|
|
576
603
|
}
|
|
577
|
-
patch[key]
|
|
604
|
+
Object.assign(patch, { [key]: value });
|
|
578
605
|
}
|
|
579
606
|
if (Object.keys(patch).length === 0) {
|
|
580
607
|
throw new CommandError8(
|
|
@@ -653,9 +680,9 @@ var stackAdoptSchema = {
|
|
|
653
680
|
owner: z.string().trim().min(1),
|
|
654
681
|
repo: z.string().trim().min(1),
|
|
655
682
|
pr_number: z.number().int().min(1),
|
|
656
|
-
auto_land: z.
|
|
683
|
+
auto_land: z.boolean().optional(),
|
|
657
684
|
auto_review: z.boolean().nullable().optional(),
|
|
658
|
-
auto_patch: z.
|
|
685
|
+
auto_patch: z.boolean().nullable().optional()
|
|
659
686
|
};
|
|
660
687
|
var inputSchema = z.object(stackAdoptSchema);
|
|
661
688
|
async function stackAdopt(input, cfg) {
|
|
@@ -694,14 +721,19 @@ async function stackAdopt(input, cfg) {
|
|
|
694
721
|
}
|
|
695
722
|
|
|
696
723
|
// src/tools/stack-list.ts
|
|
697
|
-
import {
|
|
724
|
+
import {
|
|
725
|
+
isCommandErrorCode as isCommandErrorCode4,
|
|
726
|
+
listMergeQueueEntries,
|
|
727
|
+
listStacks
|
|
728
|
+
} from "mergestorm/client";
|
|
698
729
|
|
|
699
730
|
// src/tools/stack-summary.ts
|
|
700
|
-
|
|
731
|
+
import { stackBlockers, stackBlockersSummary } from "mergestorm/client";
|
|
732
|
+
function stackSummary(stack, entries = []) {
|
|
701
733
|
const count = stack.layers.length;
|
|
702
734
|
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
703
735
|
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
704
|
-
const currentState = states.length > 0 ? `
|
|
736
|
+
const currentState = states.length > 0 ? `restack: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
|
|
705
737
|
const overrides = [];
|
|
706
738
|
if (typeof stack.autoReviewOverride === "boolean") {
|
|
707
739
|
overrides.push(`auto-review ${stack.autoReviewOverride ? "on" : "off"}`);
|
|
@@ -712,15 +744,17 @@ function stackSummary(stack) {
|
|
|
712
744
|
if (stack.cycloneOwnerMatch) {
|
|
713
745
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
714
746
|
}
|
|
715
|
-
|
|
747
|
+
const { attention, issues } = stackBlockers(stack, entries);
|
|
748
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState} \xB7 auto-land ${stack.autoEnqueueWhenReady ? "on" : "off"}${overrides.map((label) => ` \xB7 ${label}`).join("")}${stackBlockersSummary(attention, issues)}`;
|
|
716
749
|
}
|
|
717
750
|
|
|
718
751
|
// src/tools/stack-list.ts
|
|
719
752
|
async function stackList(cfg) {
|
|
720
753
|
try {
|
|
721
754
|
const stacks = await listStacks(cfg);
|
|
755
|
+
const entries = await listMergeQueueEntries(cfg);
|
|
722
756
|
return {
|
|
723
|
-
summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack)).join("\n"),
|
|
757
|
+
summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack, entries)).join("\n"),
|
|
724
758
|
data: { stacks }
|
|
725
759
|
};
|
|
726
760
|
} catch (err) {
|
|
@@ -741,8 +775,63 @@ async function stackList(cfg) {
|
|
|
741
775
|
}
|
|
742
776
|
}
|
|
743
777
|
|
|
778
|
+
// src/tools/stack-wait.ts
|
|
779
|
+
import {
|
|
780
|
+
loadConfig as loadConfig6,
|
|
781
|
+
stackBlockersSummary as stackBlockersSummary2,
|
|
782
|
+
pollStackWatch,
|
|
783
|
+
StackWatchError,
|
|
784
|
+
StackWatchTimeoutError
|
|
785
|
+
} from "mergestorm/client";
|
|
786
|
+
import { z as z2 } from "zod";
|
|
787
|
+
var stackWaitSchema = {
|
|
788
|
+
stack_id: z2.string().trim().min(1),
|
|
789
|
+
timeout_s: z2.number().finite().min(0).max(45).default(45),
|
|
790
|
+
enrolled_head_sha: z2.string().nullable().optional(),
|
|
791
|
+
after_finished_at: z2.string().nullable().optional(),
|
|
792
|
+
bounce_id: z2.string().nullable().optional()
|
|
793
|
+
};
|
|
794
|
+
async function stackWait(input, cfg, deps = {}) {
|
|
795
|
+
const args = z2.object(stackWaitSchema).parse(input);
|
|
796
|
+
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
797
|
+
stackId: args.stack_id,
|
|
798
|
+
enrolledHeadSha: args.enrolled_head_sha ?? null,
|
|
799
|
+
...args.after_finished_at !== void 0 ? { afterFinishedAt: args.after_finished_at } : {},
|
|
800
|
+
...args.bounce_id !== void 0 ? { bounceId: args.bounce_id } : {}
|
|
801
|
+
} : void 0;
|
|
802
|
+
let envelope;
|
|
803
|
+
try {
|
|
804
|
+
envelope = await (deps.pollStackWatch ?? pollStackWatch)(cfg ?? await loadConfig6(), args.stack_id, {
|
|
805
|
+
timeoutMs: args.timeout_s * 1e3,
|
|
806
|
+
cursor,
|
|
807
|
+
signal: deps.signal
|
|
808
|
+
});
|
|
809
|
+
} catch (err) {
|
|
810
|
+
if (err instanceof StackWatchError) {
|
|
811
|
+
return {
|
|
812
|
+
summary: `Stack ${err.lastEnvelope.stackId} \xB7 ${err.lastEnvelope.status}${stackBlockersSummary2(err.lastEnvelope.blocker && err.lastEnvelope.prNumber != null ? { prNumber: err.lastEnvelope.prNumber, blocker: err.lastEnvelope.blocker } : null, err.lastEnvelope.issues)}${err.lastEnvelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
|
|
813
|
+
data: {
|
|
814
|
+
...err.lastEnvelope,
|
|
815
|
+
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
816
|
+
},
|
|
817
|
+
isError: true
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (!(err instanceof StackWatchTimeoutError)) throw err;
|
|
821
|
+
envelope = err.lastEnvelope;
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
summary: `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary2(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`,
|
|
825
|
+
data: envelope
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
744
829
|
// src/tools/stack-status.ts
|
|
745
|
-
import {
|
|
830
|
+
import {
|
|
831
|
+
getEnrichedStack,
|
|
832
|
+
listMergeQueueEntries as listMergeQueueEntries2,
|
|
833
|
+
isCommandErrorCode as isCommandErrorCode5
|
|
834
|
+
} from "mergestorm/client";
|
|
746
835
|
async function stackStatus(stackId, cfg) {
|
|
747
836
|
try {
|
|
748
837
|
const stack = await getEnrichedStack(stackId, cfg);
|
|
@@ -760,8 +849,9 @@ async function stackStatus(stackId, cfg) {
|
|
|
760
849
|
isError: true
|
|
761
850
|
};
|
|
762
851
|
}
|
|
852
|
+
const entries = await listMergeQueueEntries2(cfg);
|
|
763
853
|
return {
|
|
764
|
-
summary: stackSummary(stack),
|
|
854
|
+
summary: stackSummary(stack, entries),
|
|
765
855
|
data: { stack }
|
|
766
856
|
};
|
|
767
857
|
} catch (err) {
|
|
@@ -788,11 +878,11 @@ import {
|
|
|
788
878
|
apiBase,
|
|
789
879
|
configPath,
|
|
790
880
|
getMe as getMe2,
|
|
791
|
-
loadConfig as
|
|
881
|
+
loadConfig as loadConfig7,
|
|
792
882
|
resolveApiKey
|
|
793
883
|
} from "mergestorm/client";
|
|
794
884
|
async function whoami(cfg) {
|
|
795
|
-
const resolved = cfg ?? await
|
|
885
|
+
const resolved = cfg ?? await loadConfig7();
|
|
796
886
|
const key = resolveApiKey(resolved);
|
|
797
887
|
if (!key) {
|
|
798
888
|
throw new CommandError10(
|
|
@@ -831,10 +921,20 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
831
921
|
`- 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
922
|
"- Do not prefix that comment with cyclone-outcome: (reserved)."
|
|
833
923
|
].join("\n");
|
|
924
|
+
var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
925
|
+
"Stack-watch facts (mergestorm.stack_watch/v1):",
|
|
926
|
+
"- waiting with nonempty issues[] is not idle; in_progress can also carry issues[].",
|
|
927
|
+
"- assessment unavailable does not mean issue-free. timeout_s: 0 reads one enrich and one queue snapshot.",
|
|
928
|
+
"- Restack clean does not mean mergeable or CI-green.",
|
|
929
|
+
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
930
|
+
"- verifyHeadSha is a queue verification SHA, not the live PR head.",
|
|
931
|
+
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
932
|
+
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
933
|
+
].join("\n");
|
|
834
934
|
|
|
835
935
|
// src/server.ts
|
|
836
936
|
var MCP_SERVER_NAME = "mergestorm";
|
|
837
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
937
|
+
var MCP_SERVER_VERSION = "0.1.7";
|
|
838
938
|
function stripNulls(value) {
|
|
839
939
|
if (Array.isArray(value)) {
|
|
840
940
|
return value.map((item) => stripNulls(item));
|
|
@@ -868,7 +968,9 @@ function createMergestormMcpServer() {
|
|
|
868
968
|
name: MCP_SERVER_NAME,
|
|
869
969
|
version: MCP_SERVER_VERSION
|
|
870
970
|
},
|
|
871
|
-
{ instructions: MCP_PR_LOOP_INSTRUCTIONS
|
|
971
|
+
{ instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
|
|
972
|
+
|
|
973
|
+
${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
872
974
|
);
|
|
873
975
|
const addTool = server2.registerTool.bind(server2);
|
|
874
976
|
addTool(
|
|
@@ -905,7 +1007,7 @@ function createMergestormMcpServer() {
|
|
|
905
1007
|
title: "List reviews",
|
|
906
1008
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
907
1009
|
inputSchema: {
|
|
908
|
-
limit:
|
|
1010
|
+
limit: z3.number().int().min(1).max(50).optional()
|
|
909
1011
|
}
|
|
910
1012
|
},
|
|
911
1013
|
async ({ limit }) => {
|
|
@@ -922,7 +1024,7 @@ function createMergestormMcpServer() {
|
|
|
922
1024
|
title: "Get review",
|
|
923
1025
|
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
924
1026
|
inputSchema: {
|
|
925
|
-
job_id:
|
|
1027
|
+
job_id: z3.string().min(1)
|
|
926
1028
|
}
|
|
927
1029
|
},
|
|
928
1030
|
async ({ job_id }) => {
|
|
@@ -939,12 +1041,12 @@ function createMergestormMcpServer() {
|
|
|
939
1041
|
title: "Get GitHub PR Vortex review",
|
|
940
1042
|
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
1043
|
inputSchema: {
|
|
942
|
-
owner:
|
|
943
|
-
repo:
|
|
944
|
-
pr_number:
|
|
945
|
-
after_sha:
|
|
946
|
-
pass:
|
|
947
|
-
after_pass:
|
|
1044
|
+
owner: z3.string().min(1),
|
|
1045
|
+
repo: z3.string().min(1),
|
|
1046
|
+
pr_number: z3.number().int().min(1),
|
|
1047
|
+
after_sha: z3.string().optional(),
|
|
1048
|
+
pass: z3.number().int().min(1).optional(),
|
|
1049
|
+
after_pass: z3.number().int().min(1).optional()
|
|
948
1050
|
}
|
|
949
1051
|
},
|
|
950
1052
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
@@ -967,18 +1069,18 @@ function createMergestormMcpServer() {
|
|
|
967
1069
|
title: "Submit review",
|
|
968
1070
|
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
1071
|
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:
|
|
1072
|
+
cwd: z3.string().optional(),
|
|
1073
|
+
base: z3.string().optional(),
|
|
1074
|
+
head: z3.string().optional(),
|
|
1075
|
+
diff: z3.string().optional(),
|
|
1076
|
+
router: z3.enum(["off", "standard", "max", "manual"]).optional(),
|
|
1077
|
+
specialists: z3.array(z3.string().min(1)).max(5).optional(),
|
|
1078
|
+
context: z3.string().optional(),
|
|
1079
|
+
context_files: z3.array(z3.string().min(1)).max(10).optional(),
|
|
1080
|
+
thread: z3.string().min(1).max(120).optional(),
|
|
1081
|
+
idempotency_key: z3.string().min(1).max(128).optional(),
|
|
1082
|
+
wait: z3.boolean().optional(),
|
|
1083
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
982
1084
|
}
|
|
983
1085
|
},
|
|
984
1086
|
async (args) => {
|
|
@@ -995,8 +1097,8 @@ function createMergestormMcpServer() {
|
|
|
995
1097
|
title: "Wait for review",
|
|
996
1098
|
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
1099
|
inputSchema: {
|
|
998
|
-
job_id:
|
|
999
|
-
timeout_s:
|
|
1100
|
+
job_id: z3.string().min(1),
|
|
1101
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
1000
1102
|
}
|
|
1001
1103
|
},
|
|
1002
1104
|
async ({ job_id, timeout_s }) => {
|
|
@@ -1013,13 +1115,13 @@ function createMergestormMcpServer() {
|
|
|
1013
1115
|
title: "Wait for GitHub PR Vortex review",
|
|
1014
1116
|
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
1117
|
inputSchema: {
|
|
1016
|
-
owner:
|
|
1017
|
-
repo:
|
|
1018
|
-
pr_number:
|
|
1019
|
-
after_sha:
|
|
1020
|
-
pass:
|
|
1021
|
-
after_pass:
|
|
1022
|
-
timeout_s:
|
|
1118
|
+
owner: z3.string().min(1),
|
|
1119
|
+
repo: z3.string().min(1),
|
|
1120
|
+
pr_number: z3.number().int().min(1),
|
|
1121
|
+
after_sha: z3.string().optional(),
|
|
1122
|
+
pass: z3.number().int().min(1).optional(),
|
|
1123
|
+
after_pass: z3.number().int().min(1).optional(),
|
|
1124
|
+
timeout_s: z3.number().positive().max(300).optional()
|
|
1023
1125
|
}
|
|
1024
1126
|
},
|
|
1025
1127
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
@@ -1044,7 +1146,7 @@ function createMergestormMcpServer() {
|
|
|
1044
1146
|
"stack_adopt",
|
|
1045
1147
|
{
|
|
1046
1148
|
title: "Adopt PR stack",
|
|
1047
|
-
description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land
|
|
1149
|
+
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
1150
|
inputSchema: stackAdoptSchema
|
|
1049
1151
|
},
|
|
1050
1152
|
async (args) => {
|
|
@@ -1075,10 +1177,10 @@ function createMergestormMcpServer() {
|
|
|
1075
1177
|
title: "Set stack policy",
|
|
1076
1178
|
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
1179
|
inputSchema: {
|
|
1078
|
-
stack_id:
|
|
1079
|
-
auto_land:
|
|
1080
|
-
auto_review:
|
|
1081
|
-
auto_patch:
|
|
1180
|
+
stack_id: z3.string().min(1),
|
|
1181
|
+
auto_land: z3.boolean().optional(),
|
|
1182
|
+
auto_review: z3.boolean().nullable().optional(),
|
|
1183
|
+
auto_patch: z3.boolean().nullable().optional()
|
|
1082
1184
|
}
|
|
1083
1185
|
},
|
|
1084
1186
|
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
@@ -1101,7 +1203,7 @@ function createMergestormMcpServer() {
|
|
|
1101
1203
|
title: "Get stack status",
|
|
1102
1204
|
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
1103
1205
|
inputSchema: {
|
|
1104
|
-
stack_id:
|
|
1206
|
+
stack_id: z3.string().min(1)
|
|
1105
1207
|
}
|
|
1106
1208
|
},
|
|
1107
1209
|
async ({ stack_id }) => {
|
|
@@ -1112,13 +1214,29 @@ function createMergestormMcpServer() {
|
|
|
1112
1214
|
}
|
|
1113
1215
|
}
|
|
1114
1216
|
);
|
|
1217
|
+
addTool(
|
|
1218
|
+
"stack_wait",
|
|
1219
|
+
{
|
|
1220
|
+
title: "Wait for stack attention",
|
|
1221
|
+
description: "Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure. A timeout of failed means no assessment was produced. This tool is read-only.",
|
|
1222
|
+
inputSchema: stackWaitSchema
|
|
1223
|
+
},
|
|
1224
|
+
async (args, extra) => {
|
|
1225
|
+
try {
|
|
1226
|
+
const payload = await stackWait(args, void 0, { signal: extra.signal });
|
|
1227
|
+
return { ...ok(payload), structuredContent: payload.data };
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
return fail(err);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
);
|
|
1115
1233
|
addTool(
|
|
1116
1234
|
"queue_status",
|
|
1117
1235
|
{
|
|
1118
1236
|
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.",
|
|
1237
|
+
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
1238
|
inputSchema: {
|
|
1121
|
-
stack_id:
|
|
1239
|
+
stack_id: z3.string().min(1).optional()
|
|
1122
1240
|
}
|
|
1123
1241
|
},
|
|
1124
1242
|
async ({ stack_id }) => {
|
|
@@ -1147,10 +1265,18 @@ function createMergestormMcpServer() {
|
|
|
1147
1265
|
"settings_set",
|
|
1148
1266
|
{
|
|
1149
1267
|
title: "Update settings",
|
|
1150
|
-
description: "Update writable Bearer /api/v1/settings
|
|
1151
|
-
inputSchema:
|
|
1152
|
-
|
|
1153
|
-
|
|
1268
|
+
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.",
|
|
1269
|
+
inputSchema: {
|
|
1270
|
+
...Object.fromEntries(
|
|
1271
|
+
BEARER_SETTINGS2.map((row) => [
|
|
1272
|
+
row.key,
|
|
1273
|
+
("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : z3.enum(row.values) : z3.boolean()).optional()
|
|
1274
|
+
])
|
|
1275
|
+
),
|
|
1276
|
+
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
1277
|
+
cyclone_connected: z3.unknown().optional(),
|
|
1278
|
+
github_connected: z3.unknown().optional()
|
|
1279
|
+
}
|
|
1154
1280
|
},
|
|
1155
1281
|
async (args) => {
|
|
1156
1282
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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.20",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|