mergestorm-mcp 0.2.27 → 0.2.29
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 +7 -3
- package/dist/index.js +651 -254
- package/dist/lib/hosted.d.ts +1 -1
- package/dist/lib/hosted.js +1 -1
- package/dist/lib/pr-loop-instructions.d.ts +1 -0
- package/dist/lib/pr-loop-instructions.js +13 -2
- package/dist/lib/server.d.ts +6 -4
- package/dist/lib/server.js +149 -28
- package/dist/lib/tools/chat-task.d.ts +18 -0
- package/dist/lib/tools/chat-task.js +28 -0
- package/dist/lib/tools/cyclone-job.d.ts +0 -4
- package/dist/lib/tools/cyclone-job.js +6 -5
- package/dist/lib/tools/issue-create.js +1 -1
- package/dist/lib/tools/issue-review-wait.d.ts +37 -0
- package/dist/lib/tools/issue-review-wait.js +57 -0
- package/dist/lib/tools/memory.d.ts +70 -0
- package/dist/lib/tools/memory.js +74 -0
- package/dist/lib/tools/repo-settings.d.ts +1 -2
- package/dist/lib/tools/repo-settings.js +27 -2
- package/dist/lib/tools/review-dismiss.d.ts +1 -2
- package/dist/lib/tools/review-dismiss.js +1 -1
- package/dist/lib/tools/review-start.js +3 -2
- package/dist/lib/tools/review-submit.d.ts +0 -7
- package/dist/lib/tools/review-submit.js +2 -2
- package/dist/lib/tools/review-wait-pr.js +5 -13
- package/dist/lib/tools/settings-set.d.ts +1 -9
- package/dist/lib/tools/settings-set.js +4 -4
- package/dist/lib/tools/stack-actions.js +4 -4
- package/dist/lib/tools/stack-adopt.js +2 -1
- package/dist/lib/tools/stack-publish.d.ts +2 -2
- package/dist/lib/tools/stack-publish.js +2 -1
- package/dist/lib/tools/stack-wait.d.ts +19 -1
- package/dist/lib/tools/stack-wait.js +72 -10
- package/dist/lib/tools/web.d.ts +27 -0
- package/dist/lib/tools/web.js +36 -0
- package/dist/lib/tools/whoami.js +6 -2
- package/package.json +4 -2
- package/dist/lib/tools/chat-wake.d.ts +0 -21
- package/dist/lib/tools/chat-wake.js +0 -26
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { loadConfig, stackBlockersSummary, pollStackWatch, StackWatchError, StackWatchTimeoutError, stackWatchBackgroundCommand, stackWatchObligation, } from "mergestorm/client";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
3
|
+
import { chatTaskNote, trackInChat } from "./chat-task.js";
|
|
4
4
|
export const stackWaitSchema = {
|
|
5
5
|
stack_id: z.string().trim().min(1),
|
|
6
6
|
timeout_s: z.number().finite().min(0).max(45).default(45),
|
|
@@ -18,15 +18,76 @@ export function repairSummary(repair) {
|
|
|
18
18
|
export function withWatchText(watch, body) {
|
|
19
19
|
return `${watch.message}\n\n${body}`;
|
|
20
20
|
}
|
|
21
|
+
const KEEP_EARLIER_CURSOR = "If you already hold a cursor from an earlier stack_wait on this stack, keep passing that cursor instead.";
|
|
22
|
+
export function hostedStackWatch(watch) {
|
|
23
|
+
if (watch.done) {
|
|
24
|
+
return {
|
|
25
|
+
...watch,
|
|
26
|
+
next: null,
|
|
27
|
+
message: watch.reason === "not_found"
|
|
28
|
+
? "No stack with this id exists for this account. Mergestorm deletes a stack once none of its layers is open, which can mean merged or closed, so landing is unconfirmed: check on GitHub that each PR is merged before reporting it landed. The watch is done."
|
|
29
|
+
: watch.message,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const args = watch.next?.args;
|
|
33
|
+
const attention = watch.reason === "attention";
|
|
34
|
+
const retry = watch.reason === "rate_limited" ? " Wait retry_after_seconds first." : "";
|
|
35
|
+
const failed = watch.reason === "failed" ? " This read failed; if the next one fails too, tell the human the stack is no longer watched." : "";
|
|
36
|
+
const keep = watch.message.includes(KEEP_EARLIER_CURSOR) ? ` ${KEEP_EARLIER_CURSOR}` : "";
|
|
37
|
+
const resume = args
|
|
38
|
+
? attention
|
|
39
|
+
? ` After the blocker is handled, call stack_wait ${JSON.stringify(args)}.`
|
|
40
|
+
: ` Next call: stack_wait ${JSON.stringify(args)}.`
|
|
41
|
+
: "";
|
|
42
|
+
return {
|
|
43
|
+
...watch,
|
|
44
|
+
next: args && !attention ? { tool: "stack_wait", args } : null,
|
|
45
|
+
message: `This stack is not landed and the watch is not done.${retry}${failed}${resume}${keep}`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function hostedStackRepair(repair) {
|
|
49
|
+
const { steps: _steps, note: _note, findings: _findings, ...facts } = repair;
|
|
50
|
+
return facts;
|
|
51
|
+
}
|
|
52
|
+
function hostedRepairSummary(repair) {
|
|
53
|
+
const files = "files" in repair && repair.files.length ? ` Files: ${repair.files.join(", ")}.` : "";
|
|
54
|
+
const breeze = "publishedBy" in repair && repair.publishedBy === "breeze"
|
|
55
|
+
? ` ${repair.branch} is a Breeze branch${repair.liveParent ? ` on ${repair.liveParent}` : ""}: open_pull_request publishes it with single-parent commits only, so a merge commit cannot be published.`
|
|
56
|
+
: "";
|
|
57
|
+
return `\nRepair #${repair.prNumber} (${repair.kind}).${files}${breeze}`;
|
|
58
|
+
}
|
|
59
|
+
export function hostedToolResult(result) {
|
|
60
|
+
const data = result.structuredContent;
|
|
61
|
+
const watch = data?.watch;
|
|
62
|
+
const repair = data?.repair;
|
|
63
|
+
const hasWatch = !!watch && typeof watch.message === "string";
|
|
64
|
+
const hasRepair = !!repair && typeof repair.steps === "string";
|
|
65
|
+
if (!data || (!hasWatch && !hasRepair))
|
|
66
|
+
return result;
|
|
67
|
+
let text = result.content[0]?.text ?? "";
|
|
68
|
+
const structured = { ...data };
|
|
69
|
+
if (hasWatch) {
|
|
70
|
+
const hosted = hostedStackWatch(watch);
|
|
71
|
+
structured.watch = hosted;
|
|
72
|
+
if (text.startsWith(watch.message))
|
|
73
|
+
text = `${hosted.message}${text.slice(watch.message.length)}`;
|
|
74
|
+
}
|
|
75
|
+
if (hasRepair) {
|
|
76
|
+
const hosted = hostedStackRepair(repair);
|
|
77
|
+
structured.repair = hosted;
|
|
78
|
+
text = text.replace(repairSummary(repair), hostedRepairSummary(repair));
|
|
79
|
+
}
|
|
80
|
+
return { ...result, content: [{ type: "text", text }, { type: "text", text: JSON.stringify(structured) }], structuredContent: structured };
|
|
81
|
+
}
|
|
21
82
|
function envelopeWatch(envelope) {
|
|
22
83
|
return envelope.watch ?? stackWatchObligation({
|
|
23
84
|
stackId: envelope.stackId, terminal: null, cursor: envelope.cursor, status: envelope.status,
|
|
24
85
|
});
|
|
25
86
|
}
|
|
26
|
-
function stackWaitSummary(envelope) {
|
|
27
|
-
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope)}${repairSummary(envelope.repair)}`);
|
|
87
|
+
function stackWaitSummary(envelope, hosted = false) {
|
|
88
|
+
return withWatchText(envelopeWatch(envelope), `${stackWaitStatusLine(envelope, hosted)}${repairSummary(envelope.repair)}`);
|
|
28
89
|
}
|
|
29
|
-
function stackWaitStatusLine(envelope) {
|
|
90
|
+
function stackWaitStatusLine(envelope, hosted) {
|
|
30
91
|
const base = `Stack ${envelope.stackId} · ${envelope.status}${stackBlockersSummary(envelope.blocker && envelope.prNumber != null ? { prNumber: envelope.prNumber, blocker: envelope.blocker } : null, envelope.issues, envelope.busy, envelope.landGatePending, envelope.actAfter ?? null, envelope.queueWait ?? null)}${envelope.assessment === "unavailable" ? " · assessment unavailable" : ""}`;
|
|
31
92
|
if (envelopeWatch(envelope).done || (envelope.status !== "waiting" && envelope.status !== "in_progress"))
|
|
32
93
|
return base;
|
|
@@ -36,7 +97,8 @@ function stackWaitStatusLine(envelope) {
|
|
|
36
97
|
...(envelope.cursor.afterFinishedAt !== undefined ? [`after_finished_at ${JSON.stringify(envelope.cursor.afterFinishedAt)}`] : []),
|
|
37
98
|
...(envelope.cursor.bounceId !== undefined ? [`bounce_id ${JSON.stringify(envelope.cursor.bounceId)}`] : []),
|
|
38
99
|
];
|
|
39
|
-
|
|
100
|
+
const background = hosted ? "" : `only with a confirmed host notification that resumes this task, run ${stackWatchBackgroundCommand(envelope.stackId, envelope.cursor)} as a background command and end your turn; otherwise `;
|
|
101
|
+
return `${base} · watch not finished: ${background}call stack_wait again with timeout_s 45 and the same cursor: ${selectors.join(", ")}`;
|
|
40
102
|
}
|
|
41
103
|
export async function stackWait(input, cfg, deps = {}) {
|
|
42
104
|
const args = z.object(stackWaitSchema).parse(input);
|
|
@@ -61,7 +123,7 @@ export async function stackWait(input, cfg, deps = {}) {
|
|
|
61
123
|
if (err instanceof StackWatchError) {
|
|
62
124
|
const envelope = { ...err.lastEnvelope, watch: envelopeWatch(err.lastEnvelope) };
|
|
63
125
|
return {
|
|
64
|
-
summary: stackWaitSummary(envelope),
|
|
126
|
+
summary: stackWaitSummary(envelope, deps.hosted),
|
|
65
127
|
data: {
|
|
66
128
|
...envelope,
|
|
67
129
|
...(err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}),
|
|
@@ -74,11 +136,11 @@ export async function stackWait(input, cfg, deps = {}) {
|
|
|
74
136
|
envelope = err.lastEnvelope;
|
|
75
137
|
}
|
|
76
138
|
const sealed = { ...envelope, watch: envelopeWatch(envelope) };
|
|
77
|
-
const
|
|
78
|
-
? await
|
|
139
|
+
const task = deps.chatId && !sealed.watch.done
|
|
140
|
+
? await trackInChat(config, deps.chatId, { kind: "stack", stackId: sealed.stackId }, deps.signal)
|
|
79
141
|
: null;
|
|
80
142
|
return {
|
|
81
|
-
summary:
|
|
82
|
-
data:
|
|
143
|
+
summary: task ? `${stackWaitSummary(sealed, deps.hosted)}\n\n${chatTaskNote(task)}` : stackWaitSummary(sealed, deps.hosted),
|
|
144
|
+
data: task ? { ...sealed, background_task: task } : sealed,
|
|
83
145
|
};
|
|
84
146
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WEB_SEARCH_KINDS, WEB_SEARCH_RECENCIES, readWebPage, searchWeb, type Config } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import type { ToolPayload } from "./types.js";
|
|
4
|
+
export declare const webSearchSchema: {
|
|
5
|
+
query: z.ZodString;
|
|
6
|
+
kind: z.ZodOptional<z.ZodEnum<["web", "news"]>>;
|
|
7
|
+
recency: z.ZodOptional<z.ZodEnum<["day", "week", "month", "year"]>>;
|
|
8
|
+
page: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
};
|
|
10
|
+
export declare const readWebPageSchema: {
|
|
11
|
+
url: z.ZodString;
|
|
12
|
+
};
|
|
13
|
+
export type WebApi = {
|
|
14
|
+
searchWeb: typeof searchWeb;
|
|
15
|
+
readWebPage: typeof readWebPage;
|
|
16
|
+
};
|
|
17
|
+
export declare function createWebToolHandlers(cfg: Config | undefined, api?: WebApi): {
|
|
18
|
+
search: (input: {
|
|
19
|
+
query: string;
|
|
20
|
+
kind?: (typeof WEB_SEARCH_KINDS)[number];
|
|
21
|
+
recency?: (typeof WEB_SEARCH_RECENCIES)[number];
|
|
22
|
+
page?: number;
|
|
23
|
+
}) => Promise<ToolPayload>;
|
|
24
|
+
read: (input: {
|
|
25
|
+
url: string;
|
|
26
|
+
}) => Promise<ToolPayload>;
|
|
27
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CommandError, WEB_SEARCH_KINDS, WEB_SEARCH_PAGE_MAX, WEB_SEARCH_RECENCIES, readWebPage, searchWeb } from "mergestorm/client";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const webSearchSchema = {
|
|
4
|
+
query: z.string().trim().min(1).max(400),
|
|
5
|
+
kind: z.enum(WEB_SEARCH_KINDS).optional(),
|
|
6
|
+
recency: z.enum(WEB_SEARCH_RECENCIES).optional(),
|
|
7
|
+
page: z.number().int().min(1).max(WEB_SEARCH_PAGE_MAX).optional(),
|
|
8
|
+
};
|
|
9
|
+
export const readWebPageSchema = { url: z.string().trim().url().max(2000) };
|
|
10
|
+
const liveApi = { searchWeb, readWebPage };
|
|
11
|
+
async function guarded(run) {
|
|
12
|
+
try {
|
|
13
|
+
return await run();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
17
|
+
const code = err instanceof CommandError ? err.code ?? "web_failed" : "web_failed";
|
|
18
|
+
return {
|
|
19
|
+
summary: message,
|
|
20
|
+
data: { error: { code, message, ...(err instanceof CommandError && err.retryAfterSeconds !== undefined ? { retry_after_seconds: err.retryAfterSeconds } : {}) } },
|
|
21
|
+
isError: true,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function createWebToolHandlers(cfg, api = liveApi) {
|
|
26
|
+
return {
|
|
27
|
+
search: (input) => guarded(async () => {
|
|
28
|
+
const found = await api.searchWeb(input, cfg);
|
|
29
|
+
return { summary: found.text, data: { query: input.query } };
|
|
30
|
+
}),
|
|
31
|
+
read: (input) => guarded(async () => {
|
|
32
|
+
const page = await api.readWebPage(input.url, cfg);
|
|
33
|
+
return { summary: page.text, data: { url: input.url } };
|
|
34
|
+
}),
|
|
35
|
+
};
|
|
36
|
+
}
|
package/dist/lib/tools/whoami.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CommandError, apiBase, configPath, getMe, loadConfig, resolveApiKey, } from "mergestorm/client";
|
|
1
|
+
import { CommandError, accountLabel, apiBase, configPath, getMe, loadConfig, resolveApiKey, } from "mergestorm/client";
|
|
2
2
|
export async function whoami(cfg, serverVersion) {
|
|
3
3
|
const resolved = cfg ?? (await loadConfig());
|
|
4
4
|
const key = resolveApiKey(resolved);
|
|
@@ -9,7 +9,11 @@ export async function whoami(cfg, serverVersion) {
|
|
|
9
9
|
if (!me) {
|
|
10
10
|
throw new CommandError("Live account details unavailable. Could not verify account with /me.");
|
|
11
11
|
}
|
|
12
|
+
const account = accountLabel(me.account);
|
|
12
13
|
const data = {
|
|
14
|
+
account,
|
|
15
|
+
github_login: me.account?.github_login ?? null,
|
|
16
|
+
email: me.account?.email ?? null,
|
|
13
17
|
key_prefix: me.key.prefix,
|
|
14
18
|
key_name: me.key.name ?? null,
|
|
15
19
|
plan_key: me.plan_key ?? null,
|
|
@@ -21,7 +25,7 @@ export async function whoami(cfg, serverVersion) {
|
|
|
21
25
|
};
|
|
22
26
|
const plan = data.plan_label_key ?? data.plan_key ?? "unknown plan";
|
|
23
27
|
return {
|
|
24
|
-
summary: `${data.key_prefix} · ${plan}${serverVersion ? ` · mergestorm-mcp ${serverVersion}` : ""}`,
|
|
28
|
+
summary: `${account ? `${account} · ` : ""}${data.key_prefix} · ${plan}${serverVersion ? ` · mergestorm-mcp ${serverVersion}` : ""}`,
|
|
25
29
|
data,
|
|
26
30
|
};
|
|
27
31
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mergestorm-mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.29",
|
|
4
4
|
"description": "Mergestorm MCP server — identity, review, and read-only stack tools over stdio",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
},
|
|
24
24
|
"files": [
|
|
25
25
|
"dist",
|
|
26
|
+
"!dist/.tsbuildinfo",
|
|
27
|
+
"!dist/.build-inputs",
|
|
26
28
|
"README.md",
|
|
27
29
|
"LICENSE"
|
|
28
30
|
],
|
|
@@ -37,7 +39,7 @@
|
|
|
37
39
|
},
|
|
38
40
|
"dependencies": {
|
|
39
41
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
40
|
-
"mergestorm": "0.3.
|
|
42
|
+
"mergestorm": "0.3.66",
|
|
41
43
|
"zod": "^3.24.2"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type Config } from "mergestorm/client";
|
|
2
|
-
export type ChatWakeTarget = {
|
|
3
|
-
kind: "vortex";
|
|
4
|
-
owner: string;
|
|
5
|
-
repo: string;
|
|
6
|
-
prNumber: number;
|
|
7
|
-
headSha: string | null;
|
|
8
|
-
minPass: number | null;
|
|
9
|
-
} | {
|
|
10
|
-
kind: "stack";
|
|
11
|
-
stackId: string;
|
|
12
|
-
};
|
|
13
|
-
export declare function breezeChatId(value: string | null | undefined): string | null;
|
|
14
|
-
export type ChatWake = {
|
|
15
|
-
id: string;
|
|
16
|
-
result: "waiting" | "existing" | "finished";
|
|
17
|
-
};
|
|
18
|
-
export declare const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
19
|
-
export declare const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
20
|
-
export declare const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
21
|
-
export declare function registerChatWake(cfg: Config, chatId: string, target: ChatWakeTarget, signal?: AbortSignal): Promise<ChatWake | null>;
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { apiFetch } from "mergestorm/client";
|
|
2
|
-
const CHAT_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3
|
-
export function breezeChatId(value) {
|
|
4
|
-
const trimmed = value?.trim() ?? "";
|
|
5
|
-
return CHAT_ID.test(trimmed) ? trimmed.toLowerCase() : null;
|
|
6
|
-
}
|
|
7
|
-
export const CHAT_WAKE_REVIEW_NOTE = "Keep waiting: call review_wait_pr again until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
8
|
-
export const REVIEW_START_WAIT_NOTE = "Wait for it now: call review_wait_pr for this pull request until the review finishes, then tell the user its verdict and findings. If your reply ends first, this chat is woken with the review's result.";
|
|
9
|
-
export const CHAT_WAKE_STACK_NOTE = "If your reply ends before the stack is done, this chat is woken when the stack lands, is bounced out of the merge queue, or is archived.";
|
|
10
|
-
export async function registerChatWake(cfg, chatId, target, signal) {
|
|
11
|
-
const json = target.kind === "vortex"
|
|
12
|
-
? { chat_id: chatId, kind: "vortex", owner: target.owner, repo: target.repo, pr_number: target.prNumber, head_sha: target.headSha, min_pass: target.headSha ? target.minPass : null }
|
|
13
|
-
: { chat_id: chatId, kind: "stack", stack_id: target.stackId };
|
|
14
|
-
try {
|
|
15
|
-
const response = await apiFetch(cfg, "/api/v1/stacks/chat-waits", { method: "POST", json, signal, timeoutMs: 10_000 });
|
|
16
|
-
const body = response.body;
|
|
17
|
-
if (response.status !== 200 || !body || typeof body.id !== "string")
|
|
18
|
-
return null;
|
|
19
|
-
if (body.result !== "waiting" && body.result !== "existing" && body.result !== "finished")
|
|
20
|
-
return null;
|
|
21
|
-
return { id: body.id, result: body.result };
|
|
22
|
-
}
|
|
23
|
-
catch {
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
}
|