@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/index.js
CHANGED
|
@@ -8,6 +8,72 @@ import { join } from "path";
|
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { createLogger as createLogger2, DEFAULT_API_ENDPOINT } from "@mushi-mushi/core";
|
|
10
10
|
|
|
11
|
+
// src/stdout-guard.ts
|
|
12
|
+
import { Console } from "console";
|
|
13
|
+
var GUARDED_CONSOLE_METHODS = [
|
|
14
|
+
"log",
|
|
15
|
+
"info",
|
|
16
|
+
"debug",
|
|
17
|
+
"dir",
|
|
18
|
+
"dirxml",
|
|
19
|
+
"table",
|
|
20
|
+
"group",
|
|
21
|
+
"groupCollapsed",
|
|
22
|
+
"groupEnd",
|
|
23
|
+
"count",
|
|
24
|
+
"countReset",
|
|
25
|
+
"time",
|
|
26
|
+
"timeEnd",
|
|
27
|
+
"timeLog",
|
|
28
|
+
"trace",
|
|
29
|
+
"assert",
|
|
30
|
+
"warn",
|
|
31
|
+
"error"
|
|
32
|
+
];
|
|
33
|
+
var installed = false;
|
|
34
|
+
function installStdoutGuard() {
|
|
35
|
+
if (installed) return false;
|
|
36
|
+
if (typeof process !== "undefined" && process.env?.MUSHI_MCP_STDOUT_GUARD === "off") {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (typeof process === "undefined" || typeof process.stderr?.write !== "function" || typeof console === "undefined") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const stderrConsole = new Console({
|
|
44
|
+
stdout: process.stderr,
|
|
45
|
+
stderr: process.stderr,
|
|
46
|
+
// Colour codes would land in the client's log pane as escape noise.
|
|
47
|
+
colorMode: false
|
|
48
|
+
});
|
|
49
|
+
const target = console;
|
|
50
|
+
const source = stderrConsole;
|
|
51
|
+
for (const method of GUARDED_CONSOLE_METHODS) {
|
|
52
|
+
const fn = source[method];
|
|
53
|
+
if (typeof fn === "function") {
|
|
54
|
+
target[method] = fn.bind(stderrConsole);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
const write = (...args) => process.stderr.write(
|
|
59
|
+
args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ") + "\n"
|
|
60
|
+
);
|
|
61
|
+
const target = console;
|
|
62
|
+
for (const method of GUARDED_CONSOLE_METHODS) {
|
|
63
|
+
target[method] = write;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
installed = true;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function safeStringify(value) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(value) ?? String(value);
|
|
72
|
+
} catch {
|
|
73
|
+
return String(value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
11
77
|
// src/catalog.ts
|
|
12
78
|
var ALL_SCOPES = ["mcp:read", "mcp:write"];
|
|
13
79
|
var TOOL_CATALOG = [
|
|
@@ -170,7 +236,7 @@ var TOOL_CATALOG = [
|
|
|
170
236
|
{
|
|
171
237
|
name: "dispatch_fix",
|
|
172
238
|
title: "Dispatch Mushi fix agent",
|
|
173
|
-
description: 'Start a Mushi fix agent for a classified report; it writes a branch and opens a signed draft PR. Set agent="cursor_cloud" to dispatch a Cursor Cloud Agent (default uses the in-repo worker). Requires GitHub connected + an LLM key (run diagnose_setup mode=dispatch first). Returns {
|
|
239
|
+
description: 'Start a Mushi fix agent for a classified report; it writes a branch and opens a signed draft PR. Set agent="cursor_cloud" to dispatch a Cursor Cloud Agent (default uses the in-repo worker). Requires GitHub connected + an LLM key (run diagnose_setup mode=dispatch first). Returns { fixId, status } (fixId is the dispatch id; get_fix_timeline accepts it immediately); poll get_fix_timeline for progress and merge_fix when CI is green. Write; pass the same idempotencyKey to retry safely \u2014 without it each call starts a new attempt. Report must be classified \u2014 run triage_issue if not.',
|
|
174
240
|
scope: "mcp:write",
|
|
175
241
|
hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
|
|
176
242
|
useCase: "Let the in-repo agent attempt this fix for me (or: dispatch a Cursor Cloud Agent)."
|
|
@@ -305,6 +371,14 @@ var TOOL_CATALOG = [
|
|
|
305
371
|
hints: { readOnly: true, idempotent: true, openWorld: true },
|
|
306
372
|
useCase: "Analyze this bug report end-to-end and tell me what to do."
|
|
307
373
|
},
|
|
374
|
+
{
|
|
375
|
+
name: "triage_next_steps",
|
|
376
|
+
title: "What should I work on now?",
|
|
377
|
+
description: 'Prioritised "do this next" list for the project: blocked auto-fixes first (with the unblock action), then in-flight fixes to shepherd to merge, then user-felt classified reports by severity, with robot/cron chores (dependency bumps) last. Returns { steps: [{ priority, action, reason, tool, args }], summary }. Read-only. Call this first when the user asks "what needs my attention / what should I triage or fix".',
|
|
378
|
+
scope: "mcp:read",
|
|
379
|
+
hints: { readOnly: true, idempotent: true, openWorld: true },
|
|
380
|
+
useCase: "What should I triage or fix right now?"
|
|
381
|
+
},
|
|
308
382
|
// ── Lessons / evolution loop ─────────────────────────────────────────────
|
|
309
383
|
{
|
|
310
384
|
name: "query_lessons",
|
|
@@ -546,24 +620,29 @@ var TDD_TOOL_CATALOG = [
|
|
|
546
620
|
}
|
|
547
621
|
];
|
|
548
622
|
var USE_MUSHI_INTENTS = {
|
|
623
|
+
// NOTE: every name below MUST be a registered TOOL (scopes.test.ts guards
|
|
624
|
+
// this). 2026-08-16 audit P0-3 found phantom names (get_report,
|
|
625
|
+
// get_dashboard, list_qa_stories) and prompts listed as tools
|
|
626
|
+
// (summarize_report_for_fix, mushi_setup) — agents routed here then
|
|
627
|
+
// failed the call and gave up.
|
|
549
628
|
fix: {
|
|
550
629
|
label: "Fix a bug",
|
|
551
630
|
tools: [
|
|
552
631
|
"get_recent_reports",
|
|
553
|
-
"
|
|
554
|
-
"
|
|
632
|
+
"get_report_detail",
|
|
633
|
+
"triage_issue",
|
|
555
634
|
"dispatch_fix",
|
|
556
635
|
"start_skill_pipeline",
|
|
557
636
|
"checkin_pipeline_step",
|
|
558
637
|
"get_pipeline_run"
|
|
559
638
|
],
|
|
560
|
-
hint: "Call get_recent_reports to find the top unresolved bug, then
|
|
639
|
+
hint: "Call get_recent_reports to find the top unresolved bug, then triage_issue before dispatching."
|
|
561
640
|
},
|
|
562
641
|
status: {
|
|
563
642
|
label: "Check project status",
|
|
564
643
|
tools: [
|
|
565
|
-
"get_dashboard",
|
|
566
644
|
"triage_next_steps",
|
|
645
|
+
"get_account_overview",
|
|
567
646
|
"get_usage",
|
|
568
647
|
"get_backend_health",
|
|
569
648
|
"activation_status"
|
|
@@ -573,7 +652,7 @@ var USE_MUSHI_INTENTS = {
|
|
|
573
652
|
setup: {
|
|
574
653
|
label: "Set up Mushi",
|
|
575
654
|
tools: [
|
|
576
|
-
"
|
|
655
|
+
"diagnose_setup",
|
|
577
656
|
"activation_status",
|
|
578
657
|
"get_backend_health",
|
|
579
658
|
"list_byok_keys",
|
|
@@ -581,12 +660,11 @@ var USE_MUSHI_INTENTS = {
|
|
|
581
660
|
"test_byok_key",
|
|
582
661
|
"remove_byok_key"
|
|
583
662
|
],
|
|
584
|
-
hint: "Call
|
|
663
|
+
hint: "Call diagnose_setup first \u2014 it diagnoses setup gaps and returns the next action to take."
|
|
585
664
|
},
|
|
586
665
|
qa: {
|
|
587
666
|
label: "Run / review QA tests",
|
|
588
667
|
tools: [
|
|
589
|
-
"list_qa_stories",
|
|
590
668
|
"run_qa_story",
|
|
591
669
|
"list_qa_story_runs",
|
|
592
670
|
"get_qa_story_run",
|
|
@@ -594,7 +672,7 @@ var USE_MUSHI_INTENTS = {
|
|
|
594
672
|
"approve_qa_story",
|
|
595
673
|
"improve_qa_story"
|
|
596
674
|
],
|
|
597
|
-
hint: "Call
|
|
675
|
+
hint: "Call list_qa_story_runs to see recent coverage and outcomes; run_qa_story to trigger a run."
|
|
598
676
|
},
|
|
599
677
|
pipeline: {
|
|
600
678
|
label: "Run an agent pipeline / skill",
|
|
@@ -609,7 +687,7 @@ var USE_MUSHI_INTENTS = {
|
|
|
609
687
|
},
|
|
610
688
|
audit: {
|
|
611
689
|
label: "Audit / health check",
|
|
612
|
-
tools: ["run_fullstack_audit", "get_backend_health", "
|
|
690
|
+
tools: ["run_fullstack_audit", "get_backend_health", "get_account_overview", "get_usage"],
|
|
613
691
|
hint: "Call run_fullstack_audit for a full-stack health scorecard."
|
|
614
692
|
}
|
|
615
693
|
};
|
|
@@ -981,7 +1059,40 @@ function wrapUntrustedJson(value, role) {
|
|
|
981
1059
|
}
|
|
982
1060
|
|
|
983
1061
|
// src/server.ts
|
|
1062
|
+
installStdoutGuard();
|
|
984
1063
|
var NO_ARG_INPUT = {};
|
|
1064
|
+
var DEFAULT_API_TIMEOUT_MS = 15e3;
|
|
1065
|
+
var MIN_API_TIMEOUT_MS = 250;
|
|
1066
|
+
var MAX_API_TIMEOUT_MS = 3e5;
|
|
1067
|
+
function resolveTimeoutMsFromEnv() {
|
|
1068
|
+
const raw = process.env?.MUSHI_MCP_TIMEOUT_MS?.trim();
|
|
1069
|
+
if (!raw) return DEFAULT_API_TIMEOUT_MS;
|
|
1070
|
+
const parsed = Number(raw);
|
|
1071
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
|
|
1072
|
+
return Math.min(Math.max(parsed, MIN_API_TIMEOUT_MS), MAX_API_TIMEOUT_MS);
|
|
1073
|
+
}
|
|
1074
|
+
function bareMessage(err) {
|
|
1075
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
1076
|
+
return raw.replace(/^\[[A-Z0-9_]+\]\s*/, "");
|
|
1077
|
+
}
|
|
1078
|
+
var IDEMPOTENCY_NAMESPACE = "mushi.mcp.submit_fix_result.v1";
|
|
1079
|
+
async function stableUuidFrom(...parts) {
|
|
1080
|
+
const input = parts.join("");
|
|
1081
|
+
const digest = new Uint8Array(
|
|
1082
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))
|
|
1083
|
+
);
|
|
1084
|
+
const bytes = digest.slice(0, 16);
|
|
1085
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
1086
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1087
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1088
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
1089
|
+
}
|
|
1090
|
+
function withTimeoutSignal(callerSignal, timeoutSignal) {
|
|
1091
|
+
if (!callerSignal) return timeoutSignal;
|
|
1092
|
+
const anyFn = AbortSignal.any;
|
|
1093
|
+
if (typeof anyFn === "function") return anyFn.call(AbortSignal, [callerSignal, timeoutSignal]);
|
|
1094
|
+
return timeoutSignal;
|
|
1095
|
+
}
|
|
985
1096
|
var MushiApiError = class extends Error {
|
|
986
1097
|
constructor(status, code, message) {
|
|
987
1098
|
super(`[${code}] ${message}`);
|
|
@@ -995,6 +1106,7 @@ var MushiApiError = class extends Error {
|
|
|
995
1106
|
function createMushiServer(config) {
|
|
996
1107
|
const { version, apiEndpoint, apiKey, projectId } = config;
|
|
997
1108
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
1109
|
+
const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : resolveTimeoutMsFromEnv();
|
|
998
1110
|
const apiLog = createLogger({
|
|
999
1111
|
scope: "mushi:mcp:api",
|
|
1000
1112
|
level: process.env.MUSHI_LOG_LEVEL ?? "info",
|
|
@@ -1012,22 +1124,66 @@ function createMushiServer(config) {
|
|
|
1012
1124
|
async function apiCall(path, options) {
|
|
1013
1125
|
const requestId = crypto.randomUUID().slice(0, 12);
|
|
1014
1126
|
const started = Date.now();
|
|
1015
|
-
const
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1127
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
1128
|
+
let res;
|
|
1129
|
+
try {
|
|
1130
|
+
res = await doFetch(`${apiEndpoint}${path}`, {
|
|
1131
|
+
...options,
|
|
1132
|
+
headers: {
|
|
1133
|
+
"Content-Type": "application/json",
|
|
1134
|
+
// Authorization is accepted by `adminOrApiKey` only as a JWT, but we
|
|
1135
|
+
// still send it for endpoints still behind plain `jwtAuth` (legacy)
|
|
1136
|
+
// and for transparent proxies that strip X-Mushi-* headers.
|
|
1137
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1138
|
+
"X-Mushi-Api-Key": apiKey,
|
|
1139
|
+
"X-Request-Id": requestId,
|
|
1140
|
+
...projectId ? { "X-Mushi-Project-Id": projectId } : {},
|
|
1141
|
+
...options?.headers ?? {}
|
|
1142
|
+
},
|
|
1143
|
+
// After the spread so a caller-supplied `signal` is merged, not lost.
|
|
1144
|
+
signal: withTimeoutSignal(options?.signal, timeoutSignal)
|
|
1145
|
+
});
|
|
1146
|
+
} catch (err) {
|
|
1147
|
+
throw classifyTransportError(err);
|
|
1148
|
+
}
|
|
1149
|
+
let text;
|
|
1150
|
+
try {
|
|
1151
|
+
text = await res.text();
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
throw classifyTransportError(err);
|
|
1154
|
+
}
|
|
1155
|
+
function classifyTransportError(err) {
|
|
1156
|
+
const durationMs2 = Date.now() - started;
|
|
1157
|
+
if (timeoutSignal.aborted) {
|
|
1158
|
+
apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
|
|
1159
|
+
return new MushiApiError(
|
|
1160
|
+
504,
|
|
1161
|
+
"MUSHI_TIMEOUT",
|
|
1162
|
+
`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}`
|
|
1163
|
+
);
|
|
1027
1164
|
}
|
|
1028
|
-
|
|
1165
|
+
const name = err instanceof Error ? err.name : "";
|
|
1166
|
+
if (name === "AbortError") {
|
|
1167
|
+
apiLog.warn("api.aborted", { path, requestId, durationMs: durationMs2 });
|
|
1168
|
+
return new MushiApiError(
|
|
1169
|
+
499,
|
|
1170
|
+
"REQUEST_ABORTED",
|
|
1171
|
+
`The request to ${path} was aborted by the caller after ${durationMs2}ms.`
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
apiLog.warn("api.unreachable", {
|
|
1175
|
+
path,
|
|
1176
|
+
requestId,
|
|
1177
|
+
durationMs: durationMs2,
|
|
1178
|
+
err: bareMessage(err)
|
|
1179
|
+
});
|
|
1180
|
+
return new MushiApiError(
|
|
1181
|
+
503,
|
|
1182
|
+
"NETWORK_ERROR",
|
|
1183
|
+
`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.`
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1029
1186
|
const durationMs = Date.now() - started;
|
|
1030
|
-
const text = await res.text();
|
|
1031
1187
|
let body = null;
|
|
1032
1188
|
if (text) {
|
|
1033
1189
|
try {
|
|
@@ -1057,6 +1213,7 @@ function createMushiServer(config) {
|
|
|
1057
1213
|
apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
|
|
1058
1214
|
return body;
|
|
1059
1215
|
}
|
|
1216
|
+
let resolvedAccountProjectId = null;
|
|
1060
1217
|
async function resolveProjectId(explicitId) {
|
|
1061
1218
|
if (explicitId) {
|
|
1062
1219
|
if (projectId && explicitId !== projectId) {
|
|
@@ -1069,10 +1226,12 @@ function createMushiServer(config) {
|
|
|
1069
1226
|
return explicitId;
|
|
1070
1227
|
}
|
|
1071
1228
|
if (projectId) return projectId;
|
|
1229
|
+
if (resolvedAccountProjectId) return resolvedAccountProjectId;
|
|
1072
1230
|
try {
|
|
1073
1231
|
const data = await apiCall("/v1/admin/mcp/account-overview");
|
|
1074
1232
|
if (data.total === 1 && data.projects[0]) {
|
|
1075
|
-
|
|
1233
|
+
resolvedAccountProjectId = data.projects[0].id;
|
|
1234
|
+
return resolvedAccountProjectId;
|
|
1076
1235
|
}
|
|
1077
1236
|
if (data.total === 0) {
|
|
1078
1237
|
throw new MushiApiError(
|
|
@@ -1088,11 +1247,27 @@ function createMushiServer(config) {
|
|
|
1088
1247
|
`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.`
|
|
1089
1248
|
);
|
|
1090
1249
|
} catch (err) {
|
|
1091
|
-
if (err instanceof MushiApiError)
|
|
1250
|
+
if (err instanceof MushiApiError) {
|
|
1251
|
+
if (err.code === "MUSHI_TIMEOUT" || err.code === "NETWORK_ERROR") {
|
|
1252
|
+
throw new MushiApiError(
|
|
1253
|
+
err.status,
|
|
1254
|
+
err.code,
|
|
1255
|
+
`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.`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
if (err.status === 401 || err.status === 403) {
|
|
1259
|
+
throw new MushiApiError(
|
|
1260
|
+
err.status,
|
|
1261
|
+
err.code,
|
|
1262
|
+
`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.`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
throw err;
|
|
1266
|
+
}
|
|
1092
1267
|
throw new MushiApiError(
|
|
1093
1268
|
400,
|
|
1094
1269
|
"MISSING_PROJECT_ID",
|
|
1095
|
-
|
|
1270
|
+
`projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly. (Project lookup failed with: ${bareMessage(err)})`
|
|
1096
1271
|
);
|
|
1097
1272
|
}
|
|
1098
1273
|
}
|
|
@@ -1516,12 +1691,24 @@ function createMushiServer(config) {
|
|
|
1516
1691
|
suggestedFix: z.unknown().describe("Stage-2 suggested fix, or null if not yet classified"),
|
|
1517
1692
|
reproductionSteps: z.unknown().describe("Reproduction steps recorded on the report (array; [] if none)"),
|
|
1518
1693
|
summary: z.unknown().describe("One-line report summary, or null"),
|
|
1519
|
-
component: z.unknown().describe("Component/page the bug was attributed to, or null")
|
|
1694
|
+
component: z.unknown().describe("Component/page the bug was attributed to, or null"),
|
|
1695
|
+
note: z.string().optional().describe("Present when no Stage-2 analysis exists \u2014 explains why and what to call instead")
|
|
1520
1696
|
}
|
|
1521
1697
|
},
|
|
1522
1698
|
async (args) => {
|
|
1523
1699
|
const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
|
|
1524
1700
|
const s2 = report.stage2_analysis;
|
|
1701
|
+
if (!s2 || s2.rootCause == null && s2.suggestedFix == null) {
|
|
1702
|
+
return jsonResult({
|
|
1703
|
+
reportId: args.reportId,
|
|
1704
|
+
rootCause: null,
|
|
1705
|
+
suggestedFix: null,
|
|
1706
|
+
reproductionSteps: report.reproduction_steps ?? [],
|
|
1707
|
+
summary: report.summary ?? null,
|
|
1708
|
+
component: report.component ?? null,
|
|
1709
|
+
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."
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1525
1712
|
return jsonResult({
|
|
1526
1713
|
reportId: args.reportId,
|
|
1527
1714
|
rootCause: s2?.rootCause ?? null,
|
|
@@ -1952,12 +2139,31 @@ function createMushiServer(config) {
|
|
|
1952
2139
|
const category = report ? report.category : "unknown";
|
|
1953
2140
|
const status = report ? report.status : "unknown";
|
|
1954
2141
|
const actions = [];
|
|
1955
|
-
|
|
2142
|
+
const processingError = report ? report.processing_error : null;
|
|
2143
|
+
const attempts = report ? report.fix_attempts ?? [] : [];
|
|
2144
|
+
const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
|
|
2145
|
+
const lastAttemptStatus = lastAttempt ? String(lastAttempt.status ?? "") : "";
|
|
2146
|
+
const autofixBlocked = typeof processingError === "string" && processingError.startsWith("autofix_blocked:") || lastAttemptStatus.startsWith("skipped") || lastAttemptStatus === "failed";
|
|
2147
|
+
if (autofixBlocked && status !== "fixing" && status !== "fixed") {
|
|
2148
|
+
const blockReason = typeof processingError === "string" && processingError || String(lastAttempt?.error ?? "previous fix attempt was skipped or failed");
|
|
2149
|
+
actions.push({
|
|
2150
|
+
action: "unblock_autofix",
|
|
2151
|
+
reason: `Auto-fix is blocked: ${blockReason}. Resolve the blocker (settings/integration/budget), then re-dispatch.`,
|
|
2152
|
+
tool: "diagnose_setup",
|
|
2153
|
+
args: { mode: "dispatch" }
|
|
2154
|
+
});
|
|
2155
|
+
actions.push({
|
|
2156
|
+
action: "redispatch_after_unblock",
|
|
2157
|
+
reason: "Once the blocker above is resolved, dispatch a fresh fix attempt.",
|
|
2158
|
+
tool: "dispatch_fix",
|
|
2159
|
+
args: { reportId: args.report_id }
|
|
2160
|
+
});
|
|
2161
|
+
} else if (status === "new" || status === "classified") {
|
|
1956
2162
|
actions.push({
|
|
1957
2163
|
action: "dispatch_fix",
|
|
1958
|
-
reason: "Report is classified
|
|
2164
|
+
reason: "Report is classified and no fix has been attempted",
|
|
1959
2165
|
tool: "dispatch_fix",
|
|
1960
|
-
args: { reportId: args.report_id
|
|
2166
|
+
args: { reportId: args.report_id }
|
|
1961
2167
|
});
|
|
1962
2168
|
} else if (status === "fixing") {
|
|
1963
2169
|
actions.push({
|
|
@@ -2035,7 +2241,7 @@ function createMushiServer(config) {
|
|
|
2035
2241
|
linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
|
|
2036
2242
|
summary: z.string().describe("One-line human summary of what the fix changed"),
|
|
2037
2243
|
idempotencyKey: z.string().uuid().optional().describe(
|
|
2038
|
-
"Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows"
|
|
2244
|
+
"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."
|
|
2039
2245
|
)
|
|
2040
2246
|
},
|
|
2041
2247
|
outputSchema: {
|
|
@@ -2044,27 +2250,123 @@ function createMushiServer(config) {
|
|
|
2044
2250
|
}
|
|
2045
2251
|
},
|
|
2046
2252
|
async (args) => {
|
|
2047
|
-
const idemKey = args.idempotencyKey ??
|
|
2253
|
+
const idemKey = args.idempotencyKey ?? await stableUuidFrom(
|
|
2254
|
+
IDEMPOTENCY_NAMESPACE,
|
|
2255
|
+
args.reportId,
|
|
2256
|
+
args.branch,
|
|
2257
|
+
args.prUrl ?? ""
|
|
2258
|
+
);
|
|
2048
2259
|
const created = await apiCall("/v1/admin/fixes", {
|
|
2049
2260
|
method: "POST",
|
|
2050
2261
|
headers: { "Idempotency-Key": idemKey },
|
|
2051
2262
|
body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
|
|
2052
2263
|
});
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2264
|
+
try {
|
|
2265
|
+
await apiCall(`/v1/admin/fixes/${created.fixId}`, {
|
|
2266
|
+
method: "PATCH",
|
|
2267
|
+
body: JSON.stringify({
|
|
2268
|
+
status: "completed",
|
|
2269
|
+
branch: args.branch,
|
|
2270
|
+
pr_url: args.prUrl,
|
|
2271
|
+
files_changed: args.filesChanged,
|
|
2272
|
+
lines_changed: args.linesChanged,
|
|
2273
|
+
summary: args.summary,
|
|
2274
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2275
|
+
})
|
|
2276
|
+
});
|
|
2277
|
+
} catch (err) {
|
|
2278
|
+
const status = err instanceof MushiApiError ? err.status : 500;
|
|
2279
|
+
throw new MushiApiError(
|
|
2280
|
+
status,
|
|
2281
|
+
"FIX_PATCH_FAILED",
|
|
2282
|
+
`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.`
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2065
2285
|
return jsonResult({ ok: true, fixId: created.fixId });
|
|
2066
2286
|
}
|
|
2067
2287
|
);
|
|
2288
|
+
server.registerTool(
|
|
2289
|
+
"triage_next_steps",
|
|
2290
|
+
{
|
|
2291
|
+
title: titleOf("triage_next_steps"),
|
|
2292
|
+
description: descOf("triage_next_steps"),
|
|
2293
|
+
annotations: annotationsFor("triage_next_steps"),
|
|
2294
|
+
inputSchema: {
|
|
2295
|
+
project_id: z.string().uuid().optional().describe("Project UUID (defaults to the configured project).")
|
|
2296
|
+
},
|
|
2297
|
+
outputSchema: {
|
|
2298
|
+
steps: z.array(
|
|
2299
|
+
z.object({
|
|
2300
|
+
priority: z.number(),
|
|
2301
|
+
action: z.string(),
|
|
2302
|
+
reason: z.string(),
|
|
2303
|
+
tool: z.string().optional(),
|
|
2304
|
+
args: z.record(z.string(), z.unknown()).optional()
|
|
2305
|
+
})
|
|
2306
|
+
),
|
|
2307
|
+
summary: z.string()
|
|
2308
|
+
}
|
|
2309
|
+
},
|
|
2310
|
+
async (args) => {
|
|
2311
|
+
const pid = await resolveProjectId(args.project_id);
|
|
2312
|
+
const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
|
|
2313
|
+
const data = await apiCall(
|
|
2314
|
+
"/v1/admin/reports?limit=100",
|
|
2315
|
+
{ headers: extraHeaders }
|
|
2316
|
+
);
|
|
2317
|
+
const reports = data.reports ?? [];
|
|
2318
|
+
const isCron = (r) => typeof r.reporter_token_hash === "string" && r.reporter_token_hash.startsWith("cron:");
|
|
2319
|
+
const isBlocked = (r) => typeof r.processing_error === "string" && r.processing_error.startsWith("autofix_blocked:");
|
|
2320
|
+
const label = (r) => String(r.title ?? r.summary ?? r.description ?? "").slice(0, 90);
|
|
2321
|
+
const steps = [];
|
|
2322
|
+
let priority = 1;
|
|
2323
|
+
const blocked = reports.filter(isBlocked);
|
|
2324
|
+
if (blocked.length > 0) {
|
|
2325
|
+
steps.push({
|
|
2326
|
+
priority: priority++,
|
|
2327
|
+
action: `Unblock auto-fix (${blocked.length} report${blocked.length === 1 ? "" : "s"} blocked)`,
|
|
2328
|
+
reason: String(blocked[0].processing_error).slice(0, 200),
|
|
2329
|
+
tool: "diagnose_setup",
|
|
2330
|
+
args: { mode: "dispatch" }
|
|
2331
|
+
});
|
|
2332
|
+
}
|
|
2333
|
+
const fixing = reports.filter((r) => r.status === "fixing");
|
|
2334
|
+
for (const r of fixing.slice(0, 2)) {
|
|
2335
|
+
steps.push({
|
|
2336
|
+
priority: priority++,
|
|
2337
|
+
action: `Shepherd in-flight fix: ${label(r)}`,
|
|
2338
|
+
reason: "A fix branch/PR is open \u2014 check CI and merge when green.",
|
|
2339
|
+
tool: "get_report_timeline",
|
|
2340
|
+
args: { report_id: r.id }
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
const openUserReports = reports.filter((r) => (r.status === "new" || r.status === "classified") && !isCron(r) && !isBlocked(r)).sort((a, b) => {
|
|
2344
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
2345
|
+
return (rank[String(a.severity)] ?? 4) - (rank[String(b.severity)] ?? 4);
|
|
2346
|
+
});
|
|
2347
|
+
for (const r of openUserReports.slice(0, Math.max(0, 4 - steps.length))) {
|
|
2348
|
+
steps.push({
|
|
2349
|
+
priority: priority++,
|
|
2350
|
+
action: `Triage [${r.severity}] ${label(r)}`,
|
|
2351
|
+
reason: "User-felt report awaiting triage \u2014 review the packet, then dispatch or dismiss.",
|
|
2352
|
+
tool: "triage_issue",
|
|
2353
|
+
args: { report_id: r.id }
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
const chores = reports.filter((r) => r.status === "classified" && isCron(r) && !isBlocked(r));
|
|
2357
|
+
if (chores.length > 0 && steps.length < 5) {
|
|
2358
|
+
steps.push({
|
|
2359
|
+
priority: priority++,
|
|
2360
|
+
action: `Batch ${chores.length} maintenance chore${chores.length === 1 ? "" : "s"} (dependency bumps)`,
|
|
2361
|
+
reason: "Robot-filed modernization reports \u2014 batch-dispatch or dismiss in one sitting; do not let them crowd out user bugs.",
|
|
2362
|
+
tool: "get_recent_reports",
|
|
2363
|
+
args: { status: "classified" }
|
|
2364
|
+
});
|
|
2365
|
+
}
|
|
2366
|
+
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 ");
|
|
2367
|
+
return jsonResult({ steps, summary });
|
|
2368
|
+
}
|
|
2369
|
+
);
|
|
2068
2370
|
server.registerTool(
|
|
2069
2371
|
"dispatch_fix",
|
|
2070
2372
|
{
|
|
@@ -2101,7 +2403,7 @@ function createMushiServer(config) {
|
|
|
2101
2403
|
} catch {
|
|
2102
2404
|
}
|
|
2103
2405
|
}
|
|
2104
|
-
const
|
|
2406
|
+
const dispatch = await apiCall("/v1/admin/fixes/dispatch", {
|
|
2105
2407
|
method: "POST",
|
|
2106
2408
|
headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
|
|
2107
2409
|
body: JSON.stringify({
|
|
@@ -2111,7 +2413,12 @@ function createMushiServer(config) {
|
|
|
2111
2413
|
...projectId ? { projectId } : {}
|
|
2112
2414
|
})
|
|
2113
2415
|
});
|
|
2114
|
-
|
|
2416
|
+
if (!dispatch?.dispatchId) {
|
|
2417
|
+
throw new Error(
|
|
2418
|
+
`Dispatch response missing dispatchId \u2014 got: ${JSON.stringify(dispatch).slice(0, 300)}`
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
return jsonResult({ fixId: dispatch.dispatchId, status: dispatch.status ?? "queued" });
|
|
2115
2422
|
}
|
|
2116
2423
|
);
|
|
2117
2424
|
server.registerTool(
|
|
@@ -2246,6 +2553,11 @@ function createMushiServer(config) {
|
|
|
2246
2553
|
status: z.enum([
|
|
2247
2554
|
"pending",
|
|
2248
2555
|
"classified",
|
|
2556
|
+
// Workflow states added 20260520600000 but missing from this
|
|
2557
|
+
// enum ever since (2026-08-16 audit P2-2) — agents could not
|
|
2558
|
+
// mark a report as being looked at.
|
|
2559
|
+
"triaged",
|
|
2560
|
+
"in_progress",
|
|
2249
2561
|
"grouped",
|
|
2250
2562
|
"fixing",
|
|
2251
2563
|
"fixed",
|
|
@@ -2253,7 +2565,7 @@ function createMushiServer(config) {
|
|
|
2253
2565
|
"verified",
|
|
2254
2566
|
"reopened",
|
|
2255
2567
|
"dismissed"
|
|
2256
|
-
]).describe("Target status"),
|
|
2568
|
+
]).describe("Target status (resolved is stored as fixed)"),
|
|
2257
2569
|
reason: z.string().optional().describe("Reason for the transition (audit trail)")
|
|
2258
2570
|
}
|
|
2259
2571
|
},
|
|
@@ -3470,28 +3782,30 @@ Be specific. No generic advice.`
|
|
|
3470
3782
|
|
|
3471
3783
|
// src/index.ts
|
|
3472
3784
|
import * as Sentry from "@sentry/node";
|
|
3473
|
-
|
|
3474
|
-
args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"
|
|
3475
|
-
);
|
|
3476
|
-
console.log = _writeStderr;
|
|
3477
|
-
console.warn = _writeStderr;
|
|
3785
|
+
installStdoutGuard();
|
|
3478
3786
|
var require2 = createRequire(import.meta.url);
|
|
3479
3787
|
var VERSION = require2("../package.json").version;
|
|
3480
3788
|
var log = createLogger2({ scope: "mushi:mcp", level: "info", destination: "stderr" });
|
|
3789
|
+
function resolveCliConfigPath() {
|
|
3790
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
3791
|
+
const appData = process.env.APPDATA;
|
|
3792
|
+
const base = xdg && xdg.length > 0 ? xdg : process.platform === "win32" && appData && appData.length > 0 ? appData : join(homedir(), ".config");
|
|
3793
|
+
return join(base, "mushi", "config.json");
|
|
3794
|
+
}
|
|
3481
3795
|
function readCliConfig() {
|
|
3796
|
+
const configPath = resolveCliConfigPath();
|
|
3482
3797
|
try {
|
|
3483
|
-
const
|
|
3484
|
-
const appData = process.env.APPDATA;
|
|
3485
|
-
const base = xdg && xdg.length > 0 ? xdg : process.platform === "win32" && appData && appData.length > 0 ? appData : join(homedir(), ".config");
|
|
3486
|
-
const raw = readFileSync(join(base, "mushi", "config.json"), "utf8");
|
|
3798
|
+
const raw = readFileSync(configPath, "utf8");
|
|
3487
3799
|
const parsed = JSON.parse(raw);
|
|
3488
3800
|
return {
|
|
3489
3801
|
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
|
|
3490
3802
|
projectId: typeof parsed.projectId === "string" ? parsed.projectId : void 0,
|
|
3491
|
-
endpoint: typeof parsed.endpoint === "string" ? parsed.endpoint : void 0
|
|
3803
|
+
endpoint: typeof parsed.endpoint === "string" ? parsed.endpoint : void 0,
|
|
3804
|
+
path: configPath,
|
|
3805
|
+
found: true
|
|
3492
3806
|
};
|
|
3493
3807
|
} catch {
|
|
3494
|
-
return {};
|
|
3808
|
+
return { path: configPath, found: false };
|
|
3495
3809
|
}
|
|
3496
3810
|
}
|
|
3497
3811
|
var CLI_CONFIG = readCliConfig();
|
|
@@ -3510,11 +3824,49 @@ if (MCP_SENTRY_DSN) {
|
|
|
3510
3824
|
tracesSampleRate: 0
|
|
3511
3825
|
});
|
|
3512
3826
|
}
|
|
3827
|
+
function missingApiKeyReport() {
|
|
3828
|
+
const envState = process.env.MUSHI_API_KEY === void 0 ? "not set" : process.env.MUSHI_API_KEY.trim() === "" ? "set but empty" : "set";
|
|
3829
|
+
const cliState = !CLI_CONFIG.found ? "no file at this path" : CLI_CONFIG.apiKey ? "present" : 'file exists but has no "apiKey" field';
|
|
3830
|
+
return [
|
|
3831
|
+
"",
|
|
3832
|
+
"[mushi-mcp] FATAL: no API key \u2014 the MCP server cannot serve a single tool call.",
|
|
3833
|
+
"",
|
|
3834
|
+
" Sources checked, in precedence order:",
|
|
3835
|
+
` 1. env MUSHI_API_KEY \u2192 ${envState}`,
|
|
3836
|
+
` 2. CLI config file \u2192 ${cliState}`,
|
|
3837
|
+
` ${CLI_CONFIG.path}`,
|
|
3838
|
+
"",
|
|
3839
|
+
" Fix either one:",
|
|
3840
|
+
" \u2022 Run `mushi login` (writes the config file above), or",
|
|
3841
|
+
' \u2022 Add the key to the "env" block of your MCP client config',
|
|
3842
|
+
" (.cursor/mcp.json \xB7 claude_desktop_config.json \xB7 .vscode/mcp.json):",
|
|
3843
|
+
"",
|
|
3844
|
+
' { "mcpServers": { "mushi-mushi": {',
|
|
3845
|
+
' "command": "npx", "args": ["-y", "@mushi-mushi/mcp"],',
|
|
3846
|
+
' "env": { "MUSHI_API_KEY": "mushi_\u2026", "MUSHI_PROJECT_ID": "<uuid>" } } } }',
|
|
3847
|
+
"",
|
|
3848
|
+
" A key exported in your shell does NOT reach the server: MCP clients",
|
|
3849
|
+
" spawn this process with only the env block they are given.",
|
|
3850
|
+
"",
|
|
3851
|
+
" Other env vars this server reads:",
|
|
3852
|
+
` MUSHI_API_ENDPOINT ${process.env.MUSHI_API_ENDPOINT?.trim() ? "= " + process.env.MUSHI_API_ENDPOINT.trim() : `unset \u2192 ${API_ENDPOINT}`}`,
|
|
3853
|
+
` MUSHI_PROJECT_ID ${PROJECT_ID ? "= " + PROJECT_ID : "unset (account mode)"}`,
|
|
3854
|
+
" MUSHI_SCOPES optional CSV: mcp:read,mcp:write",
|
|
3855
|
+
' MUSHI_FEATURES optional CSV of tool groups, or "all"',
|
|
3856
|
+
" MUSHI_MCP_TIMEOUT_MS optional per-request timeout in ms (default 15000)",
|
|
3857
|
+
"",
|
|
3858
|
+
" Mint a key: Console \u2192 Settings \u2192 API keys.",
|
|
3859
|
+
""
|
|
3860
|
+
].join("\n");
|
|
3861
|
+
}
|
|
3513
3862
|
async function main() {
|
|
3514
3863
|
if (!API_KEY) {
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3864
|
+
process.stderr.write(missingApiKeyReport());
|
|
3865
|
+
log.fatal("No API key found \u2014 set MUSHI_API_KEY, or run `mushi login`.", {
|
|
3866
|
+
cliConfigPath: CLI_CONFIG.path,
|
|
3867
|
+
cliConfigFound: CLI_CONFIG.found,
|
|
3868
|
+
endpoint: API_ENDPOINT
|
|
3869
|
+
});
|
|
3518
3870
|
process.exit(1);
|
|
3519
3871
|
}
|
|
3520
3872
|
if (!process.env.MUSHI_API_KEY && CLI_CONFIG.apiKey) {
|
|
@@ -3561,6 +3913,7 @@ async function main() {
|
|
|
3561
3913
|
if (pollTimer) clearInterval(pollTimer);
|
|
3562
3914
|
void transport.close().finally(() => process.exit(exitCode));
|
|
3563
3915
|
};
|
|
3916
|
+
setActiveShutdown(shutdown);
|
|
3564
3917
|
process.stdin.on("end", () => shutdown(0));
|
|
3565
3918
|
process.stdin.on("close", () => shutdown(0));
|
|
3566
3919
|
process.on("SIGINT", () => shutdown(0));
|
|
@@ -3607,6 +3960,49 @@ async function main() {
|
|
|
3607
3960
|
pollTimer.unref();
|
|
3608
3961
|
}
|
|
3609
3962
|
}
|
|
3963
|
+
var activeShutdown;
|
|
3964
|
+
var setActiveShutdown = (fn) => {
|
|
3965
|
+
activeShutdown = fn;
|
|
3966
|
+
};
|
|
3967
|
+
var exitNow = (code) => {
|
|
3968
|
+
if (activeShutdown) activeShutdown(code);
|
|
3969
|
+
else process.exit(code);
|
|
3970
|
+
};
|
|
3971
|
+
var CRASH_WINDOW_MS = 1e4;
|
|
3972
|
+
var CRASH_LIMIT = 20;
|
|
3973
|
+
var crashTimes = [];
|
|
3974
|
+
function isCrashLooping() {
|
|
3975
|
+
const now = Date.now();
|
|
3976
|
+
crashTimes = crashTimes.filter((t) => now - t < CRASH_WINDOW_MS);
|
|
3977
|
+
crashTimes.push(now);
|
|
3978
|
+
return crashTimes.length >= CRASH_LIMIT;
|
|
3979
|
+
}
|
|
3980
|
+
process.on("unhandledRejection", (reason) => {
|
|
3981
|
+
log.error("unhandledRejection \u2014 server kept alive", {
|
|
3982
|
+
err: reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason),
|
|
3983
|
+
stack: reason instanceof Error ? reason.stack?.split("\n").slice(0, 5).join(" | ") : void 0
|
|
3984
|
+
});
|
|
3985
|
+
if (isCrashLooping()) {
|
|
3986
|
+
log.fatal("too many unhandled rejections in 10s \u2014 exiting so the client can respawn");
|
|
3987
|
+
exitNow(1);
|
|
3988
|
+
}
|
|
3989
|
+
});
|
|
3990
|
+
process.on("uncaughtException", (err) => {
|
|
3991
|
+
if (err?.code === "EPIPE" || err?.code === "ERR_STREAM_DESTROYED" || err?.code === "ERR_STREAM_WRITE_AFTER_END") {
|
|
3992
|
+
log.info("stdio pipe closed by the client \u2014 shutting down", { code: err.code });
|
|
3993
|
+
exitNow(0);
|
|
3994
|
+
return;
|
|
3995
|
+
}
|
|
3996
|
+
log.error("uncaughtException \u2014 server kept alive", {
|
|
3997
|
+
err: `${err?.name ?? "Error"}: ${err?.message ?? String(err)}`,
|
|
3998
|
+
code: err?.code,
|
|
3999
|
+
stack: err?.stack?.split("\n").slice(0, 5).join(" | ")
|
|
4000
|
+
});
|
|
4001
|
+
if (isCrashLooping()) {
|
|
4002
|
+
log.fatal("too many uncaught exceptions in 10s \u2014 exiting so the client can respawn");
|
|
4003
|
+
exitNow(1);
|
|
4004
|
+
}
|
|
4005
|
+
});
|
|
3610
4006
|
main().catch((err) => {
|
|
3611
4007
|
log.fatal("MCP server crashed", { err: String(err) });
|
|
3612
4008
|
process.exit(1);
|