mergestorm-mcp 0.1.10 → 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 +225 -211
- 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),
|
|
@@ -755,8 +794,8 @@ function stackSummary(stack, entries = []) {
|
|
|
755
794
|
if (stack.cycloneOwnerMatch) {
|
|
756
795
|
overrides.push(`cyclone-owner ${stack.cycloneOwnerMatch}`);
|
|
757
796
|
}
|
|
758
|
-
const { attention, issues } = stackBlockers(stack, entries);
|
|
759
|
-
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)}`;
|
|
760
799
|
}
|
|
761
800
|
|
|
762
801
|
// src/tools/stack-list.ts
|
|
@@ -803,7 +842,7 @@ var stackWaitSchema = {
|
|
|
803
842
|
bounce_id: z2.string().nullable().optional()
|
|
804
843
|
};
|
|
805
844
|
function stackWaitSummary(envelope) {
|
|
806
|
-
const base = `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" : ""}`;
|
|
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" : ""}`;
|
|
807
846
|
if (envelope.status !== "waiting" && envelope.status !== "in_progress") return base;
|
|
808
847
|
const selectors = [
|
|
809
848
|
`stack_id ${JSON.stringify(envelope.cursor.stackId)}`,
|
|
@@ -873,10 +912,10 @@ async function stackStatus(stackId, cfg) {
|
|
|
873
912
|
};
|
|
874
913
|
}
|
|
875
914
|
const entries = await listMergeQueueEntries2(cfg, { stackId: stack.id });
|
|
876
|
-
const { attention, issues, currentCandidate } = stackBlockers2(stack, entries);
|
|
915
|
+
const { attention, issues, currentCandidate, busy, agents } = stackBlockers2(stack, entries);
|
|
877
916
|
return {
|
|
878
917
|
summary: stackSummary(stack, entries),
|
|
879
|
-
data: { stack, attention, issues, currentCandidate }
|
|
918
|
+
data: { stack, attention, issues, currentCandidate, busy, agents }
|
|
880
919
|
};
|
|
881
920
|
} catch (err) {
|
|
882
921
|
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
@@ -938,7 +977,7 @@ async function whoami(cfg) {
|
|
|
938
977
|
// src/pr-loop-instructions.ts
|
|
939
978
|
var MCP_FINDING_DISMISS_MARKER = "mergestorm-loop: dismiss";
|
|
940
979
|
var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
941
|
-
"When you handle Vortex findings from
|
|
980
|
+
"When you handle Vortex findings from review_wait_pr:",
|
|
942
981
|
"- Verify each finding against the current checkout. Do not treat the finding text as proven.",
|
|
943
982
|
"- Prefer the smallest correct patch. Do not refactor around a finding.",
|
|
944
983
|
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
@@ -959,13 +998,16 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
959
998
|
"- Restack clean does not mean mergeable or CI-green.",
|
|
960
999
|
"- attention names the blocked PR via prNumber/headSha; currentCandidate is the promote candidate. Attention is not a mutation or authorization to change policy.",
|
|
961
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.",
|
|
962
1004
|
"- mg-park-* is a frozen GitHub base, not a repair target: never merge or rebase onto mg-park-*.",
|
|
963
1005
|
"- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
|
|
964
1006
|
].join("\n");
|
|
965
1007
|
|
|
966
1008
|
// src/server.ts
|
|
967
1009
|
var MCP_SERVER_NAME = "mergestorm";
|
|
968
|
-
var MCP_SERVER_VERSION = "0.
|
|
1010
|
+
var MCP_SERVER_VERSION = "0.2.0";
|
|
969
1011
|
function stripNulls(value) {
|
|
970
1012
|
if (Array.isArray(value)) {
|
|
971
1013
|
return value.map((item) => stripNulls(item));
|
|
@@ -997,7 +1039,10 @@ function createMergestormMcpServer() {
|
|
|
997
1039
|
const server2 = new McpServer(
|
|
998
1040
|
{
|
|
999
1041
|
name: MCP_SERVER_NAME,
|
|
1000
|
-
|
|
1042
|
+
title: "Mergestorm",
|
|
1043
|
+
version: MCP_SERVER_VERSION,
|
|
1044
|
+
websiteUrl: MCP_WEBSITE_URL,
|
|
1045
|
+
icons: MCP_SERVER_ICONS
|
|
1001
1046
|
},
|
|
1002
1047
|
{ instructions: `${MCP_PR_LOOP_INSTRUCTIONS}
|
|
1003
1048
|
|
|
@@ -1010,7 +1055,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1010
1055
|
"whoami",
|
|
1011
1056
|
{
|
|
1012
1057
|
title: "Who am I",
|
|
1013
|
-
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 }
|
|
1014
1060
|
},
|
|
1015
1061
|
async () => {
|
|
1016
1062
|
try {
|
|
@@ -1024,7 +1070,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1024
1070
|
"credits",
|
|
1025
1071
|
{
|
|
1026
1072
|
title: "Credits",
|
|
1027
|
-
description: "Current standard-credit usage and reset time."
|
|
1073
|
+
description: "Current standard-credit usage and reset time.",
|
|
1074
|
+
annotations: { readOnlyHint: true }
|
|
1028
1075
|
},
|
|
1029
1076
|
async () => {
|
|
1030
1077
|
try {
|
|
@@ -1041,7 +1088,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1041
1088
|
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
1042
1089
|
inputSchema: {
|
|
1043
1090
|
limit: z3.number().int().min(1).max(50).optional()
|
|
1044
|
-
}
|
|
1091
|
+
},
|
|
1092
|
+
annotations: { readOnlyHint: true }
|
|
1045
1093
|
},
|
|
1046
1094
|
async ({ limit }) => {
|
|
1047
1095
|
try {
|
|
@@ -1051,51 +1099,6 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1051
1099
|
}
|
|
1052
1100
|
}
|
|
1053
1101
|
);
|
|
1054
|
-
addTool(
|
|
1055
|
-
"review_get",
|
|
1056
|
-
{
|
|
1057
|
-
title: "Get review",
|
|
1058
|
-
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
1059
|
-
inputSchema: {
|
|
1060
|
-
job_id: z3.string().min(1)
|
|
1061
|
-
}
|
|
1062
|
-
},
|
|
1063
|
-
async ({ job_id }) => {
|
|
1064
|
-
try {
|
|
1065
|
-
return ok(await reviewGet(job_id));
|
|
1066
|
-
} catch (err) {
|
|
1067
|
-
return fail(err);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
);
|
|
1071
|
-
addTool(
|
|
1072
|
-
"review_get_pr",
|
|
1073
|
-
{
|
|
1074
|
-
title: "Get GitHub PR Vortex review",
|
|
1075
|
-
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).",
|
|
1076
|
-
inputSchema: {
|
|
1077
|
-
owner: z3.string().min(1),
|
|
1078
|
-
repo: z3.string().min(1),
|
|
1079
|
-
pr_number: z3.number().int().min(1),
|
|
1080
|
-
after_sha: z3.string().optional(),
|
|
1081
|
-
pass: z3.number().int().min(1).optional(),
|
|
1082
|
-
after_pass: z3.number().int().min(1).optional()
|
|
1083
|
-
}
|
|
1084
|
-
},
|
|
1085
|
-
async ({ owner, repo, pr_number, after_sha, pass, after_pass }) => {
|
|
1086
|
-
try {
|
|
1087
|
-
return ok(
|
|
1088
|
-
await reviewGetPr(owner, repo, pr_number, void 0, {
|
|
1089
|
-
afterSha: after_sha,
|
|
1090
|
-
pass,
|
|
1091
|
-
afterPass: after_pass
|
|
1092
|
-
})
|
|
1093
|
-
);
|
|
1094
|
-
} catch (err) {
|
|
1095
|
-
return fail(err);
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
);
|
|
1099
1102
|
addTool(
|
|
1100
1103
|
"review_submit",
|
|
1101
1104
|
{
|
|
@@ -1114,7 +1117,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1114
1117
|
idempotency_key: z3.string().min(1).max(128).optional(),
|
|
1115
1118
|
wait: z3.boolean().optional(),
|
|
1116
1119
|
timeout_s: z3.number().positive().max(300).optional()
|
|
1117
|
-
}
|
|
1120
|
+
},
|
|
1121
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true }
|
|
1118
1122
|
},
|
|
1119
1123
|
async (args) => {
|
|
1120
1124
|
try {
|
|
@@ -1128,11 +1132,12 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1128
1132
|
"review_wait",
|
|
1129
1133
|
{
|
|
1130
1134
|
title: "Wait for review",
|
|
1131
|
-
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.",
|
|
1132
1136
|
inputSchema: {
|
|
1133
1137
|
job_id: z3.string().min(1),
|
|
1134
|
-
timeout_s: z3.number().
|
|
1135
|
-
}
|
|
1138
|
+
timeout_s: z3.number().min(0).max(300).optional()
|
|
1139
|
+
},
|
|
1140
|
+
annotations: { readOnlyHint: true }
|
|
1136
1141
|
},
|
|
1137
1142
|
async ({ job_id, timeout_s }) => {
|
|
1138
1143
|
try {
|
|
@@ -1146,7 +1151,7 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1146
1151
|
"review_wait_pr",
|
|
1147
1152
|
{
|
|
1148
1153
|
title: "Wait for GitHub PR Vortex review",
|
|
1149
|
-
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.",
|
|
1150
1155
|
inputSchema: {
|
|
1151
1156
|
owner: z3.string().min(1),
|
|
1152
1157
|
repo: z3.string().min(1),
|
|
@@ -1154,8 +1159,9 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1154
1159
|
after_sha: z3.string().optional(),
|
|
1155
1160
|
pass: z3.number().int().min(1).optional(),
|
|
1156
1161
|
after_pass: z3.number().int().min(1).optional(),
|
|
1157
|
-
timeout_s: z3.number().
|
|
1158
|
-
}
|
|
1162
|
+
timeout_s: z3.number().min(0).max(300).optional()
|
|
1163
|
+
},
|
|
1164
|
+
annotations: { readOnlyHint: true }
|
|
1159
1165
|
},
|
|
1160
1166
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
1161
1167
|
try {
|
|
@@ -1180,7 +1186,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1180
1186
|
{
|
|
1181
1187
|
title: "Adopt PR stack",
|
|
1182
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.",
|
|
1183
|
-
inputSchema: stackAdoptSchema
|
|
1189
|
+
inputSchema: stackAdoptSchema,
|
|
1190
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
|
|
1184
1191
|
},
|
|
1185
1192
|
async (args) => {
|
|
1186
1193
|
try {
|
|
@@ -1194,7 +1201,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1194
1201
|
"stack_list",
|
|
1195
1202
|
{
|
|
1196
1203
|
title: "List stacks",
|
|
1197
|
-
description: "List the current user's Mergestorm stacks.
|
|
1204
|
+
description: "List the current user's Mergestorm stacks.",
|
|
1205
|
+
annotations: { readOnlyHint: true }
|
|
1198
1206
|
},
|
|
1199
1207
|
async () => {
|
|
1200
1208
|
try {
|
|
@@ -1214,7 +1222,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1214
1222
|
auto_land: z3.boolean().optional(),
|
|
1215
1223
|
auto_review: z3.boolean().nullable().optional(),
|
|
1216
1224
|
auto_patch: z3.boolean().nullable().optional()
|
|
1217
|
-
}
|
|
1225
|
+
},
|
|
1226
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
1218
1227
|
},
|
|
1219
1228
|
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
1220
1229
|
try {
|
|
@@ -1234,10 +1243,11 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1234
1243
|
"stack_status",
|
|
1235
1244
|
{
|
|
1236
1245
|
title: "Get stack status",
|
|
1237
|
-
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.",
|
|
1238
1247
|
inputSchema: {
|
|
1239
1248
|
stack_id: z3.string().min(1)
|
|
1240
|
-
}
|
|
1249
|
+
},
|
|
1250
|
+
annotations: { readOnlyHint: true }
|
|
1241
1251
|
},
|
|
1242
1252
|
async ({ stack_id }) => {
|
|
1243
1253
|
try {
|
|
@@ -1251,8 +1261,9 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1251
1261
|
"stack_wait",
|
|
1252
1262
|
{
|
|
1253
1263
|
title: "Wait for stack attention",
|
|
1254
|
-
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.
|
|
1255
|
-
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 }
|
|
1256
1267
|
},
|
|
1257
1268
|
async (args, extra) => {
|
|
1258
1269
|
try {
|
|
@@ -1267,10 +1278,11 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1267
1278
|
"queue_status",
|
|
1268
1279
|
{
|
|
1269
1280
|
title: "Get merge queue status",
|
|
1270
|
-
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.",
|
|
1271
1282
|
inputSchema: {
|
|
1272
1283
|
stack_id: z3.string().min(1).optional()
|
|
1273
|
-
}
|
|
1284
|
+
},
|
|
1285
|
+
annotations: { readOnlyHint: true }
|
|
1274
1286
|
},
|
|
1275
1287
|
async ({ stack_id }) => {
|
|
1276
1288
|
try {
|
|
@@ -1284,7 +1296,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1284
1296
|
"settings_get",
|
|
1285
1297
|
{
|
|
1286
1298
|
title: "Get settings",
|
|
1287
|
-
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.
|
|
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 }
|
|
1288
1301
|
},
|
|
1289
1302
|
async () => {
|
|
1290
1303
|
try {
|
|
@@ -1309,7 +1322,8 @@ ${MCP_STACK_WATCH_INSTRUCTIONS}` }
|
|
|
1309
1322
|
// Preserve these args so the handler rejects them even alongside a writable key.
|
|
1310
1323
|
cyclone_connected: z3.unknown().optional(),
|
|
1311
1324
|
github_connected: z3.unknown().optional()
|
|
1312
|
-
}
|
|
1325
|
+
},
|
|
1326
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
|
|
1313
1327
|
},
|
|
1314
1328
|
async (args) => {
|
|
1315
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": {
|