mergestorm-mcp 0.1.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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/index.js +474 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Margin Systems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# mergestorm-mcp
|
|
2
|
+
|
|
3
|
+
Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_get`, `review_submit`, `review_wait`.
|
|
4
|
+
|
|
5
|
+
Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
|
|
6
|
+
|
|
7
|
+
`context_files` passed to `review_submit` may only read files under the host-configured sandbox root (`MERGESTORM_SANDBOX_ROOT`, default: the MCP server's working directory), so a prompt-injected agent cannot use them to exfiltrate files outside it.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
claude mcp add mergestorm -- npx -y mergestorm-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then call `whoami`. It should return the key prefix.
|
|
14
|
+
|
|
15
|
+
Requires Node.js 22+ and a Mergestorm API key (`mg login` or `MERGESTORM_API_KEY`).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
|
|
7
|
+
// src/server.ts
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { CommandError as CommandError6 } from "mergestorm/client";
|
|
11
|
+
|
|
12
|
+
// src/tools/credits.ts
|
|
13
|
+
import { CommandError, getMe, loadConfig } from "mergestorm/client";
|
|
14
|
+
async function credits(cfg) {
|
|
15
|
+
const resolved = cfg ?? await loadConfig();
|
|
16
|
+
const me = await getMe(resolved);
|
|
17
|
+
if (!me) {
|
|
18
|
+
throw new CommandError(
|
|
19
|
+
"Credits are not available right now (Mergestorm API is offline, unreachable, or too old)."
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!me.usage) {
|
|
23
|
+
throw new CommandError(
|
|
24
|
+
"Credits are not available on this server yet. Update the API or use the dashboard."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const standard = me.usage.standard;
|
|
28
|
+
const remaining = standard.remaining ?? (standard.limit != null ? Math.max(0, standard.limit - standard.used) : null);
|
|
29
|
+
return {
|
|
30
|
+
summary: remaining == null ? `${standard.used} used \xB7 unlimited` : `${standard.used} used \xB7 ${remaining} remaining`,
|
|
31
|
+
data: {
|
|
32
|
+
usage: me.usage,
|
|
33
|
+
resets_at: me.resets_at ?? null
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/tools/review-get.ts
|
|
39
|
+
import {
|
|
40
|
+
CommandError as CommandError2,
|
|
41
|
+
getReview,
|
|
42
|
+
toReviewJobEnvelope as toReviewJobEnvelope2
|
|
43
|
+
} from "mergestorm/client";
|
|
44
|
+
|
|
45
|
+
// src/tools/envelope.ts
|
|
46
|
+
import {
|
|
47
|
+
isCommandErrorCode,
|
|
48
|
+
toReviewJobEnvelope
|
|
49
|
+
} from "mergestorm/client";
|
|
50
|
+
function payloadFromRow(row, fallback = {}) {
|
|
51
|
+
const envelope = toReviewJobEnvelope(row, fallback);
|
|
52
|
+
const bits = [envelope.status];
|
|
53
|
+
if (envelope.verdict) bits.push(envelope.verdict);
|
|
54
|
+
return {
|
|
55
|
+
summary: `${envelope.job_id ?? fallback.jobId ?? "review"} \xB7 ${bits.join(" \xB7 ")}`,
|
|
56
|
+
data: envelope
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function payloadFromRateLimit(err, jobId) {
|
|
60
|
+
if (!isCommandErrorCode(err, "rate_limited")) return null;
|
|
61
|
+
return payloadFromRow(null, {
|
|
62
|
+
jobId,
|
|
63
|
+
status: "rate_limited",
|
|
64
|
+
error: err.message,
|
|
65
|
+
retryAfterSeconds: err.retryAfterSeconds
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/tools/review-get.ts
|
|
70
|
+
async function reviewGet(jobId, cfg) {
|
|
71
|
+
let row;
|
|
72
|
+
try {
|
|
73
|
+
row = await getReview(jobId, cfg);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
76
|
+
if (rateLimited) return rateLimited;
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
if (!row || typeof row !== "object" || typeof row.status !== "string") {
|
|
80
|
+
throw new CommandError2(`Unexpected response for review ${jobId}.`);
|
|
81
|
+
}
|
|
82
|
+
const envelope = toReviewJobEnvelope2(row, { jobId });
|
|
83
|
+
const bits = [envelope.status];
|
|
84
|
+
if (envelope.verdict) bits.push(envelope.verdict);
|
|
85
|
+
return {
|
|
86
|
+
summary: `${envelope.job_id ?? jobId} \xB7 ${bits.join(" \xB7 ")}`,
|
|
87
|
+
data: envelope
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/tools/review-list.ts
|
|
92
|
+
import { listJobs, toReviewJobEnvelope as toReviewJobEnvelope3 } from "mergestorm/client";
|
|
93
|
+
async function reviewList(limit = 10, cfg) {
|
|
94
|
+
const items = await listJobs(limit, cfg);
|
|
95
|
+
const envelopes = items.map((row) => toReviewJobEnvelope3(row));
|
|
96
|
+
const n = envelopes.length;
|
|
97
|
+
return {
|
|
98
|
+
summary: n === 0 ? "No review jobs" : `${n} review job${n === 1 ? "" : "s"}`,
|
|
99
|
+
data: { items: envelopes }
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/tools/review-submit.ts
|
|
104
|
+
import { createHash } from "node:crypto";
|
|
105
|
+
import { realpath } from "node:fs/promises";
|
|
106
|
+
import { relative, resolve, sep } from "node:path";
|
|
107
|
+
import {
|
|
108
|
+
CommandError as CommandError3,
|
|
109
|
+
REVIEW_THREAD_SLUG_RE,
|
|
110
|
+
ReviewPollTimeoutError,
|
|
111
|
+
collectReviewInput,
|
|
112
|
+
loadConfig as loadConfig2,
|
|
113
|
+
loadReviewContext,
|
|
114
|
+
pollReview,
|
|
115
|
+
rateLimitedMessage,
|
|
116
|
+
submitReview
|
|
117
|
+
} from "mergestorm/client";
|
|
118
|
+
var MCP_WAIT_CEILING_S = 300;
|
|
119
|
+
function mcpSandboxRoot() {
|
|
120
|
+
return process.env.MERGESTORM_SANDBOX_ROOT?.trim() || process.cwd();
|
|
121
|
+
}
|
|
122
|
+
function waitTimeoutMs(timeout_s) {
|
|
123
|
+
const seconds = timeout_s ?? MCP_WAIT_CEILING_S;
|
|
124
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
125
|
+
throw new CommandError3("timeout_s must be a positive number of seconds", 2, "usage");
|
|
126
|
+
}
|
|
127
|
+
return Math.max(1, Math.round(Math.min(seconds, MCP_WAIT_CEILING_S) * 1e3));
|
|
128
|
+
}
|
|
129
|
+
async function resolveInput(input) {
|
|
130
|
+
const cwd = input.cwd ?? process.cwd();
|
|
131
|
+
const head = input.head?.trim() || "HEAD";
|
|
132
|
+
if (input.diff?.trim()) {
|
|
133
|
+
const branch = "local";
|
|
134
|
+
const thread = input.thread?.trim() || `local/diff-${createHash("sha256").update(input.diff).digest("hex").slice(0, 12)}`;
|
|
135
|
+
return {
|
|
136
|
+
thread,
|
|
137
|
+
branch,
|
|
138
|
+
baseLabel: input.base?.trim() || "main",
|
|
139
|
+
headLabel: head,
|
|
140
|
+
diff: input.diff,
|
|
141
|
+
files: []
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const bases = input.base?.trim() ? [input.base.trim()] : ["main", "master"];
|
|
145
|
+
const sandboxRoot = mcpSandboxRoot();
|
|
146
|
+
try {
|
|
147
|
+
const [sandboxReal, cwdReal] = await Promise.all([
|
|
148
|
+
realpath(resolve(sandboxRoot)),
|
|
149
|
+
realpath(resolve(cwd))
|
|
150
|
+
]);
|
|
151
|
+
const relToSandbox = relative(sandboxReal, cwdReal);
|
|
152
|
+
if (relToSandbox === ".." || relToSandbox.startsWith(`..${sep}`)) {
|
|
153
|
+
throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
|
|
154
|
+
}
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err instanceof CommandError3) throw err;
|
|
157
|
+
throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
|
|
158
|
+
}
|
|
159
|
+
let lastErr;
|
|
160
|
+
let collected = null;
|
|
161
|
+
let yielded = false;
|
|
162
|
+
for (const base of bases) {
|
|
163
|
+
try {
|
|
164
|
+
const candidate = await collectReviewInput(base, head, cwd, sandboxRoot);
|
|
165
|
+
yielded = true;
|
|
166
|
+
if (!collected) collected = candidate;
|
|
167
|
+
} catch (err) {
|
|
168
|
+
if (yielded) continue;
|
|
169
|
+
lastErr = err;
|
|
170
|
+
if (input.base?.trim()) throw err;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (yielded) return collected;
|
|
174
|
+
if (lastErr) throw lastErr;
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
async function reviewSubmit(input, cfg, opts = {}) {
|
|
178
|
+
const resolved = cfg ?? await loadConfig2();
|
|
179
|
+
const wait = input.wait !== false;
|
|
180
|
+
const timeoutMs = waitTimeoutMs(input.timeout_s);
|
|
181
|
+
if (input.thread && !REVIEW_THREAD_SLUG_RE.test(input.thread)) {
|
|
182
|
+
throw new CommandError3("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
|
|
183
|
+
}
|
|
184
|
+
if (input.context === "-") {
|
|
185
|
+
throw new CommandError3(
|
|
186
|
+
'context "-" (stdin) is not available over MCP; pass the text directly',
|
|
187
|
+
2,
|
|
188
|
+
"usage"
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const collected = await resolveInput(input);
|
|
192
|
+
if (!collected) {
|
|
193
|
+
return payloadFromRow(null, { status: "no_changes" });
|
|
194
|
+
}
|
|
195
|
+
if (input.thread) collected.thread = input.thread;
|
|
196
|
+
const extras = await loadReviewContext({
|
|
197
|
+
context: input.context,
|
|
198
|
+
contextFiles: input.context_files ?? [],
|
|
199
|
+
cwd: input.cwd ?? process.cwd(),
|
|
200
|
+
sandboxCwd: mcpSandboxRoot()
|
|
201
|
+
});
|
|
202
|
+
const submitted = await submitReview(resolved, collected, {
|
|
203
|
+
routerMode: input.router,
|
|
204
|
+
specialists: input.specialists,
|
|
205
|
+
context: extras.context,
|
|
206
|
+
contextFiles: extras.contextFiles,
|
|
207
|
+
idempotencyKey: input.idempotency_key
|
|
208
|
+
});
|
|
209
|
+
const jobId = submitted.row.job_id ?? submitted.row.id ?? null;
|
|
210
|
+
if (submitted.status === 402) {
|
|
211
|
+
return payloadFromRow(submitted.row, {
|
|
212
|
+
jobId,
|
|
213
|
+
status: "quota_exceeded",
|
|
214
|
+
error: "Review limit reached for this period."
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (submitted.status === 429) {
|
|
218
|
+
return payloadFromRow(submitted.row, {
|
|
219
|
+
jobId,
|
|
220
|
+
status: "rate_limited",
|
|
221
|
+
error: rateLimitedMessage(submitted.retryAfterSeconds),
|
|
222
|
+
retryAfterSeconds: submitted.retryAfterSeconds
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (submitted.status !== 202 && submitted.status !== 200) {
|
|
226
|
+
throw new CommandError3(JSON.stringify(submitted.row));
|
|
227
|
+
}
|
|
228
|
+
if (!jobId) {
|
|
229
|
+
throw new CommandError3("Review API returned no job_id");
|
|
230
|
+
}
|
|
231
|
+
if (!wait) {
|
|
232
|
+
return payloadFromRow(submitted.row, { jobId, status: submitted.row.status ?? "queued" });
|
|
233
|
+
}
|
|
234
|
+
let row;
|
|
235
|
+
try {
|
|
236
|
+
row = await pollReview(resolved, jobId, {
|
|
237
|
+
timeoutMs,
|
|
238
|
+
intervalMs: opts.pollIntervalMs
|
|
239
|
+
});
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof ReviewPollTimeoutError) {
|
|
242
|
+
return payloadFromRow(err.lastRow, {
|
|
243
|
+
jobId,
|
|
244
|
+
status: err.lastRow.status ?? "in_progress",
|
|
245
|
+
error: "Timed out waiting for review."
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
const rateLimited = payloadFromRateLimit(err, jobId);
|
|
249
|
+
if (rateLimited) return rateLimited;
|
|
250
|
+
throw err;
|
|
251
|
+
}
|
|
252
|
+
return payloadFromRow(row, { jobId });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/tools/review-wait.ts
|
|
256
|
+
import {
|
|
257
|
+
CommandError as CommandError4,
|
|
258
|
+
ReviewPollTimeoutError as ReviewPollTimeoutError2,
|
|
259
|
+
loadConfig as loadConfig3,
|
|
260
|
+
pollReview as pollReview2
|
|
261
|
+
} from "mergestorm/client";
|
|
262
|
+
async function reviewWait(jobId, timeout_s, cfg, opts = {}) {
|
|
263
|
+
const resolved = cfg ?? await loadConfig3();
|
|
264
|
+
const id = jobId.trim();
|
|
265
|
+
if (!id) {
|
|
266
|
+
throw new CommandError4("job_id is required", 2, "usage");
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const row = await pollReview2(resolved, id, {
|
|
270
|
+
timeoutMs: waitTimeoutMs(timeout_s),
|
|
271
|
+
intervalMs: opts.pollIntervalMs
|
|
272
|
+
});
|
|
273
|
+
return payloadFromRow(row, { jobId: id });
|
|
274
|
+
} catch (err) {
|
|
275
|
+
if (err instanceof ReviewPollTimeoutError2) {
|
|
276
|
+
return payloadFromRow(err.lastRow, {
|
|
277
|
+
jobId: id,
|
|
278
|
+
status: err.lastRow.status ?? "in_progress",
|
|
279
|
+
error: "Timed out waiting for review."
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const rateLimited = payloadFromRateLimit(err, id);
|
|
283
|
+
if (rateLimited) return rateLimited;
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// src/tools/whoami.ts
|
|
289
|
+
import {
|
|
290
|
+
CommandError as CommandError5,
|
|
291
|
+
apiBase,
|
|
292
|
+
configPath,
|
|
293
|
+
getMe as getMe2,
|
|
294
|
+
keyDisplay,
|
|
295
|
+
loadConfig as loadConfig4,
|
|
296
|
+
resolveApiKey
|
|
297
|
+
} from "mergestorm/client";
|
|
298
|
+
async function whoami(cfg) {
|
|
299
|
+
const resolved = cfg ?? await loadConfig4();
|
|
300
|
+
const key = resolveApiKey(resolved);
|
|
301
|
+
if (!key) {
|
|
302
|
+
throw new CommandError5(
|
|
303
|
+
"No API key. Run `mergestorm login` or set MERGESTORM_API_KEY.",
|
|
304
|
+
1,
|
|
305
|
+
"missing_api_key"
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
const me = await getMe2(resolved);
|
|
309
|
+
const data = {
|
|
310
|
+
key_prefix: me?.key.prefix ?? keyDisplay(key),
|
|
311
|
+
key_name: me?.key.name ?? null,
|
|
312
|
+
plan_key: me?.plan_key ?? null,
|
|
313
|
+
plan_label_key: me?.plan_label_key ?? null,
|
|
314
|
+
api_base: apiBase(resolved),
|
|
315
|
+
config_path: configPath(),
|
|
316
|
+
usage: me?.usage ?? null
|
|
317
|
+
};
|
|
318
|
+
const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
|
|
319
|
+
return {
|
|
320
|
+
summary: `${data.key_prefix} \xB7 ${plan}`,
|
|
321
|
+
data
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/server.ts
|
|
326
|
+
var MCP_SERVER_NAME = "mergestorm";
|
|
327
|
+
var MCP_SERVER_VERSION = "0.1.0";
|
|
328
|
+
function stripNulls(value) {
|
|
329
|
+
if (Array.isArray(value)) {
|
|
330
|
+
return value.map((item) => stripNulls(item));
|
|
331
|
+
}
|
|
332
|
+
if (value !== null && typeof value === "object") {
|
|
333
|
+
const out = {};
|
|
334
|
+
for (const [key, val] of Object.entries(value)) {
|
|
335
|
+
if (val !== null && val !== void 0) out[key] = stripNulls(val);
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
function ok(payload) {
|
|
342
|
+
return {
|
|
343
|
+
content: [{ type: "text", text: payload.summary }],
|
|
344
|
+
structuredContent: stripNulls(payload.data)
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function fail(err) {
|
|
348
|
+
const message = err instanceof CommandError6 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
|
|
349
|
+
return {
|
|
350
|
+
isError: true,
|
|
351
|
+
content: [{ type: "text", text: message }]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function createMergestormMcpServer() {
|
|
355
|
+
const server2 = new McpServer({
|
|
356
|
+
name: MCP_SERVER_NAME,
|
|
357
|
+
version: MCP_SERVER_VERSION
|
|
358
|
+
});
|
|
359
|
+
const addTool = server2.registerTool.bind(server2);
|
|
360
|
+
addTool(
|
|
361
|
+
"whoami",
|
|
362
|
+
{
|
|
363
|
+
title: "Who am I",
|
|
364
|
+
description: "Current Mergestorm API key prefix, plan, and API base."
|
|
365
|
+
},
|
|
366
|
+
async () => {
|
|
367
|
+
try {
|
|
368
|
+
return ok(await whoami());
|
|
369
|
+
} catch (err) {
|
|
370
|
+
return fail(err);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
addTool(
|
|
375
|
+
"credits",
|
|
376
|
+
{
|
|
377
|
+
title: "Credits",
|
|
378
|
+
description: "Current standard-credit usage and reset time."
|
|
379
|
+
},
|
|
380
|
+
async () => {
|
|
381
|
+
try {
|
|
382
|
+
return ok(await credits());
|
|
383
|
+
} catch (err) {
|
|
384
|
+
return fail(err);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
addTool(
|
|
389
|
+
"review_list",
|
|
390
|
+
{
|
|
391
|
+
title: "List reviews",
|
|
392
|
+
description: "List recent Mergestorm review jobs as job envelopes.",
|
|
393
|
+
inputSchema: {
|
|
394
|
+
limit: z.number().int().min(1).max(50).optional()
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
async ({ limit }) => {
|
|
398
|
+
try {
|
|
399
|
+
return ok(await reviewList(limit));
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return fail(err);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
addTool(
|
|
406
|
+
"review_get",
|
|
407
|
+
{
|
|
408
|
+
title: "Get review",
|
|
409
|
+
description: "Fetch one Mergestorm review job as a job envelope. On rate_limited, wait retry_after_seconds before trying again.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
job_id: z.string().min(1)
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
async ({ job_id }) => {
|
|
415
|
+
try {
|
|
416
|
+
return ok(await reviewGet(job_id));
|
|
417
|
+
} catch (err) {
|
|
418
|
+
return fail(err);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
);
|
|
422
|
+
addTool(
|
|
423
|
+
"review_submit",
|
|
424
|
+
{
|
|
425
|
+
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.",
|
|
427
|
+
inputSchema: {
|
|
428
|
+
cwd: z.string().optional(),
|
|
429
|
+
base: z.string().optional(),
|
|
430
|
+
head: z.string().optional(),
|
|
431
|
+
diff: z.string().optional(),
|
|
432
|
+
router: z.enum(["off", "standard", "max", "manual"]).optional(),
|
|
433
|
+
specialists: z.array(z.string().min(1)).max(5).optional(),
|
|
434
|
+
context: z.string().optional(),
|
|
435
|
+
context_files: z.array(z.string().min(1)).max(10).optional(),
|
|
436
|
+
thread: z.string().min(1).max(120).optional(),
|
|
437
|
+
idempotency_key: z.string().min(1).max(128).optional(),
|
|
438
|
+
wait: z.boolean().optional(),
|
|
439
|
+
timeout_s: z.number().positive().max(300).optional()
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
async (args) => {
|
|
443
|
+
try {
|
|
444
|
+
return ok(await reviewSubmit(args));
|
|
445
|
+
} catch (err) {
|
|
446
|
+
return fail(err);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
);
|
|
450
|
+
addTool(
|
|
451
|
+
"review_wait",
|
|
452
|
+
{
|
|
453
|
+
title: "Wait for review",
|
|
454
|
+
description: "Poll one Mergestorm review job until it finishes, return in_progress after 300s, or return rate_limited with retry_after_seconds.",
|
|
455
|
+
inputSchema: {
|
|
456
|
+
job_id: z.string().min(1),
|
|
457
|
+
timeout_s: z.number().positive().max(300).optional()
|
|
458
|
+
}
|
|
459
|
+
},
|
|
460
|
+
async ({ job_id, timeout_s }) => {
|
|
461
|
+
try {
|
|
462
|
+
return ok(await reviewWait(job_id, timeout_s));
|
|
463
|
+
} catch (err) {
|
|
464
|
+
return fail(err);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
);
|
|
468
|
+
return server2;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/index.ts
|
|
472
|
+
var server = createMergestormMcpServer();
|
|
473
|
+
var transport = new StdioServerTransport();
|
|
474
|
+
await server.connect(transport);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mergestorm-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Mergestorm MCP server — whoami, credits, and review read tools over stdio",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"author": "Margin Systems <hello@mergestorm.ai>",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mergestorm",
|
|
10
|
+
"mcp",
|
|
11
|
+
"code-review"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"mergestorm-mcp": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --packages=external --banner:js=\"#!/usr/bin/env node\"",
|
|
23
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
24
|
+
"test": "tsx --test src/*.test.ts src/**/*.test.ts",
|
|
25
|
+
"start": "node dist/index.js"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22.0.0"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
32
|
+
"mergestorm": "^0.3.13",
|
|
33
|
+
"zod": "^3.24.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.10.2",
|
|
37
|
+
"esbuild": "^0.25.9",
|
|
38
|
+
"tsx": "^4.19.2",
|
|
39
|
+
"typescript": "^5.7.2"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/marginsystems/mergestorm-mcp.git"
|
|
44
|
+
},
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/marginsystems/mergestorm-mcp/issues"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/marginsystems/mergestorm-mcp"
|
|
49
|
+
}
|