mergestorm-mcp 0.2.11 → 0.2.13

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 +70 -6
  2. package/dist/index.js +50 -45
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Stdio MCP server for Mergestorm. Tools: `whoami`, `credits`, `review_list`, `review_submit`, `review_wait`, `review_wait_pr`, `review_dismiss`, `stack_adopt`, `stack_list`, `stack_set`, `stack_status`, `stack_wait`, `queue_status`, `settings_get`, `settings_set`.
4
4
 
5
- Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `review_submit`, `review_dismiss`, `stack_adopt`, `stack_set`, and `settings_set` carry `readOnlyHint: false` and `destructiveHint: false`.
5
+ Every tool declares MCP `annotations` for read-only access, destructive changes, idempotence, and external systems. Reads are read-only; submission, adoption, dismissal, and policy changes are writes. Tools that can change existing policy, topology, or review gates are marked potentially destructive even when some inputs only preview or read. Annotations describe effects; the client still controls approval and authorization.
6
6
 
7
7
  `review_wait({ job_id, timeout_s? })` reads a local review job by the `job_id` that `review_submit` returned. `review_wait_pr({ owner, repo, pr_number, after_sha?, pass?, after_pass?, timeout_s? })` reads the Vortex pass on a GitHub PR. Both poll one 45s slice by default; `timeout_s: 0` reads once without polling.
8
8
 
@@ -18,22 +18,86 @@ Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `re
18
18
 
19
19
  The stack tools expose reads plus one scoped policy write:
20
20
 
21
- - `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership.
21
+ - `stack_adopt({ owner, repo, pr_number, auto_land?, auto_review?, auto_patch? })` adopts an open PR chain with optional per-stack policy. Omitted policy does not force Cyclone off. `auto_review` and `auto_patch` accept `true`, `false`, or `null`; `null` clears the override. Read `stack_status` with the returned `result.stack.id` to verify policy and Cyclone ownership. Adoption needs the Mergestorm Surge GitHub App on the repository (Cyclone is the auto-patch App). A refusal keeps the code `cyclone_not_connected` or `cyclone_not_installed`; its message, and `error.reason` starting with `surge_` when Surge is the one to install, say which App is missing.
22
22
  - `stack_list` returns the current API key owner's registered stacks.
23
23
  - `stack_set({ stack_id, auto_land?, auto_review?, auto_patch? })` sets per-stack policy on one owned stack. `auto_land` flips Auto land. `auto_review` and `auto_patch` pin Vortex auto-review or Cyclone auto-patch for that stack in either direction (`true` or `false`); `null` clears the pin so the stack follows the account setting. Pass at least one key. Account settings are never changed here.
24
24
  - `stack_status({ stack_id })` returns one owned stack with enriched checks and agent state, plus `attention`, `issues`, and `currentCandidate` from the same blocker rules as `stack_wait`, read against that stack's own merge queue. It returns a structured `stack_not_found` or `rate_limited` error.
25
25
  - `stack_wait({ stack_id, timeout_s?, enrolled_head_sha?, after_finished_at?, bounce_id? })` waits for a stack or queue state change and returns the latest snapshots. `timeout_s: 0` returns one snapshot; a positive timeout after a snapshot with no attention returns the current `waiting` or `in_progress` status with selectors to use for the next call. An unread deadline returns `failed`. While Cyclone or Vortex is still working on the blocked PR, `stack_wait` and `stack_status` return `in_progress` instead of `attention`, with `busy[]` naming the PR, blocker, and agent, and `agents` carrying that PR's `vortexStatus`, `cycloneStatus`, `vortexReview`, and busy flags. A nonempty `busy[]` means wait. A queued Vortex run holds for at most 15 minutes, `seam_pending` alone does not hold, and reviewing or patching holds until the run finishes or its lease or check run is cleared. A blocker no agent run clears (a merge conflict vs the live parent, a restack `Conflict` or `Restack failed`, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that `in_progress`, in `blocker`, `prNumber`, `headSha`, and `issues[]`, with `actAfter: "agents_idle"` and `waitingOn` listing the busy agents; act when a snapshot after the agents finish returns `attention`, re-reading the blocker then. For the three stuck-seam blockers, `repair` has kind `seam_review_stuck`: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. `repair` is included when another concrete repair hint is available. Blockers the agents can change keep `blocker` and `actAfter` null. If a hold looks stuck, refresh `stack_status` and tell the human.
26
26
 
27
- Agents that cannot hold a long turn open should not poll `stack_wait` inside their turn. The server instructions tell them to run `mg stack watch <stack-id>` from the `mergestorm` CLI as a background command, notify on output matching `MS-WATCH (ATTENTION|LANDED)`, and end the turn. The watcher loops the same stack wait against the Mergestorm API with the cursor, stays silent while the stack is waiting or in progress, and exits on attention (exit 3), a failed read that failed again on retry (exit 3), or a finished watch (exit 0). Every `watch.next` carries that command as `watch.next.background`, and every not-done `watch.message` offers it before the next `stack_wait` call. On wake the agent reads `stack_status` once, fixes the blocker, pushes, and restarts the watcher with `--head <pushed-sha>`. The stack is still not done until it lands. Agents are told not to subscribe to GitHub events to wait on a stack.
27
+ The background CLI watcher is optional. It monitors Mergestorm without model calls, but does not itself resume an agent conversation. Hand waiting to `mg stack watch <stack-id>` only after the host has confirmed a notification that resumes the task on completion or output matching `MS-WATCH (ATTENTION|LANDED)`. Without that facility, use bounded `stack_wait` calls with `watch.next.args`, or tell the user automatic follow-up is unavailable. A shell session ID is not proof of a wakeup subscription.
28
+
29
+ `watch.next.background` is the monitoring command, not a registered notification. Keep the latest cursor across `stack_wait` calls and reconnects. Cancellation stops the current wait, not the remote stack. After a host notification, read `stack_status` once and follow the returned repair and ownership rules.
30
+
31
+ The watcher exits on attention (exit 3), a failed read that failed again on retry (exit 3), a closed, archived, or missing stack (exit 3), or a confirmed landing (exit 0). Verify missing stacks before reporting a landing. The stack is still not done until it lands. Do not subscribe to GitHub events to wait on a stack.
28
32
 
29
33
  Auth is `MERGESTORM_API_KEY`, then `~/.mergestorm/config.json` (same as `mg`).
30
34
 
31
35
  `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.
32
36
 
37
+ ## Client setup
38
+
39
+ Requires Node.js 22+ and a Mergestorm API key. Run `mg login` on the machine running the MCP server, or supply `MERGESTORM_API_KEY` through the host's environment. The stdio server uses that credential; it does not provide an OAuth endpoint. Remote hosts need their own configuration and access to any reviewed checkout.
40
+
41
+ ### Codex
42
+
33
43
  ```bash
34
- claude mcp add mergestorm -- npx -y mergestorm-mcp
44
+ codex mcp add mergestorm -- npx -y mergestorm-mcp
35
45
  ```
36
46
 
37
- Then call `whoami`. It should return the key prefix.
47
+ For explicit configuration in `~/.codex/config.toml` (replace the checkout path):
48
+
49
+ ```toml
50
+ [mcp_servers.mergestorm]
51
+ command = "npx"
52
+ args = ["-y", "mergestorm-mcp"]
53
+ startup_timeout_sec = 30
54
+ tool_timeout_sec = 60
55
+
56
+ [mcp_servers.mergestorm.env]
57
+ MERGESTORM_SANDBOX_ROOT = "/absolute/path/to/checkout"
58
+ ```
59
+
60
+ The server reads the login saved by `mg login`. If using an environment key instead, pass it with `env_vars = ["MERGESTORM_API_KEY"]` in the server table. Approval remains controlled by the host; annotations do not authorize a write. [Codex MCP configuration](https://learn.chatgpt.com/docs/extend/mcp?surface=cli).
61
+
62
+ ### Claude Code
63
+
64
+ ```bash
65
+ claude mcp add --transport stdio mergestorm -- npx -y mergestorm-mcp
66
+ ```
67
+
68
+ Launch with `MERGESTORM_SANDBOX_ROOT` set to the checkout when local file context is needed. [Claude Code MCP configuration](https://code.claude.com/docs/en/mcp).
69
+
70
+ ### Cursor
71
+
72
+ Add to project `.cursor/mcp.json`:
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "mergestorm": {
78
+ "type": "stdio",
79
+ "command": "npx",
80
+ "args": ["-y", "mergestorm-mcp"],
81
+ "env": {
82
+ "MERGESTORM_SANDBOX_ROOT": "${workspaceFolder}"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ ```
88
+
89
+ For global `~/.cursor/mcp.json`, use an explicit sandbox path. Authentication uses the saved `mg login` on the server machine. [Cursor MCP configuration](https://cursor.com/docs/mcp).
90
+
91
+ ### Connection and watch checks
92
+
93
+ In each client, confirm the server connects and lists tools, then call `whoami`. It should return the account key prefix without exposing the full key. On a known owned stack, `stack_wait` with `timeout_s: 0` should return one snapshot; use `watch.next.args` for the next bounded wait. Default waits use 45-second slices, so leave the host tool timeout above 45 seconds.
94
+
95
+ Standard stdio calls are portable; task wakeups are host features. Claude Code supports background notifications in some session modes, while availability in Codex or Cursor depends on the host and execution mode. Confirm the actual notification facility before claiming automatic continuation. This server does not advertise Claude channels or install a Codex automation or Cursor extension. After reconnecting, reuse the last returned cursor; if it was lost, take a fresh snapshot rather than claiming unseen events were handled.
96
+
97
+ Tests exercise the SDK protocol, not the UI or notification delivery of all three apps. Client configuration and the host's ability to resume the task should be checked in that client.
98
+
99
+ ## Results and errors
100
+
101
+ Successful results retain their existing fields in `structuredContent`, including meaningful `null` values. The same data is serialized as a text block after the human summary for clients that consume only text content. Shared workflow policy stays in server initialization instructions; stack tools include a short reminder to follow the returned watch cursor.
38
102
 
39
- Requires Node.js 22+ and a Mergestorm API key (`mg login` or `MERGESTORM_API_KEY`).
103
+ Failures retain `isError: true` and a readable message. `structuredContent.error` includes the original message and, when supplied by the CLI error, `code`, `reason`, `exit_code`, and `retry_after_seconds`. Missing retry metadata does not authorize repeating a write; inspect the relevant state first. The server does not manufacture request IDs or label arbitrary transport failures safe to retry.
package/dist/index.js CHANGED
@@ -229,7 +229,7 @@ function stackWaitStatusLine(envelope) {
229
229
  ...envelope.cursor.afterFinishedAt !== void 0 ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : [],
230
230
  ...envelope.cursor.bounceId !== void 0 ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []
231
231
  ];
232
- return `${base} \xB7 watch not finished: run ${stackWatchBackgroundCommand(envelope.stackId)} as a background command and end your turn, or call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
232
+ return `${base} \xB7 watch not finished: only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId)} as a background command and end your turn; otherwise call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
233
233
  }
234
234
  async function stackWait(input, cfg, deps = {}) {
235
235
  const args = z.object(stackWaitSchema).parse(input);
@@ -756,7 +756,9 @@ import {
756
756
  function settingsSummary(settings) {
757
757
  const autoPatch = settings.auto_patch_enabled ? "on" : "off";
758
758
  const cyclone = settings.cyclone_connected ? "Cyclone connected" : "Cyclone not connected";
759
- return `auto_patch ${autoPatch} \xB7 ${cyclone}`;
759
+ if (typeof settings.stacks_ready !== "boolean") return `auto_patch ${autoPatch} \xB7 ${cyclone}`;
760
+ const stacks = settings.stacks_ready ? "stacks, merge queue and Auto land ready" : "stacks, merge queue and Auto land not ready (install Mergestorm Surge)";
761
+ return `auto_patch ${autoPatch} \xB7 ${cyclone} (auto patch) \xB7 ${stacks}`;
760
762
  }
761
763
  async function settingsGet(cfg) {
762
764
  const settings = await getSettings(cfg);
@@ -933,6 +935,8 @@ async function stackAdopt(input, cfg) {
933
935
  error: {
934
936
  code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
935
937
  message,
938
+ ...err instanceof CommandError9 ? { exit_code: err.exitCode } : {},
939
+ ...err instanceof CommandError9 && err.reason !== void 0 ? { reason: err.reason } : {},
936
940
  ...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
937
941
  }
938
942
  },
@@ -1118,15 +1122,15 @@ var MCP_STACK_DONE_INSTRUCTIONS = [
1118
1122
  "When stack work is done:",
1119
1123
  "- Stack work is done only when the stack is landed, or when the human takes it back (archives it or tells you to stop). A clean push, a submit, an adopt, or one merged layer is not done: merging a lower layer can break a layer above it.",
1120
1124
  "- Every stack_wait, stack_status, stack_adopt, and stack_set result carries watch: {done, until, reason, next, message}. While watch.done is false, keep calling stack_wait with watch.next.args (the returned cursor), 45s at a time, or hand the waiting to the background watcher below, and do not end your task. Stop only when watch.done is true.",
1121
- "- If your host cannot hold a long turn open (Cursor, Claude Code, Codex, and other turn-based agents), do not poll stack_wait inside your turn: run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on a read that failed twice (`MS-WATCH ATTENTION failed`, exit 3; tell the human the stack is no longer watched), or when watch.done is true (`MS-WATCH LANDED stack=<id> reason=<reason>`, exit 0; only reason=landed is landed). On an attention wake, read stack_status once, fix the blocker per mergestorm-bounce-watch, push, and start `mg stack watch <stack-id> --head <pushed-sha>` again. Ending your turn while the watcher runs is not ending the task. Do not subscribe to GitHub events to wait on a stack.",
1125
+ "- Only when your host has a confirmed notification that resumes this task, run `mg stack watch <stack-id>` as a background command with a notification on output matching `MS-WATCH (ATTENTION|LANDED)`, then end your turn. The watcher runs stack_wait against the Mergestorm API in slices of at most 300s, carries the cursor, stays silent while the stack is waiting or in_progress (busy[] holds and actAfter included), and costs no model tokens. It exits on attention (`MS-WATCH ATTENTION pr=<n> head=<sha> blocker=... repair=<kind>`, exit 3), on a read that failed twice (`MS-WATCH ATTENTION failed`, exit 3; tell the human the stack is no longer watched), on a closed, archived, or missing stack (`MS-WATCH ATTENTION stack=<id> reason=<reason>`, exit 3; verify missing stacks before reporting a landing), or on a confirmed landing (`MS-WATCH LANDED stack=<id> reason=landed`, exit 0). On an attention wake, read stack_status once, fix the blocker per mergestorm-bounce-watch, push, and start `mg stack watch <stack-id> --head <pushed-sha>` again. The command monitors the stack; it cannot wake an agent by itself. Without a confirmed notification, keep using stack_wait with watch.next.args, or report that automatic follow-up is unavailable before ending the turn. Ending your turn while the watcher runs is not ending the task. Do not subscribe to GitHub events to wait on a stack.",
1122
1126
  "- A layer PR shown as merged is merged into mg-stack-<n>, not main. The stack is landed only when the land PR (head mg-stack-<n>) merges into main or watch.done says landed. Never judge landing from GitHub PR state alone.",
1123
- "- attention means something for you to fix; repair, when present, names the concrete fix. A pending land gate (landGatePending: ci_pending, tempest_pending, tempest_running) is a wait, not attention."
1127
+ "- attention means something for you to fix; repair, when present, names the concrete fix. A pending land gate (landGatePending: ci_pending) is a wait, not attention."
1124
1128
  ].join("\n");
1125
1129
  var MCP_STACK_BASE_INSTRUCTIONS = [
1126
1130
  "Stack PR bases (Mergestorm sets these; leave them):",
1127
1131
  "- Stack of 2+ PRs: after adopt (stack_adopt, mg stack adopt, or mg stack submit) the bottom PR's GitHub base is the owned trunk mg-stack-<n>, not main. That is correct. Layer 2 is based on layer 1; layer 3+ on the mg-park-* freeze; the git parent of layer 2+ is still the layer below. A 1-PR stack stays on main and gets no mg-stack-<n>.",
1128
1132
  "- Never retarget a stack PR's GitHub base (gh pr edit --base, a REST base change, or the UI). Moving the bottom to main makes Mergestorm abandon the stack's review unit; setting mg-stack-<n> again does not restore it. If a base was already moved, stop and tell the human; do not run stack land or re-adopt to repair it.",
1129
- "- mg-stack-<n> is the bottom PR's live parent: merging it into the bottom branch is that PR's conflict repair (bounce-watch rules). Never push to mg-stack-<n> except as the land PR's own repair: once every layer is promoted, the land PR's head is mg-stack-<n> and its live parent is its base (main), so merging main into mg-stack-<n>, a CI fix on it, or a fix for tempest_findings on it, with an ordinary push (no force) is that repair. Fix tempest_findings only when busy[] shows no Cyclone run on the land PR, and verify each finding first, as for Vortex findings. Never rebase or merge layer 2+ onto mg-stack-<n> or onto main. mg-park-* is never a merge or rebase target.",
1133
+ "- mg-stack-<n> is the bottom PR's live parent: merging it into the bottom branch is that PR's conflict repair (bounce-watch rules). Never push to mg-stack-<n> except as the land PR's own repair: once every layer is promoted, the land PR's head is mg-stack-<n> and its live parent is its base (main), so merging main into mg-stack-<n>, or a CI fix on it, with an ordinary push (no force) is that repair. Fix the land PR only when busy[] shows no Cyclone run on it. Never rebase or merge layer 2+ onto mg-stack-<n> or onto main. mg-park-* is never a merge or rebase target.",
1130
1134
  "- mg stack submit prints the base each PR opened with; the bottom moves to mg-stack-<n> right after. stack_status trunkBranch is the current trunk."
1131
1135
  ].join("\n");
1132
1136
  var MCP_STACK_WATCH_INSTRUCTIONS = [
@@ -1149,47 +1153,48 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
1149
1153
  `- While the stack has a live merge queue entry, stack_wait carries queueWait (the entry's wait reason, how long it has been held, and the stack queued ahead of it) and the status line names it. The watch returns attention, blocker "Merge queue stuck: ..." and repair kind queue_stalled, when the entry (or the entry ahead of it) has been held for 45 minutes without landing or bouncing, or has sat queued for 45 minutes with nothing working on it or ahead of it.`,
1150
1154
  '- When no Vortex run works on the head and its review was skipped because auto-review is off (blocker "Vortex auto-review is off, so this head was not reviewed") or because billing could not be confirmed ("Vortex skipped this head: it could not confirm billing"), or it covered only part of the PR ("Vortex reviewed only part of this PR"), the watch returns attention at once with repair kind vortex_skipped. Do not patch for it; the repair names the mention or setting that gets the head reviewed.',
1151
1155
  '- A live-head Vortex request_changes handoff returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify the review and inline findings against the live head before repair.',
1152
- "- Tempest never patches. Cyclone patches a land PR when auto-patch is on for that stack (its override, else the account setting); otherwise the watching agent fixes tempest_findings on the land PR, and repair kind tempest_findings names it. A Tempest run that breaks without a result is re-run by Mergestorm at the same head a few times; after that the land gate reads tempest_failed with repair kind tempest_rerun: there is nothing to patch, so re-run Tempest with the comment the repair names. tempest_stopped (a person stopped the review) also carries repair kind tempest_rerun: ask the human before re-running it.",
1153
- '- More states the watch names: a Vortex review skipped because the PR author is on the ignored authors list or because reviews are turned off for the repository is attention at once with repair kind vortex_skipped; a Vortex retry that is scheduled ("Vortex hit a temporary error on this head and will retry by itself") is a wait listed in issues[], never attention; "Review failed" carries repair kind vortex_failed (comment @mergestorm-vortex review or push a new commit; if it fails again tell the human); a PR open on mg-stack-N that is not a layer of the stack is attention at once, blocker "The land PR cannot be queued: ...", repair kind members_remaining (do not patch or push; close, retarget or adopt that PR); tempest_skipped on the land PR carries repair kind tempest_rerun (comment @mergestorm-tempest review); tempest_trigger_failed carries repair kind tempest_trigger_failed (Mergestorm keeps trying every 5 minutes; nothing to fix in the PR; if it stays, tell the human).',
1156
+ "- Cyclone patches a land PR when auto-patch is on for that stack (its override, else the account setting); otherwise the watching agent fixes the land PR's blocker on mg-stack-<n>.",
1157
+ '- More states the watch names: a Vortex review skipped because the PR author is on the ignored authors list or because reviews are turned off for the repository is attention at once with repair kind vortex_skipped; a Vortex retry that is scheduled ("Vortex hit a temporary error on this head and will retry by itself") is a wait listed in issues[], never attention; "Review failed" carries repair kind vortex_failed (comment @mergestorm-vortex review or push a new commit; if it fails again tell the human); a PR open on mg-stack-N that is not a layer of the stack is attention at once, blocker "The land PR cannot be queued: ...", repair kind members_remaining (do not patch or push; close, retarget or adopt that PR).',
1154
1158
  "- Use the mergestorm-bounce-watch skill for the fix-then-watch policy."
1155
1159
  ].join("\n");
1156
1160
 
1157
1161
  // src/server.ts
1158
1162
  var MCP_SERVER_NAME = "mergestorm";
1159
- var MCP_SERVER_VERSION = "0.2.11";
1163
+ var MCP_SERVER_VERSION = "0.2.13";
1160
1164
  var MCP_SERVER_INSTRUCTIONS = [
1161
1165
  MCP_STACK_DONE_INSTRUCTIONS,
1162
1166
  MCP_PR_LOOP_INSTRUCTIONS,
1163
1167
  MCP_STACK_BASE_INSTRUCTIONS,
1164
1168
  MCP_STACK_WATCH_INSTRUCTIONS
1165
1169
  ].join("\n\n");
1166
- var STACK_WATCH_RESULT_NOTE = ' The result carries watch {done, until: "landed", reason, next, message}: while watch.done is false the stack is not landed and your task is not done; call stack_wait with watch.next.args, or run watch.next.background as a background command and end your turn.';
1167
- function stripNulls(value) {
1168
- if (Array.isArray(value)) {
1169
- return value.map((item) => stripNulls(item));
1170
- }
1171
- if (value !== null && typeof value === "object") {
1172
- const out = {};
1173
- for (const [key, val] of Object.entries(value)) {
1174
- if (val !== null && val !== void 0) out[key] = stripNulls(val);
1175
- }
1176
- return out;
1177
- }
1178
- return value;
1179
- }
1170
+ var STACK_WATCH_RESULT_NOTE = " Follow watch.next.args until watch.done; background watching requires a confirmed host notification. See server instructions.";
1180
1171
  function ok(payload) {
1181
1172
  return {
1182
1173
  ...payload.isError ? { isError: true } : {},
1183
- content: [{ type: "text", text: payload.summary }],
1184
- structuredContent: stripNulls(payload.data)
1174
+ content: [
1175
+ { type: "text", text: payload.summary },
1176
+ { type: "text", text: JSON.stringify(payload.data) }
1177
+ ],
1178
+ structuredContent: payload.data
1185
1179
  };
1186
1180
  }
1187
1181
  function fail(err) {
1188
- const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1189
- return {
1182
+ const message = err instanceof CommandError11 ? err.code ? `Mergestorm request failed (${err.code}${err.reason ? `, reason ${err.reason}` : ""}): ${err.message}.` : err.message : err instanceof Error ? err.message : String(err);
1183
+ return ok({
1190
1184
  isError: true,
1191
- content: [{ type: "text", text: message }]
1192
- };
1185
+ summary: message,
1186
+ data: {
1187
+ error: {
1188
+ message: err instanceof Error ? err.message : String(err),
1189
+ ...err instanceof CommandError11 ? {
1190
+ exit_code: err.exitCode,
1191
+ ...err.code !== void 0 ? { code: err.code } : {},
1192
+ ...err.reason !== void 0 ? { reason: err.reason } : {},
1193
+ ...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
1194
+ } : {}
1195
+ }
1196
+ }
1197
+ });
1193
1198
  }
1194
1199
  function createMergestormMcpServer() {
1195
1200
  const server2 = new McpServer(
@@ -1208,7 +1213,7 @@ function createMergestormMcpServer() {
1208
1213
  {
1209
1214
  title: "Who am I",
1210
1215
  description: "Current Mergestorm API key prefix, plan, and API base.",
1211
- annotations: { readOnlyHint: true }
1216
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1212
1217
  },
1213
1218
  async () => {
1214
1219
  try {
@@ -1223,7 +1228,7 @@ function createMergestormMcpServer() {
1223
1228
  {
1224
1229
  title: "Credits",
1225
1230
  description: "Current standard-credit usage and reset time.",
1226
- annotations: { readOnlyHint: true }
1231
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1227
1232
  },
1228
1233
  async () => {
1229
1234
  try {
@@ -1241,7 +1246,7 @@ function createMergestormMcpServer() {
1241
1246
  inputSchema: {
1242
1247
  limit: z3.number().int().min(1).max(50).optional()
1243
1248
  },
1244
- annotations: { readOnlyHint: true }
1249
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1245
1250
  },
1246
1251
  async ({ limit }) => {
1247
1252
  try {
@@ -1289,7 +1294,7 @@ function createMergestormMcpServer() {
1289
1294
  job_id: z3.string().min(1),
1290
1295
  timeout_s: z3.number().min(0).max(300).optional()
1291
1296
  },
1292
- annotations: { readOnlyHint: true }
1297
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1293
1298
  },
1294
1299
  async ({ job_id, timeout_s }) => {
1295
1300
  try {
@@ -1313,7 +1318,7 @@ function createMergestormMcpServer() {
1313
1318
  after_pass: z3.number().int().min(1).optional(),
1314
1319
  timeout_s: z3.number().min(0).max(300).optional()
1315
1320
  },
1316
- annotations: { readOnlyHint: true }
1321
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1317
1322
  },
1318
1323
  async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
1319
1324
  try {
@@ -1350,7 +1355,7 @@ function createMergestormMcpServer() {
1350
1355
  evidence_url: z3.string().url().optional(),
1351
1356
  preview: z3.boolean().optional()
1352
1357
  },
1353
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1358
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1354
1359
  },
1355
1360
  async (args) => {
1356
1361
  try {
@@ -1364,9 +1369,9 @@ function createMergestormMcpServer() {
1364
1369
  "stack_adopt",
1365
1370
  {
1366
1371
  title: "Adopt PR stack",
1367
- description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption requires a Cyclone GitHub App install; without it the API returns cyclone_not_connected \u2014 do not retry, tell the human. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + STACK_WATCH_RESULT_NOTE,
1372
+ description: "Adopt an open GitHub PR chain into a Mergestorm stack. auto_land is boolean; auto_review and auto_patch accept true, false, or null, where null clears the override. Omitted auto_land seeds from auto_land_default; omitted auto_review and auto_patch overrides are not seeded. Never changes account settings. Adoption needs the Mergestorm Surge GitHub App on the repository. Stacks, the merge queue and Auto land run through that App; Cyclone is the auto-patch App. Without it the API returns cyclone_not_connected or cyclone_not_installed; the message, and error.reason starting with surge_ when Surge is the one to install, say which App is missing \u2014 do not retry, tell the human what the message says. Read stack_status with result.stack.id afterward to verify policy and Cyclone ownership. With 2+ PRs, adopt moves the bottom PR's GitHub base to mg-stack-<n> and parks PR3+ on an mg-park-* freeze; a 1-PR stack stays on main. Leave those bases. Never retarget the bottom back to main." + STACK_WATCH_RESULT_NOTE,
1368
1373
  inputSchema: stackAdoptSchema,
1369
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
1374
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1370
1375
  },
1371
1376
  async (args) => {
1372
1377
  try {
@@ -1381,7 +1386,7 @@ function createMergestormMcpServer() {
1381
1386
  {
1382
1387
  title: "List stacks",
1383
1388
  description: "List the current user's Mergestorm stacks.",
1384
- annotations: { readOnlyHint: true }
1389
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1385
1390
  },
1386
1391
  async () => {
1387
1392
  try {
@@ -1402,7 +1407,7 @@ function createMergestormMcpServer() {
1402
1407
  auto_review: z3.boolean().nullable().optional(),
1403
1408
  auto_patch: z3.boolean().nullable().optional()
1404
1409
  },
1405
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1410
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1406
1411
  },
1407
1412
  async ({ stack_id, auto_land, auto_review, auto_patch }) => {
1408
1413
  try {
@@ -1426,7 +1431,7 @@ function createMergestormMcpServer() {
1426
1431
  inputSchema: {
1427
1432
  stack_id: z3.string().min(1)
1428
1433
  },
1429
- annotations: { readOnlyHint: true }
1434
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1430
1435
  },
1431
1436
  async ({ stack_id }) => {
1432
1437
  try {
@@ -1442,12 +1447,12 @@ function createMergestormMcpServer() {
1442
1447
  title: "Wait for stack attention",
1443
1448
  description: `Wait for a stack to need attention. Returns a mergestorm.stack_watch/v1 envelope; timeout_s: 0 returns one snapshot and timeout_s: 1-45 waits up to that many seconds, returning the current status (waiting or in_progress) if a snapshot was read and no attention is found. A timeout with either of those statuses is not a failure; waiting and in_progress end this slice, and the next call is stack_wait again with the same cursor. A timeout of failed means no assessment was produced. A stack that no longer exists returns status failed with watch.done true (reason not_found) and is not an error. While Cyclone or Vortex is still working on the blocked PR it returns in_progress, not attention, with busy[] naming the PR, blocker, and agent; agents carries that PR's vortexStatus, cycloneStatus, vortexReview, and busy flags. A blocker no agent run clears (a merge conflict vs the live parent, a restack Conflict or Restack failed, a Draft PR, a seam review pending with no review running, "Seam review stuck in reviewing, no review running", "Seam re-review pending, no review running", "Seam verdict is for an older head, no review running", or a review skipped for review quota) is still named during that in_progress, in blocker, prNumber, headSha, and issues[], with actAfter: "agents_idle" and waitingOn listing the busy agents: plan the fix, and act when a snapshot after the agents finish returns attention, re-reading the blocker then. Blockers the agents can change keep blocker and actAfter null. Attention never calls for changing a PR's GitHub base. When present, repair names the concrete fix for attention (restack_conflict and merge_conflict carry liveParent, never mg-park-*). For the three stuck-seam blockers, repair kind seam_review_stuck: do not patch or push; tell the human to click Continue, which re-queues the seam review at the live head. A pending land gate (landGatePending) returns in_progress, not attention. Auto land off after a merge_failed, gh_error, or ci_timeout bounce (the stack's autoLandOff reads reason bounced) returns attention with the blocker "Auto land off after <kind> bounce", the bounce in issues[], and a repair of kind auto_land_off: fix the cause, then turn Auto land back on or enqueue. A Vortex request_changes handoff at the live head returns attention with blocker "Vortex findings need a person" and repair kind vortex_findings; verify its findings against the live head.` + STACK_WATCH_RESULT_NOTE,
1444
1449
  inputSchema: stackWaitSchema,
1445
- annotations: { readOnlyHint: true }
1450
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1446
1451
  },
1447
1452
  async (args, extra) => {
1448
1453
  try {
1449
1454
  const payload = await stackWait(args, void 0, { signal: extra.signal });
1450
- return { ...ok(payload), structuredContent: payload.data };
1455
+ return ok(payload);
1451
1456
  } catch (err) {
1452
1457
  return fail(err);
1453
1458
  }
@@ -1461,7 +1466,7 @@ function createMergestormMcpServer() {
1461
1466
  inputSchema: {
1462
1467
  stack_id: z3.string().min(1).optional()
1463
1468
  },
1464
- annotations: { readOnlyHint: true }
1469
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1465
1470
  },
1466
1471
  async ({ stack_id }) => {
1467
1472
  try {
@@ -1475,8 +1480,8 @@ function createMergestormMcpServer() {
1475
1480
  "settings_get",
1476
1481
  {
1477
1482
  title: "Get settings",
1478
- description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), and cyclone_connected.",
1479
- annotations: { readOnlyHint: true }
1483
+ description: "Read the Bearer /api/v1/settings toggles, including auto_patch_enabled, auto_land_settle_seconds (how long Auto land waits before queueing), merge_queue_batch_enabled and merge_queue_batch_size (whether the merge queue tests several pull requests on one CI run, and how many), cyclone_connected (the Cyclone auto-patch App), and stacks_ready (whether stacks, the merge queue and Auto land can run for this account, which needs Mergestorm Surge).",
1484
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1480
1485
  },
1481
1486
  async () => {
1482
1487
  try {
@@ -1502,7 +1507,7 @@ function createMergestormMcpServer() {
1502
1507
  cyclone_connected: z3.unknown().optional(),
1503
1508
  github_connected: z3.unknown().optional()
1504
1509
  },
1505
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1510
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1506
1511
  },
1507
1512
  async (args) => {
1508
1513
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@modelcontextprotocol/sdk": "^1.30.0",
32
- "mergestorm": "0.3.39",
32
+ "mergestorm": "0.3.41",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {