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