mergestorm-mcp 0.2.12 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -7
- package/dist/index.js +55 -46
- 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
|
|
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
|
|
|
@@ -13,27 +13,91 @@ Every tool declares MCP `annotations`. The reads carry `readOnlyHint: true`; `re
|
|
|
13
13
|
- **Authorization.** The API key's account must monitor the repository or own the PR's stack, and its linked GitHub account needs write access to the repository.
|
|
14
14
|
- **Refusals.** A moved head (`stale_head`), another review (`review_mismatch`, `review_not_found`), an id outside that review (`unknown_finding`), a thin `reason` (`invalid_input`) or a missing permission (`forbidden`) is refused, and nothing is written.
|
|
15
15
|
- **Audit and retries.** Each finding is stored with the actor's GitHub login, the reason, `evidence_url`, the scope and the time. A retry reports the earlier record under `already_dismissed`.
|
|
16
|
-
- **Gates.** `gate.seam.cleared` is true only when every finding of that integration review is dismissed and it is still the member's current seam verdict.
|
|
16
|
+
- **Gates.** `gate.seam.cleared` is true only when every finding of that integration review is dismissed and it is still the member's current seam verdict. `gate.vortex.cleared` is true once every Vortex Blocker (error finding) at the live head is dismissed (every Core review merged into that head) and no Vortex review of the head is running or incomplete; with a warning still open the head reads `comment`, which Auto land accepts; otherwise `gate.vortex.reason` and `message` say what still holds it: that head's Vortex verdict becomes approve for Auto land, the stack watch, the dashboard and Cyclone. A partial dismissal clears nothing. CI and Auto land policy are unchanged (`gate.other_gates: "unchanged"`). For a stack PR the result carries `watch` with the next `stack_wait` call.
|
|
17
17
|
- **No resurfacing.** Vortex does not raise a dismissed finding again on the same diff: the same head, or a restack whose diff is unchanged.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
44
|
+
codex mcp add mergestorm -- npx -y mergestorm-mcp
|
|
35
45
|
```
|
|
36
46
|
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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);
|
|
@@ -292,6 +292,12 @@ function reviewDismissSummary(result) {
|
|
|
292
292
|
seam.cleared ? "Seam gate cleared at this head. CI, other reviews and Auto land policy are unchanged." : seam.blocking ? `Seam gate still blocking (seam_state=${seam.state}).` : `Seam gate not blocking (seam_state=${seam.state}).`
|
|
293
293
|
);
|
|
294
294
|
}
|
|
295
|
+
const vortex = result.gate.vortex;
|
|
296
|
+
if (vortex?.cleared) {
|
|
297
|
+
lines.push("Vortex gate cleared at this head: every Vortex Blocker (error finding) at this head is dismissed. CI and Auto land policy are unchanged.");
|
|
298
|
+
} else if (vortex?.message && vortex.reason !== "review_findings_open") {
|
|
299
|
+
lines.push(`Vortex gate not cleared: ${vortex.message}`);
|
|
300
|
+
}
|
|
295
301
|
return lines.join("\n");
|
|
296
302
|
}
|
|
297
303
|
async function reviewDismiss(input, cfg) {
|
|
@@ -935,6 +941,7 @@ async function stackAdopt(input, cfg) {
|
|
|
935
941
|
error: {
|
|
936
942
|
code: err instanceof CommandError9 ? err.code ?? "stack_adopt_failed" : "stack_adopt_failed",
|
|
937
943
|
message,
|
|
944
|
+
...err instanceof CommandError9 ? { exit_code: err.exitCode } : {},
|
|
938
945
|
...err instanceof CommandError9 && err.reason !== void 0 ? { reason: err.reason } : {},
|
|
939
946
|
...err instanceof CommandError9 && err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
940
947
|
}
|
|
@@ -1075,7 +1082,7 @@ import {
|
|
|
1075
1082
|
loadConfig as loadConfig7,
|
|
1076
1083
|
resolveApiKey
|
|
1077
1084
|
} from "mergestorm/client";
|
|
1078
|
-
async function whoami(cfg) {
|
|
1085
|
+
async function whoami(cfg, serverVersion) {
|
|
1079
1086
|
const resolved = cfg ?? await loadConfig7();
|
|
1080
1087
|
const key = resolveApiKey(resolved);
|
|
1081
1088
|
if (!key) {
|
|
@@ -1096,11 +1103,12 @@ async function whoami(cfg) {
|
|
|
1096
1103
|
plan_label_key: me.plan_label_key ?? null,
|
|
1097
1104
|
api_base: apiBase(resolved),
|
|
1098
1105
|
config_path: configPath(),
|
|
1099
|
-
usage: me.usage ?? null
|
|
1106
|
+
usage: me.usage ?? null,
|
|
1107
|
+
mcp_server_version: serverVersion ?? null
|
|
1100
1108
|
};
|
|
1101
1109
|
const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
|
|
1102
1110
|
return {
|
|
1103
|
-
summary: `${data.key_prefix} \xB7 ${plan}`,
|
|
1111
|
+
summary: `${data.key_prefix} \xB7 ${plan}${serverVersion ? ` \xB7 mergestorm-mcp ${serverVersion}` : ""}`,
|
|
1104
1112
|
data
|
|
1105
1113
|
};
|
|
1106
1114
|
}
|
|
@@ -1114,14 +1122,14 @@ var MCP_PR_LOOP_INSTRUCTIONS = [
|
|
|
1114
1122
|
"- Patch concrete bugs. A chat-only explanation is not a dismiss.",
|
|
1115
1123
|
`- If you skip a finding (not reproducible, policy fork, or needs a human), post a public GitHub PR comment before you stop. First line must be: ${MCP_FINDING_DISMISS_MARKER}. Then one line per skipped finding (path, severity, why).`,
|
|
1116
1124
|
"- Do not prefix that comment with cyclone-outcome: (reserved).",
|
|
1117
|
-
"- A finding you verified is wrong can be dismissed on the record with review_dismiss (or mg dismiss): pass the PR's live head_sha, the review_id, the finding_ids (preview: true lists them) and a reason. That is the only agent path that clears a seam gate, and only once every finding of that seam review is dismissed. The dismiss comment alone resumes the review but does not clear the seam gate.",
|
|
1125
|
+
"- A finding you verified is wrong can be dismissed on the record with review_dismiss (or mg dismiss): pass the PR's live head_sha, the review_id, the finding_ids (preview: true lists them) and a reason. That is the only agent path that clears a seam gate, and only once every finding of that seam review is dismissed. Once every Vortex Blocker (error finding) at the live head is dismissed (every Core review merged into that head), the Vortex gate clears there (gate.vortex.cleared); otherwise gate.vortex.message says what still holds it. The dismiss comment alone resumes the review but does not clear the seam gate.",
|
|
1118
1126
|
"- Never dismiss a finding you did not check, one a human must decide, or findings of a newer review."
|
|
1119
1127
|
].join("\n");
|
|
1120
1128
|
var MCP_STACK_DONE_INSTRUCTIONS = [
|
|
1121
1129
|
"When stack work is done:",
|
|
1122
1130
|
"- 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
1131
|
"- 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
|
-
"-
|
|
1132
|
+
"- 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. A confirmed notification is a host feature you can name that turns the command's exit or matching output into your next turn; a process left running, or output written to a log or terminal you do not read, is not one. 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
1133
|
"- 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
1134
|
"- 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
1135
|
].join("\n");
|
|
@@ -1159,40 +1167,41 @@ var MCP_STACK_WATCH_INSTRUCTIONS = [
|
|
|
1159
1167
|
|
|
1160
1168
|
// src/server.ts
|
|
1161
1169
|
var MCP_SERVER_NAME = "mergestorm";
|
|
1162
|
-
var MCP_SERVER_VERSION = "0.2.
|
|
1170
|
+
var MCP_SERVER_VERSION = "0.2.14";
|
|
1163
1171
|
var MCP_SERVER_INSTRUCTIONS = [
|
|
1164
1172
|
MCP_STACK_DONE_INSTRUCTIONS,
|
|
1165
1173
|
MCP_PR_LOOP_INSTRUCTIONS,
|
|
1166
1174
|
MCP_STACK_BASE_INSTRUCTIONS,
|
|
1167
1175
|
MCP_STACK_WATCH_INSTRUCTIONS
|
|
1168
1176
|
].join("\n\n");
|
|
1169
|
-
var STACK_WATCH_RESULT_NOTE =
|
|
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
|
-
}
|
|
1177
|
+
var STACK_WATCH_RESULT_NOTE = " Follow watch.next.args until watch.done; background watching requires a confirmed host notification. See server instructions.";
|
|
1183
1178
|
function ok(payload) {
|
|
1184
1179
|
return {
|
|
1185
1180
|
...payload.isError ? { isError: true } : {},
|
|
1186
|
-
content: [
|
|
1187
|
-
|
|
1181
|
+
content: [
|
|
1182
|
+
{ type: "text", text: payload.summary },
|
|
1183
|
+
{ type: "text", text: JSON.stringify(payload.data) }
|
|
1184
|
+
],
|
|
1185
|
+
structuredContent: payload.data
|
|
1188
1186
|
};
|
|
1189
1187
|
}
|
|
1190
1188
|
function fail(err) {
|
|
1191
1189
|
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 {
|
|
1190
|
+
return ok({
|
|
1193
1191
|
isError: true,
|
|
1194
|
-
|
|
1195
|
-
|
|
1192
|
+
summary: message,
|
|
1193
|
+
data: {
|
|
1194
|
+
error: {
|
|
1195
|
+
message: err instanceof Error ? err.message : String(err),
|
|
1196
|
+
...err instanceof CommandError11 ? {
|
|
1197
|
+
exit_code: err.exitCode,
|
|
1198
|
+
...err.code !== void 0 ? { code: err.code } : {},
|
|
1199
|
+
...err.reason !== void 0 ? { reason: err.reason } : {},
|
|
1200
|
+
...err.retryAfterSeconds !== void 0 ? { retry_after_seconds: err.retryAfterSeconds } : {}
|
|
1201
|
+
} : {}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1196
1205
|
}
|
|
1197
1206
|
function createMergestormMcpServer() {
|
|
1198
1207
|
const server2 = new McpServer(
|
|
@@ -1210,12 +1219,12 @@ function createMergestormMcpServer() {
|
|
|
1210
1219
|
"whoami",
|
|
1211
1220
|
{
|
|
1212
1221
|
title: "Who am I",
|
|
1213
|
-
description: "Current Mergestorm API key prefix, plan, and
|
|
1214
|
-
annotations: { readOnlyHint: true }
|
|
1222
|
+
description: "Current Mergestorm API key prefix, plan, API base, and the version of this running mergestorm-mcp server.",
|
|
1223
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1215
1224
|
},
|
|
1216
1225
|
async () => {
|
|
1217
1226
|
try {
|
|
1218
|
-
return ok(await whoami());
|
|
1227
|
+
return ok(await whoami(void 0, MCP_SERVER_VERSION));
|
|
1219
1228
|
} catch (err) {
|
|
1220
1229
|
return fail(err);
|
|
1221
1230
|
}
|
|
@@ -1226,7 +1235,7 @@ function createMergestormMcpServer() {
|
|
|
1226
1235
|
{
|
|
1227
1236
|
title: "Credits",
|
|
1228
1237
|
description: "Current standard-credit usage and reset time.",
|
|
1229
|
-
annotations: { readOnlyHint: true }
|
|
1238
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1230
1239
|
},
|
|
1231
1240
|
async () => {
|
|
1232
1241
|
try {
|
|
@@ -1244,7 +1253,7 @@ function createMergestormMcpServer() {
|
|
|
1244
1253
|
inputSchema: {
|
|
1245
1254
|
limit: z3.number().int().min(1).max(50).optional()
|
|
1246
1255
|
},
|
|
1247
|
-
annotations: { readOnlyHint: true }
|
|
1256
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1248
1257
|
},
|
|
1249
1258
|
async ({ limit }) => {
|
|
1250
1259
|
try {
|
|
@@ -1292,7 +1301,7 @@ function createMergestormMcpServer() {
|
|
|
1292
1301
|
job_id: z3.string().min(1),
|
|
1293
1302
|
timeout_s: z3.number().min(0).max(300).optional()
|
|
1294
1303
|
},
|
|
1295
|
-
annotations: { readOnlyHint: true }
|
|
1304
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1296
1305
|
},
|
|
1297
1306
|
async ({ job_id, timeout_s }) => {
|
|
1298
1307
|
try {
|
|
@@ -1316,7 +1325,7 @@ function createMergestormMcpServer() {
|
|
|
1316
1325
|
after_pass: z3.number().int().min(1).optional(),
|
|
1317
1326
|
timeout_s: z3.number().min(0).max(300).optional()
|
|
1318
1327
|
},
|
|
1319
|
-
annotations: { readOnlyHint: true }
|
|
1328
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1320
1329
|
},
|
|
1321
1330
|
async ({ owner, repo, pr_number, after_sha, pass, after_pass, timeout_s }) => {
|
|
1322
1331
|
try {
|
|
@@ -1340,7 +1349,7 @@ function createMergestormMcpServer() {
|
|
|
1340
1349
|
"review_dismiss",
|
|
1341
1350
|
{
|
|
1342
1351
|
title: "Dismiss Vortex findings",
|
|
1343
|
-
description: `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed;
|
|
1352
|
+
description: `Record an audited dismissal of Vortex findings you verified are wrong or not actionable on a GitHub PR. Identity is exact: head_sha must be the PR's live head, review_id a Vortex review (Core or seam) made at that head, and finding_ids the GitHub review comment ids of that review (offdiff-<n> for a body-only finding), or scope "review" with no finding_ids to dismiss every finding of that review. reason must say why (at least 20 characters); evidence_url is optional. Call with preview: true first to list the review's finding ids and the seam gate without writing. The caller's linked GitHub account needs write access to the repository. A moved head, another review, an unknown id or a missing permission is refused and nothing is written. Retries are idempotent. The seam gate clears only when every finding of that integration review is dismissed; the Vortex gate at the head clears only once every Vortex Blocker (error finding) at that head is dismissed and no Vortex review of it is running (gate.vortex.cleared, else gate.vortex.message); CI and Auto land policy are unchanged. Vortex does not raise a dismissed finding again on the same diff. Never dismiss a finding you did not check.` + STACK_WATCH_RESULT_NOTE,
|
|
1344
1353
|
inputSchema: {
|
|
1345
1354
|
owner: z3.string().min(1),
|
|
1346
1355
|
repo: z3.string().min(1),
|
|
@@ -1353,7 +1362,7 @@ function createMergestormMcpServer() {
|
|
|
1353
1362
|
evidence_url: z3.string().url().optional(),
|
|
1354
1363
|
preview: z3.boolean().optional()
|
|
1355
1364
|
},
|
|
1356
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
1365
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
1357
1366
|
},
|
|
1358
1367
|
async (args) => {
|
|
1359
1368
|
try {
|
|
@@ -1367,9 +1376,9 @@ function createMergestormMcpServer() {
|
|
|
1367
1376
|
"stack_adopt",
|
|
1368
1377
|
{
|
|
1369
1378
|
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
|
|
1379
|
+
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
1380
|
inputSchema: stackAdoptSchema,
|
|
1372
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
1381
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
1373
1382
|
},
|
|
1374
1383
|
async (args) => {
|
|
1375
1384
|
try {
|
|
@@ -1384,7 +1393,7 @@ function createMergestormMcpServer() {
|
|
|
1384
1393
|
{
|
|
1385
1394
|
title: "List stacks",
|
|
1386
1395
|
description: "List the current user's Mergestorm stacks.",
|
|
1387
|
-
annotations: { readOnlyHint: true }
|
|
1396
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1388
1397
|
},
|
|
1389
1398
|
async () => {
|
|
1390
1399
|
try {
|
|
@@ -1405,7 +1414,7 @@ function createMergestormMcpServer() {
|
|
|
1405
1414
|
auto_review: z3.boolean().nullable().optional(),
|
|
1406
1415
|
auto_patch: z3.boolean().nullable().optional()
|
|
1407
1416
|
},
|
|
1408
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
1417
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
|
|
1409
1418
|
},
|
|
1410
1419
|
async ({ stack_id, auto_land, auto_review, auto_patch }) => {
|
|
1411
1420
|
try {
|
|
@@ -1429,7 +1438,7 @@ function createMergestormMcpServer() {
|
|
|
1429
1438
|
inputSchema: {
|
|
1430
1439
|
stack_id: z3.string().min(1)
|
|
1431
1440
|
},
|
|
1432
|
-
annotations: { readOnlyHint: true }
|
|
1441
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1433
1442
|
},
|
|
1434
1443
|
async ({ stack_id }) => {
|
|
1435
1444
|
try {
|
|
@@ -1445,12 +1454,12 @@ function createMergestormMcpServer() {
|
|
|
1445
1454
|
title: "Wait for stack attention",
|
|
1446
1455
|
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
1456
|
inputSchema: stackWaitSchema,
|
|
1448
|
-
annotations: { readOnlyHint: true }
|
|
1457
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1449
1458
|
},
|
|
1450
1459
|
async (args, extra) => {
|
|
1451
1460
|
try {
|
|
1452
1461
|
const payload = await stackWait(args, void 0, { signal: extra.signal });
|
|
1453
|
-
return
|
|
1462
|
+
return ok(payload);
|
|
1454
1463
|
} catch (err) {
|
|
1455
1464
|
return fail(err);
|
|
1456
1465
|
}
|
|
@@ -1464,7 +1473,7 @@ function createMergestormMcpServer() {
|
|
|
1464
1473
|
inputSchema: {
|
|
1465
1474
|
stack_id: z3.string().min(1).optional()
|
|
1466
1475
|
},
|
|
1467
|
-
annotations: { readOnlyHint: true }
|
|
1476
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1468
1477
|
},
|
|
1469
1478
|
async ({ stack_id }) => {
|
|
1470
1479
|
try {
|
|
@@ -1478,8 +1487,8 @@ function createMergestormMcpServer() {
|
|
|
1478
1487
|
"settings_get",
|
|
1479
1488
|
{
|
|
1480
1489
|
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
|
|
1482
|
-
annotations: { readOnlyHint: true }
|
|
1490
|
+
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).",
|
|
1491
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true }
|
|
1483
1492
|
},
|
|
1484
1493
|
async () => {
|
|
1485
1494
|
try {
|
|
@@ -1505,7 +1514,7 @@ function createMergestormMcpServer() {
|
|
|
1505
1514
|
cyclone_connected: z3.unknown().optional(),
|
|
1506
1515
|
github_connected: z3.unknown().optional()
|
|
1507
1516
|
},
|
|
1508
|
-
annotations: { readOnlyHint: false, destructiveHint:
|
|
1517
|
+
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true }
|
|
1509
1518
|
},
|
|
1510
1519
|
async (args) => {
|
|
1511
1520
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
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.
|
|
32
|
+
"mergestorm": "0.3.42",
|
|
33
33
|
"zod": "^3.24.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|