@mushi-mushi/mcp 0.20.1 → 0.20.2
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 +3 -3
- package/dist/catalog.js +1 -1
- package/dist/{chunk-TP2QQWDJ.js → chunk-RVZIKP4X.js} +22 -10
- package/dist/clients.d.ts +1 -1
- package/dist/clients.js +1 -1
- package/dist/index.js +458 -62
- package/dist/server.d.ts +9 -1
- package/dist/server.js +341 -40
- package/package.json +2 -2
package/dist/server.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { FeatureFilter } from './feature-groups.js';
|
|
|
19
19
|
* a proxy strips first.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
/** Resolve the per-request timeout from `MUSHI_MCP_TIMEOUT_MS`, clamped. Exported for tests. */
|
|
23
|
+
declare function resolveTimeoutMsFromEnv(): number;
|
|
22
24
|
declare class MushiApiError extends Error {
|
|
23
25
|
readonly status: number;
|
|
24
26
|
readonly code: string;
|
|
@@ -49,6 +51,12 @@ interface MushiServerConfig {
|
|
|
49
51
|
* Set via `MUSHI_FEATURES` env (CSV) on stdio; HTTP uses `?features=` query.
|
|
50
52
|
*/
|
|
51
53
|
features?: FeatureFilter;
|
|
54
|
+
/**
|
|
55
|
+
* Per-request timeout in ms for every Mushi API call a tool makes.
|
|
56
|
+
* Defaults to `MUSHI_MCP_TIMEOUT_MS` (clamped to 250ms…5min) or 15000.
|
|
57
|
+
* Explicit values are honoured as given — tests use small ones.
|
|
58
|
+
*/
|
|
59
|
+
timeoutMs?: number;
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* Build an MCP server instance pre-wired with every Mushi tool, resource,
|
|
@@ -57,4 +65,4 @@ interface MushiServerConfig {
|
|
|
57
65
|
*/
|
|
58
66
|
declare function createMushiServer(config: MushiServerConfig): McpServer;
|
|
59
67
|
|
|
60
|
-
export { MushiApiError, type MushiServerConfig, createMushiServer };
|
|
68
|
+
export { MushiApiError, type MushiServerConfig, createMushiServer, resolveTimeoutMsFromEnv };
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
TDD_TOOL_CATALOG,
|
|
4
4
|
TOOL_CATALOG,
|
|
5
5
|
USE_MUSHI_INTENTS
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-RVZIKP4X.js";
|
|
7
7
|
import {
|
|
8
8
|
MUSHI_SERVER_METADATA
|
|
9
9
|
} from "./chunk-EANW5YTI.js";
|
|
@@ -143,8 +143,107 @@ function wrapUntrustedJson(value, role) {
|
|
|
143
143
|
return wrapUntrusted(serialised, role);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// src/stdout-guard.ts
|
|
147
|
+
import { Console } from "console";
|
|
148
|
+
var GUARDED_CONSOLE_METHODS = [
|
|
149
|
+
"log",
|
|
150
|
+
"info",
|
|
151
|
+
"debug",
|
|
152
|
+
"dir",
|
|
153
|
+
"dirxml",
|
|
154
|
+
"table",
|
|
155
|
+
"group",
|
|
156
|
+
"groupCollapsed",
|
|
157
|
+
"groupEnd",
|
|
158
|
+
"count",
|
|
159
|
+
"countReset",
|
|
160
|
+
"time",
|
|
161
|
+
"timeEnd",
|
|
162
|
+
"timeLog",
|
|
163
|
+
"trace",
|
|
164
|
+
"assert",
|
|
165
|
+
"warn",
|
|
166
|
+
"error"
|
|
167
|
+
];
|
|
168
|
+
var installed = false;
|
|
169
|
+
function installStdoutGuard() {
|
|
170
|
+
if (installed) return false;
|
|
171
|
+
if (typeof process !== "undefined" && process.env?.MUSHI_MCP_STDOUT_GUARD === "off") {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (typeof process === "undefined" || typeof process.stderr?.write !== "function" || typeof console === "undefined") {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const stderrConsole = new Console({
|
|
179
|
+
stdout: process.stderr,
|
|
180
|
+
stderr: process.stderr,
|
|
181
|
+
// Colour codes would land in the client's log pane as escape noise.
|
|
182
|
+
colorMode: false
|
|
183
|
+
});
|
|
184
|
+
const target = console;
|
|
185
|
+
const source = stderrConsole;
|
|
186
|
+
for (const method of GUARDED_CONSOLE_METHODS) {
|
|
187
|
+
const fn = source[method];
|
|
188
|
+
if (typeof fn === "function") {
|
|
189
|
+
target[method] = fn.bind(stderrConsole);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch {
|
|
193
|
+
const write = (...args) => process.stderr.write(
|
|
194
|
+
args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ") + "\n"
|
|
195
|
+
);
|
|
196
|
+
const target = console;
|
|
197
|
+
for (const method of GUARDED_CONSOLE_METHODS) {
|
|
198
|
+
target[method] = write;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
installed = true;
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
function safeStringify(value) {
|
|
205
|
+
try {
|
|
206
|
+
return JSON.stringify(value) ?? String(value);
|
|
207
|
+
} catch {
|
|
208
|
+
return String(value);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
146
212
|
// src/server.ts
|
|
213
|
+
installStdoutGuard();
|
|
147
214
|
var NO_ARG_INPUT = {};
|
|
215
|
+
var DEFAULT_API_TIMEOUT_MS = 15e3;
|
|
216
|
+
var MIN_API_TIMEOUT_MS = 250;
|
|
217
|
+
var MAX_API_TIMEOUT_MS = 3e5;
|
|
218
|
+
function resolveTimeoutMsFromEnv() {
|
|
219
|
+
const raw = process.env?.MUSHI_MCP_TIMEOUT_MS?.trim();
|
|
220
|
+
if (!raw) return DEFAULT_API_TIMEOUT_MS;
|
|
221
|
+
const parsed = Number(raw);
|
|
222
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
|
|
223
|
+
return Math.min(Math.max(parsed, MIN_API_TIMEOUT_MS), MAX_API_TIMEOUT_MS);
|
|
224
|
+
}
|
|
225
|
+
function bareMessage(err) {
|
|
226
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
227
|
+
return raw.replace(/^\[[A-Z0-9_]+\]\s*/, "");
|
|
228
|
+
}
|
|
229
|
+
var IDEMPOTENCY_NAMESPACE = "mushi.mcp.submit_fix_result.v1";
|
|
230
|
+
async function stableUuidFrom(...parts) {
|
|
231
|
+
const input = parts.join("");
|
|
232
|
+
const digest = new Uint8Array(
|
|
233
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))
|
|
234
|
+
);
|
|
235
|
+
const bytes = digest.slice(0, 16);
|
|
236
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
237
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
238
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
239
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
240
|
+
}
|
|
241
|
+
function withTimeoutSignal(callerSignal, timeoutSignal) {
|
|
242
|
+
if (!callerSignal) return timeoutSignal;
|
|
243
|
+
const anyFn = AbortSignal.any;
|
|
244
|
+
if (typeof anyFn === "function") return anyFn.call(AbortSignal, [callerSignal, timeoutSignal]);
|
|
245
|
+
return timeoutSignal;
|
|
246
|
+
}
|
|
148
247
|
var MushiApiError = class extends Error {
|
|
149
248
|
constructor(status, code, message) {
|
|
150
249
|
super(`[${code}] ${message}`);
|
|
@@ -158,6 +257,7 @@ var MushiApiError = class extends Error {
|
|
|
158
257
|
function createMushiServer(config) {
|
|
159
258
|
const { version, apiEndpoint, apiKey, projectId } = config;
|
|
160
259
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
260
|
+
const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : resolveTimeoutMsFromEnv();
|
|
161
261
|
const apiLog = createLogger({
|
|
162
262
|
scope: "mushi:mcp:api",
|
|
163
263
|
level: process.env.MUSHI_LOG_LEVEL ?? "info",
|
|
@@ -175,22 +275,66 @@ function createMushiServer(config) {
|
|
|
175
275
|
async function apiCall(path, options) {
|
|
176
276
|
const requestId = crypto.randomUUID().slice(0, 12);
|
|
177
277
|
const started = Date.now();
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
278
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
279
|
+
let res;
|
|
280
|
+
try {
|
|
281
|
+
res = await doFetch(`${apiEndpoint}${path}`, {
|
|
282
|
+
...options,
|
|
283
|
+
headers: {
|
|
284
|
+
"Content-Type": "application/json",
|
|
285
|
+
// Authorization is accepted by `adminOrApiKey` only as a JWT, but we
|
|
286
|
+
// still send it for endpoints still behind plain `jwtAuth` (legacy)
|
|
287
|
+
// and for transparent proxies that strip X-Mushi-* headers.
|
|
288
|
+
Authorization: `Bearer ${apiKey}`,
|
|
289
|
+
"X-Mushi-Api-Key": apiKey,
|
|
290
|
+
"X-Request-Id": requestId,
|
|
291
|
+
...projectId ? { "X-Mushi-Project-Id": projectId } : {},
|
|
292
|
+
...options?.headers ?? {}
|
|
293
|
+
},
|
|
294
|
+
// After the spread so a caller-supplied `signal` is merged, not lost.
|
|
295
|
+
signal: withTimeoutSignal(options?.signal, timeoutSignal)
|
|
296
|
+
});
|
|
297
|
+
} catch (err) {
|
|
298
|
+
throw classifyTransportError(err);
|
|
299
|
+
}
|
|
300
|
+
let text;
|
|
301
|
+
try {
|
|
302
|
+
text = await res.text();
|
|
303
|
+
} catch (err) {
|
|
304
|
+
throw classifyTransportError(err);
|
|
305
|
+
}
|
|
306
|
+
function classifyTransportError(err) {
|
|
307
|
+
const durationMs2 = Date.now() - started;
|
|
308
|
+
if (timeoutSignal.aborted) {
|
|
309
|
+
apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
|
|
310
|
+
return new MushiApiError(
|
|
311
|
+
504,
|
|
312
|
+
"MUSHI_TIMEOUT",
|
|
313
|
+
`Timed out after ${timeoutMs}ms waiting for ${path}. The Mushi API accepted the connection but did not answer in time \u2014 retry, or raise the budget with MUSHI_MCP_TIMEOUT_MS (ms) in your MCP env block if you self-host and cold starts are slow. Endpoint: ${apiEndpoint}`
|
|
314
|
+
);
|
|
190
315
|
}
|
|
191
|
-
|
|
316
|
+
const name = err instanceof Error ? err.name : "";
|
|
317
|
+
if (name === "AbortError") {
|
|
318
|
+
apiLog.warn("api.aborted", { path, requestId, durationMs: durationMs2 });
|
|
319
|
+
return new MushiApiError(
|
|
320
|
+
499,
|
|
321
|
+
"REQUEST_ABORTED",
|
|
322
|
+
`The request to ${path} was aborted by the caller after ${durationMs2}ms.`
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
apiLog.warn("api.unreachable", {
|
|
326
|
+
path,
|
|
327
|
+
requestId,
|
|
328
|
+
durationMs: durationMs2,
|
|
329
|
+
err: bareMessage(err)
|
|
330
|
+
});
|
|
331
|
+
return new MushiApiError(
|
|
332
|
+
503,
|
|
333
|
+
"NETWORK_ERROR",
|
|
334
|
+
`Could not reach the Mushi API at ${apiEndpoint}${path} \u2014 ${bareMessage(err)}. This is a connectivity failure, not an API rejection: check that you are online, that MUSHI_API_ENDPOINT points at your \`.../functions/v1/api\` URL, and that no proxy/VPN is blocking it.`
|
|
335
|
+
);
|
|
336
|
+
}
|
|
192
337
|
const durationMs = Date.now() - started;
|
|
193
|
-
const text = await res.text();
|
|
194
338
|
let body = null;
|
|
195
339
|
if (text) {
|
|
196
340
|
try {
|
|
@@ -220,6 +364,7 @@ function createMushiServer(config) {
|
|
|
220
364
|
apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
|
|
221
365
|
return body;
|
|
222
366
|
}
|
|
367
|
+
let resolvedAccountProjectId = null;
|
|
223
368
|
async function resolveProjectId(explicitId) {
|
|
224
369
|
if (explicitId) {
|
|
225
370
|
if (projectId && explicitId !== projectId) {
|
|
@@ -232,10 +377,12 @@ function createMushiServer(config) {
|
|
|
232
377
|
return explicitId;
|
|
233
378
|
}
|
|
234
379
|
if (projectId) return projectId;
|
|
380
|
+
if (resolvedAccountProjectId) return resolvedAccountProjectId;
|
|
235
381
|
try {
|
|
236
382
|
const data = await apiCall("/v1/admin/mcp/account-overview");
|
|
237
383
|
if (data.total === 1 && data.projects[0]) {
|
|
238
|
-
|
|
384
|
+
resolvedAccountProjectId = data.projects[0].id;
|
|
385
|
+
return resolvedAccountProjectId;
|
|
239
386
|
}
|
|
240
387
|
if (data.total === 0) {
|
|
241
388
|
throw new MushiApiError(
|
|
@@ -251,11 +398,27 @@ function createMushiServer(config) {
|
|
|
251
398
|
`Multiple projects are accessible with this key: ${projectList}. Pass projectId explicitly to target a specific project, or set MUSHI_PROJECT_ID in your MCP env config.`
|
|
252
399
|
);
|
|
253
400
|
} catch (err) {
|
|
254
|
-
if (err instanceof MushiApiError)
|
|
401
|
+
if (err instanceof MushiApiError) {
|
|
402
|
+
if (err.code === "MUSHI_TIMEOUT" || err.code === "NETWORK_ERROR") {
|
|
403
|
+
throw new MushiApiError(
|
|
404
|
+
err.status,
|
|
405
|
+
err.code,
|
|
406
|
+
`Could not resolve which project to use because the Mushi API is unreachable: ${bareMessage(err)} This is a connectivity problem \u2014 setting MUSHI_PROJECT_ID would only skip the lookup, not fix the tool call that follows it.`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
if (err.status === 401 || err.status === 403) {
|
|
410
|
+
throw new MushiApiError(
|
|
411
|
+
err.status,
|
|
412
|
+
err.code,
|
|
413
|
+
`The API key was rejected while resolving the project: ${bareMessage(err)} Mint a fresh key in the console (Settings \u2192 API keys) and update MUSHI_API_KEY, or re-run \`mushi login\`. This is an authentication problem, not a missing project id.`
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
255
418
|
throw new MushiApiError(
|
|
256
419
|
400,
|
|
257
420
|
"MISSING_PROJECT_ID",
|
|
258
|
-
|
|
421
|
+
`projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly. (Project lookup failed with: ${bareMessage(err)})`
|
|
259
422
|
);
|
|
260
423
|
}
|
|
261
424
|
}
|
|
@@ -679,12 +842,24 @@ function createMushiServer(config) {
|
|
|
679
842
|
suggestedFix: z.unknown().describe("Stage-2 suggested fix, or null if not yet classified"),
|
|
680
843
|
reproductionSteps: z.unknown().describe("Reproduction steps recorded on the report (array; [] if none)"),
|
|
681
844
|
summary: z.unknown().describe("One-line report summary, or null"),
|
|
682
|
-
component: z.unknown().describe("Component/page the bug was attributed to, or null")
|
|
845
|
+
component: z.unknown().describe("Component/page the bug was attributed to, or null"),
|
|
846
|
+
note: z.string().optional().describe("Present when no Stage-2 analysis exists \u2014 explains why and what to call instead")
|
|
683
847
|
}
|
|
684
848
|
},
|
|
685
849
|
async (args) => {
|
|
686
850
|
const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
|
|
687
851
|
const s2 = report.stage2_analysis;
|
|
852
|
+
if (!s2 || s2.rootCause == null && s2.suggestedFix == null) {
|
|
853
|
+
return jsonResult({
|
|
854
|
+
reportId: args.reportId,
|
|
855
|
+
rootCause: null,
|
|
856
|
+
suggestedFix: null,
|
|
857
|
+
reproductionSteps: report.reproduction_steps ?? [],
|
|
858
|
+
summary: report.summary ?? null,
|
|
859
|
+
component: report.component ?? null,
|
|
860
|
+
note: "No Stage-2 analysis exists for this report (it was classified by the fast Stage-1 path, which does not produce root-cause/fix hints). Use triage_issue for the full review packet, or dispatch_fix \u2014 the fix worker does its own grounded analysis."
|
|
861
|
+
});
|
|
862
|
+
}
|
|
688
863
|
return jsonResult({
|
|
689
864
|
reportId: args.reportId,
|
|
690
865
|
rootCause: s2?.rootCause ?? null,
|
|
@@ -1115,12 +1290,31 @@ function createMushiServer(config) {
|
|
|
1115
1290
|
const category = report ? report.category : "unknown";
|
|
1116
1291
|
const status = report ? report.status : "unknown";
|
|
1117
1292
|
const actions = [];
|
|
1118
|
-
|
|
1293
|
+
const processingError = report ? report.processing_error : null;
|
|
1294
|
+
const attempts = report ? report.fix_attempts ?? [] : [];
|
|
1295
|
+
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
|
|
1296
|
+
const lastAttemptStatus = lastAttempt ? String(lastAttempt.status ?? "") : "";
|
|
1297
|
+
const autofixBlocked = typeof processingError === "string" && processingError.startsWith("autofix_blocked:") || lastAttemptStatus.startsWith("skipped") || lastAttemptStatus === "failed";
|
|
1298
|
+
if (autofixBlocked && status !== "fixing" && status !== "fixed") {
|
|
1299
|
+
const blockReason = typeof processingError === "string" && processingError || String(lastAttempt?.error ?? "previous fix attempt was skipped or failed");
|
|
1300
|
+
actions.push({
|
|
1301
|
+
action: "unblock_autofix",
|
|
1302
|
+
reason: `Auto-fix is blocked: ${blockReason}. Resolve the blocker (settings/integration/budget), then re-dispatch.`,
|
|
1303
|
+
tool: "diagnose_setup",
|
|
1304
|
+
args: { mode: "dispatch" }
|
|
1305
|
+
});
|
|
1306
|
+
actions.push({
|
|
1307
|
+
action: "redispatch_after_unblock",
|
|
1308
|
+
reason: "Once the blocker above is resolved, dispatch a fresh fix attempt.",
|
|
1309
|
+
tool: "dispatch_fix",
|
|
1310
|
+
args: { reportId: args.report_id }
|
|
1311
|
+
});
|
|
1312
|
+
} else if (status === "new" || status === "classified") {
|
|
1119
1313
|
actions.push({
|
|
1120
1314
|
action: "dispatch_fix",
|
|
1121
|
-
reason: "Report is classified
|
|
1315
|
+
reason: "Report is classified and no fix has been attempted",
|
|
1122
1316
|
tool: "dispatch_fix",
|
|
1123
|
-
args: { reportId: args.report_id
|
|
1317
|
+
args: { reportId: args.report_id }
|
|
1124
1318
|
});
|
|
1125
1319
|
} else if (status === "fixing") {
|
|
1126
1320
|
actions.push({
|
|
@@ -1198,7 +1392,7 @@ function createMushiServer(config) {
|
|
|
1198
1392
|
linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
|
|
1199
1393
|
summary: z.string().describe("One-line human summary of what the fix changed"),
|
|
1200
1394
|
idempotencyKey: z.string().uuid().optional().describe(
|
|
1201
|
-
"Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows"
|
|
1395
|
+
"Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows. Omit it and a stable key is derived from reportId + branch + prUrl, so a retried submission carries the identical key instead of a fresh random one."
|
|
1202
1396
|
)
|
|
1203
1397
|
},
|
|
1204
1398
|
outputSchema: {
|
|
@@ -1207,27 +1401,123 @@ function createMushiServer(config) {
|
|
|
1207
1401
|
}
|
|
1208
1402
|
},
|
|
1209
1403
|
async (args) => {
|
|
1210
|
-
const idemKey = args.idempotencyKey ??
|
|
1404
|
+
const idemKey = args.idempotencyKey ?? await stableUuidFrom(
|
|
1405
|
+
IDEMPOTENCY_NAMESPACE,
|
|
1406
|
+
args.reportId,
|
|
1407
|
+
args.branch,
|
|
1408
|
+
args.prUrl ?? ""
|
|
1409
|
+
);
|
|
1211
1410
|
const created = await apiCall("/v1/admin/fixes", {
|
|
1212
1411
|
method: "POST",
|
|
1213
1412
|
headers: { "Idempotency-Key": idemKey },
|
|
1214
1413
|
body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
|
|
1215
1414
|
});
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1415
|
+
try {
|
|
1416
|
+
await apiCall(`/v1/admin/fixes/${created.fixId}`, {
|
|
1417
|
+
method: "PATCH",
|
|
1418
|
+
body: JSON.stringify({
|
|
1419
|
+
status: "completed",
|
|
1420
|
+
branch: args.branch,
|
|
1421
|
+
pr_url: args.prUrl,
|
|
1422
|
+
files_changed: args.filesChanged,
|
|
1423
|
+
lines_changed: args.linesChanged,
|
|
1424
|
+
summary: args.summary,
|
|
1425
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
1426
|
+
})
|
|
1427
|
+
});
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
const status = err instanceof MushiApiError ? err.status : 500;
|
|
1430
|
+
throw new MushiApiError(
|
|
1431
|
+
status,
|
|
1432
|
+
"FIX_PATCH_FAILED",
|
|
1433
|
+
`Fix row ${created.fixId} was created for report ${args.reportId}, but marking it completed failed: ${bareMessage(err)} \u2014 the fix row exists and is still in its initial (pending) state, so no work is lost. Preferred repair: PATCH /v1/admin/fixes/${created.fixId} with {"status":"completed"} to finish this exact row. Re-running submit_fix_result re-sends the same derived Idempotency-Key (reportId + branch + prUrl) rather than a fresh one, so it dedupes wherever the API honours that header \u2014 retry only with identical arguments.`
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1228
1436
|
return jsonResult({ ok: true, fixId: created.fixId });
|
|
1229
1437
|
}
|
|
1230
1438
|
);
|
|
1439
|
+
server.registerTool(
|
|
1440
|
+
"triage_next_steps",
|
|
1441
|
+
{
|
|
1442
|
+
title: titleOf("triage_next_steps"),
|
|
1443
|
+
description: descOf("triage_next_steps"),
|
|
1444
|
+
annotations: annotationsFor("triage_next_steps"),
|
|
1445
|
+
inputSchema: {
|
|
1446
|
+
project_id: z.string().uuid().optional().describe("Project UUID (defaults to the configured project).")
|
|
1447
|
+
},
|
|
1448
|
+
outputSchema: {
|
|
1449
|
+
steps: z.array(
|
|
1450
|
+
z.object({
|
|
1451
|
+
priority: z.number(),
|
|
1452
|
+
action: z.string(),
|
|
1453
|
+
reason: z.string(),
|
|
1454
|
+
tool: z.string().optional(),
|
|
1455
|
+
args: z.record(z.string(), z.unknown()).optional()
|
|
1456
|
+
})
|
|
1457
|
+
),
|
|
1458
|
+
summary: z.string()
|
|
1459
|
+
}
|
|
1460
|
+
},
|
|
1461
|
+
async (args) => {
|
|
1462
|
+
const pid = await resolveProjectId(args.project_id);
|
|
1463
|
+
const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
|
|
1464
|
+
const data = await apiCall(
|
|
1465
|
+
"/v1/admin/reports?limit=100",
|
|
1466
|
+
{ headers: extraHeaders }
|
|
1467
|
+
);
|
|
1468
|
+
const reports = data.reports ?? [];
|
|
1469
|
+
const isCron = (r) => typeof r.reporter_token_hash === "string" && r.reporter_token_hash.startsWith("cron:");
|
|
1470
|
+
const isBlocked = (r) => typeof r.processing_error === "string" && r.processing_error.startsWith("autofix_blocked:");
|
|
1471
|
+
const label = (r) => String(r.title ?? r.summary ?? r.description ?? "").slice(0, 90);
|
|
1472
|
+
const steps = [];
|
|
1473
|
+
let priority = 1;
|
|
1474
|
+
const blocked = reports.filter(isBlocked);
|
|
1475
|
+
if (blocked.length > 0) {
|
|
1476
|
+
steps.push({
|
|
1477
|
+
priority: priority++,
|
|
1478
|
+
action: `Unblock auto-fix (${blocked.length} report${blocked.length === 1 ? "" : "s"} blocked)`,
|
|
1479
|
+
reason: String(blocked[0].processing_error).slice(0, 200),
|
|
1480
|
+
tool: "diagnose_setup",
|
|
1481
|
+
args: { mode: "dispatch" }
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
const fixing = reports.filter((r) => r.status === "fixing");
|
|
1485
|
+
for (const r of fixing.slice(0, 2)) {
|
|
1486
|
+
steps.push({
|
|
1487
|
+
priority: priority++,
|
|
1488
|
+
action: `Shepherd in-flight fix: ${label(r)}`,
|
|
1489
|
+
reason: "A fix branch/PR is open \u2014 check CI and merge when green.",
|
|
1490
|
+
tool: "get_report_timeline",
|
|
1491
|
+
args: { report_id: r.id }
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
const openUserReports = reports.filter((r) => (r.status === "new" || r.status === "classified") && !isCron(r) && !isBlocked(r)).sort((a, b) => {
|
|
1495
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1496
|
+
return (rank[String(a.severity)] ?? 4) - (rank[String(b.severity)] ?? 4);
|
|
1497
|
+
});
|
|
1498
|
+
for (const r of openUserReports.slice(0, Math.max(0, 4 - steps.length))) {
|
|
1499
|
+
steps.push({
|
|
1500
|
+
priority: priority++,
|
|
1501
|
+
action: `Triage [${r.severity}] ${label(r)}`,
|
|
1502
|
+
reason: "User-felt report awaiting triage \u2014 review the packet, then dispatch or dismiss.",
|
|
1503
|
+
tool: "triage_issue",
|
|
1504
|
+
args: { report_id: r.id }
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
const chores = reports.filter((r) => r.status === "classified" && isCron(r) && !isBlocked(r));
|
|
1508
|
+
if (chores.length > 0 && steps.length < 5) {
|
|
1509
|
+
steps.push({
|
|
1510
|
+
priority: priority++,
|
|
1511
|
+
action: `Batch ${chores.length} maintenance chore${chores.length === 1 ? "" : "s"} (dependency bumps)`,
|
|
1512
|
+
reason: "Robot-filed modernization reports \u2014 batch-dispatch or dismiss in one sitting; do not let them crowd out user bugs.",
|
|
1513
|
+
tool: "get_recent_reports",
|
|
1514
|
+
args: { status: "classified" }
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
const summary = steps.length === 0 ? "Inbox is clear \u2014 no blocked fixes, no open user reports, no chores. Nothing needs your attention." : `${steps.length} prioritised step${steps.length === 1 ? "" : "s"}: ` + steps.map((s) => s.action).join(" \u2192 ");
|
|
1518
|
+
return jsonResult({ steps, summary });
|
|
1519
|
+
}
|
|
1520
|
+
);
|
|
1231
1521
|
server.registerTool(
|
|
1232
1522
|
"dispatch_fix",
|
|
1233
1523
|
{
|
|
@@ -1264,7 +1554,7 @@ function createMushiServer(config) {
|
|
|
1264
1554
|
} catch {
|
|
1265
1555
|
}
|
|
1266
1556
|
}
|
|
1267
|
-
const
|
|
1557
|
+
const dispatch = await apiCall("/v1/admin/fixes/dispatch", {
|
|
1268
1558
|
method: "POST",
|
|
1269
1559
|
headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
|
|
1270
1560
|
body: JSON.stringify({
|
|
@@ -1274,7 +1564,12 @@ function createMushiServer(config) {
|
|
|
1274
1564
|
...projectId ? { projectId } : {}
|
|
1275
1565
|
})
|
|
1276
1566
|
});
|
|
1277
|
-
|
|
1567
|
+
if (!dispatch?.dispatchId) {
|
|
1568
|
+
throw new Error(
|
|
1569
|
+
`Dispatch response missing dispatchId \u2014 got: ${JSON.stringify(dispatch).slice(0, 300)}`
|
|
1570
|
+
);
|
|
1571
|
+
}
|
|
1572
|
+
return jsonResult({ fixId: dispatch.dispatchId, status: dispatch.status ?? "queued" });
|
|
1278
1573
|
}
|
|
1279
1574
|
);
|
|
1280
1575
|
server.registerTool(
|
|
@@ -1409,6 +1704,11 @@ function createMushiServer(config) {
|
|
|
1409
1704
|
status: z.enum([
|
|
1410
1705
|
"pending",
|
|
1411
1706
|
"classified",
|
|
1707
|
+
// Workflow states added 20260520600000 but missing from this
|
|
1708
|
+
// enum ever since (2026-08-16 audit P2-2) — agents could not
|
|
1709
|
+
// mark a report as being looked at.
|
|
1710
|
+
"triaged",
|
|
1711
|
+
"in_progress",
|
|
1412
1712
|
"grouped",
|
|
1413
1713
|
"fixing",
|
|
1414
1714
|
"fixed",
|
|
@@ -1416,7 +1716,7 @@ function createMushiServer(config) {
|
|
|
1416
1716
|
"verified",
|
|
1417
1717
|
"reopened",
|
|
1418
1718
|
"dismissed"
|
|
1419
|
-
]).describe("Target status"),
|
|
1719
|
+
]).describe("Target status (resolved is stored as fixed)"),
|
|
1420
1720
|
reason: z.string().optional().describe("Reason for the transition (audit trail)")
|
|
1421
1721
|
}
|
|
1422
1722
|
},
|
|
@@ -2632,5 +2932,6 @@ Be specific. No generic advice.`
|
|
|
2632
2932
|
}
|
|
2633
2933
|
export {
|
|
2634
2934
|
MushiApiError,
|
|
2635
|
-
createMushiServer
|
|
2935
|
+
createMushiServer,
|
|
2936
|
+
resolveTimeoutMsFromEnv
|
|
2636
2937
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/mcp",
|
|
3
3
|
"mcpName": "io.github.kensaurus/mushi-mushi",
|
|
4
|
-
"version": "0.20.
|
|
4
|
+
"version": "0.20.2",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "MCP server for Mushi Mushi — triage user-felt bugs, pull evidence (screenshots, console logs, network requests), check blast radius, and dispatch fix PRs, all from your AI coding agent",
|
|
7
7
|
"type": "module",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
49
|
"@sentry/node": "^10.57.0",
|
|
50
50
|
"zod": "^4.4.3",
|
|
51
|
-
"@mushi-mushi/core": "^1.27.
|
|
51
|
+
"@mushi-mushi/core": "^1.27.2"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.20.0",
|