mergestorm-mcp 0.2.12 → 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 +41 -39
  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. Adoption needs the infrastructure GitHub App on the repository: Mergestorm Surge, or Cyclone on accounts not yet moved to Surge (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.
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);
@@ -935,6 +935,7 @@ async function stackAdopt(input, cfg) {
935
935
  error: {
936
936
  code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
937
937
  message,
938
+ ...err instanceof CommandError9 ? { exit_code: err.exitCode } : {},
938
939
  ...err instanceof CommandError9 && err.reason !== void 0 ? { reason: err.reason } : {},
939
940
  ...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
940
941
  }
@@ -1121,7 +1122,7 @@ var MCP_STACK_DONE_INSTRUCTIONS = [
1121
1122
  "When stack work is done:",
1122
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.",
1123
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.",
1124
- "- 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.",
1125
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.",
1126
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."
1127
1128
  ].join("\n");
@@ -1159,40 +1160,41 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
1159
1160
 
1160
1161
  // src/server.ts
1161
1162
  var MCP_SERVER_NAME = "mergestorm";
1162
- var MCP_SERVER_VERSION = "0.2.12";
1163
+ var MCP_SERVER_VERSION = "0.2.13";
1163
1164
  var MCP_SERVER_INSTRUCTIONS = [
1164
1165
  MCP_STACK_DONE_INSTRUCTIONS,
1165
1166
  MCP_PR_LOOP_INSTRUCTIONS,
1166
1167
  MCP_STACK_BASE_INSTRUCTIONS,
1167
1168
  MCP_STACK_WATCH_INSTRUCTIONS
1168
1169
  ].join("\n\n");
1169
- 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.';
1170
- function stripNulls(value) {
1171
- if (Array.isArray(value)) {
1172
- return value.map((item) => stripNulls(item));
1173
- }
1174
- if (value !== null && typeof value === "object") {
1175
- const out = {};
1176
- for (const [key, val] of Object.entries(value)) {
1177
- if (val !== null && val !== void 0) out[key] = stripNulls(val);
1178
- }
1179
- return out;
1180
- }
1181
- return value;
1182
- }
1170
+ var STACK_WATCH_RESULT_NOTE = " Follow watch.next.args until watch.done; background watching requires a confirmed host notification. See server instructions.";
1183
1171
  function ok(payload) {
1184
1172
  return {
1185
1173
  ...payload.isError ? { isError: true } : {},
1186
- content: [{ type: "text", text: payload.summary }],
1187
- 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
1188
1179
  };
1189
1180
  }
1190
1181
  function fail(err) {
1191
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);
1192
- return {
1183
+ return ok({
1193
1184
  isError: true,
1194
- content: [{ type: "text", text: message }]
1195
- };
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
+ });
1196
1198
  }
1197
1199
  function createMergestormMcpServer() {
1198
1200
  const server2 = new McpServer(
@@ -1211,7 +1213,7 @@ function createMergestormMcpServer() {
1211
1213
  {
1212
1214
  title: "Who am I",
1213
1215
  description: "Current Mergestorm API key prefix, plan, and API base.",
1214
- annotations: { readOnlyHint: true }
1216
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1215
1217
  },
1216
1218
  async () => {
1217
1219
  try {
@@ -1226,7 +1228,7 @@ function createMergestormMcpServer() {
1226
1228
  {
1227
1229
  title: "Credits",
1228
1230
  description: "Current standard-credit usage and reset time.",
1229
- annotations: { readOnlyHint: true }
1231
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1230
1232
  },
1231
1233
  async () => {
1232
1234
  try {
@@ -1244,7 +1246,7 @@ function createMergestormMcpServer() {
1244
1246
  inputSchema: {
1245
1247
  limit: z3.number().int().min(1).max(50).optional()
1246
1248
  },
1247
- annotations: { readOnlyHint: true }
1249
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1248
1250
  },
1249
1251
  async ({ limit }) => {
1250
1252
  try {
@@ -1292,7 +1294,7 @@ function createMergestormMcpServer() {
1292
1294
  job_id: z3.string().min(1),
1293
1295
  timeout_s: z3.number().min(0).max(300).optional()
1294
1296
  },
1295
- annotations: { readOnlyHint: true }
1297
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1296
1298
  },
1297
1299
  async ({ job_id, timeout_s }) => {
1298
1300
  try {
@@ -1316,7 +1318,7 @@ function createMergestormMcpServer() {
1316
1318
  after_pass: z3.number().int().min(1).optional(),
1317
1319
  timeout_s: z3.number().min(0).max(300).optional()
1318
1320
  },
1319
- annotations: { readOnlyHint: true }
1321
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1320
1322
  },
1321
1323
  async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
1322
1324
  try {
@@ -1353,7 +1355,7 @@ function createMergestormMcpServer() {
1353
1355
  evidence_url: z3.string().url().optional(),
1354
1356
  preview: z3.boolean().optional()
1355
1357
  },
1356
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1358
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1357
1359
  },
1358
1360
  async (args) => {
1359
1361
  try {
@@ -1367,9 +1369,9 @@ function createMergestormMcpServer() {
1367
1369
  "stack_adopt",
1368
1370
  {
1369
1371
  title: "Adopt PR stack",
1370
- 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 infrastructure GitHub App on the repository: Mergestorm Surge, or Cyclone on accounts not yet moved to Surge. 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,
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,
1371
1373
  inputSchema: stackAdoptSchema,
1372
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false }
1374
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1373
1375
  },
1374
1376
  async (args) => {
1375
1377
  try {
@@ -1384,7 +1386,7 @@ function createMergestormMcpServer() {
1384
1386
  {
1385
1387
  title: "List stacks",
1386
1388
  description: "List the current user's Mergestorm stacks.",
1387
- annotations: { readOnlyHint: true }
1389
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1388
1390
  },
1389
1391
  async () => {
1390
1392
  try {
@@ -1405,7 +1407,7 @@ function createMergestormMcpServer() {
1405
1407
  auto_review: z3.boolean().nullable().optional(),
1406
1408
  auto_patch: z3.boolean().nullable().optional()
1407
1409
  },
1408
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1410
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
1409
1411
  },
1410
1412
  async ({ stack_id, auto_land, auto_review, auto_patch }) => {
1411
1413
  try {
@@ -1429,7 +1431,7 @@ function createMergestormMcpServer() {
1429
1431
  inputSchema: {
1430
1432
  stack_id: z3.string().min(1)
1431
1433
  },
1432
- annotations: { readOnlyHint: true }
1434
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1433
1435
  },
1434
1436
  async ({ stack_id }) => {
1435
1437
  try {
@@ -1445,12 +1447,12 @@ function createMergestormMcpServer() {
1445
1447
  title: "Wait for stack attention",
1446
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,
1447
1449
  inputSchema: stackWaitSchema,
1448
- annotations: { readOnlyHint: true }
1450
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1449
1451
  },
1450
1452
  async (args, extra) => {
1451
1453
  try {
1452
1454
  const payload = await stackWait(args, void 0, { signal: extra.signal });
1453
- return { ...ok(payload), structuredContent: payload.data };
1455
+ return ok(payload);
1454
1456
  } catch (err) {
1455
1457
  return fail(err);
1456
1458
  }
@@ -1464,7 +1466,7 @@ function createMergestormMcpServer() {
1464
1466
  inputSchema: {
1465
1467
  stack_id: z3.string().min(1).optional()
1466
1468
  },
1467
- annotations: { readOnlyHint: true }
1469
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
1468
1470
  },
1469
1471
  async ({ stack_id }) => {
1470
1472
  try {
@@ -1478,8 +1480,8 @@ function createMergestormMcpServer() {
1478
1480
  "settings_get",
1479
1481
  {
1480
1482
  title: "Get settings",
1481
- 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, when the server sends it, stacks_ready (whether stacks, the merge queue and Auto land can run for this account, through Mergestorm Surge or, on accounts not yet moved, Cyclone).",
1482
- 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 }
1483
1485
  },
1484
1486
  async () => {
1485
1487
  try {
@@ -1505,7 +1507,7 @@ function createMergestormMcpServer() {
1505
1507
  cyclone_connected: z3.unknown().optional(),
1506
1508
  github_connected: z3.unknown().optional()
1507
1509
  },
1508
- annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true }
1510
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
1509
1511
  },
1510
1512
  async (args) => {
1511
1513
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mergestorm-mcp",
3
- "version": "0.2.12",
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.40",
32
+ "mergestorm": "0.3.41",
33
33
  "zod": "^3.24.2"
34
34
  },
35
35
  "devDependencies": {