@mushi-mushi/mcp 0.13.1 → 0.14.0
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 +29 -0
- package/dist/catalog.d.ts +69 -0
- package/dist/catalog.js +14 -0
- package/dist/chunk-QWZEQYCN.js +595 -0
- package/dist/index.js +301 -20
- package/dist/server.d.ts +54 -0
- package/dist/server.js +1715 -0
- package/package.json +24 -12
- package/dist/index.d.ts +0 -2
package/dist/server.js
ADDED
|
@@ -0,0 +1,1715 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TDD_TOOL_CATALOG,
|
|
3
|
+
TOOL_CATALOG
|
|
4
|
+
} from "./chunk-QWZEQYCN.js";
|
|
5
|
+
|
|
6
|
+
// src/server.ts
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
var MushiApiError = class extends Error {
|
|
10
|
+
constructor(status, code, message) {
|
|
11
|
+
super(`[${code}] ${message}`);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.code = code;
|
|
14
|
+
this.name = "MushiApiError";
|
|
15
|
+
}
|
|
16
|
+
status;
|
|
17
|
+
code;
|
|
18
|
+
};
|
|
19
|
+
function createMushiServer(config) {
|
|
20
|
+
const { version, apiEndpoint, apiKey, projectId } = config;
|
|
21
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
22
|
+
if (config.scopes !== void 0 && config.scopes.length === 0) {
|
|
23
|
+
return new McpServer({ name: "mushi-mushi", version });
|
|
24
|
+
}
|
|
25
|
+
async function apiCall(path, options) {
|
|
26
|
+
const res = await doFetch(`${apiEndpoint}${path}`, {
|
|
27
|
+
...options,
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/json",
|
|
30
|
+
// Authorization is accepted by `adminOrApiKey` only as a JWT, but we
|
|
31
|
+
// still send it for endpoints still behind plain `jwtAuth` (legacy)
|
|
32
|
+
// and for transparent proxies that strip X-Mushi-* headers.
|
|
33
|
+
"Authorization": `Bearer ${apiKey}`,
|
|
34
|
+
"X-Mushi-Api-Key": apiKey,
|
|
35
|
+
...projectId ? { "X-Mushi-Project": projectId } : {},
|
|
36
|
+
...options?.headers ?? {}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
const text = await res.text();
|
|
40
|
+
let body = null;
|
|
41
|
+
if (text) {
|
|
42
|
+
try {
|
|
43
|
+
body = JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
body = { raw: text };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok) {
|
|
49
|
+
const envelope2 = body;
|
|
50
|
+
const code = envelope2?.error?.code ?? `HTTP_${res.status}`;
|
|
51
|
+
const message = envelope2?.error?.message ?? text.slice(0, 500) ?? `Request failed with ${res.status}`;
|
|
52
|
+
throw new MushiApiError(res.status, code, message);
|
|
53
|
+
}
|
|
54
|
+
const envelope = body;
|
|
55
|
+
if (envelope && typeof envelope === "object" && "ok" in envelope) {
|
|
56
|
+
if (!envelope.ok) {
|
|
57
|
+
const code = envelope.error?.code ?? "API_ERROR";
|
|
58
|
+
const message = envelope.error?.message ?? "API returned ok=false";
|
|
59
|
+
throw new MushiApiError(res.status, code, message);
|
|
60
|
+
}
|
|
61
|
+
return envelope.data ?? {};
|
|
62
|
+
}
|
|
63
|
+
return body;
|
|
64
|
+
}
|
|
65
|
+
function jsonText(value) {
|
|
66
|
+
return {
|
|
67
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function jsonResult(value) {
|
|
71
|
+
return {
|
|
72
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
73
|
+
structuredContent: value
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const server = new McpServer({
|
|
77
|
+
name: "mushi-mushi",
|
|
78
|
+
version
|
|
79
|
+
});
|
|
80
|
+
const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
|
|
81
|
+
function annotationsFor(name, catalog = ALL_TOOL_CATALOG) {
|
|
82
|
+
const spec = catalog.find((t) => t.name === name);
|
|
83
|
+
if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
|
|
84
|
+
const a = {
|
|
85
|
+
title: spec.title,
|
|
86
|
+
readOnlyHint: spec.hints.readOnly
|
|
87
|
+
};
|
|
88
|
+
if (spec.hints.destructive !== void 0) a.destructiveHint = spec.hints.destructive;
|
|
89
|
+
if (spec.hints.idempotent !== void 0) a.idempotentHint = spec.hints.idempotent;
|
|
90
|
+
if (spec.hints.openWorld !== void 0) a.openWorldHint = spec.hints.openWorld;
|
|
91
|
+
return a;
|
|
92
|
+
}
|
|
93
|
+
function descOf(name, catalog = ALL_TOOL_CATALOG) {
|
|
94
|
+
const spec = catalog.find((t) => t.name === name);
|
|
95
|
+
if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
|
|
96
|
+
return spec.description;
|
|
97
|
+
}
|
|
98
|
+
function titleOf(name, catalog = ALL_TOOL_CATALOG) {
|
|
99
|
+
const spec = catalog.find((t) => t.name === name);
|
|
100
|
+
if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
|
|
101
|
+
return spec.title;
|
|
102
|
+
}
|
|
103
|
+
server.registerTool(
|
|
104
|
+
"get_recent_reports",
|
|
105
|
+
{
|
|
106
|
+
title: titleOf("get_recent_reports"),
|
|
107
|
+
description: descOf("get_recent_reports"),
|
|
108
|
+
annotations: annotationsFor("get_recent_reports"),
|
|
109
|
+
inputSchema: {
|
|
110
|
+
status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
|
|
111
|
+
category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
|
|
112
|
+
severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
|
|
113
|
+
limit: z.number().optional().describe("Max reports to return (default 20, max 100)")
|
|
114
|
+
},
|
|
115
|
+
outputSchema: {
|
|
116
|
+
reports: z.array(z.unknown()),
|
|
117
|
+
total: z.number()
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async (args) => {
|
|
121
|
+
const params = new URLSearchParams();
|
|
122
|
+
if (args.status) params.set("status", args.status);
|
|
123
|
+
if (args.category) params.set("category", args.category);
|
|
124
|
+
if (args.severity) params.set("severity", args.severity);
|
|
125
|
+
params.set("limit", String(Math.min(args.limit ?? 20, 100)));
|
|
126
|
+
const data = await apiCall(`/v1/admin/reports?${params}`);
|
|
127
|
+
return jsonResult(data);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
server.registerTool(
|
|
131
|
+
"get_report_detail",
|
|
132
|
+
{
|
|
133
|
+
title: titleOf("get_report_detail"),
|
|
134
|
+
description: descOf("get_report_detail"),
|
|
135
|
+
annotations: annotationsFor("get_report_detail"),
|
|
136
|
+
inputSchema: { reportId: z.string().describe("The report UUID") }
|
|
137
|
+
},
|
|
138
|
+
async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}`))
|
|
139
|
+
);
|
|
140
|
+
server.registerTool(
|
|
141
|
+
"get_report_timeline",
|
|
142
|
+
{
|
|
143
|
+
title: titleOf("get_report_timeline"),
|
|
144
|
+
description: descOf("get_report_timeline"),
|
|
145
|
+
annotations: annotationsFor("get_report_timeline"),
|
|
146
|
+
inputSchema: { reportId: z.string().describe("The report UUID") }
|
|
147
|
+
},
|
|
148
|
+
async (args) => jsonText(await apiCall(`/v1/sync/reports/${args.reportId}/timeline`))
|
|
149
|
+
);
|
|
150
|
+
server.registerTool(
|
|
151
|
+
"get_two_way_comms_health",
|
|
152
|
+
{
|
|
153
|
+
title: titleOf("get_two_way_comms_health"),
|
|
154
|
+
description: descOf("get_two_way_comms_health"),
|
|
155
|
+
annotations: annotationsFor("get_two_way_comms_health"),
|
|
156
|
+
inputSchema: {}
|
|
157
|
+
},
|
|
158
|
+
async () => jsonText(await apiCall("/v1/sync/two-way-health"))
|
|
159
|
+
);
|
|
160
|
+
server.registerTool(
|
|
161
|
+
"search_reports",
|
|
162
|
+
{
|
|
163
|
+
title: titleOf("search_reports"),
|
|
164
|
+
description: descOf("search_reports"),
|
|
165
|
+
annotations: annotationsFor("search_reports"),
|
|
166
|
+
inputSchema: {
|
|
167
|
+
query: z.string().describe("Natural-language search text or component path"),
|
|
168
|
+
limit: z.number().optional().describe("Max results (default 10, max 50)"),
|
|
169
|
+
threshold: z.number().optional().describe("Similarity threshold 0..1, default 0.2")
|
|
170
|
+
},
|
|
171
|
+
outputSchema: {
|
|
172
|
+
results: z.array(z.unknown())
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
async (args) => {
|
|
176
|
+
const data = await apiCall("/v1/admin/reports/similarity", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
body: JSON.stringify({
|
|
179
|
+
query: args.query,
|
|
180
|
+
k: Math.min(args.limit ?? 10, 50),
|
|
181
|
+
threshold: args.threshold ?? 0.2,
|
|
182
|
+
...projectId ? { projectId } : {}
|
|
183
|
+
})
|
|
184
|
+
});
|
|
185
|
+
return jsonResult(data);
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
server.registerTool(
|
|
189
|
+
"get_similar_bugs",
|
|
190
|
+
{
|
|
191
|
+
title: titleOf("get_similar_bugs"),
|
|
192
|
+
description: descOf("get_similar_bugs"),
|
|
193
|
+
annotations: annotationsFor("get_similar_bugs"),
|
|
194
|
+
inputSchema: {
|
|
195
|
+
query: z.string().describe("Component name, page path, or bug description"),
|
|
196
|
+
limit: z.number().optional().describe("Max results (default 5, max 20)")
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
async (args) => {
|
|
200
|
+
const data = await apiCall("/v1/admin/reports/similarity", {
|
|
201
|
+
method: "POST",
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
query: args.query,
|
|
204
|
+
k: Math.min(args.limit ?? 5, 20),
|
|
205
|
+
threshold: 0.3,
|
|
206
|
+
...projectId ? { projectId } : {}
|
|
207
|
+
})
|
|
208
|
+
});
|
|
209
|
+
return jsonText(data);
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
server.registerTool(
|
|
213
|
+
"get_fix_context",
|
|
214
|
+
{
|
|
215
|
+
title: titleOf("get_fix_context"),
|
|
216
|
+
description: descOf("get_fix_context"),
|
|
217
|
+
annotations: annotationsFor("get_fix_context"),
|
|
218
|
+
inputSchema: { reportId: z.string().describe("The report UUID to fix") }
|
|
219
|
+
},
|
|
220
|
+
async (args) => {
|
|
221
|
+
const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
|
|
222
|
+
return jsonText({
|
|
223
|
+
report,
|
|
224
|
+
reproductionSteps: report.reproduction_steps ?? [],
|
|
225
|
+
component: report.component,
|
|
226
|
+
rootCause: report.stage2_analysis?.rootCause,
|
|
227
|
+
bugOntologyTags: report.bug_ontology_tags
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
);
|
|
231
|
+
server.registerTool(
|
|
232
|
+
"get_fix_timeline",
|
|
233
|
+
{
|
|
234
|
+
title: titleOf("get_fix_timeline"),
|
|
235
|
+
description: descOf("get_fix_timeline"),
|
|
236
|
+
annotations: annotationsFor("get_fix_timeline"),
|
|
237
|
+
inputSchema: { fixId: z.string().describe("fix_attempt UUID") }
|
|
238
|
+
},
|
|
239
|
+
async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
|
|
240
|
+
);
|
|
241
|
+
server.registerTool(
|
|
242
|
+
"get_blast_radius",
|
|
243
|
+
{
|
|
244
|
+
title: titleOf("get_blast_radius"),
|
|
245
|
+
description: descOf("get_blast_radius"),
|
|
246
|
+
annotations: annotationsFor("get_blast_radius"),
|
|
247
|
+
inputSchema: { nodeId: z.string().describe("Graph node UUID") }
|
|
248
|
+
},
|
|
249
|
+
async (args) => jsonText(await apiCall(`/v1/admin/graph/blast-radius/${args.nodeId}`))
|
|
250
|
+
);
|
|
251
|
+
server.registerTool(
|
|
252
|
+
"get_knowledge_graph",
|
|
253
|
+
{
|
|
254
|
+
title: titleOf("get_knowledge_graph"),
|
|
255
|
+
description: descOf("get_knowledge_graph"),
|
|
256
|
+
annotations: annotationsFor("get_knowledge_graph"),
|
|
257
|
+
inputSchema: {
|
|
258
|
+
seed: z.string().describe("Starting node id or label"),
|
|
259
|
+
depth: z.number().optional().describe("Traversal depth (default 2, max 4)")
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
async (args) => {
|
|
263
|
+
const params = new URLSearchParams({
|
|
264
|
+
seed: args.seed,
|
|
265
|
+
depth: String(Math.min(args.depth ?? 2, 4))
|
|
266
|
+
});
|
|
267
|
+
return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
|
|
268
|
+
}
|
|
269
|
+
);
|
|
270
|
+
server.registerTool(
|
|
271
|
+
"graph_neighborhood",
|
|
272
|
+
{
|
|
273
|
+
title: titleOf("graph_neighborhood"),
|
|
274
|
+
description: descOf("graph_neighborhood"),
|
|
275
|
+
annotations: annotationsFor("graph_neighborhood"),
|
|
276
|
+
inputSchema: {
|
|
277
|
+
seed: z.string().describe("Starting node id or label"),
|
|
278
|
+
depth: z.number().optional().describe("Traversal depth (default 2, max 4)")
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
async (args) => {
|
|
282
|
+
const params = new URLSearchParams({
|
|
283
|
+
seed: args.seed,
|
|
284
|
+
depth: String(Math.min(args.depth ?? 2, 4))
|
|
285
|
+
});
|
|
286
|
+
return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
server.registerTool(
|
|
290
|
+
"graph_node_status",
|
|
291
|
+
{
|
|
292
|
+
title: titleOf("graph_node_status"),
|
|
293
|
+
description: descOf("graph_node_status"),
|
|
294
|
+
annotations: annotationsFor("graph_node_status"),
|
|
295
|
+
inputSchema: { nodeId: z.string().describe("graph_nodes.id") }
|
|
296
|
+
},
|
|
297
|
+
async (args) => jsonText(await apiCall(`/v1/admin/graph/node/${args.nodeId}`))
|
|
298
|
+
);
|
|
299
|
+
server.registerTool(
|
|
300
|
+
"inventory_get",
|
|
301
|
+
{
|
|
302
|
+
title: titleOf("inventory_get"),
|
|
303
|
+
description: descOf("inventory_get"),
|
|
304
|
+
annotations: annotationsFor("inventory_get"),
|
|
305
|
+
inputSchema: {
|
|
306
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
async (args) => {
|
|
310
|
+
const pid = args.projectId ?? projectId;
|
|
311
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for inventory_get");
|
|
312
|
+
return jsonText(await apiCall(`/v1/admin/inventory/${pid}`));
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
server.registerTool(
|
|
316
|
+
"inventory_diff",
|
|
317
|
+
{
|
|
318
|
+
title: titleOf("inventory_diff"),
|
|
319
|
+
description: descOf("inventory_diff"),
|
|
320
|
+
annotations: annotationsFor("inventory_diff"),
|
|
321
|
+
inputSchema: {
|
|
322
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
|
|
323
|
+
fromSha: z.string().describe("Older commit SHA (baseline)"),
|
|
324
|
+
toSha: z.string().describe("Newer commit SHA (candidate)")
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
async (args) => {
|
|
328
|
+
const pid = args.projectId ?? projectId;
|
|
329
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for inventory_diff");
|
|
330
|
+
const q = new URLSearchParams({ from: args.fromSha, to: args.toSha });
|
|
331
|
+
return jsonText(await apiCall(`/v1/admin/inventory/${pid}/diff?${q}`));
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
server.registerTool(
|
|
335
|
+
"inventory_findings",
|
|
336
|
+
{
|
|
337
|
+
title: titleOf("inventory_findings"),
|
|
338
|
+
description: descOf("inventory_findings"),
|
|
339
|
+
annotations: annotationsFor("inventory_findings"),
|
|
340
|
+
inputSchema: {
|
|
341
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
|
|
342
|
+
gate: z.string().optional().describe("Filter by gate id (e.g. dead_handler, status_claim)"),
|
|
343
|
+
severity: z.string().optional().describe("Filter findings by severity")
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
async (args) => {
|
|
347
|
+
const pid = args.projectId ?? projectId;
|
|
348
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for inventory_findings");
|
|
349
|
+
const q = new URLSearchParams();
|
|
350
|
+
if (args.gate) q.set("gate", args.gate);
|
|
351
|
+
if (args.severity) q.set("severity", args.severity);
|
|
352
|
+
const suffix = q.toString() ? `?${q}` : "";
|
|
353
|
+
return jsonText(await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`));
|
|
354
|
+
}
|
|
355
|
+
);
|
|
356
|
+
server.registerTool(
|
|
357
|
+
"fix_suggest",
|
|
358
|
+
{
|
|
359
|
+
title: titleOf("fix_suggest"),
|
|
360
|
+
description: descOf("fix_suggest"),
|
|
361
|
+
annotations: annotationsFor("fix_suggest"),
|
|
362
|
+
inputSchema: { reportId: z.string().describe("Report UUID") }
|
|
363
|
+
},
|
|
364
|
+
async (args) => {
|
|
365
|
+
const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
|
|
366
|
+
const s2 = report.stage2_analysis;
|
|
367
|
+
return jsonText({
|
|
368
|
+
reportId: args.reportId,
|
|
369
|
+
rootCause: s2?.rootCause ?? null,
|
|
370
|
+
suggestedFix: s2?.suggestedFix ?? null,
|
|
371
|
+
reproductionSteps: report.reproduction_steps ?? [],
|
|
372
|
+
summary: report.summary ?? null,
|
|
373
|
+
component: report.component ?? null
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
);
|
|
377
|
+
server.registerTool(
|
|
378
|
+
"run_nl_query",
|
|
379
|
+
{
|
|
380
|
+
title: titleOf("run_nl_query"),
|
|
381
|
+
description: descOf("run_nl_query"),
|
|
382
|
+
annotations: annotationsFor("run_nl_query"),
|
|
383
|
+
inputSchema: { question: z.string().describe('Question in plain English, e.g. "Which components had the most critical bugs this week?"') }
|
|
384
|
+
},
|
|
385
|
+
async (args) => {
|
|
386
|
+
const data = await apiCall("/v1/admin/query", {
|
|
387
|
+
method: "POST",
|
|
388
|
+
body: JSON.stringify({ question: args.question })
|
|
389
|
+
});
|
|
390
|
+
return jsonText(data);
|
|
391
|
+
}
|
|
392
|
+
);
|
|
393
|
+
server.registerTool(
|
|
394
|
+
"setup_check",
|
|
395
|
+
{
|
|
396
|
+
title: titleOf("setup_check"),
|
|
397
|
+
description: descOf("setup_check"),
|
|
398
|
+
annotations: annotationsFor("setup_check"),
|
|
399
|
+
inputSchema: {
|
|
400
|
+
projectId: z.string().optional().describe(
|
|
401
|
+
"Project UUID to check. Falls back to the projectId the server was initialised with."
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
async (args) => {
|
|
406
|
+
const resolvedId = args.projectId ?? projectId;
|
|
407
|
+
if (!resolvedId) {
|
|
408
|
+
return jsonText({
|
|
409
|
+
ok: false,
|
|
410
|
+
error: "No projectId provided and none configured on the MCP server. Pass projectId explicitly."
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
|
|
414
|
+
const summary = data.checks.map((c) => ({
|
|
415
|
+
check: c.key,
|
|
416
|
+
label: c.label,
|
|
417
|
+
passed: c.ready,
|
|
418
|
+
hint: c.hint,
|
|
419
|
+
fixPath: c.fixHref
|
|
420
|
+
}));
|
|
421
|
+
return jsonText({
|
|
422
|
+
ready: data.ready,
|
|
423
|
+
repoUrl: data.repoUrl ?? null,
|
|
424
|
+
checks: summary,
|
|
425
|
+
// Human-readable summary for agents that paste the result into a prompt
|
|
426
|
+
summary: data.ready ? `Project ${resolvedId} is ready to dispatch auto-fixes${data.repoUrl ? ` (target: ${data.repoUrl})` : ""}.` : `Project ${resolvedId} cannot dispatch yet \u2014 ${summary.filter((c) => !c.passed).map((c) => c.label).join(", ")}.`
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
server.registerTool(
|
|
431
|
+
"ingest_setup_check",
|
|
432
|
+
{
|
|
433
|
+
title: titleOf("ingest_setup_check"),
|
|
434
|
+
description: descOf("ingest_setup_check"),
|
|
435
|
+
annotations: annotationsFor("ingest_setup_check"),
|
|
436
|
+
inputSchema: {}
|
|
437
|
+
},
|
|
438
|
+
async () => {
|
|
439
|
+
const data = await apiCall("/v1/sync/ingest-setup");
|
|
440
|
+
const failed = data.steps.filter((s) => s.required && !s.complete);
|
|
441
|
+
const summary = data.ready ? `Ingest setup complete (${data.required_complete}/${data.required_total}) for ${data.project_name}.` : `Ingest incomplete (${data.required_complete}/${data.required_total}) \u2014 still need: ${failed.map((s) => s.label).join(", ")}.`;
|
|
442
|
+
return jsonText({
|
|
443
|
+
ready: data.ready,
|
|
444
|
+
projectId: data.project_id,
|
|
445
|
+
projectName: data.project_name,
|
|
446
|
+
requiredComplete: data.required_complete,
|
|
447
|
+
requiredTotal: data.required_total,
|
|
448
|
+
steps: data.steps.map((s) => ({
|
|
449
|
+
id: s.id,
|
|
450
|
+
label: s.label,
|
|
451
|
+
passed: s.complete,
|
|
452
|
+
required: s.required,
|
|
453
|
+
hint: s.hint
|
|
454
|
+
})),
|
|
455
|
+
diagnostic: data.diagnostic ?? null,
|
|
456
|
+
summary
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
server.registerTool(
|
|
461
|
+
"diagnose_connection",
|
|
462
|
+
{
|
|
463
|
+
title: titleOf("diagnose_connection"),
|
|
464
|
+
description: descOf("diagnose_connection"),
|
|
465
|
+
annotations: annotationsFor("diagnose_connection"),
|
|
466
|
+
inputSchema: {}
|
|
467
|
+
},
|
|
468
|
+
async () => {
|
|
469
|
+
const issues = [];
|
|
470
|
+
if (!apiKey?.startsWith("mushi_")) {
|
|
471
|
+
issues.push({
|
|
472
|
+
check: "mcp_api_key",
|
|
473
|
+
detail: "MCP server API key missing or malformed",
|
|
474
|
+
fix: "Run `mushi connect` to write MUSHI_API_KEY into .cursor/mcp.json, then restart the MCP server."
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (!projectId) {
|
|
478
|
+
issues.push({
|
|
479
|
+
check: "mcp_project_id",
|
|
480
|
+
detail: "No projectId configured on the MCP server",
|
|
481
|
+
fix: "Add MUSHI_PROJECT_ID to .cursor/mcp.json (copy UUID from console \u2192 Projects)."
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
if (!apiEndpoint) {
|
|
485
|
+
issues.push({
|
|
486
|
+
check: "mcp_endpoint",
|
|
487
|
+
detail: "No API endpoint configured",
|
|
488
|
+
fix: "Set MUSHI_API_ENDPOINT to your `\u2026/functions/v1/api` URL in .cursor/mcp.json."
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
let healthOk = false;
|
|
492
|
+
if (apiEndpoint) {
|
|
493
|
+
try {
|
|
494
|
+
const healthRes = await doFetch(`${apiEndpoint.replace(/\/$/, "")}/health`, {
|
|
495
|
+
signal: AbortSignal.timeout(5e3)
|
|
496
|
+
});
|
|
497
|
+
healthOk = healthRes.status === 200;
|
|
498
|
+
if (!healthOk) {
|
|
499
|
+
issues.push({
|
|
500
|
+
check: "endpoint_health",
|
|
501
|
+
detail: `GET /health \u2192 HTTP ${healthRes.status}`,
|
|
502
|
+
fix: "Verify MUSHI_API_ENDPOINT and that the Supabase edge function is deployed."
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
} catch (err) {
|
|
506
|
+
issues.push({
|
|
507
|
+
check: "endpoint_health",
|
|
508
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
509
|
+
fix: "Check network connectivity and the endpoint URL in .cursor/mcp.json."
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
let ingestReady = false;
|
|
514
|
+
let dispatchReady = false;
|
|
515
|
+
try {
|
|
516
|
+
const ingest = await apiCall("/v1/sync/ingest-setup");
|
|
517
|
+
ingestReady = ingest.ready;
|
|
518
|
+
if (!ingest.ready) {
|
|
519
|
+
const failed = ingest.steps.filter((s) => s.required && !s.complete);
|
|
520
|
+
issues.push({
|
|
521
|
+
check: "ingest_setup",
|
|
522
|
+
detail: `Incomplete: ${failed.map((s) => s.label).join(", ")}`,
|
|
523
|
+
fix: failed[0]?.hint ?? "Paste the SDK snippet, start your dev server, submit a test report."
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
issues.push({
|
|
528
|
+
check: "ingest_setup",
|
|
529
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
530
|
+
fix: "Confirm API key is active for this project (Projects \u2192 API Keys)."
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
if (projectId) {
|
|
534
|
+
try {
|
|
535
|
+
const preflight = await apiCall(`/v1/admin/projects/${projectId}/preflight`);
|
|
536
|
+
dispatchReady = preflight.ready;
|
|
537
|
+
if (!preflight.ready) {
|
|
538
|
+
const failed = preflight.checks.filter((c) => !c.ready);
|
|
539
|
+
issues.push({
|
|
540
|
+
check: "dispatch_preflight",
|
|
541
|
+
detail: `Blocked: ${failed.map((c) => c.label).join(", ")}`,
|
|
542
|
+
fix: failed[0]?.hint ?? "Open Settings \u2192 Integrations and complete GitHub + BYOK setup."
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
const ready = issues.length === 0 && healthOk && ingestReady;
|
|
549
|
+
const nextAction = issues[0]?.fix ?? (ready ? "Connection healthy \u2014 SDK ingest is working. Submit a report to confirm end-to-end." : "Run `mushi doctor` in your app repo for a full local checklist.");
|
|
550
|
+
return jsonText({
|
|
551
|
+
ready,
|
|
552
|
+
healthOk,
|
|
553
|
+
ingestReady,
|
|
554
|
+
dispatchReady,
|
|
555
|
+
endpoint: apiEndpoint ?? null,
|
|
556
|
+
projectId: projectId ?? null,
|
|
557
|
+
issues,
|
|
558
|
+
nextAction,
|
|
559
|
+
summary: ready ? "MCP credentials valid; ingest pipeline ready." : `Connection issue \u2014 ${issues[0]?.check ?? "unknown"}: ${nextAction}`
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
);
|
|
563
|
+
server.registerTool(
|
|
564
|
+
"list_projects",
|
|
565
|
+
{
|
|
566
|
+
title: titleOf("list_projects"),
|
|
567
|
+
description: descOf("list_projects"),
|
|
568
|
+
annotations: annotationsFor("list_projects"),
|
|
569
|
+
inputSchema: {}
|
|
570
|
+
},
|
|
571
|
+
async () => {
|
|
572
|
+
const data = await apiCall("/v1/admin/mcp/projects");
|
|
573
|
+
return jsonText(data);
|
|
574
|
+
}
|
|
575
|
+
);
|
|
576
|
+
server.registerTool(
|
|
577
|
+
"get_project_context",
|
|
578
|
+
{
|
|
579
|
+
title: titleOf("get_project_context"),
|
|
580
|
+
description: descOf("get_project_context"),
|
|
581
|
+
annotations: annotationsFor("get_project_context"),
|
|
582
|
+
inputSchema: {
|
|
583
|
+
project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
async (args) => {
|
|
587
|
+
const pid = args.project_id ?? projectId;
|
|
588
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
|
|
589
|
+
const [preflightRes, activationRes] = await Promise.allSettled([
|
|
590
|
+
apiCall(`/v1/admin/projects/${pid}/preflight`),
|
|
591
|
+
apiCall(`/v1/admin/activation?project_id=${encodeURIComponent(pid)}`)
|
|
592
|
+
]);
|
|
593
|
+
return jsonText({
|
|
594
|
+
project_id: pid,
|
|
595
|
+
preflight: preflightRes.status === "fulfilled" ? preflightRes.value : { error: String(preflightRes.reason) },
|
|
596
|
+
activation: activationRes.status === "fulfilled" ? activationRes.value : { error: String(activationRes.reason) }
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
);
|
|
600
|
+
server.registerTool(
|
|
601
|
+
"get_pipeline_logs",
|
|
602
|
+
{
|
|
603
|
+
title: titleOf("get_pipeline_logs"),
|
|
604
|
+
description: descOf("get_pipeline_logs"),
|
|
605
|
+
annotations: annotationsFor("get_pipeline_logs"),
|
|
606
|
+
inputSchema: {
|
|
607
|
+
project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
|
|
608
|
+
service: z.enum(["classify-report", "fix-worker", "qa-story-runner", "mcp", "webhook", "api", "all"]).optional().describe("Pipeline service to filter (default: all)."),
|
|
609
|
+
since: z.string().optional().describe("ISO-8601 timestamp \u2014 return only entries after this time."),
|
|
610
|
+
limit: z.number().optional().describe("Max entries to return (default 50, max 200)."),
|
|
611
|
+
level: z.enum(["info", "warn", "error", "fatal"]).optional().describe("Minimum severity level (default: warn).")
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
async (args) => {
|
|
615
|
+
const pid = args.project_id ?? projectId;
|
|
616
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
|
|
617
|
+
const qs = new URLSearchParams();
|
|
618
|
+
if (args.service && args.service !== "all") qs.set("service", args.service);
|
|
619
|
+
if (args.since) qs.set("since", args.since);
|
|
620
|
+
qs.set("limit", String(Math.min(args.limit ?? 50, 200)));
|
|
621
|
+
if (args.level) qs.set("level", args.level);
|
|
622
|
+
const data = await apiCall(`/v1/admin/mcp/logs/${pid}?${qs}`);
|
|
623
|
+
return jsonText(data);
|
|
624
|
+
}
|
|
625
|
+
);
|
|
626
|
+
server.registerTool(
|
|
627
|
+
"get_report_evidence",
|
|
628
|
+
{
|
|
629
|
+
title: titleOf("get_report_evidence"),
|
|
630
|
+
description: descOf("get_report_evidence"),
|
|
631
|
+
annotations: annotationsFor("get_report_evidence"),
|
|
632
|
+
inputSchema: {
|
|
633
|
+
report_id: z.string().describe("Report UUID.")
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
async (args) => {
|
|
637
|
+
const [reportRes, timelineRes] = await Promise.allSettled([
|
|
638
|
+
apiCall(`/v1/admin/reports/${args.report_id}`),
|
|
639
|
+
apiCall(`/v1/admin/reports/${args.report_id}/timeline`)
|
|
640
|
+
]);
|
|
641
|
+
const report = reportRes.status === "fulfilled" ? reportRes.value : null;
|
|
642
|
+
const timeline = timelineRes.status === "fulfilled" ? timelineRes.value : null;
|
|
643
|
+
const evidence = report ? {
|
|
644
|
+
report_id: args.report_id,
|
|
645
|
+
description: report.description,
|
|
646
|
+
summary: report.summary,
|
|
647
|
+
screenshot_url: report.screenshot_url ?? null,
|
|
648
|
+
environment: report.environment ?? null,
|
|
649
|
+
breadcrumbs: report.breadcrumbs ?? null,
|
|
650
|
+
sentry_replay_id: report.sentry_replay_id ?? null,
|
|
651
|
+
sentry_trace_id: report.sentry_trace_id ?? null,
|
|
652
|
+
sentry_event_id: report.sentry_event_id ?? null,
|
|
653
|
+
session_id: report.session_id ?? null,
|
|
654
|
+
created_at: report.created_at,
|
|
655
|
+
tags: report.tags ?? null
|
|
656
|
+
} : { error: String(reportRes.reason) };
|
|
657
|
+
return jsonText({
|
|
658
|
+
evidence,
|
|
659
|
+
reporter_thread: timeline ?? { error: String(timelineRes.reason) }
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
);
|
|
663
|
+
server.registerTool(
|
|
664
|
+
"triage_issue",
|
|
665
|
+
{
|
|
666
|
+
title: titleOf("triage_issue"),
|
|
667
|
+
description: descOf("triage_issue"),
|
|
668
|
+
annotations: annotationsFor("triage_issue"),
|
|
669
|
+
inputSchema: {
|
|
670
|
+
report_id: z.string().describe("Report UUID to triage."),
|
|
671
|
+
project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
|
|
672
|
+
include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
|
|
673
|
+
}
|
|
674
|
+
},
|
|
675
|
+
async (args) => {
|
|
676
|
+
const pid = args.project_id ?? projectId;
|
|
677
|
+
const includeLogs = args.include_logs !== false;
|
|
678
|
+
const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
|
|
679
|
+
apiCall(`/v1/admin/reports/${args.report_id}`),
|
|
680
|
+
apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
|
|
681
|
+
apiCall(`/v1/admin/reports/similarity`, {
|
|
682
|
+
method: "POST",
|
|
683
|
+
body: JSON.stringify({ report_id: args.report_id })
|
|
684
|
+
}).catch(() => null),
|
|
685
|
+
pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
|
|
686
|
+
pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
|
|
687
|
+
includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
|
|
688
|
+
]);
|
|
689
|
+
const report = reportRes.status === "fulfilled" ? reportRes.value : null;
|
|
690
|
+
const severity = report ? report.severity : "unknown";
|
|
691
|
+
const category = report ? report.category : "unknown";
|
|
692
|
+
const status = report ? report.status : "unknown";
|
|
693
|
+
const actions = [];
|
|
694
|
+
if (status === "new" || status === "classified") {
|
|
695
|
+
actions.push({
|
|
696
|
+
action: "dispatch_fix",
|
|
697
|
+
reason: "Report is classified but no fix has been attempted",
|
|
698
|
+
tool: "dispatch_fix",
|
|
699
|
+
args: { reportId: args.report_id, agent: "cursor_cloud" }
|
|
700
|
+
});
|
|
701
|
+
} else if (status === "fixing") {
|
|
702
|
+
actions.push({
|
|
703
|
+
action: "check_fix_progress",
|
|
704
|
+
reason: "Fix is in progress \u2014 check the fix timeline for latest status",
|
|
705
|
+
tool: "get_fix_timeline",
|
|
706
|
+
args: { reportId: args.report_id }
|
|
707
|
+
});
|
|
708
|
+
} else if (status === "fixed") {
|
|
709
|
+
actions.push({
|
|
710
|
+
action: "verify_fix",
|
|
711
|
+
reason: "Fix was applied \u2014 verify it resolved the issue",
|
|
712
|
+
tool: "get_fix_context",
|
|
713
|
+
args: { reportId: args.report_id }
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
return jsonText({
|
|
717
|
+
report_id: args.report_id,
|
|
718
|
+
severity,
|
|
719
|
+
category,
|
|
720
|
+
status,
|
|
721
|
+
report: reportRes.status === "fulfilled" ? reportRes.value : { error: String(reportRes.reason) },
|
|
722
|
+
reporter_thread: evidenceRes.status === "fulfilled" ? evidenceRes.value : null,
|
|
723
|
+
similar_bugs: similarRes.status === "fulfilled" ? similarRes.value : null,
|
|
724
|
+
fix_context: fixCtxRes.status === "fulfilled" ? fixCtxRes.value : null,
|
|
725
|
+
blast_radius: blastRes.status === "fulfilled" ? blastRes.value : null,
|
|
726
|
+
recent_logs: logsRes.status === "fulfilled" ? logsRes.value : null,
|
|
727
|
+
recommended_actions: actions,
|
|
728
|
+
triage_summary: `[${severity}] ${category} \u2014 status: ${status}. ${actions.length > 0 ? `Recommended: ${actions[0]?.action}.` : "No action required."}`
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
);
|
|
732
|
+
server.registerTool(
|
|
733
|
+
"submit_fix_result",
|
|
734
|
+
{
|
|
735
|
+
title: titleOf("submit_fix_result"),
|
|
736
|
+
description: descOf("submit_fix_result"),
|
|
737
|
+
annotations: annotationsFor("submit_fix_result"),
|
|
738
|
+
inputSchema: {
|
|
739
|
+
reportId: z.string().describe("The report UUID"),
|
|
740
|
+
branch: z.string().describe("Git branch name"),
|
|
741
|
+
prUrl: z.string().optional().describe("GitHub PR URL"),
|
|
742
|
+
filesChanged: z.array(z.string()).describe("Files modified"),
|
|
743
|
+
linesChanged: z.number().describe("Total lines changed"),
|
|
744
|
+
summary: z.string().describe("Fix summary"),
|
|
745
|
+
idempotencyKey: z.string().uuid().optional().describe("Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows")
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
async (args) => {
|
|
749
|
+
const idemKey = args.idempotencyKey ?? crypto.randomUUID();
|
|
750
|
+
const created = await apiCall("/v1/admin/fixes", {
|
|
751
|
+
method: "POST",
|
|
752
|
+
headers: { "Idempotency-Key": idemKey },
|
|
753
|
+
body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
|
|
754
|
+
});
|
|
755
|
+
await apiCall(`/v1/admin/fixes/${created.fixId}`, {
|
|
756
|
+
method: "PATCH",
|
|
757
|
+
body: JSON.stringify({
|
|
758
|
+
status: "completed",
|
|
759
|
+
branch: args.branch,
|
|
760
|
+
pr_url: args.prUrl,
|
|
761
|
+
files_changed: args.filesChanged,
|
|
762
|
+
lines_changed: args.linesChanged,
|
|
763
|
+
summary: args.summary,
|
|
764
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
765
|
+
})
|
|
766
|
+
});
|
|
767
|
+
return jsonText({ ok: true, fixId: created.fixId });
|
|
768
|
+
}
|
|
769
|
+
);
|
|
770
|
+
server.registerTool(
|
|
771
|
+
"dispatch_fix",
|
|
772
|
+
{
|
|
773
|
+
title: titleOf("dispatch_fix"),
|
|
774
|
+
description: descOf("dispatch_fix"),
|
|
775
|
+
annotations: annotationsFor("dispatch_fix"),
|
|
776
|
+
inputSchema: {
|
|
777
|
+
reportId: z.string().describe("Report UUID to fix"),
|
|
778
|
+
agent: z.enum(["claude_code", "codex", "rest_worker", "mcp"]).optional().describe("Override the agent adapter"),
|
|
779
|
+
idempotencyKey: z.string().uuid().optional().describe("Optional RFC 4122 UUID. Resend the same key to safely retry without dispatching a duplicate fix job (Idempotency-Key IETF draft)."),
|
|
780
|
+
inventoryActionNodeId: z.string().uuid().optional().describe("Optional inventory Action node UUID for spec-traceability (\xA72.10). When provided, the fix-worker embeds the expected_outcome contract in the LLM prompt and runs validateAgainstSpec before opening the PR.")
|
|
781
|
+
},
|
|
782
|
+
outputSchema: {
|
|
783
|
+
fixId: z.string(),
|
|
784
|
+
status: z.string()
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
async (args, extra) => {
|
|
788
|
+
if (extra?.sendNotification && extra?._meta?.progressToken) {
|
|
789
|
+
try {
|
|
790
|
+
await extra.sendNotification({
|
|
791
|
+
method: "notifications/progress",
|
|
792
|
+
params: {
|
|
793
|
+
progressToken: extra._meta.progressToken,
|
|
794
|
+
progress: 0,
|
|
795
|
+
total: 100,
|
|
796
|
+
message: "Dispatching Mushi fix orchestrator\u2026"
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
} catch {
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const data = await apiCall("/v1/admin/fixes/dispatch", {
|
|
803
|
+
method: "POST",
|
|
804
|
+
headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
|
|
805
|
+
body: JSON.stringify({
|
|
806
|
+
reportId: args.reportId,
|
|
807
|
+
agent: args.agent,
|
|
808
|
+
inventoryActionNodeId: args.inventoryActionNodeId,
|
|
809
|
+
...projectId ? { projectId } : {}
|
|
810
|
+
})
|
|
811
|
+
});
|
|
812
|
+
return jsonResult(data);
|
|
813
|
+
}
|
|
814
|
+
);
|
|
815
|
+
server.registerTool(
|
|
816
|
+
"trigger_judge",
|
|
817
|
+
{
|
|
818
|
+
title: titleOf("trigger_judge"),
|
|
819
|
+
description: descOf("trigger_judge"),
|
|
820
|
+
annotations: annotationsFor("trigger_judge"),
|
|
821
|
+
inputSchema: {
|
|
822
|
+
limit: z.number().optional().describe("Max reports to judge in this batch (default 25, max 100)"),
|
|
823
|
+
projectId: z.string().optional().describe("Restrict to one project when the API key owns multiple")
|
|
824
|
+
}
|
|
825
|
+
},
|
|
826
|
+
async (args) => {
|
|
827
|
+
const data = await apiCall("/v1/admin/judge/run", {
|
|
828
|
+
method: "POST",
|
|
829
|
+
body: JSON.stringify({
|
|
830
|
+
limit: Math.min(args.limit ?? 25, 100),
|
|
831
|
+
projectId: args.projectId ?? projectId ?? void 0
|
|
832
|
+
})
|
|
833
|
+
});
|
|
834
|
+
return jsonText(data);
|
|
835
|
+
}
|
|
836
|
+
);
|
|
837
|
+
server.registerTool(
|
|
838
|
+
"test_gen_from_report",
|
|
839
|
+
{
|
|
840
|
+
title: titleOf("test_gen_from_report"),
|
|
841
|
+
description: descOf("test_gen_from_report"),
|
|
842
|
+
annotations: annotationsFor("test_gen_from_report"),
|
|
843
|
+
inputSchema: {
|
|
844
|
+
reportId: z.string().describe("Report UUID to turn into a Playwright PR"),
|
|
845
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
async (args) => {
|
|
849
|
+
const pid = args.projectId ?? projectId;
|
|
850
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for test_gen_from_report");
|
|
851
|
+
const data = await apiCall(`/v1/admin/inventory/${pid}/test-gen/from-report/${args.reportId}`, {
|
|
852
|
+
method: "POST",
|
|
853
|
+
body: JSON.stringify({})
|
|
854
|
+
});
|
|
855
|
+
return jsonText(data);
|
|
856
|
+
}
|
|
857
|
+
);
|
|
858
|
+
server.registerTool(
|
|
859
|
+
"merge_fix",
|
|
860
|
+
{
|
|
861
|
+
title: "Merge fix PR",
|
|
862
|
+
description: "Squash-merge a fix attempt PR and mark the linked report fixed.",
|
|
863
|
+
annotations: annotationsFor("merge_fix"),
|
|
864
|
+
inputSchema: {
|
|
865
|
+
fixId: z.string().describe("Fix attempt UUID"),
|
|
866
|
+
mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method")
|
|
867
|
+
}
|
|
868
|
+
},
|
|
869
|
+
async (args) => {
|
|
870
|
+
const data = await apiCall(`/v1/admin/fixes/${args.fixId}/merge`, {
|
|
871
|
+
method: "POST",
|
|
872
|
+
body: JSON.stringify({ mergeMethod: args.mergeMethod ?? "squash" })
|
|
873
|
+
});
|
|
874
|
+
return jsonText(data);
|
|
875
|
+
}
|
|
876
|
+
);
|
|
877
|
+
server.registerTool(
|
|
878
|
+
"refresh_ci",
|
|
879
|
+
{
|
|
880
|
+
title: "Refresh fix CI status",
|
|
881
|
+
description: "Pull the latest GitHub check-run status for a fix attempt.",
|
|
882
|
+
annotations: annotationsFor("refresh_ci"),
|
|
883
|
+
inputSchema: {
|
|
884
|
+
fixId: z.string().describe("Fix attempt UUID")
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
async (args) => {
|
|
888
|
+
const data = await apiCall(`/v1/admin/fixes/${args.fixId}/refresh-ci`, { method: "POST" });
|
|
889
|
+
return jsonText(data);
|
|
890
|
+
}
|
|
891
|
+
);
|
|
892
|
+
server.registerTool(
|
|
893
|
+
"reopen_report",
|
|
894
|
+
{
|
|
895
|
+
title: titleOf("reopen_report"),
|
|
896
|
+
description: descOf("reopen_report"),
|
|
897
|
+
annotations: annotationsFor("reopen_report"),
|
|
898
|
+
inputSchema: {
|
|
899
|
+
reportId: z.string().describe("Report UUID"),
|
|
900
|
+
note: z.string().optional().describe("Triage note")
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
async (args) => {
|
|
904
|
+
const data = await apiCall(`/v1/sync/reports/${args.reportId}`, {
|
|
905
|
+
method: "PATCH",
|
|
906
|
+
body: JSON.stringify({ status: "reopened", note: args.note })
|
|
907
|
+
});
|
|
908
|
+
return jsonText(data);
|
|
909
|
+
}
|
|
910
|
+
);
|
|
911
|
+
server.registerTool(
|
|
912
|
+
"transition_status",
|
|
913
|
+
{
|
|
914
|
+
title: titleOf("transition_status"),
|
|
915
|
+
description: descOf("transition_status"),
|
|
916
|
+
annotations: annotationsFor("transition_status"),
|
|
917
|
+
inputSchema: {
|
|
918
|
+
reportId: z.string().describe("Report UUID"),
|
|
919
|
+
status: z.enum(["pending", "classified", "grouped", "fixing", "fixed", "resolved", "verified", "reopened", "dismissed"]).describe("Target status"),
|
|
920
|
+
reason: z.string().optional().describe("Reason for the transition (audit trail)")
|
|
921
|
+
}
|
|
922
|
+
},
|
|
923
|
+
async (args) => {
|
|
924
|
+
const data = await apiCall(`/v1/admin/reports/${args.reportId}`, {
|
|
925
|
+
method: "PATCH",
|
|
926
|
+
body: JSON.stringify({ status: args.status, reason: args.reason })
|
|
927
|
+
});
|
|
928
|
+
return jsonText(data);
|
|
929
|
+
}
|
|
930
|
+
);
|
|
931
|
+
server.registerTool(
|
|
932
|
+
"setup_repo_for_mushi",
|
|
933
|
+
{
|
|
934
|
+
title: titleOf("setup_repo_for_mushi"),
|
|
935
|
+
description: descOf("setup_repo_for_mushi"),
|
|
936
|
+
annotations: annotationsFor("setup_repo_for_mushi"),
|
|
937
|
+
inputSchema: {
|
|
938
|
+
projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
|
|
939
|
+
}
|
|
940
|
+
},
|
|
941
|
+
async (args) => {
|
|
942
|
+
const pid = args.projectId ?? projectId;
|
|
943
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for setup_repo_for_mushi");
|
|
944
|
+
const data = await apiCall(`/v1/admin/projects/${pid}/repo/bootstrap`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
body: JSON.stringify({})
|
|
947
|
+
});
|
|
948
|
+
return jsonText(data);
|
|
949
|
+
}
|
|
950
|
+
);
|
|
951
|
+
server.registerTool(
|
|
952
|
+
"query_lessons",
|
|
953
|
+
{
|
|
954
|
+
title: titleOf("query_lessons"),
|
|
955
|
+
description: descOf("query_lessons"),
|
|
956
|
+
annotations: annotationsFor("query_lessons"),
|
|
957
|
+
inputSchema: {
|
|
958
|
+
diff_text: z.string().describe("The PR diff, code snippet, or description of the change being made."),
|
|
959
|
+
max_tokens: z.number().optional().describe("Maximum tokens for returned lessons context (default 3000, max 8000)."),
|
|
960
|
+
top_k: z.number().optional().describe("Max number of lessons to return (default 15, max 50)."),
|
|
961
|
+
project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
|
|
962
|
+
}
|
|
963
|
+
},
|
|
964
|
+
async (args) => {
|
|
965
|
+
const pid = args.project_id ?? projectId;
|
|
966
|
+
const params = new URLSearchParams();
|
|
967
|
+
params.set("diff_text", args.diff_text);
|
|
968
|
+
if (args.max_tokens !== void 0) params.set("max_tokens", String(args.max_tokens));
|
|
969
|
+
if (args.top_k !== void 0) params.set("top_k", String(args.top_k));
|
|
970
|
+
if (pid) params.set("projectId", pid);
|
|
971
|
+
const data = await apiCall(`/v1/admin/lessons/query?${params}`);
|
|
972
|
+
return jsonText(data);
|
|
973
|
+
}
|
|
974
|
+
);
|
|
975
|
+
server.registerTool(
|
|
976
|
+
"list_lessons",
|
|
977
|
+
{
|
|
978
|
+
title: titleOf("list_lessons"),
|
|
979
|
+
description: descOf("list_lessons"),
|
|
980
|
+
annotations: annotationsFor("list_lessons"),
|
|
981
|
+
inputSchema: {
|
|
982
|
+
severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter by severity level."),
|
|
983
|
+
limit: z.number().optional().describe("Max number of lessons to return (default 50, max 200)."),
|
|
984
|
+
project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
async (args) => {
|
|
988
|
+
const pid = args.project_id ?? projectId;
|
|
989
|
+
const params = new URLSearchParams();
|
|
990
|
+
if (args.severity) params.set("severity", args.severity);
|
|
991
|
+
params.set("limit", String(Math.min(args.limit ?? 50, 200)));
|
|
992
|
+
if (pid) params.set("projectId", pid);
|
|
993
|
+
const data = await apiCall(`/v1/admin/lessons?${params}`);
|
|
994
|
+
return jsonText(data);
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
server.resource(
|
|
998
|
+
"project_stats",
|
|
999
|
+
"project://stats",
|
|
1000
|
+
{ description: "Report counts, category breakdown, severity distribution" },
|
|
1001
|
+
async () => ({
|
|
1002
|
+
contents: [{ uri: "project://stats", mimeType: "application/json", text: JSON.stringify(await apiCall("/v1/admin/stats"), null, 2) }]
|
|
1003
|
+
})
|
|
1004
|
+
);
|
|
1005
|
+
server.resource(
|
|
1006
|
+
"project_settings",
|
|
1007
|
+
"project://settings",
|
|
1008
|
+
{ description: "Project configuration \u2014 autofix agent, plugins enabled, ontology, LLM budgets" },
|
|
1009
|
+
async () => ({
|
|
1010
|
+
contents: [{ uri: "project://settings", mimeType: "application/json", text: JSON.stringify(await apiCall("/v1/admin/settings"), null, 2) }]
|
|
1011
|
+
})
|
|
1012
|
+
);
|
|
1013
|
+
server.resource(
|
|
1014
|
+
"project_dashboard",
|
|
1015
|
+
"project://dashboard",
|
|
1016
|
+
{ description: "PDCA health snapshot \u2014 stage counts, bottleneck, recent activity (same payload the admin console polls)" },
|
|
1017
|
+
async () => ({
|
|
1018
|
+
contents: [{ uri: "project://dashboard", mimeType: "application/json", text: JSON.stringify(await apiCall("/v1/admin/dashboard"), null, 2) }]
|
|
1019
|
+
})
|
|
1020
|
+
);
|
|
1021
|
+
server.registerTool(
|
|
1022
|
+
"list_top_contributors",
|
|
1023
|
+
{
|
|
1024
|
+
title: titleOf("list_top_contributors"),
|
|
1025
|
+
description: descOf("list_top_contributors"),
|
|
1026
|
+
annotations: annotationsFor("list_top_contributors"),
|
|
1027
|
+
inputSchema: {
|
|
1028
|
+
limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
|
|
1029
|
+
range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
async ({ limit, range }) => ({
|
|
1033
|
+
content: [{
|
|
1034
|
+
type: "text",
|
|
1035
|
+
text: JSON.stringify(
|
|
1036
|
+
await apiCall(`/v1/admin/rewards/leaderboard?range=${range}&limit=${limit}`),
|
|
1037
|
+
null,
|
|
1038
|
+
2
|
|
1039
|
+
)
|
|
1040
|
+
}]
|
|
1041
|
+
})
|
|
1042
|
+
);
|
|
1043
|
+
server.registerTool(
|
|
1044
|
+
"award_bonus_points",
|
|
1045
|
+
{
|
|
1046
|
+
title: titleOf("award_bonus_points"),
|
|
1047
|
+
description: descOf("award_bonus_points"),
|
|
1048
|
+
annotations: annotationsFor("award_bonus_points"),
|
|
1049
|
+
inputSchema: {
|
|
1050
|
+
external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
|
|
1051
|
+
points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
|
|
1052
|
+
reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
async ({ external_user_id, points, reason }) => ({
|
|
1056
|
+
content: [{
|
|
1057
|
+
type: "text",
|
|
1058
|
+
text: JSON.stringify(
|
|
1059
|
+
await apiCall("/v1/admin/rewards/bonus-points", {
|
|
1060
|
+
method: "POST",
|
|
1061
|
+
headers: { "Content-Type": "application/json" },
|
|
1062
|
+
body: JSON.stringify({ external_user_id, points, reason })
|
|
1063
|
+
}),
|
|
1064
|
+
null,
|
|
1065
|
+
2
|
|
1066
|
+
)
|
|
1067
|
+
}]
|
|
1068
|
+
})
|
|
1069
|
+
);
|
|
1070
|
+
server.registerTool(
|
|
1071
|
+
"set_tier",
|
|
1072
|
+
{
|
|
1073
|
+
title: titleOf("set_tier"),
|
|
1074
|
+
description: descOf("set_tier"),
|
|
1075
|
+
annotations: annotationsFor("set_tier"),
|
|
1076
|
+
inputSchema: {
|
|
1077
|
+
external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
|
|
1078
|
+
tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
|
|
1079
|
+
reason: z.string().max(200).optional().describe("Optional reason for manual override")
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
1082
|
+
async ({ external_user_id, tier_slug, reason }) => ({
|
|
1083
|
+
content: [{
|
|
1084
|
+
type: "text",
|
|
1085
|
+
text: JSON.stringify(
|
|
1086
|
+
await apiCall("/v1/admin/rewards/set-tier", {
|
|
1087
|
+
method: "POST",
|
|
1088
|
+
headers: { "Content-Type": "application/json" },
|
|
1089
|
+
body: JSON.stringify({ external_user_id, tier_slug, reason })
|
|
1090
|
+
}),
|
|
1091
|
+
null,
|
|
1092
|
+
2
|
|
1093
|
+
)
|
|
1094
|
+
}]
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
server.resource(
|
|
1098
|
+
"privacy_status",
|
|
1099
|
+
"privacy://status",
|
|
1100
|
+
{
|
|
1101
|
+
description: "Returns the privacy posture for this project: storage region, LLM provider, whether BYOK is configured, data retention window, and last audit timestamp."
|
|
1102
|
+
},
|
|
1103
|
+
async () => ({
|
|
1104
|
+
contents: [{
|
|
1105
|
+
uri: "privacy://status",
|
|
1106
|
+
mimeType: "application/json",
|
|
1107
|
+
text: JSON.stringify(await apiCall("/v1/admin/privacy/status"), null, 2)
|
|
1108
|
+
}]
|
|
1109
|
+
})
|
|
1110
|
+
);
|
|
1111
|
+
server.resource(
|
|
1112
|
+
"evolution_history",
|
|
1113
|
+
"evolution://history",
|
|
1114
|
+
{
|
|
1115
|
+
description: "Returns the project's last 30 days of judge scores, prompt promotions, fixed-bug count, and lesson inductions. Agents can read this to see whether the loop is converging (rising judge scores, falling recurrence) or stalling."
|
|
1116
|
+
},
|
|
1117
|
+
async () => ({
|
|
1118
|
+
contents: [{
|
|
1119
|
+
uri: "evolution://history",
|
|
1120
|
+
mimeType: "application/json",
|
|
1121
|
+
text: JSON.stringify(await apiCall("/v1/admin/evolution/history"), null, 2)
|
|
1122
|
+
}]
|
|
1123
|
+
})
|
|
1124
|
+
);
|
|
1125
|
+
server.resource(
|
|
1126
|
+
"project_integration_health",
|
|
1127
|
+
"project://integration-health",
|
|
1128
|
+
{
|
|
1129
|
+
description: "Live health status of every configured BYOK channel (Sentry, GitHub, LangFuse, PagerDuty, \u2026). Orchestrators should check this before dispatching a fix to fail-fast on broken channels rather than burning LLM budget and discovering the failure mid-run."
|
|
1130
|
+
},
|
|
1131
|
+
async () => ({
|
|
1132
|
+
contents: [{
|
|
1133
|
+
uri: "project://integration-health",
|
|
1134
|
+
mimeType: "application/json",
|
|
1135
|
+
text: JSON.stringify(await apiCall("/v1/admin/integrations/health"), null, 2)
|
|
1136
|
+
}]
|
|
1137
|
+
})
|
|
1138
|
+
);
|
|
1139
|
+
server.resource(
|
|
1140
|
+
"inventory_current",
|
|
1141
|
+
"inventory://current",
|
|
1142
|
+
{
|
|
1143
|
+
description: "Current inventory snapshot for the active project \u2014 all Action nodes with their spec contract (expected_outcome), build-gate status, linked reports, and fix attempts. Subscribe to this resource to receive notifications/resources/updated when the inventory is re-crawled (e.g. after a PR merge or manual trigger). Orchestrators can use this to enumerate work items and pick the next Action to fix."
|
|
1144
|
+
},
|
|
1145
|
+
async () => {
|
|
1146
|
+
const path = projectId ? `/v1/admin/inventory/${projectId}` : "/v1/admin/inventory";
|
|
1147
|
+
return {
|
|
1148
|
+
contents: [{
|
|
1149
|
+
uri: "inventory://current",
|
|
1150
|
+
mimeType: "application/json",
|
|
1151
|
+
text: JSON.stringify(await apiCall(path), null, 2)
|
|
1152
|
+
}]
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
);
|
|
1156
|
+
server.resource(
|
|
1157
|
+
"activation_status",
|
|
1158
|
+
"mushi://activation",
|
|
1159
|
+
{
|
|
1160
|
+
description: "Unified setup posture \u2014 SDK heartbeat, reports, GitHub, MCP readiness, QA stories, and the next best action."
|
|
1161
|
+
},
|
|
1162
|
+
async () => {
|
|
1163
|
+
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : "";
|
|
1164
|
+
return {
|
|
1165
|
+
contents: [{
|
|
1166
|
+
uri: "mushi://activation",
|
|
1167
|
+
mimeType: "application/json",
|
|
1168
|
+
text: JSON.stringify(await apiCall(`/v1/admin/activation${qs}`), null, 2)
|
|
1169
|
+
}]
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
);
|
|
1173
|
+
server.prompt(
|
|
1174
|
+
"summarize_report_for_fix",
|
|
1175
|
+
"Turn a Mushi report into a one-line root cause, smallest file set, repro steps, and blast-radius warnings. Use before asking an agent to write the patch.",
|
|
1176
|
+
{ reportId: z.string().describe("The report UUID to summarize") },
|
|
1177
|
+
({ reportId }) => ({
|
|
1178
|
+
messages: [{
|
|
1179
|
+
role: "user",
|
|
1180
|
+
content: {
|
|
1181
|
+
type: "text",
|
|
1182
|
+
text: `You are a senior engineer preparing a fix plan. Use the Mushi MCP tools to:
|
|
1183
|
+
1. Call get_fix_context for reportId "${reportId}".
|
|
1184
|
+
2. Call get_blast_radius if the report has a component node id.
|
|
1185
|
+
3. Call get_similar_bugs with the component or summary as the query.
|
|
1186
|
+
|
|
1187
|
+
Then produce a markdown fix plan with exactly these sections:
|
|
1188
|
+
- One-line root cause
|
|
1189
|
+
- Files likely to change (smallest set that fixes the root cause)
|
|
1190
|
+
- Reproduction steps (numbered, \u22645)
|
|
1191
|
+
- Blast-radius warnings (what else might break)
|
|
1192
|
+
- Confidence (low/medium/high) with a one-line justification
|
|
1193
|
+
|
|
1194
|
+
Lead with the fix. Skip the preamble.`
|
|
1195
|
+
}
|
|
1196
|
+
}]
|
|
1197
|
+
})
|
|
1198
|
+
);
|
|
1199
|
+
server.prompt(
|
|
1200
|
+
"explain_judge_result",
|
|
1201
|
+
"Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
|
|
1202
|
+
{ fixId: z.string().describe("The fix_attempt UUID to explain") },
|
|
1203
|
+
({ fixId }) => ({
|
|
1204
|
+
messages: [{
|
|
1205
|
+
role: "user",
|
|
1206
|
+
content: {
|
|
1207
|
+
type: "text",
|
|
1208
|
+
text: `You are a release engineer. Use the Mushi MCP tools:
|
|
1209
|
+
1. Call get_fix_timeline for fixId "${fixId}" to see the full PDCA journey.
|
|
1210
|
+
|
|
1211
|
+
Then write a short verdict in this format:
|
|
1212
|
+
- **Recommendation:** ship / iterate / dismiss
|
|
1213
|
+
- **Why:** 1\u20132 sentences referencing the judge scores and CI signal
|
|
1214
|
+
- **If iterate:** bullet list of the smallest next patch
|
|
1215
|
+
|
|
1216
|
+
No preamble, no score-by-score recap \u2014 the numbers are in the tool output.`
|
|
1217
|
+
}
|
|
1218
|
+
}]
|
|
1219
|
+
})
|
|
1220
|
+
);
|
|
1221
|
+
server.prompt(
|
|
1222
|
+
"triage_next_steps",
|
|
1223
|
+
'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
|
|
1224
|
+
{},
|
|
1225
|
+
() => ({
|
|
1226
|
+
messages: [{
|
|
1227
|
+
role: "user",
|
|
1228
|
+
content: {
|
|
1229
|
+
type: "text",
|
|
1230
|
+
text: `You are a tech lead looking at the Mushi cockpit. Use the MCP tools:
|
|
1231
|
+
1. Read the project://dashboard resource for the PDCA snapshot.
|
|
1232
|
+
2. Call get_recent_reports with status="classified", limit=10.
|
|
1233
|
+
|
|
1234
|
+
Then output exactly 5 bullets, in priority order, each formatted as:
|
|
1235
|
+
\`**Action** \u2014 why it matters \u2014 suggested tool call\`
|
|
1236
|
+
|
|
1237
|
+
Prefer items that are bottlenecks or critical severity. Skip filler.`
|
|
1238
|
+
}
|
|
1239
|
+
}]
|
|
1240
|
+
})
|
|
1241
|
+
);
|
|
1242
|
+
server.prompt(
|
|
1243
|
+
"mushi_setup",
|
|
1244
|
+
"Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
|
|
1245
|
+
{},
|
|
1246
|
+
() => ({
|
|
1247
|
+
messages: [{
|
|
1248
|
+
role: "user",
|
|
1249
|
+
content: {
|
|
1250
|
+
type: "text",
|
|
1251
|
+
text: `You are a Mushi onboarding copilot. Use the MCP tools:
|
|
1252
|
+
1. Read mushi://activation for unified setup posture.
|
|
1253
|
+
2. Read project://integration-health if GitHub or Sentry looks blocked.
|
|
1254
|
+
3. Call get_activation_status if the resource is unavailable.
|
|
1255
|
+
|
|
1256
|
+
Then output:
|
|
1257
|
+
- **Status:** one sentence on what is done vs blocked
|
|
1258
|
+
- **Next step:** the single highest-leverage action (console link or CLI command)
|
|
1259
|
+
- **Prove it:** how the user verifies the step worked
|
|
1260
|
+
- **If still stuck:** one diagnostic command (e.g. mushi doctor --fix)
|
|
1261
|
+
|
|
1262
|
+
Be specific. No generic advice.`
|
|
1263
|
+
}
|
|
1264
|
+
}]
|
|
1265
|
+
})
|
|
1266
|
+
);
|
|
1267
|
+
server.registerTool(
|
|
1268
|
+
"map_user_stories",
|
|
1269
|
+
{
|
|
1270
|
+
title: titleOf("map_user_stories", TDD_TOOL_CATALOG),
|
|
1271
|
+
description: descOf("map_user_stories", TDD_TOOL_CATALOG),
|
|
1272
|
+
annotations: annotationsFor("map_user_stories", TDD_TOOL_CATALOG),
|
|
1273
|
+
inputSchema: {
|
|
1274
|
+
projectId: z.string().describe("Project id to map stories for"),
|
|
1275
|
+
baseUrl: z.string().url().describe("Live app URL to crawl"),
|
|
1276
|
+
maxPages: z.number().int().min(1).max(50).optional().describe("Max pages to crawl (default 20)"),
|
|
1277
|
+
provider: z.enum(["firecrawl", "browserbase"]).optional().describe("Crawl provider (default: firecrawl)"),
|
|
1278
|
+
cursorCloudRefine: z.boolean().optional().describe("Dispatch Cursor Cloud agent to refine and open a PR")
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
async ({ projectId: projectId2, baseUrl, maxPages, provider, cursorCloudRefine }) => {
|
|
1282
|
+
if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
|
|
1283
|
+
const data = await apiCall(
|
|
1284
|
+
`/v1/admin/inventory/${projectId2}/map-from-live`,
|
|
1285
|
+
{ method: "POST", body: JSON.stringify({ base_url: baseUrl, max_pages: maxPages, provider, cursor_cloud_refine: cursorCloudRefine }) }
|
|
1286
|
+
);
|
|
1287
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1288
|
+
}
|
|
1289
|
+
);
|
|
1290
|
+
server.registerTool(
|
|
1291
|
+
"get_map_run_status",
|
|
1292
|
+
{
|
|
1293
|
+
title: titleOf("get_map_run_status", TDD_TOOL_CATALOG),
|
|
1294
|
+
description: descOf("get_map_run_status", TDD_TOOL_CATALOG),
|
|
1295
|
+
annotations: annotationsFor("get_map_run_status", TDD_TOOL_CATALOG),
|
|
1296
|
+
inputSchema: {
|
|
1297
|
+
projectId: z.string().describe("Project id")
|
|
1298
|
+
}
|
|
1299
|
+
},
|
|
1300
|
+
async ({ projectId: projectId2 }) => {
|
|
1301
|
+
const data = await apiCall(`/v1/admin/inventory/${projectId2}/map-runs`);
|
|
1302
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
server.registerTool(
|
|
1306
|
+
"generate_tdd_from_story",
|
|
1307
|
+
{
|
|
1308
|
+
title: titleOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
|
|
1309
|
+
description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
|
|
1310
|
+
annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
|
|
1311
|
+
inputSchema: {
|
|
1312
|
+
projectId: z.string().describe("Project id"),
|
|
1313
|
+
storyNodeId: z.string().describe("User story id slug from the accepted inventory"),
|
|
1314
|
+
automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Gate mode for the generated test (default: review)"),
|
|
1315
|
+
baseUrl: z.string().url().optional().describe("Override the app base URL"),
|
|
1316
|
+
openPr: z.boolean().optional().describe("Open a draft GitHub PR (default: true)")
|
|
1317
|
+
}
|
|
1318
|
+
},
|
|
1319
|
+
async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
|
|
1320
|
+
if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
|
|
1321
|
+
const data = await apiCall(
|
|
1322
|
+
`/v1/admin/inventory/${projectId2}/stories/${storyNodeId}/generate-test`,
|
|
1323
|
+
{ method: "POST", body: JSON.stringify({ automation_mode: automationMode, base_url: baseUrl, open_pr: openPr }) }
|
|
1324
|
+
);
|
|
1325
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1326
|
+
}
|
|
1327
|
+
);
|
|
1328
|
+
server.registerTool(
|
|
1329
|
+
"improve_qa_story",
|
|
1330
|
+
{
|
|
1331
|
+
title: titleOf("improve_qa_story", TDD_TOOL_CATALOG),
|
|
1332
|
+
description: descOf("improve_qa_story", TDD_TOOL_CATALOG),
|
|
1333
|
+
annotations: annotationsFor("improve_qa_story", TDD_TOOL_CATALOG),
|
|
1334
|
+
inputSchema: {
|
|
1335
|
+
projectId: z.string().optional().describe("Project id (omit to run across all projects)")
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
async ({ projectId: projectId2 }) => {
|
|
1339
|
+
const data = await apiCall(
|
|
1340
|
+
"/v1/admin/pdca/improve-qa-stories",
|
|
1341
|
+
{ method: "POST", body: JSON.stringify({ project_id: projectId2 }) }
|
|
1342
|
+
);
|
|
1343
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1344
|
+
}
|
|
1345
|
+
);
|
|
1346
|
+
server.registerTool(
|
|
1347
|
+
"run_qa_story",
|
|
1348
|
+
{
|
|
1349
|
+
title: titleOf("run_qa_story", TDD_TOOL_CATALOG),
|
|
1350
|
+
description: descOf("run_qa_story", TDD_TOOL_CATALOG),
|
|
1351
|
+
annotations: annotationsFor("run_qa_story", TDD_TOOL_CATALOG),
|
|
1352
|
+
inputSchema: {
|
|
1353
|
+
projectId: z.string().describe("Project id"),
|
|
1354
|
+
qaStoryId: z.string().describe("qa_story id to run")
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
async ({ projectId: projectId2, qaStoryId }) => {
|
|
1358
|
+
const data = await apiCall(
|
|
1359
|
+
`/v1/admin/projects/${projectId2}/qa-stories/${qaStoryId}/run`,
|
|
1360
|
+
{ method: "POST" }
|
|
1361
|
+
);
|
|
1362
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1363
|
+
}
|
|
1364
|
+
);
|
|
1365
|
+
server.registerTool(
|
|
1366
|
+
"list_byok_keys",
|
|
1367
|
+
{
|
|
1368
|
+
title: titleOf("list_byok_keys", TDD_TOOL_CATALOG),
|
|
1369
|
+
description: descOf("list_byok_keys", TDD_TOOL_CATALOG),
|
|
1370
|
+
annotations: annotationsFor("list_byok_keys", TDD_TOOL_CATALOG),
|
|
1371
|
+
inputSchema: {
|
|
1372
|
+
projectId: z.string().describe("Project id")
|
|
1373
|
+
}
|
|
1374
|
+
},
|
|
1375
|
+
async ({ projectId: projectId2 }) => {
|
|
1376
|
+
const data = await apiCall(`/v1/admin/byok/keys?project_id=${encodeURIComponent(projectId2)}`);
|
|
1377
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1378
|
+
}
|
|
1379
|
+
);
|
|
1380
|
+
server.registerTool(
|
|
1381
|
+
"add_byok_key",
|
|
1382
|
+
{
|
|
1383
|
+
title: titleOf("add_byok_key", TDD_TOOL_CATALOG),
|
|
1384
|
+
description: descOf("add_byok_key", TDD_TOOL_CATALOG),
|
|
1385
|
+
annotations: annotationsFor("add_byok_key", TDD_TOOL_CATALOG),
|
|
1386
|
+
inputSchema: {
|
|
1387
|
+
projectId: z.string().describe("Project id"),
|
|
1388
|
+
provider: z.enum(["anthropic", "openai", "firecrawl", "browserbase", "cursor"]).describe("Provider slug"),
|
|
1389
|
+
key: z.string().min(10).describe("The API key value to add"),
|
|
1390
|
+
label: z.string().optional().describe("Human-readable label for this key"),
|
|
1391
|
+
priority: z.number().int().min(1).max(999).optional().describe("Priority for ordering (lower = higher priority)")
|
|
1392
|
+
}
|
|
1393
|
+
},
|
|
1394
|
+
async ({ projectId: projectId2, provider, key, label, priority }) => {
|
|
1395
|
+
const data = await apiCall(
|
|
1396
|
+
"/v1/admin/byok/keys",
|
|
1397
|
+
{ method: "POST", body: JSON.stringify({ project_id: projectId2, provider_slug: provider, key, label, priority }) }
|
|
1398
|
+
);
|
|
1399
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
server.registerTool(
|
|
1403
|
+
"list_pending_review_stories",
|
|
1404
|
+
{
|
|
1405
|
+
title: titleOf("list_pending_review_stories", TDD_TOOL_CATALOG),
|
|
1406
|
+
description: descOf("list_pending_review_stories", TDD_TOOL_CATALOG),
|
|
1407
|
+
annotations: annotationsFor("list_pending_review_stories", TDD_TOOL_CATALOG),
|
|
1408
|
+
inputSchema: {
|
|
1409
|
+
projectId: z.string().describe("Project id")
|
|
1410
|
+
}
|
|
1411
|
+
},
|
|
1412
|
+
async ({ projectId: projectId2 }) => {
|
|
1413
|
+
const data = await apiCall(`/v1/admin/inventory/${projectId2}/stories/pending-review`);
|
|
1414
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1415
|
+
}
|
|
1416
|
+
);
|
|
1417
|
+
server.registerTool(
|
|
1418
|
+
"approve_qa_story",
|
|
1419
|
+
{
|
|
1420
|
+
title: titleOf("approve_qa_story", TDD_TOOL_CATALOG),
|
|
1421
|
+
description: descOf("approve_qa_story", TDD_TOOL_CATALOG),
|
|
1422
|
+
annotations: annotationsFor("approve_qa_story", TDD_TOOL_CATALOG),
|
|
1423
|
+
inputSchema: {
|
|
1424
|
+
projectId: z.string().describe("Project id"),
|
|
1425
|
+
qaStoryId: z.string().describe("QA story id to approve or reject"),
|
|
1426
|
+
status: z.enum(["approved", "rejected"]).describe("New approval status")
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
async ({ projectId: projectId2, qaStoryId, status }) => {
|
|
1430
|
+
const data = await apiCall(
|
|
1431
|
+
`/v1/admin/inventory/${projectId2}/stories/${qaStoryId}/approval`,
|
|
1432
|
+
{ method: "PATCH", body: JSON.stringify({ status }) }
|
|
1433
|
+
);
|
|
1434
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1435
|
+
}
|
|
1436
|
+
);
|
|
1437
|
+
server.registerTool(
|
|
1438
|
+
"reply_to_reporter",
|
|
1439
|
+
{
|
|
1440
|
+
title: titleOf("reply_to_reporter", TDD_TOOL_CATALOG),
|
|
1441
|
+
description: descOf("reply_to_reporter", TDD_TOOL_CATALOG),
|
|
1442
|
+
annotations: annotationsFor("reply_to_reporter", TDD_TOOL_CATALOG),
|
|
1443
|
+
inputSchema: {
|
|
1444
|
+
reportId: z.string().describe("Report id to reply to"),
|
|
1445
|
+
message: z.string().min(1).max(1e4).describe("Message text to send to the reporter"),
|
|
1446
|
+
authorName: z.string().optional().describe('Display name for the admin sender (default: "Mushi Admin")')
|
|
1447
|
+
}
|
|
1448
|
+
},
|
|
1449
|
+
async ({ reportId, message, authorName }) => {
|
|
1450
|
+
const data = await apiCall(
|
|
1451
|
+
`/v1/sync/reports/${reportId}/reply`,
|
|
1452
|
+
{
|
|
1453
|
+
method: "POST",
|
|
1454
|
+
body: JSON.stringify({ message, author_name: authorName })
|
|
1455
|
+
}
|
|
1456
|
+
);
|
|
1457
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1458
|
+
}
|
|
1459
|
+
);
|
|
1460
|
+
server.registerTool(
|
|
1461
|
+
"list_qa_story_runs",
|
|
1462
|
+
{
|
|
1463
|
+
title: titleOf("list_qa_story_runs", TDD_TOOL_CATALOG),
|
|
1464
|
+
description: descOf("list_qa_story_runs", TDD_TOOL_CATALOG),
|
|
1465
|
+
annotations: annotationsFor("list_qa_story_runs", TDD_TOOL_CATALOG),
|
|
1466
|
+
inputSchema: {
|
|
1467
|
+
storyId: z.string().describe("QA story id (uuid)"),
|
|
1468
|
+
limit: z.number().int().min(1).max(50).optional().default(10).describe("Max runs to return (default 10)")
|
|
1469
|
+
}
|
|
1470
|
+
},
|
|
1471
|
+
async ({ storyId, limit }) => {
|
|
1472
|
+
const data = await apiCall(
|
|
1473
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=${limit ?? 10}`
|
|
1474
|
+
);
|
|
1475
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1476
|
+
}
|
|
1477
|
+
);
|
|
1478
|
+
server.registerTool(
|
|
1479
|
+
"get_qa_story_run",
|
|
1480
|
+
{
|
|
1481
|
+
title: titleOf("get_qa_story_run", TDD_TOOL_CATALOG),
|
|
1482
|
+
description: descOf("get_qa_story_run", TDD_TOOL_CATALOG),
|
|
1483
|
+
annotations: annotationsFor("get_qa_story_run", TDD_TOOL_CATALOG),
|
|
1484
|
+
inputSchema: {
|
|
1485
|
+
storyId: z.string().describe("QA story id (uuid)"),
|
|
1486
|
+
runId: z.string().describe("Run id (uuid) to fetch detail for")
|
|
1487
|
+
}
|
|
1488
|
+
},
|
|
1489
|
+
async ({ storyId, runId }) => {
|
|
1490
|
+
const data = await apiCall(
|
|
1491
|
+
`/v1/admin/projects/${config.projectId}/qa-stories/${storyId}/runs?limit=50`
|
|
1492
|
+
);
|
|
1493
|
+
const run = data?.data?.runs?.find((r) => r.id === runId) ?? null;
|
|
1494
|
+
if (!run) {
|
|
1495
|
+
throw new MushiApiError(
|
|
1496
|
+
404,
|
|
1497
|
+
"RUN_NOT_FOUND",
|
|
1498
|
+
`Run ${runId} not found in the 50 most recent runs for story ${storyId}`
|
|
1499
|
+
);
|
|
1500
|
+
}
|
|
1501
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
1502
|
+
}
|
|
1503
|
+
);
|
|
1504
|
+
server.registerTool(
|
|
1505
|
+
"test_notification_channel",
|
|
1506
|
+
{
|
|
1507
|
+
title: titleOf("test_notification_channel", TDD_TOOL_CATALOG),
|
|
1508
|
+
description: descOf("test_notification_channel", TDD_TOOL_CATALOG),
|
|
1509
|
+
annotations: annotationsFor("test_notification_channel", TDD_TOOL_CATALOG),
|
|
1510
|
+
inputSchema: {
|
|
1511
|
+
kind: z.enum(["slack", "discord"]).describe("Notification channel kind to test")
|
|
1512
|
+
}
|
|
1513
|
+
},
|
|
1514
|
+
async ({ kind }) => {
|
|
1515
|
+
const data = await apiCall(
|
|
1516
|
+
`/v1/admin/projects/${config.projectId}/integrations/${kind}/test`,
|
|
1517
|
+
{ method: "POST" }
|
|
1518
|
+
);
|
|
1519
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1520
|
+
}
|
|
1521
|
+
);
|
|
1522
|
+
server.registerTool(
|
|
1523
|
+
"run_fullstack_audit",
|
|
1524
|
+
{
|
|
1525
|
+
title: titleOf("run_fullstack_audit"),
|
|
1526
|
+
description: descOf("run_fullstack_audit"),
|
|
1527
|
+
annotations: annotationsFor("run_fullstack_audit"),
|
|
1528
|
+
inputSchema: {
|
|
1529
|
+
project_id: z.string().optional().describe("Project ID to audit. Defaults to the configured project.")
|
|
1530
|
+
}
|
|
1531
|
+
},
|
|
1532
|
+
async ({ project_id }) => {
|
|
1533
|
+
const pid = project_id ?? config.projectId;
|
|
1534
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
|
|
1535
|
+
const data = await apiCall(
|
|
1536
|
+
`/v1/admin/projects/${pid}/audit`,
|
|
1537
|
+
{ method: "POST", body: "{}" }
|
|
1538
|
+
);
|
|
1539
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
1540
|
+
}
|
|
1541
|
+
);
|
|
1542
|
+
server.registerTool(
|
|
1543
|
+
"get_backend_health",
|
|
1544
|
+
{
|
|
1545
|
+
title: titleOf("get_backend_health"),
|
|
1546
|
+
description: descOf("get_backend_health"),
|
|
1547
|
+
annotations: annotationsFor("get_backend_health"),
|
|
1548
|
+
inputSchema: {
|
|
1549
|
+
project_id: z.string().optional().describe("Project ID. Defaults to the configured project."),
|
|
1550
|
+
include_logs: z.boolean().optional().describe("Whether to include recent backend error logs (default: true).")
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
async ({ project_id, include_logs = true }) => {
|
|
1554
|
+
const pid = project_id ?? config.projectId;
|
|
1555
|
+
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
|
|
1556
|
+
const [schemaRes, advisorsRes, logsRes] = await Promise.allSettled([
|
|
1557
|
+
apiCall(`/v1/admin/projects/${pid}/backend/schema`),
|
|
1558
|
+
apiCall(`/v1/admin/projects/${pid}/db-advisors`),
|
|
1559
|
+
include_logs ? apiCall(`/v1/admin/projects/${pid}/backend/logs?service=api`) : Promise.resolve(null)
|
|
1560
|
+
]);
|
|
1561
|
+
const result = {
|
|
1562
|
+
schema: schemaRes.status === "fulfilled" ? schemaRes.value : { error: String(schemaRes.reason) },
|
|
1563
|
+
advisors: advisorsRes.status === "fulfilled" ? advisorsRes.value : { error: String(advisorsRes.reason) },
|
|
1564
|
+
logs: logsRes.status === "fulfilled" ? logsRes.value : include_logs ? { error: String(logsRes.reason) } : null
|
|
1565
|
+
};
|
|
1566
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
server.registerTool(
|
|
1570
|
+
"list_skills",
|
|
1571
|
+
{
|
|
1572
|
+
title: titleOf("list_skills"),
|
|
1573
|
+
description: descOf("list_skills"),
|
|
1574
|
+
annotations: annotationsFor("list_skills"),
|
|
1575
|
+
inputSchema: {
|
|
1576
|
+
category: z.string().optional().describe("Filter by category: workflow, debug, test, audit, enhance, \u2026"),
|
|
1577
|
+
search: z.string().optional().describe("Free-text search across slug, title, description"),
|
|
1578
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
1579
|
+
limit: z.number().optional().describe("Max results per page (default 200, max 200)")
|
|
1580
|
+
}
|
|
1581
|
+
},
|
|
1582
|
+
async (args) => {
|
|
1583
|
+
const qs = new URLSearchParams();
|
|
1584
|
+
if (args.category) qs.set("category", args.category);
|
|
1585
|
+
if (args.search) qs.set("q", args.search);
|
|
1586
|
+
if (args.page) qs.set("page", String(args.page));
|
|
1587
|
+
qs.set("limit", String(Math.min(args.limit ?? 200, 200)));
|
|
1588
|
+
const data = await apiCall(`/v1/admin/skills?${qs}`);
|
|
1589
|
+
const skills = Array.isArray(data) ? data : [];
|
|
1590
|
+
return jsonText({ skills, count: skills.length });
|
|
1591
|
+
}
|
|
1592
|
+
);
|
|
1593
|
+
server.registerTool(
|
|
1594
|
+
"get_skill",
|
|
1595
|
+
{
|
|
1596
|
+
title: titleOf("get_skill"),
|
|
1597
|
+
description: descOf("get_skill"),
|
|
1598
|
+
annotations: annotationsFor("get_skill"),
|
|
1599
|
+
inputSchema: {
|
|
1600
|
+
slug: z.string().describe('Skill slug, e.g. "workflow-fix-and-ship"')
|
|
1601
|
+
}
|
|
1602
|
+
},
|
|
1603
|
+
async (args) => {
|
|
1604
|
+
const data = await apiCall(`/v1/admin/skills/${args.slug}`);
|
|
1605
|
+
return jsonText(data);
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
server.registerTool(
|
|
1609
|
+
"start_skill_pipeline",
|
|
1610
|
+
{
|
|
1611
|
+
title: titleOf("start_skill_pipeline"),
|
|
1612
|
+
description: descOf("start_skill_pipeline"),
|
|
1613
|
+
annotations: annotationsFor("start_skill_pipeline"),
|
|
1614
|
+
inputSchema: {
|
|
1615
|
+
root_skill_slug: z.string().describe('Root skill slug to run, e.g. "workflow-fix-and-ship"'),
|
|
1616
|
+
report_id: z.string().optional().describe("Report UUID to attach the pipeline to"),
|
|
1617
|
+
mode: z.enum(["handoff", "cloud"]).optional().describe("handoff (default): get context packet for local agent. cloud: auto-dispatch via Cursor Cloud."),
|
|
1618
|
+
project_id: z.string().optional().describe("Project UUID. Falls back to the configured project.")
|
|
1619
|
+
}
|
|
1620
|
+
},
|
|
1621
|
+
async (args) => {
|
|
1622
|
+
const resolvedProjectId = args.project_id ?? projectId;
|
|
1623
|
+
if (!resolvedProjectId) return jsonText({ error: "No project_id provided or configured." });
|
|
1624
|
+
const data = await apiCall(
|
|
1625
|
+
"/v1/admin/skills/pipelines",
|
|
1626
|
+
{ method: "POST", body: JSON.stringify({ ...args, project_id: resolvedProjectId }) }
|
|
1627
|
+
);
|
|
1628
|
+
return jsonText(data);
|
|
1629
|
+
}
|
|
1630
|
+
);
|
|
1631
|
+
server.registerTool(
|
|
1632
|
+
"get_pipeline_run",
|
|
1633
|
+
{
|
|
1634
|
+
title: titleOf("get_pipeline_run"),
|
|
1635
|
+
description: descOf("get_pipeline_run"),
|
|
1636
|
+
annotations: annotationsFor("get_pipeline_run"),
|
|
1637
|
+
inputSchema: {
|
|
1638
|
+
run_id: z.string().describe("Pipeline run UUID")
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
async (args) => {
|
|
1642
|
+
const data = await apiCall(`/v1/admin/skills/pipelines/${args.run_id}`);
|
|
1643
|
+
return jsonText(data);
|
|
1644
|
+
}
|
|
1645
|
+
);
|
|
1646
|
+
server.registerTool(
|
|
1647
|
+
"checkin_pipeline_step",
|
|
1648
|
+
{
|
|
1649
|
+
title: titleOf("checkin_pipeline_step"),
|
|
1650
|
+
description: descOf("checkin_pipeline_step"),
|
|
1651
|
+
annotations: annotationsFor("checkin_pipeline_step"),
|
|
1652
|
+
inputSchema: {
|
|
1653
|
+
run_id: z.string().describe("Pipeline run UUID"),
|
|
1654
|
+
step_index: z.number().describe("Step index (0-based)"),
|
|
1655
|
+
status: z.enum(["running", "passed", "failed", "skipped"]).describe("Step status"),
|
|
1656
|
+
notes: z.string().optional().describe("Optional notes or output summary"),
|
|
1657
|
+
pr_url: z.string().optional().describe("PR URL opened during this step"),
|
|
1658
|
+
agent_ref: z.string().optional().describe("Cursor agentId or external agent reference")
|
|
1659
|
+
}
|
|
1660
|
+
},
|
|
1661
|
+
async (args) => {
|
|
1662
|
+
const { run_id, step_index, ...body } = args;
|
|
1663
|
+
await apiCall(
|
|
1664
|
+
`/v1/admin/skills/pipelines/${run_id}/steps/${step_index}/checkin`,
|
|
1665
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
1666
|
+
);
|
|
1667
|
+
return jsonText({ ok: true, message: `Step ${step_index} \u2192 ${args.status}` });
|
|
1668
|
+
}
|
|
1669
|
+
);
|
|
1670
|
+
server.registerTool(
|
|
1671
|
+
"get_activation_status",
|
|
1672
|
+
{
|
|
1673
|
+
title: titleOf("get_activation_status"),
|
|
1674
|
+
description: descOf("get_activation_status"),
|
|
1675
|
+
annotations: annotationsFor("get_activation_status"),
|
|
1676
|
+
inputSchema: {
|
|
1677
|
+
project_id: z.string().optional().describe("Optional project UUID override")
|
|
1678
|
+
}
|
|
1679
|
+
},
|
|
1680
|
+
async (args) => {
|
|
1681
|
+
const qs = args.project_id ? `?project_id=${encodeURIComponent(args.project_id)}` : "";
|
|
1682
|
+
const data = await apiCall(`/v1/admin/activation${qs}`);
|
|
1683
|
+
return jsonText(data);
|
|
1684
|
+
}
|
|
1685
|
+
);
|
|
1686
|
+
server.registerTool(
|
|
1687
|
+
"get_reporter_thread",
|
|
1688
|
+
{
|
|
1689
|
+
title: titleOf("get_reporter_thread"),
|
|
1690
|
+
description: descOf("get_reporter_thread"),
|
|
1691
|
+
annotations: annotationsFor("get_reporter_thread"),
|
|
1692
|
+
inputSchema: { reportId: z.string().describe("The report UUID") }
|
|
1693
|
+
},
|
|
1694
|
+
// The reporter thread is the `comments` lane of the unified report
|
|
1695
|
+
// timeline. There is no standalone admin `/comments` route (only the
|
|
1696
|
+
// reporter-token-gated `/v1/reporter/reports/:id/comments`), so we read the
|
|
1697
|
+
// admin-authed timeline, which also carries fix / QA / status lanes.
|
|
1698
|
+
async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}/timeline`))
|
|
1699
|
+
);
|
|
1700
|
+
const grantedScopes = config.scopes;
|
|
1701
|
+
if (grantedScopes !== void 0) {
|
|
1702
|
+
const toolRegistry = server._registeredTools;
|
|
1703
|
+
const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
|
|
1704
|
+
for (const spec of allSpecs) {
|
|
1705
|
+
if (!grantedScopes.includes(spec.scope)) {
|
|
1706
|
+
toolRegistry[spec.name]?.remove();
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
return server;
|
|
1711
|
+
}
|
|
1712
|
+
export {
|
|
1713
|
+
MushiApiError,
|
|
1714
|
+
createMushiServer
|
|
1715
|
+
};
|