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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +534 -30
  3. 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 CommandError6 } from "mergestorm/client";
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/review-get.ts
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 (!isCommandErrorCode(err, "rate_limited")) return null;
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 CommandError2(`Unexpected response for review ${jobId}.`);
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 CommandError3,
258
+ CommandError as CommandError5,
109
259
  REVIEW_THREAD_SLUG_RE,
110
260
  ReviewPollTimeoutError,
111
261
  collectReviewInput,
112
- loadConfig as loadConfig2,
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 CommandError3("timeout_s must be a positive number of seconds", 2, "usage");
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 CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
324
+ throw new CommandError5("cwd must be inside the configured sandbox root", 2, "usage");
154
325
  }
155
326
  } catch (err) {
156
- if (err instanceof CommandError3) throw err;
157
- throw new CommandError3("cwd must be inside the configured sandbox root", 2, "usage");
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 loadConfig2();
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 CommandError3("thread must be 1\u2013120 chars [A-Za-z0-9._/-]", 2, "usage");
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 CommandError3(
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: rateLimitedMessage(submitted.retryAfterSeconds),
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 CommandError3(JSON.stringify(submitted.row));
398
+ throw new CommandError5(JSON.stringify(submitted.row));
227
399
  }
228
400
  if (!jobId) {
229
- throw new CommandError3("Review API returned no job_id");
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 CommandError4,
479
+ CommandError as CommandError6,
258
480
  ReviewPollTimeoutError as ReviewPollTimeoutError2,
259
- loadConfig as loadConfig3,
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 loadConfig3();
485
+ const resolved = cfg ?? await loadConfig5();
264
486
  const id = jobId.trim();
265
487
  if (!id) {
266
- throw new CommandError4("job_id is required", 2, "usage");
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 CommandError5,
660
+ CommandError as CommandError9,
291
661
  apiBase,
292
662
  configPath,
293
663
  getMe as getMe2,
294
664
  keyDisplay,
295
- loadConfig as loadConfig4,
665
+ loadConfig as loadConfig6,
296
666
  resolveApiKey
297
667
  } from "mergestorm/client";
298
668
  async function whoami(cfg) {
299
- const resolved = cfg ?? await loadConfig4();
669
+ const resolved = cfg ?? await loadConfig6();
300
670
  const key = resolveApiKey(resolved);
301
671
  if (!key) {
302
- throw new CommandError5(
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.0";
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 CommandError6 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
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.0",
4
- "description": "Mergestorm MCP server — whoami, credits, and review read tools over stdio",
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.13",
32
+ "mergestorm": "^0.3.15",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {