mergestorm-mcp 0.1.9 → 0.2.0
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 +6 -2
- package/dist/index.js +250 -214
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# mergestorm-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
|
|
4
|
+
|
|
5
|
+
Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `review_submit`, `stack_adopt`, `stack_set`, and `settings_set` carry `readOnlyHint: false` and `destructiveHint: false`.
|
|
6
|
+
|
|
7
|
+
`review_wait({ job_id, timeout_s? })` reads a local review job by the `job_id` that `review_submit` returned. `review_wait_pr({ owner, repo, pr_number, after_sha?, pass?, after_pass?, timeout_s? })` reads the Vortex pass on a GitHub PR. Both poll one 45s slice by default; `timeout_s: 0` reads once without polling.
|
|
4
8
|
|
|
5
9
|
The stack tools expose reads plus one scoped policy write:
|
|
6
10
|
|
|
@@ -8,7 +12,7 @@ The stack tools expose reads plus one scoped policy write:
|
|
|
8
12
|
- `stack_list` returns the current API key owner's registered stacks.
|
|
9
13
|
- `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
14
|
- `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, plus `attention`, `issues`, and `currentCandidate` from the same blocker rules as `stack_wait`, read against that stack's own merge queue. It returns a structured `stack_not_found` or `rate_limited` error.
|
|
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`.
|
|
15
|
+
- `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`. While Cyclone or Vortex is still working on the blocked PR, `stack_wait` and `stack_status` return `in_progress` instead of `attention`, with `busy[]` naming the PR, blocker, and agent, and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and busy flags. A nonempty `busy[]` means wait. A queued Vortex run holds for at most 15 minutes, `seam_pending` alone does not hold, and reviewing or patching holds until the run finishes or its lease or check run is cleared. A hold can hide a blocker no agent clears (such as Draft); if it looks stuck, refresh `stack_status` and tell the human.
|
|
12
16
|
|
|
13
17
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
14
18
|
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,33 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
8
8
|
import { z as z3 } from "zod";
|
|
9
9
|
import { CommandError as CommandError11, BEARER_SETTINGS as BEARER_SETTINGS2 } from "mergestorm/client";
|
|
10
10
|
|
|
11
|
+
// src/brand-icons.ts
|
|
12
|
+
var MARK_RECTS = [
|
|
13
|
+
{ x: 103.685, y: 79.639, width: 336.635, height: 32.005 },
|
|
14
|
+
{ x: 71.68, y: 143.816, width: 288.545, height: 32.005 },
|
|
15
|
+
{ x: 103.685, y: 207.993, width: 192.363, height: 32.005 },
|
|
16
|
+
{ x: 151.775, y: 272.002, width: 176.277, height: 32.005 },
|
|
17
|
+
{ x: 215.952, y: 336.179, width: 96.182, height: 32.005 },
|
|
18
|
+
{ x: 264.043, y: 400.356, width: 32.005, height: 32.005 }
|
|
19
|
+
];
|
|
20
|
+
var SITE = "https://mergestorm.ai";
|
|
21
|
+
function markSvg(fill) {
|
|
22
|
+
const rects = MARK_RECTS.map(
|
|
23
|
+
(r) => `<rect x="${r.x}" y="${r.y}" width="${r.width}" height="${r.height}" fill="${fill}"/>`
|
|
24
|
+
).join("");
|
|
25
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="64 64 384 384">${rects}</svg>`;
|
|
26
|
+
}
|
|
27
|
+
function svgDataUri(svg) {
|
|
28
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg, "utf8").toString("base64")}`;
|
|
29
|
+
}
|
|
30
|
+
var MCP_SERVER_ICONS = [
|
|
31
|
+
{ src: svgDataUri(markSvg("#000000")), mimeType: "image/svg+xml", sizes: ["any"], theme: "light" },
|
|
32
|
+
{ src: svgDataUri(markSvg("#ffffff")), mimeType: "image/svg+xml", sizes: ["any"], theme: "dark" },
|
|
33
|
+
{ src: `${SITE}/images/android-chrome-192x192.png`, mimeType: "image/png", sizes: ["192x192"] },
|
|
34
|
+
{ src: `${SITE}/images/android-chrome-512x512.png`, mimeType: "image/png", sizes: ["512x512"] }
|
|
35
|
+
];
|
|
36
|
+
var MCP_WEBSITE_URL = SITE;
|
|
37
|
+
|
|
11
38
|
// src/tools/credits.ts
|
|
12
39
|
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
13
40
|
async function credits(cfg) {
|
|
@@ -135,95 +162,43 @@ async function queueStatus(stackId, cfg) {
|
|
|
135
162
|
}
|
|
136
163
|
}
|
|
137
164
|
|
|
138
|
-
// src/tools/review-
|
|
139
|
-
import {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
function prReviewTarget(owner, repo, prNumber) {
|
|
145
|
-
const cleanOwner = owner.trim();
|
|
146
|
-
const cleanRepo = repo.trim();
|
|
147
|
-
if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
|
|
148
|
-
throw new CommandError3(
|
|
149
|
-
"owner, repo, and a positive pr_number are required",
|
|
150
|
-
2,
|
|
151
|
-
"usage"
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
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
|
-
}
|
|
168
|
-
function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
169
|
-
return {
|
|
170
|
-
summary: prReviewSummary(owner, repo, prNumber, envelope),
|
|
171
|
-
data: { ...envelope }
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
175
|
-
if (!isCommandErrorCode2(err, "rate_limited")) return null;
|
|
165
|
+
// src/tools/review-list.ts
|
|
166
|
+
import { listJobs, toReviewJobEnvelope } from "mergestorm/client";
|
|
167
|
+
async function reviewList(limit = 10, cfg) {
|
|
168
|
+
const items = await listJobs(limit, cfg);
|
|
169
|
+
const envelopes = items.map((row) => toReviewJobEnvelope(row));
|
|
170
|
+
const n = envelopes.length;
|
|
176
171
|
return {
|
|
177
|
-
summary: `${
|
|
178
|
-
data: {
|
|
179
|
-
status: "rate_limited",
|
|
180
|
-
error: err.message,
|
|
181
|
-
retry_after_seconds: err.retryAfterSeconds
|
|
182
|
-
}
|
|
172
|
+
summary: n === 0 ? "No review jobs" : `${n} review job${n === 1 ? "" : "s"}`,
|
|
173
|
+
data: { items: envelopes }
|
|
183
174
|
};
|
|
184
175
|
}
|
|
185
|
-
async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
186
|
-
const target = prReviewTarget(owner, repo, prNumber);
|
|
187
|
-
try {
|
|
188
|
-
const envelope = await getPrVortexReview(
|
|
189
|
-
target.owner,
|
|
190
|
-
target.repo,
|
|
191
|
-
target.prNumber,
|
|
192
|
-
cfg,
|
|
193
|
-
{ afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
|
|
194
|
-
);
|
|
195
|
-
return payloadFromPrReview(
|
|
196
|
-
target.owner,
|
|
197
|
-
target.repo,
|
|
198
|
-
target.prNumber,
|
|
199
|
-
envelope
|
|
200
|
-
);
|
|
201
|
-
} catch (err) {
|
|
202
|
-
const rateLimited = payloadFromPrRateLimit(
|
|
203
|
-
err,
|
|
204
|
-
target.owner,
|
|
205
|
-
target.repo,
|
|
206
|
-
target.prNumber
|
|
207
|
-
);
|
|
208
|
-
if (rateLimited) return rateLimited;
|
|
209
|
-
throw err;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
176
|
|
|
213
|
-
// src/tools/review-
|
|
177
|
+
// src/tools/review-submit.ts
|
|
178
|
+
import { createHash } from "node:crypto";
|
|
179
|
+
import { realpath } from "node:fs/promises";
|
|
180
|
+
import { relative, resolve, sep } from "node:path";
|
|
214
181
|
import {
|
|
215
|
-
CommandError as
|
|
216
|
-
|
|
217
|
-
|
|
182
|
+
CommandError as CommandError3,
|
|
183
|
+
REVIEW_THREAD_SLUG_RE,
|
|
184
|
+
ReviewPollTimeoutError,
|
|
185
|
+
collectReviewInput,
|
|
186
|
+
discoverTrunk,
|
|
187
|
+
formatReviewSubmitError,
|
|
188
|
+
loadConfig as loadConfig3,
|
|
189
|
+
loadReviewContext,
|
|
190
|
+
pollReview,
|
|
191
|
+
rateLimitedMessage as rateLimitedMessage2,
|
|
192
|
+
submitReview
|
|
218
193
|
} from "mergestorm/client";
|
|
219
194
|
|
|
220
195
|
// src/tools/envelope.ts
|
|
221
196
|
import {
|
|
222
|
-
isCommandErrorCode as
|
|
223
|
-
toReviewJobEnvelope
|
|
197
|
+
isCommandErrorCode as isCommandErrorCode2,
|
|
198
|
+
toReviewJobEnvelope as toReviewJobEnvelope2
|
|
224
199
|
} from "mergestorm/client";
|
|
225
200
|
function payloadFromRow(row, fallback = {}) {
|
|
226
|
-
const envelope =
|
|
201
|
+
const envelope = toReviewJobEnvelope2(row, fallback);
|
|
227
202
|
const bits = [envelope.status];
|
|
228
203
|
if (envelope.verdict) bits.push(envelope.verdict);
|
|
229
204
|
return {
|
|
@@ -232,7 +207,7 @@ function payloadFromRow(row, fallback = {}) {
|
|
|
232
207
|
};
|
|
233
208
|
}
|
|
234
209
|
function payloadFromRateLimit(err, jobId) {
|
|
235
|
-
if (!
|
|
210
|
+
if (!isCommandErrorCode2(err, "rate_limited")) return null;
|
|
236
211
|
return payloadFromRow(null, {
|
|
237
212
|
jobId,
|
|
238
213
|
status: "rate_limited",
|
|
@@ -241,57 +216,7 @@ function payloadFromRateLimit(err, jobId) {
|
|
|
241
216
|
});
|
|
242
217
|
}
|
|
243
218
|
|
|
244
|
-
// src/tools/review-get.ts
|
|
245
|
-
async function reviewGet(jobId, cfg) {
|
|
246
|
-
let row;
|
|
247
|
-
try {
|
|
248
|
-
row = await getReview(jobId, cfg);
|
|
249
|
-
} catch (err) {
|
|
250
|
-
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
251
|
-
if (rateLimited) return rateLimited;
|
|
252
|
-
throw err;
|
|
253
|
-
}
|
|
254
|
-
if (!row || typeof row !== "object" || typeof row.status !== "string") {
|
|
255
|
-
throw new CommandError4(`Unexpected response for review ${jobId}.`);
|
|
256
|
-
}
|
|
257
|
-
const envelope = toReviewJobEnvelope2(row, { jobId });
|
|
258
|
-
const bits = [envelope.status];
|
|
259
|
-
if (envelope.verdict) bits.push(envelope.verdict);
|
|
260
|
-
return {
|
|
261
|
-
summary: `${envelope.job_id ?? jobId} \xB7 ${bits.join(" \xB7 ")}`,
|
|
262
|
-
data: envelope
|
|
263
|
-
};
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// src/tools/review-list.ts
|
|
267
|
-
import { listJobs, toReviewJobEnvelope as toReviewJobEnvelope3 } from "mergestorm/client";
|
|
268
|
-
async function reviewList(limit = 10, cfg) {
|
|
269
|
-
const items = await listJobs(limit, cfg);
|
|
270
|
-
const envelopes = items.map((row) => toReviewJobEnvelope3(row));
|
|
271
|
-
const n = envelopes.length;
|
|
272
|
-
return {
|
|
273
|
-
summary: n === 0 ? "No review jobs" : `${n} review job${n === 1 ? "" : "s"}`,
|
|
274
|
-
data: { items: envelopes }
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
|
|
278
219
|
// src/tools/review-submit.ts
|
|
279
|
-
import { createHash } from "node:crypto";
|
|
280
|
-
import { realpath } from "node:fs/promises";
|
|
281
|
-
import { relative, resolve, sep } from "node:path";
|
|
282
|
-
import {
|
|
283
|
-
CommandError as CommandError5,
|
|
284
|
-
REVIEW_THREAD_SLUG_RE,
|
|
285
|
-
ReviewPollTimeoutError,
|
|
286
|
-
collectReviewInput,
|
|
287
|
-
discoverTrunk,
|
|
288
|
-
formatReviewSubmitError,
|
|
289
|
-
loadConfig as loadConfig3,
|
|
290
|
-
loadReviewContext,
|
|
291
|
-
pollReview,
|
|
292
|
-
rateLimitedMessage as rateLimitedMessage2,
|
|
293
|
-
submitReview
|
|
294
|
-
} from "mergestorm/client";
|
|
295
220
|
var MCP_WAIT_CEILING_S = 300;
|
|
296
221
|
var MCP_PR_WAIT_DEFAULT_S = 45;
|
|
297
222
|
var PINNABLE_SPECIALISTS = [
|
|
@@ -309,7 +234,7 @@ function assertPinnableSpecialists(specialists) {
|
|
|
309
234
|
(id) => !PINNABLE_SPECIALISTS.includes(id)
|
|
310
235
|
);
|
|
311
236
|
if (unknown.length === 0) return;
|
|
312
|
-
throw new
|
|
237
|
+
throw new CommandError3(
|
|
313
238
|
`Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
|
|
314
239
|
2,
|
|
315
240
|
"usage"
|
|
@@ -321,7 +246,7 @@ function mcpSandboxRoot() {
|
|
|
321
246
|
function waitTimeoutMs(timeout_s, defaultSeconds = MCP_PR_WAIT_DEFAULT_S) {
|
|
322
247
|
const seconds = timeout_s ?? defaultSeconds;
|
|
323
248
|
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
324
|
-
throw new
|
|
249
|
+
throw new CommandError3("timeout_s must be a positive number of seconds", 2, "usage");
|
|
325
250
|
}
|
|
326
251
|
return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
|
|
327
252
|
}
|
|
@@ -354,11 +279,11 @@ async function resolveInput(input) {
|
|
|
354
279
|
]);
|
|
355
280
|
const relToSandbox = relative(sandboxReal, cwdReal);
|
|
356
281
|
if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
|
|
357
|
-
throw new
|
|
282
|
+
throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
|
|
358
283
|
}
|
|
359
284
|
} catch (err) {
|
|
360
|
-
if (err instanceof
|
|
361
|
-
throw new
|
|
285
|
+
if (err instanceof CommandError3) throw err;
|
|
286
|
+
throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
|
|
362
287
|
}
|
|
363
288
|
const base = input.base?.trim() || discoverTrunk(cwd);
|
|
364
289
|
return collectReviewInput(base, head, cwd, sandboxRoot);
|
|
@@ -369,10 +294,10 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
369
294
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
370
295
|
assertPinnableSpecialists(input.specialists);
|
|
371
296
|
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
372
|
-
throw new
|
|
297
|
+
throw new CommandError3("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
|
|
373
298
|
}
|
|
374
299
|
if (input.context === "-") {
|
|
375
|
-
throw new
|
|
300
|
+
throw new CommandError3(
|
|
376
301
|
'context "-" (stdin) is not available over MCP; pass the text directly',
|
|
377
302
|
2,
|
|
378
303
|
"usage"
|
|
@@ -422,7 +347,7 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
422
347
|
};
|
|
423
348
|
}
|
|
424
349
|
if (!jobId) {
|
|
425
|
-
throw new
|
|
350
|
+
throw new CommandError3("Review API returned no job_id");
|
|
426
351
|
}
|
|
427
352
|
if (!wait) {
|
|
428
353
|
return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
|
|
@@ -454,9 +379,93 @@ import {
|
|
|
454
379
|
loadConfig as loadConfig4,
|
|
455
380
|
pollPrVortexReview
|
|
456
381
|
} from "mergestorm/client";
|
|
382
|
+
|
|
383
|
+
// src/tools/review-get-pr.ts
|
|
384
|
+
import {
|
|
385
|
+
CommandError as CommandError4,
|
|
386
|
+
getPrVortexReview,
|
|
387
|
+
isCommandErrorCode as isCommandErrorCode3
|
|
388
|
+
} from "mergestorm/client";
|
|
389
|
+
function prReviewTarget(owner, repo, prNumber) {
|
|
390
|
+
const cleanOwner = owner.trim();
|
|
391
|
+
const cleanRepo = repo.trim();
|
|
392
|
+
if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
|
|
393
|
+
throw new CommandError4(
|
|
394
|
+
"owner, repo, and a positive pr_number are required",
|
|
395
|
+
2,
|
|
396
|
+
"usage"
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
400
|
+
}
|
|
401
|
+
function prReviewSummary(owner, repo, prNumber, envelope) {
|
|
402
|
+
const bits = [`${owner}/${repo}#${prNumber}`, envelope.status];
|
|
403
|
+
const sha = envelope.head_sha?.trim();
|
|
404
|
+
if (sha) bits.push(`sha ${sha.slice(0, 7)}`);
|
|
405
|
+
if (typeof envelope.finding_count === "number") {
|
|
406
|
+
bits.push(
|
|
407
|
+
`${envelope.finding_count} finding${envelope.finding_count === 1 ? "" : "s"}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (envelope.phase) bits.push(String(envelope.phase));
|
|
411
|
+
return bits.join(" \xB7 ");
|
|
412
|
+
}
|
|
413
|
+
function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
414
|
+
return {
|
|
415
|
+
summary: prReviewSummary(owner, repo, prNumber, envelope),
|
|
416
|
+
data: { ...envelope }
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
420
|
+
if (!isCommandErrorCode3(err, "rate_limited")) return null;
|
|
421
|
+
return {
|
|
422
|
+
summary: `${owner}/${repo}#${prNumber} \xB7 rate_limited`,
|
|
423
|
+
data: {
|
|
424
|
+
status: "rate_limited",
|
|
425
|
+
error: err.message,
|
|
426
|
+
retry_after_seconds: err.retryAfterSeconds
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
async function reviewGetPr(owner, repo, prNumber, cfg, opts = {}) {
|
|
431
|
+
const target = prReviewTarget(owner, repo, prNumber);
|
|
432
|
+
try {
|
|
433
|
+
const envelope = await getPrVortexReview(
|
|
434
|
+
target.owner,
|
|
435
|
+
target.repo,
|
|
436
|
+
target.prNumber,
|
|
437
|
+
cfg,
|
|
438
|
+
{ afterSha: opts.afterSha, pass: opts.pass, afterPass: opts.afterPass }
|
|
439
|
+
);
|
|
440
|
+
return payloadFromPrReview(
|
|
441
|
+
target.owner,
|
|
442
|
+
target.repo,
|
|
443
|
+
target.prNumber,
|
|
444
|
+
envelope
|
|
445
|
+
);
|
|
446
|
+
} catch (err) {
|
|
447
|
+
const rateLimited = payloadFromPrRateLimit(
|
|
448
|
+
err,
|
|
449
|
+
target.owner,
|
|
450
|
+
target.repo,
|
|
451
|
+
target.prNumber
|
|
452
|
+
);
|
|
453
|
+
if (rateLimited) return rateLimited;
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/tools/review-wait-pr.ts
|
|
457
459
|
async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
458
460
|
const target = prReviewTarget(owner, repo, prNumber);
|
|
459
461
|
const resolved = cfg ?? await loadConfig4();
|
|
462
|
+
if (timeout_s === 0) {
|
|
463
|
+
return reviewGetPr(target.owner, target.repo, target.prNumber, resolved, {
|
|
464
|
+
afterSha,
|
|
465
|
+
pass: opts.pass,
|
|
466
|
+
afterPass: opts.afterPass
|
|
467
|
+
});
|
|
468
|
+
}
|
|
460
469
|
try {
|
|
461
470
|
const envelope = await pollPrVortexReview(
|
|
462
471
|
resolved,
|
|
@@ -521,12 +530,42 @@ import {
|
|
|
521
530
|
loadConfig as loadConfig5,
|
|
522
531
|
pollReview as pollReview2
|
|
523
532
|
} from "mergestorm/client";
|
|
533
|
+
|
|
534
|
+
// src/tools/review-get.ts
|
|
535
|
+
import {
|
|
536
|
+
CommandError as CommandError5,
|
|
537
|
+
getReview,
|
|
538
|
+
toReviewJobEnvelope as toReviewJobEnvelope3
|
|
539
|
+
} from "mergestorm/client";
|
|
540
|
+
async function reviewGet(jobId, cfg) {
|
|
541
|
+
let row;
|
|
542
|
+
try {
|
|
543
|
+
row = await getReview(jobId, cfg);
|
|
544
|
+
} catch (err) {
|
|
545
|
+
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
546
|
+
if (rateLimited) return rateLimited;
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
549
|
+
if (!row || typeof row !== "object" || typeof row.status !== "string") {
|
|
550
|
+
throw new CommandError5(`Unexpected response for review ${jobId}.`);
|
|
551
|
+
}
|
|
552
|
+
const envelope = toReviewJobEnvelope3(row, { jobId });
|
|
553
|
+
const bits = [envelope.status];
|
|
554
|
+
if (envelope.verdict) bits.push(envelope.verdict);
|
|
555
|
+
return {
|
|
556
|
+
summary: `${envelope.job_id ?? jobId} \xB7 ${bits.join(" \xB7 ")}`,
|
|
557
|
+
data: envelope
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// src/tools/review-wait.ts
|
|
524
562
|
async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
525
563
|
const resolved = cfg ?? await loadConfig5();
|
|
526
564
|
const id = jobId.trim();
|
|
527
565
|
if (!id) {
|
|
528
566
|
throw new CommandError6("job_id is required", 2, "usage");
|
|
529
567
|
}
|
|
568
|
+
if (timeout_s === 0) return reviewGet(id, resolved);
|
|
530
569
|
try {
|
|
531
570
|
const row = await pollReview2(resolved, id, {
|
|
532
571
|
timeoutMs: waitTimeoutMs(timeout_s),
|
|
@@ -592,6 +631,17 @@ function settingsPatchFromArgs(args) {
|
|
|
592
631
|
const value = args[key];
|
|
593
632
|
if (value === void 0) continue;
|
|
594
633
|
const row = BEARER_SETTINGS.find((row2) => row2.key === key);
|
|
634
|
+
if ("kind" in row && row.kind === "seconds") {
|
|
635
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < row.min || value > row.max) {
|
|
636
|
+
throw new CommandError8(
|
|
637
|
+
`${key} takes a whole number of seconds from ${row.min} through ${row.max}.`,
|
|
638
|
+
2,
|
|
639
|
+
"usage"
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
Object.assign(patch, { [key]: value });
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
595
645
|
if ("kind" in row) {
|
|
596
646
|
const valid = row.kind === "logins" ? Array.isArray(value) && value.every((login) => typeof login === "string") : typeof value === "string" && row.values.includes(value);
|
|
597
647
|
if (!valid) throw new CommandError8(`Invalid value for ${key}.`, 2, "usage");
|
|
@@ -744,8 +794,8 @@ function stackSummary(stack, entries = []) {
|
|
|
744
794
|
if (stack.cycloneOwnerMatch) {
|
|
745
795
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
746
796
|
}
|
|
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)}`;
|
|
797
|
+
const { attention, issues, busy } = stackBlockers(stack, entries);
|
|
798
|
+
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, busy)}`;
|
|
749
799
|
}
|
|
750
800
|
|
|
751
801
|
// src/tools/stack-list.ts
|
|
@@ -791,6 +841,17 @@ var stackWaitSchema = {
|
|
|
791
841
|
after_finished_at: z2.string().nullable().optional(),
|
|
792
842
|
bounce_id: z2.string().nullable().optional()
|
|
793
843
|
};
|
|
844
|
+
function stackWaitSummary(envelope) {
|
|
845
|
+
const base = `Stack ${envelope.stackId} \xB7 ${envelope.status}${stackBlockersSummary2(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy)}${envelope.assessment === "unavailable" ? " \xB7 assessment unavailable" : ""}`;
|
|
846
|
+
if (envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
847
|
+
const selectors = [
|
|
848
|
+
`stack_id ${JSON.stringify(envelope.cursor.stackId)}`,
|
|
849
|
+
`enrolled_head_sha ${JSON.stringify(envelope.cursor.enrolledHeadSha)}`,
|
|
850
|
+
...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
|
|
851
|
+
...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
|
|
852
|
+
];
|
|
853
|
+
return `${base} \xB7 watch not finished: call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
854
|
+
}
|
|
794
855
|
async function stackWait(input, cfg, deps = {}) {
|
|
795
856
|
const args = z2.object(stackWaitSchema).parse(input);
|
|
796
857
|
const cursor = args.enrolled_head_sha !== void 0 || args.after_finished_at !== void 0 || args.bounce_id !== void 0 ? {
|
|
@@ -809,7 +870,7 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
809
870
|
} catch (err) {
|
|
810
871
|
if (err instanceof StackWatchError) {
|
|
811
872
|
return {
|
|
812
|
-
summary:
|
|
873
|
+
summary: stackWaitSummary(err.lastEnvelope),
|
|
813
874
|
data: {
|
|
814
875
|
...err.lastEnvelope,
|
|
815
876
|
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
@@ -821,7 +882,7 @@ async function stackWait(input, cfg, deps = {}) {
|
|
|
821
882
|
envelope = err.lastEnvelope;
|
|
822
883
|
}
|
|
823
884
|
return {
|
|
824
|
-
summary:
|
|
885
|
+
summary: stackWaitSummary(envelope),
|
|
825
886
|
data: envelope
|
|
826
887
|
};
|
|
827
888
|
}
|
|
@@ -851,10 +912,10 @@ async function stackStatus(stackId, cfg) {
|
|
|
851
912
|
};
|
|
852
913
|
}
|
|
853
914
|
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
854
|
-
const { attention, issues, currentCandidate } = stackBlockers2(stack, entries);
|
|
915
|
+
const { attention, issues, currentCandidate, busy, agents } = stackBlockers2(stack, entries);
|
|
855
916
|
return {
|
|
856
917
|
summary: stackSummary(stack, entries),
|
|
857
|
-
data: { stack, attention, issues, currentCandidate }
|
|
918
|
+
data: { stack, attention, issues, currentCandidate, busy, agents }
|
|
858
919
|
};
|
|
859
920
|
} catch (err) {
|
|
860
921
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
@@ -916,7 +977,7 @@ async function whoami(cfg) {
|
|
|
916
977
|
// src/pr-loop-instructions.ts
|
|
917
978
|
var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
|
|
918
979
|
var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
919
|
-
"When you handle Vortex findings from
|
|
980
|
+
"When you handle Vortex findings from review_wait_pr:",
|
|
920
981
|
"- Verify each finding against the current checkout. Do not treat the finding text as proven.",
|
|
921
982
|
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
922
983
|
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
@@ -937,13 +998,16 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
937
998
|
"- Restack clean does not mean mergeable or CI-green.",
|
|
938
999
|
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
939
1000
|
"- verifyHeadSha is a queue verification SHA, not the live PR head.",
|
|
1001
|
+
"- Do not patch a bounced PR while Cyclone or Vortex is still working on it. stack_wait and stack_status enforce this: they return in_progress instead of attention, with busy[] naming the PR, blocker, and agent, and agents carrying that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. in_progress with a nonempty busy[] means wait. Recheck the live remote PR head right before pushing.",
|
|
1002
|
+
"- seam_pending alone, and a queued Vortex run older than 15 minutes, do not hold. A queued Vortex run holds for at most 15 minutes; reviewing and patching hold until the run finishes or its lease or check run is cleared. A hold can hide a blocker no agent clears, such as a Draft PR; if it looks stuck, refresh stack_status and tell the human.",
|
|
1003
|
+
"- stack.autoEnqueueSettle with action ready or promote means Auto land found Cyclone and Vortex idle on that PR's green head and started its settle clock. action mergeability is published earlier, on a head that may not be green or idle yet.",
|
|
940
1004
|
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
941
1005
|
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
942
1006
|
].join("\n");
|
|
943
1007
|
|
|
944
1008
|
// src/server.ts
|
|
945
1009
|
var MCP_SERVER_NAME = "mergestorm";
|
|
946
|
-
var MCP_SERVER_VERSION = "0.
|
|
1010
|
+
var MCP_SERVER_VERSION = "0.2.0";
|
|
947
1011
|
function stripNulls(value) {
|
|
948
1012
|
if (Array.isArray(value)) {
|
|
949
1013
|
return value.map((item) => stripNulls(item));
|
|
@@ -975,7 +1039,10 @@ function createMergestormMcpServer() {
|
|
|
975
1039
|
const server2 = new McpServer(
|
|
976
1040
|
{
|
|
977
1041
|
name: MCP_SERVER_NAME,
|
|
978
|
-
|
|
1042
|
+
title: "Mergestorm",
|
|
1043
|
+
version: MCP_SERVER_VERSION,
|
|
1044
|
+
websiteUrl: MCP_WEBSITE_URL,
|
|
1045
|
+
icons: MCP_SERVER_ICONS
|
|
979
1046
|
},
|
|
980
1047
|
{ instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
|
|
981
1048
|
|
|
@@ -988,7 +1055,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
988
1055
|
"whoami",
|
|
989
1056
|
{
|
|
990
1057
|
title: "Who am I",
|
|
991
|
-
description: "Current Mergestorm API key prefix, plan, and API base."
|
|
1058
|
+
description: "Current Mergestorm API key prefix, plan, and API base.",
|
|
1059
|
+
annotations: { readOnlyHint: true }
|
|
992
1060
|
},
|
|
993
1061
|
async () => {
|
|
994
1062
|
try {
|
|
@@ -1002,7 +1070,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1002
1070
|
"credits",
|
|
1003
1071
|
{
|
|
1004
1072
|
title: "Credits",
|
|
1005
|
-
description: "Current standard-credit usage and reset time."
|
|
1073
|
+
description: "Current standard-credit usage and reset time.",
|
|
1074
|
+
annotations: { readOnlyHint: true }
|
|
1006
1075
|
},
|
|
1007
1076
|
async () => {
|
|
1008
1077
|
try {
|
|
@@ -1019,7 +1088,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1019
1088
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
1020
1089
|
inputSchema: {
|
|
1021
1090
|
limit: z3.number().int().min(1).max(50).optional()
|
|
1022
|
-
}
|
|
1091
|
+
},
|
|
1092
|
+
annotations: { readOnlyHint: true }
|
|
1023
1093
|
},
|
|
1024
1094
|
async ({ limit }) => {
|
|
1025
1095
|
try {
|
|
@@ -1029,51 +1099,6 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1029
1099
|
}
|
|
1030
1100
|
}
|
|
1031
1101
|
);
|
|
1032
|
-
addTool(
|
|
1033
|
-
"review_get",
|
|
1034
|
-
{
|
|
1035
|
-
title: "Get review",
|
|
1036
|
-
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
1037
|
-
inputSchema: {
|
|
1038
|
-
job_id: z3.string().min(1)
|
|
1039
|
-
}
|
|
1040
|
-
},
|
|
1041
|
-
async ({ job_id }) => {
|
|
1042
|
-
try {
|
|
1043
|
-
return ok(await reviewGet(job_id));
|
|
1044
|
-
} catch (err) {
|
|
1045
|
-
return fail(err);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
);
|
|
1049
|
-
addTool(
|
|
1050
|
-
"review_get_pr",
|
|
1051
|
-
{
|
|
1052
|
-
title: "Get GitHub PR Vortex review",
|
|
1053
|
-
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).",
|
|
1054
|
-
inputSchema: {
|
|
1055
|
-
owner: z3.string().min(1),
|
|
1056
|
-
repo: z3.string().min(1),
|
|
1057
|
-
pr_number: z3.number().int().min(1),
|
|
1058
|
-
after_sha: z3.string().optional(),
|
|
1059
|
-
pass: z3.number().int().min(1).optional(),
|
|
1060
|
-
after_pass: z3.number().int().min(1).optional()
|
|
1061
|
-
}
|
|
1062
|
-
},
|
|
1063
|
-
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
1064
|
-
try {
|
|
1065
|
-
return ok(
|
|
1066
|
-
await reviewGetPr(owner, repo, pr_number, void 0, {
|
|
1067
|
-
afterSha: after_sha,
|
|
1068
|
-
pass,
|
|
1069
|
-
afterPass: after_pass
|
|
1070
|
-
})
|
|
1071
|
-
);
|
|
1072
|
-
} catch (err) {
|
|
1073
|
-
return fail(err);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
);
|
|
1077
1102
|
addTool(
|
|
1078
1103
|
"review_submit",
|
|
1079
1104
|
{
|
|
@@ -1092,7 +1117,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1092
1117
|
idempotency_key: z3.string().min(1).max(128).optional(),
|
|
1093
1118
|
wait: z3.boolean().optional(),
|
|
1094
1119
|
timeout_s: z3.number().positive().max(300).optional()
|
|
1095
|
-
}
|
|
1120
|
+
},
|
|
1121
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
1096
1122
|
},
|
|
1097
1123
|
async (args) => {
|
|
1098
1124
|
try {
|
|
@@ -1106,11 +1132,12 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1106
1132
|
"review_wait",
|
|
1107
1133
|
{
|
|
1108
1134
|
title: "Wait for review",
|
|
1109
|
-
description: "
|
|
1135
|
+
description: "Read one local Mergestorm review job by the job_id that review_submit returned, as a job envelope. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300s). On timeout, returns in_progress; call again with the same job_id until done. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again.",
|
|
1110
1136
|
inputSchema: {
|
|
1111
1137
|
job_id: z3.string().min(1),
|
|
1112
|
-
timeout_s: z3.number().
|
|
1113
|
-
}
|
|
1138
|
+
timeout_s: z3.number().min(0).max(300).optional()
|
|
1139
|
+
},
|
|
1140
|
+
annotations: { readOnlyHint: true }
|
|
1114
1141
|
},
|
|
1115
1142
|
async ({ job_id, timeout_s }) => {
|
|
1116
1143
|
try {
|
|
@@ -1124,7 +1151,7 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1124
1151
|
"review_wait_pr",
|
|
1125
1152
|
{
|
|
1126
1153
|
title: "Wait for GitHub PR Vortex review",
|
|
1127
|
-
description: "
|
|
1154
|
+
description: "Read the Vortex pass on a GitHub PR from the DB-only endpoint; review identity is head SHA + pass. timeout_s: 0 reads it once without polling; otherwise it polls for one 45s slice by default (max 300) and returns in_progress on timeout so you can call again. Pass after_sha for the head you pushed and pass to read one exact attempt; when you already hold a resting envelope for that head, also pass after_pass set to its pass and keep it unchanged across retries; never replace it with the pass of an in-progress envelope, and omit after_pass for a head you hold no envelope for. With timeout_s: 0, a pass that does not exist yet returns not_found, so after a push use a positive timeout. Do not pass 300; hosts drop long MCP calls. On rate_limited, wait retry_after_seconds before trying again. After a resting pass, verify each finding; prefer the smallest correct patch; if you skip, post a PR comment starting with mergestorm-loop: dismiss.",
|
|
1128
1155
|
inputSchema: {
|
|
1129
1156
|
owner: z3.string().min(1),
|
|
1130
1157
|
repo: z3.string().min(1),
|
|
@@ -1132,8 +1159,9 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1132
1159
|
after_sha: z3.string().optional(),
|
|
1133
1160
|
pass: z3.number().int().min(1).optional(),
|
|
1134
1161
|
after_pass: z3.number().int().min(1).optional(),
|
|
1135
|
-
timeout_s: z3.number().
|
|
1136
|
-
}
|
|
1162
|
+
timeout_s: z3.number().min(0).max(300).optional()
|
|
1163
|
+
},
|
|
1164
|
+
annotations: { readOnlyHint: true }
|
|
1137
1165
|
},
|
|
1138
1166
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
1139
1167
|
try {
|
|
@@ -1158,7 +1186,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1158
1186
|
{
|
|
1159
1187
|
title: "Adopt PR stack",
|
|
1160
1188
|
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. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main.",
|
|
1161
|
-
inputSchema: stackAdoptSchema
|
|
1189
|
+
inputSchema: stackAdoptSchema,
|
|
1190
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
1162
1191
|
},
|
|
1163
1192
|
async (args) => {
|
|
1164
1193
|
try {
|
|
@@ -1172,7 +1201,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1172
1201
|
"stack_list",
|
|
1173
1202
|
{
|
|
1174
1203
|
title: "List stacks",
|
|
1175
|
-
description: "List the current user's Mergestorm stacks.
|
|
1204
|
+
description: "List the current user's Mergestorm stacks.",
|
|
1205
|
+
annotations: { readOnlyHint: true }
|
|
1176
1206
|
},
|
|
1177
1207
|
async () => {
|
|
1178
1208
|
try {
|
|
@@ -1192,7 +1222,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1192
1222
|
auto_land: z3.boolean().optional(),
|
|
1193
1223
|
auto_review: z3.boolean().nullable().optional(),
|
|
1194
1224
|
auto_patch: z3.boolean().nullable().optional()
|
|
1195
|
-
}
|
|
1225
|
+
},
|
|
1226
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
1196
1227
|
},
|
|
1197
1228
|
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
1198
1229
|
try {
|
|
@@ -1212,10 +1243,11 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1212
1243
|
"stack_status",
|
|
1213
1244
|
{
|
|
1214
1245
|
title: "Get stack status",
|
|
1215
|
-
description: "Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue.
|
|
1246
|
+
description: "Fetch one owned Mergestorm stack with enriched checks and agent state, plus attention, issues, and currentCandidate from the same blocker rules as stack_wait, read against that stack's own merge queue. trunkBranch is mg-stack-<n> for an adopted 2+ PR stack and matches the bottom open PR's GitHub base; an mg-park-* parentBranch is a freeze. Neither is drift to fix.",
|
|
1216
1247
|
inputSchema: {
|
|
1217
1248
|
stack_id: z3.string().min(1)
|
|
1218
|
-
}
|
|
1249
|
+
},
|
|
1250
|
+
annotations: { readOnlyHint: true }
|
|
1219
1251
|
},
|
|
1220
1252
|
async ({ stack_id }) => {
|
|
1221
1253
|
try {
|
|
@@ -1229,8 +1261,9 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1229
1261
|
"stack_wait",
|
|
1230
1262
|
{
|
|
1231
1263
|
title: "Wait for stack attention",
|
|
1232
|
-
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.
|
|
1233
|
-
inputSchema: stackWaitSchema
|
|
1264
|
+
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; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. Attention never calls for changing a PR's GitHub base.",
|
|
1265
|
+
inputSchema: stackWaitSchema,
|
|
1266
|
+
annotations: { readOnlyHint: true }
|
|
1234
1267
|
},
|
|
1235
1268
|
async (args, extra) => {
|
|
1236
1269
|
try {
|
|
@@ -1245,10 +1278,11 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1245
1278
|
"queue_status",
|
|
1246
1279
|
{
|
|
1247
1280
|
title: "Get merge queue status",
|
|
1248
|
-
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.
|
|
1281
|
+
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.",
|
|
1249
1282
|
inputSchema: {
|
|
1250
1283
|
stack_id: z3.string().min(1).optional()
|
|
1251
|
-
}
|
|
1284
|
+
},
|
|
1285
|
+
annotations: { readOnlyHint: true }
|
|
1252
1286
|
},
|
|
1253
1287
|
async ({ stack_id }) => {
|
|
1254
1288
|
try {
|
|
@@ -1262,7 +1296,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1262
1296
|
"settings_get",
|
|
1263
1297
|
{
|
|
1264
1298
|
title: "Get settings",
|
|
1265
|
-
description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled
|
|
1299
|
+
description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), and cyclone_connected.",
|
|
1300
|
+
annotations: { readOnlyHint: true }
|
|
1266
1301
|
},
|
|
1267
1302
|
async () => {
|
|
1268
1303
|
try {
|
|
@@ -1276,18 +1311,19 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1276
1311
|
"settings_set",
|
|
1277
1312
|
{
|
|
1278
1313
|
title: "Update settings",
|
|
1279
|
-
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.",
|
|
1314
|
+
description: "Update writable Bearer /api/v1/settings values and return the stored result. At least one key is required. auto_land_settle_seconds is a whole number of seconds from 15 through 300; it applies to settle clocks that start after the write. cyclone_connected and github_connected are read-only and cannot be set.",
|
|
1280
1315
|
inputSchema: {
|
|
1281
1316
|
...Object.fromEntries(
|
|
1282
1317
|
BEARER_SETTINGS2.map((row) => [
|
|
1283
1318
|
row.key,
|
|
1284
|
-
("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : z3.enum(row.values) : z3.boolean()).optional()
|
|
1319
|
+
("kind" in row ? row.kind === "logins" ? z3.array(z3.string()) : row.kind === "seconds" ? z3.number().int().min(row.min).max(row.max) : z3.enum(row.values) : z3.boolean()).optional()
|
|
1285
1320
|
])
|
|
1286
1321
|
),
|
|
1287
1322
|
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
1288
1323
|
cyclone_connected: z3.unknown().optional(),
|
|
1289
1324
|
github_connected: z3.unknown().optional()
|
|
1290
|
-
}
|
|
1325
|
+
},
|
|
1326
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
1291
1327
|
},
|
|
1292
1328
|
async (args) => {
|
|
1293
1329
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
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.25",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|