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