mergestorm-mcp 0.1.0 → 0.1.2
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 -1
- package/dist/index.js +534 -29
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# mergestorm-mcp
|
|
2
2
|
|
|
3
|
-
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_submit`, `review_wait`.
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_get_pr`, `review_submit`, `review_wait`, `review_wait_pr`, `stack_list`, `stack_status`, `queue_status`, `settings_get`, `settings_set`.
|
|
4
|
+
|
|
5
|
+
The stack tools are read-only:
|
|
6
|
+
|
|
7
|
+
- `stack_list` returns the current API key owner's registered stacks.
|
|
8
|
+
- `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, or a structured `stack_not_found` error.
|
|
4
9
|
|
|
5
10
|
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
6
11
|
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
// src/server.ts
|
|
8
8
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
-
import { CommandError as
|
|
10
|
+
import { CommandError as CommandError10 } from "mergestorm/client";
|
|
11
11
|
|
|
12
12
|
// src/tools/credits.ts
|
|
13
13
|
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
@@ -35,16 +35,167 @@ async function credits(cfg) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
// src/tools/
|
|
38
|
+
// src/tools/queue-status.ts
|
|
39
39
|
import {
|
|
40
|
+
apiFetch,
|
|
40
41
|
CommandError as CommandError2,
|
|
42
|
+
isCommandErrorCode,
|
|
43
|
+
loadConfig as loadConfig2,
|
|
44
|
+
rateLimitedMessage,
|
|
45
|
+
REVIEW_EXIT
|
|
46
|
+
} from "mergestorm/client";
|
|
47
|
+
function entrySummary(entry) {
|
|
48
|
+
const repo = entry.owner && entry.repo ? `${entry.owner}/${entry.repo}` : "unknown repo";
|
|
49
|
+
const position = entry.position === void 0 ? "queue" : `#${entry.position}`;
|
|
50
|
+
return `${position} \xB7 ${repo} \xB7 stack ${entry.stackId ?? "unknown"} \xB7 ${entry.state ?? "unknown"}`;
|
|
51
|
+
}
|
|
52
|
+
async function queueStatus(stackId, cfg) {
|
|
53
|
+
try {
|
|
54
|
+
const resolved = cfg ?? await loadConfig2();
|
|
55
|
+
const { status, body, retryAfterSeconds } = await apiFetch(
|
|
56
|
+
resolved,
|
|
57
|
+
"/api/v1/stacks/queue"
|
|
58
|
+
);
|
|
59
|
+
if (status === 404) {
|
|
60
|
+
throw new CommandError2(
|
|
61
|
+
"Merge queue API is not available on this server yet. Deploy the API update or use the dashboard."
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
if (status === 429) {
|
|
65
|
+
throw new CommandError2(
|
|
66
|
+
rateLimitedMessage(retryAfterSeconds),
|
|
67
|
+
REVIEW_EXIT.rate_limited,
|
|
68
|
+
"rate_limited",
|
|
69
|
+
{ retryAfterSeconds }
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (status !== 200) {
|
|
73
|
+
throw new CommandError2(
|
|
74
|
+
`Failed to get queue status (HTTP ${status}): ${JSON.stringify(body)}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
const entries = body.entries;
|
|
78
|
+
if (!Array.isArray(entries)) {
|
|
79
|
+
throw new CommandError2(
|
|
80
|
+
`Failed to get queue status (HTTP 200): ${JSON.stringify(body)}`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
const id = stackId?.trim();
|
|
84
|
+
if (id) {
|
|
85
|
+
const entry = entries.find((candidate) => candidate?.stackId === id);
|
|
86
|
+
if (!entry) {
|
|
87
|
+
const message = `Live queue entry not found for stack: ${id}`;
|
|
88
|
+
return {
|
|
89
|
+
summary: message,
|
|
90
|
+
data: {
|
|
91
|
+
error: {
|
|
92
|
+
code: "queue_entry_not_found",
|
|
93
|
+
message,
|
|
94
|
+
stack_id: id
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
isError: true
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
summary: entrySummary(entry),
|
|
102
|
+
data: { entry }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
summary: entries.length === 0 ? "No live merge queue entries" : entries.map(entrySummary).join("\n"),
|
|
107
|
+
data: { entries }
|
|
108
|
+
};
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (isCommandErrorCode(err, "rate_limited")) {
|
|
111
|
+
return {
|
|
112
|
+
summary: err.message,
|
|
113
|
+
data: {
|
|
114
|
+
error: {
|
|
115
|
+
code: "rate_limited",
|
|
116
|
+
message: err.message,
|
|
117
|
+
retry_after_seconds: err.retryAfterSeconds
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
isError: true
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/tools/review-get-pr.ts
|
|
128
|
+
import {
|
|
129
|
+
CommandError as CommandError3,
|
|
130
|
+
getPrVortexReview,
|
|
131
|
+
isCommandErrorCode as isCommandErrorCode2
|
|
132
|
+
} from "mergestorm/client";
|
|
133
|
+
function prReviewTarget(owner, repo, prNumber) {
|
|
134
|
+
const cleanOwner = owner.trim();
|
|
135
|
+
const cleanRepo = repo.trim();
|
|
136
|
+
if (!cleanOwner || !cleanRepo || !Number.isInteger(prNumber) || prNumber <= 0) {
|
|
137
|
+
throw new CommandError3(
|
|
138
|
+
"owner, repo, and a positive pr_number are required",
|
|
139
|
+
2,
|
|
140
|
+
"usage"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
return { owner: cleanOwner, repo: cleanRepo, prNumber };
|
|
144
|
+
}
|
|
145
|
+
function payloadFromPrReview(owner, repo, prNumber, envelope) {
|
|
146
|
+
return {
|
|
147
|
+
summary: `${owner}/${repo}#${prNumber} \xB7 ${envelope.status}`,
|
|
148
|
+
data: { ...envelope }
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function payloadFromPrRateLimit(err, owner, repo, prNumber) {
|
|
152
|
+
if (!isCommandErrorCode2(err, "rate_limited")) return null;
|
|
153
|
+
return {
|
|
154
|
+
summary: `${owner}/${repo}#${prNumber} \xB7 rate_limited`,
|
|
155
|
+
data: {
|
|
156
|
+
status: "rate_limited",
|
|
157
|
+
error: err.message,
|
|
158
|
+
retry_after_seconds: err.retryAfterSeconds
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
async function reviewGetPr(owner, repo, prNumber, cfg) {
|
|
163
|
+
const target = prReviewTarget(owner, repo, prNumber);
|
|
164
|
+
try {
|
|
165
|
+
const envelope = await getPrVortexReview(
|
|
166
|
+
target.owner,
|
|
167
|
+
target.repo,
|
|
168
|
+
target.prNumber,
|
|
169
|
+
cfg
|
|
170
|
+
);
|
|
171
|
+
return payloadFromPrReview(
|
|
172
|
+
target.owner,
|
|
173
|
+
target.repo,
|
|
174
|
+
target.prNumber,
|
|
175
|
+
envelope
|
|
176
|
+
);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const rateLimited = payloadFromPrRateLimit(
|
|
179
|
+
err,
|
|
180
|
+
target.owner,
|
|
181
|
+
target.repo,
|
|
182
|
+
target.prNumber
|
|
183
|
+
);
|
|
184
|
+
if (rateLimited) return rateLimited;
|
|
185
|
+
throw err;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/tools/review-get.ts
|
|
190
|
+
import {
|
|
191
|
+
CommandError as CommandError4,
|
|
41
192
|
getReview,
|
|
42
193
|
toReviewJobEnvelope as toReviewJobEnvelope2
|
|
43
194
|
} from "mergestorm/client";
|
|
44
195
|
|
|
45
196
|
// src/tools/envelope.ts
|
|
46
197
|
import {
|
|
47
|
-
isCommandErrorCode,
|
|
198
|
+
isCommandErrorCode as isCommandErrorCode3,
|
|
48
199
|
toReviewJobEnvelope
|
|
49
200
|
} from "mergestorm/client";
|
|
50
201
|
function payloadFromRow(row, fallback = {}) {
|
|
@@ -57,7 +208,7 @@ function payloadFromRow(row, fallback = {}) {
|
|
|
57
208
|
};
|
|
58
209
|
}
|
|
59
210
|
function payloadFromRateLimit(err, jobId) {
|
|
60
|
-
if (!
|
|
211
|
+
if (!isCommandErrorCode3(err, "rate_limited")) return null;
|
|
61
212
|
return payloadFromRow(null, {
|
|
62
213
|
jobId,
|
|
63
214
|
status: "rate_limited",
|
|
@@ -77,7 +228,7 @@ async function reviewGet(jobId, cfg) {
|
|
|
77
228
|
throw err;
|
|
78
229
|
}
|
|
79
230
|
if (!row || typeof row !== "object" || typeof row.status !== "string") {
|
|
80
|
-
throw new
|
|
231
|
+
throw new CommandError4(`Unexpected response for review ${jobId}.`);
|
|
81
232
|
}
|
|
82
233
|
const envelope = toReviewJobEnvelope2(row, { jobId });
|
|
83
234
|
const bits = [envelope.status];
|
|
@@ -105,24 +256,45 @@ import { createHash } from "node:crypto";
|
|
|
105
256
|
import { realpath } from "node:fs/promises";
|
|
106
257
|
import { relative, resolve, sep } from "node:path";
|
|
107
258
|
import {
|
|
108
|
-
CommandError as
|
|
259
|
+
CommandError as CommandError5,
|
|
109
260
|
REVIEW_THREAD_SLUG_RE,
|
|
110
261
|
ReviewPollTimeoutError,
|
|
111
262
|
collectReviewInput,
|
|
112
|
-
loadConfig as
|
|
263
|
+
loadConfig as loadConfig3,
|
|
113
264
|
loadReviewContext,
|
|
114
265
|
pollReview,
|
|
115
|
-
rateLimitedMessage,
|
|
266
|
+
rateLimitedMessage as rateLimitedMessage2,
|
|
116
267
|
submitReview
|
|
117
268
|
} from "mergestorm/client";
|
|
118
269
|
var MCP_WAIT_CEILING_S = 300;
|
|
270
|
+
var PINNABLE_SPECIALISTS = [
|
|
271
|
+
"security",
|
|
272
|
+
"performance",
|
|
273
|
+
"architecture",
|
|
274
|
+
"tests",
|
|
275
|
+
"data",
|
|
276
|
+
"api",
|
|
277
|
+
"frontend"
|
|
278
|
+
];
|
|
279
|
+
function assertPinnableSpecialists(specialists) {
|
|
280
|
+
if (!specialists?.length) return;
|
|
281
|
+
const unknown = specialists.filter(
|
|
282
|
+
(id) => !PINNABLE_SPECIALISTS.includes(id)
|
|
283
|
+
);
|
|
284
|
+
if (unknown.length === 0) return;
|
|
285
|
+
throw new CommandError5(
|
|
286
|
+
`Unknown specialist: ${unknown.join(", ")}. Valid lanes: ${PINNABLE_SPECIALISTS.join(", ")}. governance and seam cannot be requested.`,
|
|
287
|
+
2,
|
|
288
|
+
"usage"
|
|
289
|
+
);
|
|
290
|
+
}
|
|
119
291
|
function mcpSandboxRoot() {
|
|
120
292
|
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
121
293
|
}
|
|
122
294
|
function waitTimeoutMs(timeout_s) {
|
|
123
295
|
const seconds = timeout_s ?? MCP_WAIT_CEILING_S;
|
|
124
296
|
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
125
|
-
throw new
|
|
297
|
+
throw new CommandError5("timeout_s must be a positive number of seconds", 2, "usage");
|
|
126
298
|
}
|
|
127
299
|
return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
|
|
128
300
|
}
|
|
@@ -150,11 +322,11 @@ async function resolveInput(input) {
|
|
|
150
322
|
]);
|
|
151
323
|
const relToSandbox = relative(sandboxReal, cwdReal);
|
|
152
324
|
if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
|
|
153
|
-
throw new
|
|
325
|
+
throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
|
|
154
326
|
}
|
|
155
327
|
} catch (err) {
|
|
156
|
-
if (err instanceof
|
|
157
|
-
throw new
|
|
328
|
+
if (err instanceof CommandError5) throw err;
|
|
329
|
+
throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
|
|
158
330
|
}
|
|
159
331
|
let lastErr;
|
|
160
332
|
let collected = null;
|
|
@@ -175,14 +347,15 @@ async function resolveInput(input) {
|
|
|
175
347
|
return null;
|
|
176
348
|
}
|
|
177
349
|
async function reviewSubmit(input, cfg, opts = {}) {
|
|
178
|
-
const resolved = cfg ?? await
|
|
350
|
+
const resolved = cfg ?? await loadConfig3();
|
|
179
351
|
const wait = input.wait !== false;
|
|
180
352
|
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
353
|
+
assertPinnableSpecialists(input.specialists);
|
|
181
354
|
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
182
|
-
throw new
|
|
355
|
+
throw new CommandError5("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
|
|
183
356
|
}
|
|
184
357
|
if (input.context === "-") {
|
|
185
|
-
throw new
|
|
358
|
+
throw new CommandError5(
|
|
186
359
|
'context "-" (stdin) is not available over MCP; pass the text directly',
|
|
187
360
|
2,
|
|
188
361
|
"usage"
|
|
@@ -218,15 +391,15 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
218
391
|
return payloadFromRow(submitted.row, {
|
|
219
392
|
jobId,
|
|
220
393
|
status: "rate_limited",
|
|
221
|
-
error:
|
|
394
|
+
error: rateLimitedMessage2(submitted.retryAfterSeconds),
|
|
222
395
|
retryAfterSeconds: submitted.retryAfterSeconds
|
|
223
396
|
});
|
|
224
397
|
}
|
|
225
398
|
if (submitted.status !== 202 && submitted.status !== 200) {
|
|
226
|
-
throw new
|
|
399
|
+
throw new CommandError5(JSON.stringify(submitted.row));
|
|
227
400
|
}
|
|
228
401
|
if (!jobId) {
|
|
229
|
-
throw new
|
|
402
|
+
throw new CommandError5("Review API returned no job_id");
|
|
230
403
|
}
|
|
231
404
|
if (!wait) {
|
|
232
405
|
return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
|
|
@@ -252,18 +425,68 @@ async function reviewSubmit(input, cfg, opts = {}) {
|
|
|
252
425
|
return payloadFromRow(row, { jobId });
|
|
253
426
|
}
|
|
254
427
|
|
|
428
|
+
// src/tools/review-wait-pr.ts
|
|
429
|
+
import {
|
|
430
|
+
PrReviewPollTimeoutError,
|
|
431
|
+
loadConfig as loadConfig4,
|
|
432
|
+
pollPrVortexReview
|
|
433
|
+
} from "mergestorm/client";
|
|
434
|
+
async function reviewWaitPr(owner, repo, prNumber, afterSha, timeout_s, cfg, opts = {}) {
|
|
435
|
+
const target = prReviewTarget(owner, repo, prNumber);
|
|
436
|
+
const resolved = cfg ?? await loadConfig4();
|
|
437
|
+
try {
|
|
438
|
+
const envelope = await pollPrVortexReview(
|
|
439
|
+
resolved,
|
|
440
|
+
target.owner,
|
|
441
|
+
target.repo,
|
|
442
|
+
target.prNumber,
|
|
443
|
+
{
|
|
444
|
+
timeoutMs: waitTimeoutMs(timeout_s),
|
|
445
|
+
afterSha,
|
|
446
|
+
intervalMs: opts.pollIntervalMs
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
return payloadFromPrReview(
|
|
450
|
+
target.owner,
|
|
451
|
+
target.repo,
|
|
452
|
+
target.prNumber,
|
|
453
|
+
envelope
|
|
454
|
+
);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
if (err instanceof PrReviewPollTimeoutError) {
|
|
457
|
+
const status = err.lastEnvelope?.status ?? "in_progress";
|
|
458
|
+
return {
|
|
459
|
+
summary: `${target.owner}/${target.repo}#${target.prNumber} \xB7 ${status}`,
|
|
460
|
+
data: {
|
|
461
|
+
...err.lastEnvelope ?? {},
|
|
462
|
+
status,
|
|
463
|
+
error: err.message
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const rateLimited = payloadFromPrRateLimit(
|
|
468
|
+
err,
|
|
469
|
+
target.owner,
|
|
470
|
+
target.repo,
|
|
471
|
+
target.prNumber
|
|
472
|
+
);
|
|
473
|
+
if (rateLimited) return rateLimited;
|
|
474
|
+
throw err;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
255
478
|
// src/tools/review-wait.ts
|
|
256
479
|
import {
|
|
257
|
-
CommandError as
|
|
480
|
+
CommandError as CommandError6,
|
|
258
481
|
ReviewPollTimeoutError as ReviewPollTimeoutError2,
|
|
259
|
-
loadConfig as
|
|
482
|
+
loadConfig as loadConfig5,
|
|
260
483
|
pollReview as pollReview2
|
|
261
484
|
} from "mergestorm/client";
|
|
262
485
|
async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
263
|
-
const resolved = cfg ?? await
|
|
486
|
+
const resolved = cfg ?? await loadConfig5();
|
|
264
487
|
const id = jobId.trim();
|
|
265
488
|
if (!id) {
|
|
266
|
-
throw new
|
|
489
|
+
throw new CommandError6("job_id is required", 2, "usage");
|
|
267
490
|
}
|
|
268
491
|
try {
|
|
269
492
|
const row = await pollReview2(resolved, id, {
|
|
@@ -285,21 +508,169 @@ async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
|
285
508
|
}
|
|
286
509
|
}
|
|
287
510
|
|
|
511
|
+
// src/tools/settings-get.ts
|
|
512
|
+
import {
|
|
513
|
+
CommandError as CommandError7,
|
|
514
|
+
getSettings
|
|
515
|
+
} from "mergestorm/client";
|
|
516
|
+
function settingsSummary(settings) {
|
|
517
|
+
const autoPatch = settings.auto_patch_enabled ? "on" : "off";
|
|
518
|
+
const cyclone = settings.cyclone_connected ? "Cyclone connected" : "Cyclone not connected";
|
|
519
|
+
return `auto_patch ${autoPatch} \xB7 ${cyclone}`;
|
|
520
|
+
}
|
|
521
|
+
async function settingsGet(cfg) {
|
|
522
|
+
const settings = await getSettings(cfg);
|
|
523
|
+
if (!settings) {
|
|
524
|
+
throw new CommandError7(
|
|
525
|
+
"Settings are not available (offline or API too old). Try again or open https://mergestorm.ai/settings"
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
summary: settingsSummary(settings),
|
|
530
|
+
data: { ...settings }
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/tools/settings-set.ts
|
|
535
|
+
import {
|
|
536
|
+
CommandError as CommandError8,
|
|
537
|
+
SETTINGS_WRITABLE_KEYS,
|
|
538
|
+
patchSettings
|
|
539
|
+
} from "mergestorm/client";
|
|
540
|
+
var SETTINGS_READ_ONLY_KEYS = [
|
|
541
|
+
"cyclone_connected",
|
|
542
|
+
"github_connected"
|
|
543
|
+
];
|
|
544
|
+
function settingsPatchFromArgs(args) {
|
|
545
|
+
for (const key of SETTINGS_READ_ONLY_KEYS) {
|
|
546
|
+
if (args[key] !== void 0) {
|
|
547
|
+
throw new CommandError8(`${key} is read-only and cannot be set.`, 2, "usage");
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const patch = {};
|
|
551
|
+
for (const key of SETTINGS_WRITABLE_KEYS) {
|
|
552
|
+
const value = args[key];
|
|
553
|
+
if (value === void 0) continue;
|
|
554
|
+
if (typeof value !== "boolean") {
|
|
555
|
+
throw new CommandError8(`${key} takes a boolean.`, 2, "usage");
|
|
556
|
+
}
|
|
557
|
+
if (key === "auto_patch_enabled" && value === true) {
|
|
558
|
+
throw new CommandError8("auto_patch_enabled cannot be enabled by this tool.", 2, "usage");
|
|
559
|
+
}
|
|
560
|
+
patch[key] = value;
|
|
561
|
+
}
|
|
562
|
+
if (Object.keys(patch).length === 0) {
|
|
563
|
+
throw new CommandError8(
|
|
564
|
+
`At least one settings key is required: ${SETTINGS_WRITABLE_KEYS.join(", ")}.`,
|
|
565
|
+
2,
|
|
566
|
+
"usage"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
return patch;
|
|
570
|
+
}
|
|
571
|
+
async function settingsSet(args, cfg) {
|
|
572
|
+
const patch = settingsPatchFromArgs(args);
|
|
573
|
+
const settings = await patchSettings(patch, cfg);
|
|
574
|
+
return {
|
|
575
|
+
summary: settingsSummary(settings),
|
|
576
|
+
data: { ...settings }
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// src/tools/stack-list.ts
|
|
581
|
+
import { isCommandErrorCode as isCommandErrorCode4, listStacks } from "mergestorm/client";
|
|
582
|
+
|
|
583
|
+
// src/tools/stack-summary.ts
|
|
584
|
+
function stackSummary(stack) {
|
|
585
|
+
const count = stack.layers.length;
|
|
586
|
+
const layerLabel = `${count} layer${count === 1 ? "" : "s"}`;
|
|
587
|
+
const states = [...new Set(stack.layers.map((layer) => layer.state))];
|
|
588
|
+
const currentState = states.length > 0 ? `layers: ${states.join(", ")}` : stack.unit ? `unit: ${stack.unit.state}` : "no open layers";
|
|
589
|
+
return `${stack.owner}/${stack.repo} \xB7 ${layerLabel} \xB7 ${currentState}`;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/tools/stack-list.ts
|
|
593
|
+
async function stackList(cfg) {
|
|
594
|
+
try {
|
|
595
|
+
const stacks = await listStacks(cfg);
|
|
596
|
+
return {
|
|
597
|
+
summary: stacks.length === 0 ? "No stacks" : stacks.map((stack) => stackSummary(stack)).join("\n"),
|
|
598
|
+
data: { stacks }
|
|
599
|
+
};
|
|
600
|
+
} catch (err) {
|
|
601
|
+
if (isCommandErrorCode4(err, "rate_limited")) {
|
|
602
|
+
return {
|
|
603
|
+
summary: err.message,
|
|
604
|
+
data: {
|
|
605
|
+
error: {
|
|
606
|
+
code: "rate_limited",
|
|
607
|
+
message: err.message,
|
|
608
|
+
retry_after_seconds: err.retryAfterSeconds
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
isError: true
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
throw err;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// src/tools/stack-status.ts
|
|
619
|
+
import { getEnrichedStack, isCommandErrorCode as isCommandErrorCode5 } from "mergestorm/client";
|
|
620
|
+
async function stackStatus(stackId, cfg) {
|
|
621
|
+
try {
|
|
622
|
+
const stack = await getEnrichedStack(stackId, cfg);
|
|
623
|
+
if (!stack) {
|
|
624
|
+
const message = `Stack not found or not owned by the current user: ${stackId}`;
|
|
625
|
+
return {
|
|
626
|
+
summary: message,
|
|
627
|
+
data: {
|
|
628
|
+
error: {
|
|
629
|
+
code: "stack_not_found",
|
|
630
|
+
message,
|
|
631
|
+
stack_id: stackId
|
|
632
|
+
}
|
|
633
|
+
},
|
|
634
|
+
isError: true
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
summary: stackSummary(stack),
|
|
639
|
+
data: { stack }
|
|
640
|
+
};
|
|
641
|
+
} catch (err) {
|
|
642
|
+
if (isCommandErrorCode5(err, "rate_limited")) {
|
|
643
|
+
return {
|
|
644
|
+
summary: err.message,
|
|
645
|
+
data: {
|
|
646
|
+
error: {
|
|
647
|
+
code: "rate_limited",
|
|
648
|
+
message: err.message,
|
|
649
|
+
retry_after_seconds: err.retryAfterSeconds
|
|
650
|
+
}
|
|
651
|
+
},
|
|
652
|
+
isError: true
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
throw err;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
288
659
|
// src/tools/whoami.ts
|
|
289
660
|
import {
|
|
290
|
-
CommandError as
|
|
661
|
+
CommandError as CommandError9,
|
|
291
662
|
apiBase,
|
|
292
663
|
configPath,
|
|
293
664
|
getMe as getMe2,
|
|
294
665
|
keyDisplay,
|
|
295
|
-
loadConfig as
|
|
666
|
+
loadConfig as loadConfig6,
|
|
296
667
|
resolveApiKey
|
|
297
668
|
} from "mergestorm/client";
|
|
298
669
|
async function whoami(cfg) {
|
|
299
|
-
const resolved = cfg ?? await
|
|
670
|
+
const resolved = cfg ?? await loadConfig6();
|
|
300
671
|
const key = resolveApiKey(resolved);
|
|
301
672
|
if (!key) {
|
|
302
|
-
throw new
|
|
673
|
+
throw new CommandError9(
|
|
303
674
|
"No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
|
|
304
675
|
1,
|
|
305
676
|
"missing_api_key"
|
|
@@ -324,7 +695,7 @@ async function whoami(cfg) {
|
|
|
324
695
|
|
|
325
696
|
// src/server.ts
|
|
326
697
|
var MCP_SERVER_NAME = "mergestorm";
|
|
327
|
-
var MCP_SERVER_VERSION = "0.1.
|
|
698
|
+
var MCP_SERVER_VERSION = "0.1.2";
|
|
328
699
|
function stripNulls(value) {
|
|
329
700
|
if (Array.isArray(value)) {
|
|
330
701
|
return value.map((item) => stripNulls(item));
|
|
@@ -340,12 +711,13 @@ function stripNulls(value) {
|
|
|
340
711
|
}
|
|
341
712
|
function ok(payload) {
|
|
342
713
|
return {
|
|
714
|
+
...payload.isError ? { isError: true } : {},
|
|
343
715
|
content: [{ type: "text", text: payload.summary }],
|
|
344
716
|
structuredContent: stripNulls(payload.data)
|
|
345
717
|
};
|
|
346
718
|
}
|
|
347
719
|
function fail(err) {
|
|
348
|
-
const message = err instanceof
|
|
720
|
+
const message = err instanceof CommandError10 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
|
|
349
721
|
return {
|
|
350
722
|
isError: true,
|
|
351
723
|
content: [{ type: "text", text: message }]
|
|
@@ -419,11 +791,30 @@ function createMergestormMcpServer() {
|
|
|
419
791
|
}
|
|
420
792
|
}
|
|
421
793
|
);
|
|
794
|
+
addTool(
|
|
795
|
+
"review_get_pr",
|
|
796
|
+
{
|
|
797
|
+
title: "Get GitHub PR Vortex review",
|
|
798
|
+
description: "Fetch the latest GitHub PR Vortex pass from the DB-only endpoint. This tool is read-only. On rate_limited, wait retry_after_seconds before trying again.",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
owner: z.string().min(1),
|
|
801
|
+
repo: z.string().min(1),
|
|
802
|
+
pr_number: z.number().int().min(1)
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
async ({ owner, repo, pr_number }) => {
|
|
806
|
+
try {
|
|
807
|
+
return ok(await reviewGetPr(owner, repo, pr_number));
|
|
808
|
+
} catch (err) {
|
|
809
|
+
return fail(err);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
);
|
|
422
813
|
addTool(
|
|
423
814
|
"review_submit",
|
|
424
815
|
{
|
|
425
816
|
title: "Submit review",
|
|
426
|
-
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Default waits up to 300s. On rate_limited, wait retry_after_seconds and inspect or wait for an in-flight review before resubmitting.",
|
|
817
|
+
description: "Submit a local git diff (or an explicit diff) as a Mergestorm review job. Default waits up to 300s. 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.",
|
|
427
818
|
inputSchema: {
|
|
428
819
|
cwd: z.string().optional(),
|
|
429
820
|
base: z.string().optional(),
|
|
@@ -465,6 +856,120 @@ function createMergestormMcpServer() {
|
|
|
465
856
|
}
|
|
466
857
|
}
|
|
467
858
|
);
|
|
859
|
+
addTool(
|
|
860
|
+
"review_wait_pr",
|
|
861
|
+
{
|
|
862
|
+
title: "Wait for GitHub PR Vortex review",
|
|
863
|
+
description: "Poll the DB-only GitHub PR Vortex pass until it rests. This tool is read-only. On timeout, returns in_progress. On rate_limited, wait retry_after_seconds before trying again.",
|
|
864
|
+
inputSchema: {
|
|
865
|
+
owner: z.string().min(1),
|
|
866
|
+
repo: z.string().min(1),
|
|
867
|
+
pr_number: z.number().int().min(1),
|
|
868
|
+
after_sha: z.string().optional(),
|
|
869
|
+
timeout_s: z.number().positive().max(300).optional()
|
|
870
|
+
}
|
|
871
|
+
},
|
|
872
|
+
async ({ owner, repo, pr_number, after_sha, timeout_s }) => {
|
|
873
|
+
try {
|
|
874
|
+
return ok(
|
|
875
|
+
await reviewWaitPr(
|
|
876
|
+
owner,
|
|
877
|
+
repo,
|
|
878
|
+
pr_number,
|
|
879
|
+
after_sha,
|
|
880
|
+
timeout_s
|
|
881
|
+
)
|
|
882
|
+
);
|
|
883
|
+
} catch (err) {
|
|
884
|
+
return fail(err);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
addTool(
|
|
889
|
+
"stack_list",
|
|
890
|
+
{
|
|
891
|
+
title: "List stacks",
|
|
892
|
+
description: "List the current user's Mergestorm stacks. This tool is read-only."
|
|
893
|
+
},
|
|
894
|
+
async () => {
|
|
895
|
+
try {
|
|
896
|
+
return ok(await stackList());
|
|
897
|
+
} catch (err) {
|
|
898
|
+
return fail(err);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
addTool(
|
|
903
|
+
"stack_status",
|
|
904
|
+
{
|
|
905
|
+
title: "Get stack status",
|
|
906
|
+
description: "Fetch one owned Mergestorm stack with enriched checks and agent state. This tool is read-only.",
|
|
907
|
+
inputSchema: {
|
|
908
|
+
stack_id: z.string().min(1)
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
async ({ stack_id }) => {
|
|
912
|
+
try {
|
|
913
|
+
return ok(await stackStatus(stack_id));
|
|
914
|
+
} catch (err) {
|
|
915
|
+
return fail(err);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
);
|
|
919
|
+
addTool(
|
|
920
|
+
"queue_status",
|
|
921
|
+
{
|
|
922
|
+
title: "Get merge queue status",
|
|
923
|
+
description: "List live merge queue entries for the current user, optionally filtered by stack. This tool is read-only.",
|
|
924
|
+
inputSchema: {
|
|
925
|
+
stack_id: z.string().min(1).optional()
|
|
926
|
+
}
|
|
927
|
+
},
|
|
928
|
+
async ({ stack_id }) => {
|
|
929
|
+
try {
|
|
930
|
+
return ok(await queueStatus(stack_id));
|
|
931
|
+
} catch (err) {
|
|
932
|
+
return fail(err);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
);
|
|
936
|
+
addTool(
|
|
937
|
+
"settings_get",
|
|
938
|
+
{
|
|
939
|
+
title: "Get settings",
|
|
940
|
+
description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled and cyclone_connected. This tool is read-only."
|
|
941
|
+
},
|
|
942
|
+
async () => {
|
|
943
|
+
try {
|
|
944
|
+
return ok(await settingsGet());
|
|
945
|
+
} catch (err) {
|
|
946
|
+
return fail(err);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
addTool(
|
|
951
|
+
"settings_set",
|
|
952
|
+
{
|
|
953
|
+
title: "Update settings",
|
|
954
|
+
description: "Update writable Bearer /api/v1/settings toggles and return the stored result. At least one key is required. cyclone_connected and github_connected are read-only and cannot be set.",
|
|
955
|
+
inputSchema: {
|
|
956
|
+
auto_review_enabled: z.boolean().optional(),
|
|
957
|
+
auto_patch_enabled: z.boolean().optional(),
|
|
958
|
+
vortex_show_thinking_traces: z.boolean().optional(),
|
|
959
|
+
repo_overview_enabled: z.boolean().optional(),
|
|
960
|
+
review_unit_land_prs_enabled: z.boolean().optional(),
|
|
961
|
+
cyclone_review_unit_land_prs_enabled: z.boolean().optional(),
|
|
962
|
+
vortex_seam_specialist_enabled: z.boolean().optional()
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
async (args) => {
|
|
966
|
+
try {
|
|
967
|
+
return ok(await settingsSet(args ?? {}));
|
|
968
|
+
} catch (err) {
|
|
969
|
+
return fail(err);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
);
|
|
468
973
|
return server2;
|
|
469
974
|
}
|
|
470
975
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Mergestorm MCP server —
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Margin Systems <hello@mergestorm.ai>",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
|
-
"mergestorm": "^0.3.
|
|
32
|
+
"mergestorm": "^0.3.15",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|