@mushi-mushi/mcp 0.20.0 → 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-OZJEY3QR.js → chunk-RVZIKP4X.js} +43 -18
- package/dist/{chunk-VBXXUK5G.js → chunk-ZSVRLSW5.js} +4 -2
- package/dist/clients.d.ts +1 -1
- package/dist/clients.js +2 -2
- package/dist/feature-groups.js +1 -1
- package/dist/index.js +856 -253
- package/dist/server.d.ts +9 -1
- package/dist/server.js +715 -222
- 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",
|
|
@@ -375,7 +449,7 @@ var TDD_TOOL_CATALOG = [
|
|
|
375
449
|
{
|
|
376
450
|
name: "list_byok_keys",
|
|
377
451
|
title: "List your API key pool",
|
|
378
|
-
description: "List the project's BYOK API keys grouped by provider (anthropic | openai | firecrawl | browserbase | cursor). Returns { keys: [{ id,
|
|
452
|
+
description: "List the project's BYOK API keys grouped by provider (anthropic | openai | firecrawl | browserbase | cursor). Returns pooled { keys: [{ id, provider_slug, label, priority, status, cooldown_until, test_status, key_hint, base_url, last_tested_at, last_used_at }] } plus read-only { legacyKeys } metadata for credentials saved before the pooled lifecycle \u2014 never the raw secret. Read-only. Use to see which keys are validated, active, pending, legacy, or rate-limited; use add_byok_key to add one.",
|
|
379
453
|
scope: "mcp:read",
|
|
380
454
|
hints: { readOnly: true, idempotent: true, openWorld: true },
|
|
381
455
|
useCase: "Which API keys are active and which are rate-limited?"
|
|
@@ -383,11 +457,27 @@ var TDD_TOOL_CATALOG = [
|
|
|
383
457
|
{
|
|
384
458
|
name: "add_byok_key",
|
|
385
459
|
title: "Add an API key",
|
|
386
|
-
description: "Add a BYOK API key
|
|
460
|
+
description: "Add and immediately validate a BYOK API key for anthropic | openai | firecrawl | browserbase | cursor. The raw key is stored encrypted in Supabase Vault and never returned; failed probes remain quarantined. Optional baseUrl is accepted only for allow-listed OpenAI-compatible HTTPS providers. Write; NOT idempotent.",
|
|
387
461
|
scope: "mcp:write",
|
|
388
462
|
hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
|
|
389
463
|
useCase: "Add a backup Anthropic key to the pool."
|
|
390
464
|
},
|
|
465
|
+
{
|
|
466
|
+
name: "test_byok_key",
|
|
467
|
+
title: "Test an API key",
|
|
468
|
+
description: "Re-test one pooled BYOK credential by key id. A successful provider probe activates the key; auth, quota, and network failures keep it out of the runtime pool. Returns the sanitized validation result and updated key metadata. Write; idempotent.",
|
|
469
|
+
scope: "mcp:write",
|
|
470
|
+
hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
|
|
471
|
+
useCase: "Re-test a quarantined OpenAI key after fixing its account quota."
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "remove_byok_key",
|
|
475
|
+
title: "Remove an API key",
|
|
476
|
+
description: "Permanently remove one pooled BYOK credential by key id from the authenticated project, including its Vault secret. Write; destructive; idempotent.",
|
|
477
|
+
scope: "mcp:write",
|
|
478
|
+
hints: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
|
|
479
|
+
useCase: "Remove a rotated or compromised provider key from the runtime pool."
|
|
480
|
+
},
|
|
391
481
|
{
|
|
392
482
|
name: "list_pending_review_stories",
|
|
393
483
|
title: "List QA stories pending review",
|
|
@@ -530,24 +620,29 @@ var TDD_TOOL_CATALOG = [
|
|
|
530
620
|
}
|
|
531
621
|
];
|
|
532
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.
|
|
533
628
|
fix: {
|
|
534
629
|
label: "Fix a bug",
|
|
535
630
|
tools: [
|
|
536
631
|
"get_recent_reports",
|
|
537
|
-
"
|
|
538
|
-
"
|
|
632
|
+
"get_report_detail",
|
|
633
|
+
"triage_issue",
|
|
539
634
|
"dispatch_fix",
|
|
540
635
|
"start_skill_pipeline",
|
|
541
636
|
"checkin_pipeline_step",
|
|
542
637
|
"get_pipeline_run"
|
|
543
638
|
],
|
|
544
|
-
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."
|
|
545
640
|
},
|
|
546
641
|
status: {
|
|
547
642
|
label: "Check project status",
|
|
548
643
|
tools: [
|
|
549
|
-
"get_dashboard",
|
|
550
644
|
"triage_next_steps",
|
|
645
|
+
"get_account_overview",
|
|
551
646
|
"get_usage",
|
|
552
647
|
"get_backend_health",
|
|
553
648
|
"activation_status"
|
|
@@ -557,18 +652,19 @@ var USE_MUSHI_INTENTS = {
|
|
|
557
652
|
setup: {
|
|
558
653
|
label: "Set up Mushi",
|
|
559
654
|
tools: [
|
|
560
|
-
"
|
|
655
|
+
"diagnose_setup",
|
|
561
656
|
"activation_status",
|
|
562
657
|
"get_backend_health",
|
|
563
658
|
"list_byok_keys",
|
|
564
|
-
"add_byok_key"
|
|
659
|
+
"add_byok_key",
|
|
660
|
+
"test_byok_key",
|
|
661
|
+
"remove_byok_key"
|
|
565
662
|
],
|
|
566
|
-
hint: "Call
|
|
663
|
+
hint: "Call diagnose_setup first \u2014 it diagnoses setup gaps and returns the next action to take."
|
|
567
664
|
},
|
|
568
665
|
qa: {
|
|
569
666
|
label: "Run / review QA tests",
|
|
570
667
|
tools: [
|
|
571
|
-
"list_qa_stories",
|
|
572
668
|
"run_qa_story",
|
|
573
669
|
"list_qa_story_runs",
|
|
574
670
|
"get_qa_story_run",
|
|
@@ -576,7 +672,7 @@ var USE_MUSHI_INTENTS = {
|
|
|
576
672
|
"approve_qa_story",
|
|
577
673
|
"improve_qa_story"
|
|
578
674
|
],
|
|
579
|
-
hint: "Call
|
|
675
|
+
hint: "Call list_qa_story_runs to see recent coverage and outcomes; run_qa_story to trigger a run."
|
|
580
676
|
},
|
|
581
677
|
pipeline: {
|
|
582
678
|
label: "Run an agent pipeline / skill",
|
|
@@ -591,12 +687,7 @@ var USE_MUSHI_INTENTS = {
|
|
|
591
687
|
},
|
|
592
688
|
audit: {
|
|
593
689
|
label: "Audit / health check",
|
|
594
|
-
tools: [
|
|
595
|
-
"run_fullstack_audit",
|
|
596
|
-
"get_backend_health",
|
|
597
|
-
"get_dashboard",
|
|
598
|
-
"get_usage"
|
|
599
|
-
],
|
|
690
|
+
tools: ["run_fullstack_audit", "get_backend_health", "get_account_overview", "get_usage"],
|
|
600
691
|
hint: "Call run_fullstack_audit for a full-stack health scorecard."
|
|
601
692
|
}
|
|
602
693
|
};
|
|
@@ -759,8 +850,10 @@ var TOOL_FEATURE_MAP = {
|
|
|
759
850
|
start_skill_pipeline: "skills",
|
|
760
851
|
get_pipeline_run: "skills",
|
|
761
852
|
checkin_pipeline_step: "skills",
|
|
762
|
-
list_byok_keys: "
|
|
763
|
-
add_byok_key: "
|
|
853
|
+
list_byok_keys: "setup",
|
|
854
|
+
add_byok_key: "setup",
|
|
855
|
+
test_byok_key: "setup",
|
|
856
|
+
remove_byok_key: "setup",
|
|
764
857
|
// audit
|
|
765
858
|
run_fullstack_audit: "audit",
|
|
766
859
|
get_backend_health: "audit",
|
|
@@ -966,7 +1059,40 @@ function wrapUntrustedJson(value, role) {
|
|
|
966
1059
|
}
|
|
967
1060
|
|
|
968
1061
|
// src/server.ts
|
|
1062
|
+
installStdoutGuard();
|
|
969
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
|
+
}
|
|
970
1096
|
var MushiApiError = class extends Error {
|
|
971
1097
|
constructor(status, code, message) {
|
|
972
1098
|
super(`[${code}] ${message}`);
|
|
@@ -980,6 +1106,7 @@ var MushiApiError = class extends Error {
|
|
|
980
1106
|
function createMushiServer(config) {
|
|
981
1107
|
const { version, apiEndpoint, apiKey, projectId } = config;
|
|
982
1108
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
1109
|
+
const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : resolveTimeoutMsFromEnv();
|
|
983
1110
|
const apiLog = createLogger({
|
|
984
1111
|
scope: "mushi:mcp:api",
|
|
985
1112
|
level: process.env.MUSHI_LOG_LEVEL ?? "info",
|
|
@@ -997,22 +1124,66 @@ function createMushiServer(config) {
|
|
|
997
1124
|
async function apiCall(path, options) {
|
|
998
1125
|
const requestId = crypto.randomUUID().slice(0, 12);
|
|
999
1126
|
const started = Date.now();
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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
|
+
);
|
|
1012
1164
|
}
|
|
1013
|
-
|
|
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
|
+
}
|
|
1014
1186
|
const durationMs = Date.now() - started;
|
|
1015
|
-
const text = await res.text();
|
|
1016
1187
|
let body = null;
|
|
1017
1188
|
if (text) {
|
|
1018
1189
|
try {
|
|
@@ -1042,6 +1213,7 @@ function createMushiServer(config) {
|
|
|
1042
1213
|
apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
|
|
1043
1214
|
return body;
|
|
1044
1215
|
}
|
|
1216
|
+
let resolvedAccountProjectId = null;
|
|
1045
1217
|
async function resolveProjectId(explicitId) {
|
|
1046
1218
|
if (explicitId) {
|
|
1047
1219
|
if (projectId && explicitId !== projectId) {
|
|
@@ -1054,12 +1226,12 @@ function createMushiServer(config) {
|
|
|
1054
1226
|
return explicitId;
|
|
1055
1227
|
}
|
|
1056
1228
|
if (projectId) return projectId;
|
|
1229
|
+
if (resolvedAccountProjectId) return resolvedAccountProjectId;
|
|
1057
1230
|
try {
|
|
1058
|
-
const data = await apiCall(
|
|
1059
|
-
"/v1/admin/mcp/account-overview"
|
|
1060
|
-
);
|
|
1231
|
+
const data = await apiCall("/v1/admin/mcp/account-overview");
|
|
1061
1232
|
if (data.total === 1 && data.projects[0]) {
|
|
1062
|
-
|
|
1233
|
+
resolvedAccountProjectId = data.projects[0].id;
|
|
1234
|
+
return resolvedAccountProjectId;
|
|
1063
1235
|
}
|
|
1064
1236
|
if (data.total === 0) {
|
|
1065
1237
|
throw new MushiApiError(
|
|
@@ -1075,11 +1247,27 @@ function createMushiServer(config) {
|
|
|
1075
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.`
|
|
1076
1248
|
);
|
|
1077
1249
|
} catch (err) {
|
|
1078
|
-
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
|
+
}
|
|
1079
1267
|
throw new MushiApiError(
|
|
1080
1268
|
400,
|
|
1081
1269
|
"MISSING_PROJECT_ID",
|
|
1082
|
-
|
|
1270
|
+
`projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly. (Project lookup failed with: ${bareMessage(err)})`
|
|
1083
1271
|
);
|
|
1084
1272
|
}
|
|
1085
1273
|
}
|
|
@@ -1168,9 +1356,12 @@ function createMushiServer(config) {
|
|
|
1168
1356
|
params.set("limit", String(Math.min(args.limit ?? 20, 100)));
|
|
1169
1357
|
const pid = await resolveProjectId(args.project_id);
|
|
1170
1358
|
const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
|
|
1171
|
-
const data = await apiCall(
|
|
1172
|
-
|
|
1173
|
-
|
|
1359
|
+
const data = await apiCall(
|
|
1360
|
+
`/v1/admin/reports?${params}`,
|
|
1361
|
+
{
|
|
1362
|
+
headers: extraHeaders
|
|
1363
|
+
}
|
|
1364
|
+
);
|
|
1174
1365
|
return jsonResult(data);
|
|
1175
1366
|
}
|
|
1176
1367
|
);
|
|
@@ -1193,7 +1384,10 @@ function createMushiServer(config) {
|
|
|
1193
1384
|
return {
|
|
1194
1385
|
...res,
|
|
1195
1386
|
resource_links: [
|
|
1196
|
-
{
|
|
1387
|
+
{
|
|
1388
|
+
uri: `project://reports/${args.reportId}`,
|
|
1389
|
+
title: `Report ${args.reportId.slice(0, 8)}\u2026`
|
|
1390
|
+
}
|
|
1197
1391
|
]
|
|
1198
1392
|
};
|
|
1199
1393
|
}
|
|
@@ -1297,17 +1491,20 @@ function createMushiServer(config) {
|
|
|
1297
1491
|
const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
|
|
1298
1492
|
headers
|
|
1299
1493
|
});
|
|
1300
|
-
return wrappedJsonResult(
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1494
|
+
return wrappedJsonResult(
|
|
1495
|
+
{
|
|
1496
|
+
report,
|
|
1497
|
+
// Paste-ready fix prompt composed server-side by composeFixPacket()
|
|
1498
|
+
// (diagnosis + repro + suggested fix + relevant code + blast radius).
|
|
1499
|
+
// Hand this straight to the editing agent — no second LLM key needed.
|
|
1500
|
+
fixPrompt: report.fix_packet ?? null,
|
|
1501
|
+
reproductionSteps: report.reproduction_steps ?? [],
|
|
1502
|
+
component: report.component,
|
|
1503
|
+
rootCause: report.stage2_analysis?.rootCause,
|
|
1504
|
+
bugOntologyTags: report.bug_ontology_tags
|
|
1505
|
+
},
|
|
1506
|
+
"fix context"
|
|
1507
|
+
);
|
|
1311
1508
|
}
|
|
1312
1509
|
);
|
|
1313
1510
|
server.registerTool(
|
|
@@ -1356,7 +1553,9 @@ function createMushiServer(config) {
|
|
|
1356
1553
|
seed: args.seed,
|
|
1357
1554
|
depth: String(Math.min(args.depth ?? 2, 4))
|
|
1358
1555
|
});
|
|
1359
|
-
const data = await apiCall(
|
|
1556
|
+
const data = await apiCall(
|
|
1557
|
+
`/v1/admin/graph/traverse?${params}`
|
|
1558
|
+
);
|
|
1360
1559
|
return jsonResult(data);
|
|
1361
1560
|
}
|
|
1362
1561
|
);
|
|
@@ -1367,7 +1566,9 @@ function createMushiServer(config) {
|
|
|
1367
1566
|
description: descOf("get_graph_neighborhood"),
|
|
1368
1567
|
annotations: annotationsFor("get_graph_neighborhood"),
|
|
1369
1568
|
inputSchema: {
|
|
1370
|
-
seed: z.string().describe(
|
|
1569
|
+
seed: z.string().describe(
|
|
1570
|
+
"Starting graph node id or label to expand around (e.g. an inventory Action id or component name)"
|
|
1571
|
+
),
|
|
1371
1572
|
depth: z.number().int().min(1).max(4).optional().describe("BFS hops to traverse outward. Default 2; clamped to a max of 4.")
|
|
1372
1573
|
},
|
|
1373
1574
|
outputSchema: {
|
|
@@ -1380,7 +1581,9 @@ function createMushiServer(config) {
|
|
|
1380
1581
|
seed: args.seed,
|
|
1381
1582
|
depth: String(Math.min(args.depth ?? 2, 4))
|
|
1382
1583
|
});
|
|
1383
|
-
const data = await apiCall(
|
|
1584
|
+
const data = await apiCall(
|
|
1585
|
+
`/v1/admin/graph/traverse?${params}`
|
|
1586
|
+
);
|
|
1384
1587
|
return jsonResult(data);
|
|
1385
1588
|
}
|
|
1386
1589
|
);
|
|
@@ -1390,7 +1593,9 @@ function createMushiServer(config) {
|
|
|
1390
1593
|
title: titleOf("get_graph_node"),
|
|
1391
1594
|
description: descOf("get_graph_node"),
|
|
1392
1595
|
annotations: annotationsFor("get_graph_node"),
|
|
1393
|
-
inputSchema: {
|
|
1596
|
+
inputSchema: {
|
|
1597
|
+
nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).")
|
|
1598
|
+
},
|
|
1394
1599
|
outputSchema: {
|
|
1395
1600
|
node: z.unknown().describe("Single graph_nodes row including metadata (Action nodes carry v2 status)")
|
|
1396
1601
|
}
|
|
@@ -1412,7 +1617,8 @@ function createMushiServer(config) {
|
|
|
1412
1617
|
},
|
|
1413
1618
|
async (args) => {
|
|
1414
1619
|
const pid = args.projectId ?? projectId;
|
|
1415
|
-
if (!pid)
|
|
1620
|
+
if (!pid)
|
|
1621
|
+
throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for get_inventory");
|
|
1416
1622
|
return jsonText(await apiCall(`/v1/admin/inventory/${pid}`));
|
|
1417
1623
|
}
|
|
1418
1624
|
);
|
|
@@ -1430,7 +1636,8 @@ function createMushiServer(config) {
|
|
|
1430
1636
|
},
|
|
1431
1637
|
async (args) => {
|
|
1432
1638
|
const pid = args.projectId ?? projectId;
|
|
1433
|
-
if (!pid)
|
|
1639
|
+
if (!pid)
|
|
1640
|
+
throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for diff_inventory");
|
|
1434
1641
|
const q = new URLSearchParams({ from: args.fromSha, to: args.toSha });
|
|
1435
1642
|
return jsonText(await apiCall(`/v1/admin/inventory/${pid}/diff?${q}`));
|
|
1436
1643
|
}
|
|
@@ -1453,12 +1660,19 @@ function createMushiServer(config) {
|
|
|
1453
1660
|
},
|
|
1454
1661
|
async (args) => {
|
|
1455
1662
|
const pid = args.projectId ?? projectId;
|
|
1456
|
-
if (!pid)
|
|
1663
|
+
if (!pid)
|
|
1664
|
+
throw new MushiApiError(
|
|
1665
|
+
400,
|
|
1666
|
+
"MISSING_PROJECT",
|
|
1667
|
+
"projectId is required for list_gate_findings"
|
|
1668
|
+
);
|
|
1457
1669
|
const q = new URLSearchParams();
|
|
1458
1670
|
if (args.gate) q.set("gate", args.gate);
|
|
1459
1671
|
if (args.severity) q.set("severity", args.severity);
|
|
1460
1672
|
const suffix = q.toString() ? `?${q}` : "";
|
|
1461
|
-
const data = await apiCall(
|
|
1673
|
+
const data = await apiCall(
|
|
1674
|
+
`/v1/admin/inventory/${pid}/findings${suffix}`
|
|
1675
|
+
);
|
|
1462
1676
|
return jsonResult(data);
|
|
1463
1677
|
}
|
|
1464
1678
|
);
|
|
@@ -1468,19 +1682,33 @@ function createMushiServer(config) {
|
|
|
1468
1682
|
title: titleOf("suggest_fix"),
|
|
1469
1683
|
description: descOf("suggest_fix"),
|
|
1470
1684
|
annotations: annotationsFor("suggest_fix"),
|
|
1471
|
-
inputSchema: {
|
|
1685
|
+
inputSchema: {
|
|
1686
|
+
reportId: z.string().describe("Report UUID to read the Stage-2 suggested-fix slice for")
|
|
1687
|
+
},
|
|
1472
1688
|
outputSchema: {
|
|
1473
1689
|
reportId: z.string().describe("The report this slice was read from"),
|
|
1474
1690
|
rootCause: z.unknown().describe("Stage-2 root-cause hint, or null if not yet classified"),
|
|
1475
1691
|
suggestedFix: z.unknown().describe("Stage-2 suggested fix, or null if not yet classified"),
|
|
1476
1692
|
reproductionSteps: z.unknown().describe("Reproduction steps recorded on the report (array; [] if none)"),
|
|
1477
1693
|
summary: z.unknown().describe("One-line report summary, or null"),
|
|
1478
|
-
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")
|
|
1479
1696
|
}
|
|
1480
1697
|
},
|
|
1481
1698
|
async (args) => {
|
|
1482
1699
|
const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
|
|
1483
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
|
+
}
|
|
1484
1712
|
return jsonResult({
|
|
1485
1713
|
reportId: args.reportId,
|
|
1486
1714
|
rootCause: s2?.rootCause ?? null,
|
|
@@ -1497,7 +1725,11 @@ function createMushiServer(config) {
|
|
|
1497
1725
|
title: titleOf("run_nl_query"),
|
|
1498
1726
|
description: descOf("run_nl_query"),
|
|
1499
1727
|
annotations: annotationsFor("run_nl_query"),
|
|
1500
|
-
inputSchema: {
|
|
1728
|
+
inputSchema: {
|
|
1729
|
+
question: z.string().describe(
|
|
1730
|
+
'Question in plain English, e.g. "Which components had the most critical bugs this week?"'
|
|
1731
|
+
)
|
|
1732
|
+
}
|
|
1501
1733
|
},
|
|
1502
1734
|
async (args) => {
|
|
1503
1735
|
const data = await apiCall("/v1/admin/query", {
|
|
@@ -1514,7 +1746,9 @@ function createMushiServer(config) {
|
|
|
1514
1746
|
description: descOf("diagnose_setup"),
|
|
1515
1747
|
annotations: annotationsFor("diagnose_setup"),
|
|
1516
1748
|
inputSchema: {
|
|
1517
|
-
mode: z.enum(["full", "ingest", "dispatch"]).optional().describe(
|
|
1749
|
+
mode: z.enum(["full", "ingest", "dispatch"]).optional().describe(
|
|
1750
|
+
"full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."
|
|
1751
|
+
),
|
|
1518
1752
|
project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
|
|
1519
1753
|
projectId: z.string().optional().describe("Alias for project_id.")
|
|
1520
1754
|
},
|
|
@@ -1552,7 +1786,11 @@ function createMushiServer(config) {
|
|
|
1552
1786
|
}
|
|
1553
1787
|
if (mode === "dispatch") {
|
|
1554
1788
|
if (!resolvedId) {
|
|
1555
|
-
throw new MushiApiError(
|
|
1789
|
+
throw new MushiApiError(
|
|
1790
|
+
400,
|
|
1791
|
+
"MISSING_PROJECT",
|
|
1792
|
+
"project_id is required for dispatch mode"
|
|
1793
|
+
);
|
|
1556
1794
|
}
|
|
1557
1795
|
const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
|
|
1558
1796
|
const failed = data.checks.filter((c) => !c.ready);
|
|
@@ -1686,7 +1924,12 @@ function createMushiServer(config) {
|
|
|
1686
1924
|
async (args) => {
|
|
1687
1925
|
const query = args.query ?? "";
|
|
1688
1926
|
const hits = searchMushiDocs(query, args.limit ?? 8);
|
|
1689
|
-
const results = hits.map(({ title, path, excerpt, score }) => ({
|
|
1927
|
+
const results = hits.map(({ title, path, excerpt, score }) => ({
|
|
1928
|
+
title,
|
|
1929
|
+
path,
|
|
1930
|
+
excerpt,
|
|
1931
|
+
score
|
|
1932
|
+
}));
|
|
1690
1933
|
return jsonResult({ query, results });
|
|
1691
1934
|
}
|
|
1692
1935
|
);
|
|
@@ -1836,7 +2079,9 @@ function createMushiServer(config) {
|
|
|
1836
2079
|
} : { error: String(reportRes.reason) };
|
|
1837
2080
|
return jsonText({
|
|
1838
2081
|
evidence,
|
|
1839
|
-
reporter_thread: timeline ?? {
|
|
2082
|
+
reporter_thread: timeline ?? {
|
|
2083
|
+
error: String(timelineRes.reason)
|
|
2084
|
+
}
|
|
1840
2085
|
});
|
|
1841
2086
|
}
|
|
1842
2087
|
);
|
|
@@ -1871,27 +2116,54 @@ function createMushiServer(config) {
|
|
|
1871
2116
|
const includeLogs = args.include_logs !== false;
|
|
1872
2117
|
const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
|
|
1873
2118
|
apiCall(`/v1/admin/reports/${args.report_id}`, { headers }),
|
|
1874
|
-
apiCall(`/v1/admin/reports/${args.report_id}/timeline`, {
|
|
2119
|
+
apiCall(`/v1/admin/reports/${args.report_id}/timeline`, {
|
|
2120
|
+
headers
|
|
2121
|
+
}),
|
|
1875
2122
|
apiCall(`/v1/admin/reports/similarity`, {
|
|
1876
2123
|
method: "POST",
|
|
1877
2124
|
headers,
|
|
1878
2125
|
body: JSON.stringify({ report_id: args.report_id })
|
|
1879
2126
|
}).catch(() => null),
|
|
1880
|
-
apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(
|
|
1881
|
-
|
|
1882
|
-
|
|
2127
|
+
apiCall(`/v1/admin/reports/${args.report_id}/fix-context`, { headers }).catch(
|
|
2128
|
+
() => null
|
|
2129
|
+
),
|
|
2130
|
+
apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`, { headers }).catch(
|
|
2131
|
+
() => null
|
|
2132
|
+
),
|
|
2133
|
+
includeLogs ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`, { headers }).catch(
|
|
2134
|
+
() => null
|
|
2135
|
+
) : Promise.resolve(null)
|
|
1883
2136
|
]);
|
|
1884
2137
|
const report = reportRes.status === "fulfilled" ? reportRes.value : null;
|
|
1885
2138
|
const severity = report ? report.severity : "unknown";
|
|
1886
2139
|
const category = report ? report.category : "unknown";
|
|
1887
2140
|
const status = report ? report.status : "unknown";
|
|
1888
2141
|
const actions = [];
|
|
1889
|
-
|
|
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") {
|
|
1890
2162
|
actions.push({
|
|
1891
2163
|
action: "dispatch_fix",
|
|
1892
|
-
reason: "Report is classified
|
|
2164
|
+
reason: "Report is classified and no fix has been attempted",
|
|
1893
2165
|
tool: "dispatch_fix",
|
|
1894
|
-
args: { reportId: args.report_id
|
|
2166
|
+
args: { reportId: args.report_id }
|
|
1895
2167
|
});
|
|
1896
2168
|
} else if (status === "fixing") {
|
|
1897
2169
|
actions.push({
|
|
@@ -1946,7 +2218,10 @@ function createMushiServer(config) {
|
|
|
1946
2218
|
return {
|
|
1947
2219
|
...res,
|
|
1948
2220
|
resource_links: [
|
|
1949
|
-
{
|
|
2221
|
+
{
|
|
2222
|
+
uri: `project://reports/${args.report_id}`,
|
|
2223
|
+
title: `Report ${args.report_id.slice(0, 8)}\u2026`
|
|
2224
|
+
},
|
|
1950
2225
|
...pid ? [{ uri: `project://dashboard`, title: "Project dashboard" }] : []
|
|
1951
2226
|
]
|
|
1952
2227
|
};
|
|
@@ -1965,7 +2240,9 @@ function createMushiServer(config) {
|
|
|
1965
2240
|
filesChanged: z.array(z.string()).describe("Repo-relative paths modified by the fix"),
|
|
1966
2241
|
linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
|
|
1967
2242
|
summary: z.string().describe("One-line human summary of what the fix changed"),
|
|
1968
|
-
idempotencyKey: z.string().uuid().optional().describe(
|
|
2243
|
+
idempotencyKey: z.string().uuid().optional().describe(
|
|
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."
|
|
2245
|
+
)
|
|
1969
2246
|
},
|
|
1970
2247
|
outputSchema: {
|
|
1971
2248
|
ok: z.boolean().describe("True when the fix_attempt row was created and marked completed"),
|
|
@@ -1973,27 +2250,123 @@ function createMushiServer(config) {
|
|
|
1973
2250
|
}
|
|
1974
2251
|
},
|
|
1975
2252
|
async (args) => {
|
|
1976
|
-
const idemKey = args.idempotencyKey ??
|
|
2253
|
+
const idemKey = args.idempotencyKey ?? await stableUuidFrom(
|
|
2254
|
+
IDEMPOTENCY_NAMESPACE,
|
|
2255
|
+
args.reportId,
|
|
2256
|
+
args.branch,
|
|
2257
|
+
args.prUrl ?? ""
|
|
2258
|
+
);
|
|
1977
2259
|
const created = await apiCall("/v1/admin/fixes", {
|
|
1978
2260
|
method: "POST",
|
|
1979
2261
|
headers: { "Idempotency-Key": idemKey },
|
|
1980
2262
|
body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
|
|
1981
2263
|
});
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
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
|
+
}
|
|
1994
2285
|
return jsonResult({ ok: true, fixId: created.fixId });
|
|
1995
2286
|
}
|
|
1996
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
|
+
);
|
|
1997
2370
|
server.registerTool(
|
|
1998
2371
|
"dispatch_fix",
|
|
1999
2372
|
{
|
|
@@ -2003,8 +2376,12 @@ function createMushiServer(config) {
|
|
|
2003
2376
|
inputSchema: {
|
|
2004
2377
|
reportId: z.string().uuid().describe("Report UUID to fix"),
|
|
2005
2378
|
agent: z.enum(["claude_code", "codex", "rest_worker", "mcp"]).optional().describe("Override the agent adapter"),
|
|
2006
|
-
idempotencyKey: z.string().uuid().optional().describe(
|
|
2007
|
-
|
|
2379
|
+
idempotencyKey: z.string().uuid().optional().describe(
|
|
2380
|
+
"Optional RFC 4122 UUID. Resend the same key to safely retry without dispatching a duplicate fix job (Idempotency-Key IETF draft)."
|
|
2381
|
+
),
|
|
2382
|
+
inventoryActionNodeId: z.string().uuid().optional().describe(
|
|
2383
|
+
"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."
|
|
2384
|
+
)
|
|
2008
2385
|
},
|
|
2009
2386
|
outputSchema: {
|
|
2010
2387
|
fixId: z.string(),
|
|
@@ -2026,7 +2403,7 @@ function createMushiServer(config) {
|
|
|
2026
2403
|
} catch {
|
|
2027
2404
|
}
|
|
2028
2405
|
}
|
|
2029
|
-
const
|
|
2406
|
+
const dispatch = await apiCall("/v1/admin/fixes/dispatch", {
|
|
2030
2407
|
method: "POST",
|
|
2031
2408
|
headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
|
|
2032
2409
|
body: JSON.stringify({
|
|
@@ -2036,7 +2413,12 @@ function createMushiServer(config) {
|
|
|
2036
2413
|
...projectId ? { projectId } : {}
|
|
2037
2414
|
})
|
|
2038
2415
|
});
|
|
2039
|
-
|
|
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" });
|
|
2040
2422
|
}
|
|
2041
2423
|
);
|
|
2042
2424
|
server.registerTool(
|
|
@@ -2077,11 +2459,19 @@ function createMushiServer(config) {
|
|
|
2077
2459
|
},
|
|
2078
2460
|
async (args) => {
|
|
2079
2461
|
const pid = args.projectId ?? projectId;
|
|
2080
|
-
if (!pid)
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2462
|
+
if (!pid)
|
|
2463
|
+
throw new MushiApiError(
|
|
2464
|
+
400,
|
|
2465
|
+
"MISSING_PROJECT",
|
|
2466
|
+
"projectId is required for test_gen_from_report"
|
|
2467
|
+
);
|
|
2468
|
+
const data = await apiCall(
|
|
2469
|
+
`/v1/admin/inventory/${pid}/test-gen/from-report/${args.reportId}`,
|
|
2470
|
+
{
|
|
2471
|
+
method: "POST",
|
|
2472
|
+
body: JSON.stringify({})
|
|
2473
|
+
}
|
|
2474
|
+
);
|
|
2085
2475
|
return jsonText(data);
|
|
2086
2476
|
}
|
|
2087
2477
|
);
|
|
@@ -2160,7 +2550,22 @@ function createMushiServer(config) {
|
|
|
2160
2550
|
annotations: annotationsFor("transition_status"),
|
|
2161
2551
|
inputSchema: {
|
|
2162
2552
|
reportId: z.string().describe("Report UUID"),
|
|
2163
|
-
status: z.enum([
|
|
2553
|
+
status: z.enum([
|
|
2554
|
+
"pending",
|
|
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",
|
|
2561
|
+
"grouped",
|
|
2562
|
+
"fixing",
|
|
2563
|
+
"fixed",
|
|
2564
|
+
"resolved",
|
|
2565
|
+
"verified",
|
|
2566
|
+
"reopened",
|
|
2567
|
+
"dismissed"
|
|
2568
|
+
]).describe("Target status (resolved is stored as fixed)"),
|
|
2164
2569
|
reason: z.string().optional().describe("Reason for the transition (audit trail)")
|
|
2165
2570
|
}
|
|
2166
2571
|
},
|
|
@@ -2184,7 +2589,12 @@ function createMushiServer(config) {
|
|
|
2184
2589
|
},
|
|
2185
2590
|
async (args) => {
|
|
2186
2591
|
const pid = args.projectId ?? projectId;
|
|
2187
|
-
if (!pid)
|
|
2592
|
+
if (!pid)
|
|
2593
|
+
throw new MushiApiError(
|
|
2594
|
+
400,
|
|
2595
|
+
"MISSING_PROJECT",
|
|
2596
|
+
"projectId is required for setup_repo_for_mushi"
|
|
2597
|
+
);
|
|
2188
2598
|
const data = await apiCall(`/v1/admin/projects/${pid}/repo/bootstrap`, {
|
|
2189
2599
|
method: "POST",
|
|
2190
2600
|
body: JSON.stringify({})
|
|
@@ -2249,23 +2659,45 @@ function createMushiServer(config) {
|
|
|
2249
2659
|
"project://stats",
|
|
2250
2660
|
{ description: "Report counts, category breakdown, severity distribution" },
|
|
2251
2661
|
async () => ({
|
|
2252
|
-
contents: [
|
|
2662
|
+
contents: [
|
|
2663
|
+
{
|
|
2664
|
+
uri: "project://stats",
|
|
2665
|
+
mimeType: "application/json",
|
|
2666
|
+
text: JSON.stringify(await apiCall("/v1/admin/stats"), null, 2)
|
|
2667
|
+
}
|
|
2668
|
+
]
|
|
2253
2669
|
})
|
|
2254
2670
|
);
|
|
2255
2671
|
server.resource(
|
|
2256
2672
|
"project_settings",
|
|
2257
2673
|
"project://settings",
|
|
2258
|
-
{
|
|
2674
|
+
{
|
|
2675
|
+
description: "Project configuration \u2014 autofix agent, plugins enabled, ontology, LLM budgets"
|
|
2676
|
+
},
|
|
2259
2677
|
async () => ({
|
|
2260
|
-
contents: [
|
|
2678
|
+
contents: [
|
|
2679
|
+
{
|
|
2680
|
+
uri: "project://settings",
|
|
2681
|
+
mimeType: "application/json",
|
|
2682
|
+
text: JSON.stringify(await apiCall("/v1/admin/settings"), null, 2)
|
|
2683
|
+
}
|
|
2684
|
+
]
|
|
2261
2685
|
})
|
|
2262
2686
|
);
|
|
2263
2687
|
server.resource(
|
|
2264
2688
|
"project_dashboard",
|
|
2265
2689
|
"project://dashboard",
|
|
2266
|
-
{
|
|
2690
|
+
{
|
|
2691
|
+
description: "PDCA health snapshot \u2014 stage counts, bottleneck, recent activity (same payload the admin console polls)"
|
|
2692
|
+
},
|
|
2267
2693
|
async () => ({
|
|
2268
|
-
contents: [
|
|
2694
|
+
contents: [
|
|
2695
|
+
{
|
|
2696
|
+
uri: "project://dashboard",
|
|
2697
|
+
mimeType: "application/json",
|
|
2698
|
+
text: JSON.stringify(await apiCall("/v1/admin/dashboard"), null, 2)
|
|
2699
|
+
}
|
|
2700
|
+
]
|
|
2269
2701
|
})
|
|
2270
2702
|
);
|
|
2271
2703
|
server.registerTool(
|
|
@@ -2280,14 +2712,16 @@ function createMushiServer(config) {
|
|
|
2280
2712
|
}
|
|
2281
2713
|
},
|
|
2282
2714
|
async ({ limit, range }) => ({
|
|
2283
|
-
content: [
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2715
|
+
content: [
|
|
2716
|
+
{
|
|
2717
|
+
type: "text",
|
|
2718
|
+
text: JSON.stringify(
|
|
2719
|
+
await apiCall(`/v1/admin/rewards/leaderboard?range=${range}&limit=${limit}`),
|
|
2720
|
+
null,
|
|
2721
|
+
2
|
|
2722
|
+
)
|
|
2723
|
+
}
|
|
2724
|
+
]
|
|
2291
2725
|
})
|
|
2292
2726
|
);
|
|
2293
2727
|
server.registerTool(
|
|
@@ -2303,18 +2737,20 @@ function createMushiServer(config) {
|
|
|
2303
2737
|
}
|
|
2304
2738
|
},
|
|
2305
2739
|
async ({ external_user_id, points, reason }) => ({
|
|
2306
|
-
content: [
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2740
|
+
content: [
|
|
2741
|
+
{
|
|
2742
|
+
type: "text",
|
|
2743
|
+
text: JSON.stringify(
|
|
2744
|
+
await apiCall("/v1/admin/rewards/bonus-points", {
|
|
2745
|
+
method: "POST",
|
|
2746
|
+
headers: { "Content-Type": "application/json" },
|
|
2747
|
+
body: JSON.stringify({ external_user_id, points, reason })
|
|
2748
|
+
}),
|
|
2749
|
+
null,
|
|
2750
|
+
2
|
|
2751
|
+
)
|
|
2752
|
+
}
|
|
2753
|
+
]
|
|
2318
2754
|
})
|
|
2319
2755
|
);
|
|
2320
2756
|
server.registerTool(
|
|
@@ -2330,18 +2766,20 @@ function createMushiServer(config) {
|
|
|
2330
2766
|
}
|
|
2331
2767
|
},
|
|
2332
2768
|
async ({ external_user_id, tier_slug, reason }) => ({
|
|
2333
|
-
content: [
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2769
|
+
content: [
|
|
2770
|
+
{
|
|
2771
|
+
type: "text",
|
|
2772
|
+
text: JSON.stringify(
|
|
2773
|
+
await apiCall("/v1/admin/rewards/set-tier", {
|
|
2774
|
+
method: "POST",
|
|
2775
|
+
headers: { "Content-Type": "application/json" },
|
|
2776
|
+
body: JSON.stringify({ external_user_id, tier_slug, reason })
|
|
2777
|
+
}),
|
|
2778
|
+
null,
|
|
2779
|
+
2
|
|
2780
|
+
)
|
|
2781
|
+
}
|
|
2782
|
+
]
|
|
2345
2783
|
})
|
|
2346
2784
|
);
|
|
2347
2785
|
server.resource(
|
|
@@ -2351,11 +2789,13 @@ function createMushiServer(config) {
|
|
|
2351
2789
|
description: "Returns the privacy posture for this project: storage region, LLM provider, whether BYOK is configured, data retention window, and last audit timestamp."
|
|
2352
2790
|
},
|
|
2353
2791
|
async () => ({
|
|
2354
|
-
contents: [
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2792
|
+
contents: [
|
|
2793
|
+
{
|
|
2794
|
+
uri: "privacy://status",
|
|
2795
|
+
mimeType: "application/json",
|
|
2796
|
+
text: JSON.stringify(await apiCall("/v1/admin/privacy-status"), null, 2)
|
|
2797
|
+
}
|
|
2798
|
+
]
|
|
2359
2799
|
})
|
|
2360
2800
|
);
|
|
2361
2801
|
server.resource(
|
|
@@ -2365,15 +2805,19 @@ function createMushiServer(config) {
|
|
|
2365
2805
|
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."
|
|
2366
2806
|
},
|
|
2367
2807
|
async () => ({
|
|
2368
|
-
contents: [
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2808
|
+
contents: [
|
|
2809
|
+
{
|
|
2810
|
+
uri: "evolution://history",
|
|
2811
|
+
mimeType: "application/json",
|
|
2812
|
+
text: projectId ? JSON.stringify(
|
|
2813
|
+
await apiCall(
|
|
2814
|
+
`/v1/admin/projects/${encodeURIComponent(projectId)}/evolution-history`
|
|
2815
|
+
),
|
|
2816
|
+
null,
|
|
2817
|
+
2
|
|
2818
|
+
) : JSON.stringify({ ok: false, error: "project_id required" })
|
|
2819
|
+
}
|
|
2820
|
+
]
|
|
2377
2821
|
})
|
|
2378
2822
|
);
|
|
2379
2823
|
server.resource(
|
|
@@ -2383,11 +2827,13 @@ function createMushiServer(config) {
|
|
|
2383
2827
|
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."
|
|
2384
2828
|
},
|
|
2385
2829
|
async () => ({
|
|
2386
|
-
contents: [
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2830
|
+
contents: [
|
|
2831
|
+
{
|
|
2832
|
+
uri: "project://integration-health",
|
|
2833
|
+
mimeType: "application/json",
|
|
2834
|
+
text: JSON.stringify(await apiCall("/v1/admin/integrations/health"), null, 2)
|
|
2835
|
+
}
|
|
2836
|
+
]
|
|
2391
2837
|
})
|
|
2392
2838
|
);
|
|
2393
2839
|
server.resource(
|
|
@@ -2399,11 +2845,13 @@ function createMushiServer(config) {
|
|
|
2399
2845
|
async () => {
|
|
2400
2846
|
const path = projectId ? `/v1/admin/inventory/${projectId}` : "/v1/admin/inventory";
|
|
2401
2847
|
return {
|
|
2402
|
-
contents: [
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2848
|
+
contents: [
|
|
2849
|
+
{
|
|
2850
|
+
uri: "inventory://current",
|
|
2851
|
+
mimeType: "application/json",
|
|
2852
|
+
text: JSON.stringify(await apiCall(path), null, 2)
|
|
2853
|
+
}
|
|
2854
|
+
]
|
|
2407
2855
|
};
|
|
2408
2856
|
}
|
|
2409
2857
|
);
|
|
@@ -2432,11 +2880,13 @@ function createMushiServer(config) {
|
|
|
2432
2880
|
async () => {
|
|
2433
2881
|
const qs = projectId ? `?project_id=${encodeURIComponent(projectId)}` : "";
|
|
2434
2882
|
return {
|
|
2435
|
-
contents: [
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2883
|
+
contents: [
|
|
2884
|
+
{
|
|
2885
|
+
uri: "mushi://activation",
|
|
2886
|
+
mimeType: "application/json",
|
|
2887
|
+
text: JSON.stringify(await apiCall(`/v1/admin/activation${qs}`), null, 2)
|
|
2888
|
+
}
|
|
2889
|
+
]
|
|
2440
2890
|
};
|
|
2441
2891
|
}
|
|
2442
2892
|
);
|
|
@@ -2445,11 +2895,12 @@ function createMushiServer(config) {
|
|
|
2445
2895
|
"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.",
|
|
2446
2896
|
{ reportId: z.string().describe("The report UUID to summarize") },
|
|
2447
2897
|
({ reportId }) => ({
|
|
2448
|
-
messages: [
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2898
|
+
messages: [
|
|
2899
|
+
{
|
|
2900
|
+
role: "user",
|
|
2901
|
+
content: {
|
|
2902
|
+
type: "text",
|
|
2903
|
+
text: `You are a senior engineer preparing a fix plan. Use the Mushi MCP tools to:
|
|
2453
2904
|
1. Call get_fix_context for reportId "${reportId}".
|
|
2454
2905
|
2. Call get_blast_radius if the report has a component node id.
|
|
2455
2906
|
3. Call get_similar_bugs with the component or summary as the query.
|
|
@@ -2462,8 +2913,9 @@ Then produce a markdown fix plan with exactly these sections:
|
|
|
2462
2913
|
- Confidence (low/medium/high) with a one-line justification
|
|
2463
2914
|
|
|
2464
2915
|
Lead with the fix. Skip the preamble.`
|
|
2916
|
+
}
|
|
2465
2917
|
}
|
|
2466
|
-
|
|
2918
|
+
]
|
|
2467
2919
|
})
|
|
2468
2920
|
);
|
|
2469
2921
|
server.prompt(
|
|
@@ -2471,11 +2923,12 @@ Lead with the fix. Skip the preamble.`
|
|
|
2471
2923
|
"Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
|
|
2472
2924
|
{ fixId: z.string().describe("The fix_attempt UUID to explain") },
|
|
2473
2925
|
({ fixId }) => ({
|
|
2474
|
-
messages: [
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2926
|
+
messages: [
|
|
2927
|
+
{
|
|
2928
|
+
role: "user",
|
|
2929
|
+
content: {
|
|
2930
|
+
type: "text",
|
|
2931
|
+
text: `You are a release engineer. Use the Mushi MCP tools:
|
|
2479
2932
|
1. Call get_fix_timeline for fixId "${fixId}" to see the full PDCA journey.
|
|
2480
2933
|
|
|
2481
2934
|
Then write a short verdict in this format:
|
|
@@ -2484,8 +2937,9 @@ Then write a short verdict in this format:
|
|
|
2484
2937
|
- **If iterate:** bullet list of the smallest next patch
|
|
2485
2938
|
|
|
2486
2939
|
No preamble, no score-by-score recap \u2014 the numbers are in the tool output.`
|
|
2940
|
+
}
|
|
2487
2941
|
}
|
|
2488
|
-
|
|
2942
|
+
]
|
|
2489
2943
|
})
|
|
2490
2944
|
);
|
|
2491
2945
|
server.prompt(
|
|
@@ -2493,11 +2947,12 @@ No preamble, no score-by-score recap \u2014 the numbers are in the tool output.`
|
|
|
2493
2947
|
'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
|
|
2494
2948
|
{},
|
|
2495
2949
|
() => ({
|
|
2496
|
-
messages: [
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2950
|
+
messages: [
|
|
2951
|
+
{
|
|
2952
|
+
role: "user",
|
|
2953
|
+
content: {
|
|
2954
|
+
type: "text",
|
|
2955
|
+
text: `You are a tech lead looking at the Mushi cockpit. Use the MCP tools:
|
|
2501
2956
|
1. Read the project://dashboard resource for the PDCA snapshot.
|
|
2502
2957
|
2. Call get_recent_reports with status="classified", limit=10.
|
|
2503
2958
|
|
|
@@ -2505,8 +2960,9 @@ Then output exactly 5 bullets, in priority order, each formatted as:
|
|
|
2505
2960
|
\`**Action** \u2014 why it matters \u2014 suggested tool call\`
|
|
2506
2961
|
|
|
2507
2962
|
Prefer items that are bottlenecks or critical severity. Skip filler.`
|
|
2963
|
+
}
|
|
2508
2964
|
}
|
|
2509
|
-
|
|
2965
|
+
]
|
|
2510
2966
|
})
|
|
2511
2967
|
);
|
|
2512
2968
|
server.prompt(
|
|
@@ -2514,11 +2970,12 @@ Prefer items that are bottlenecks or critical severity. Skip filler.`
|
|
|
2514
2970
|
"Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
|
|
2515
2971
|
{},
|
|
2516
2972
|
() => ({
|
|
2517
|
-
messages: [
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2973
|
+
messages: [
|
|
2974
|
+
{
|
|
2975
|
+
role: "user",
|
|
2976
|
+
content: {
|
|
2977
|
+
type: "text",
|
|
2978
|
+
text: `You are a Mushi onboarding copilot. Use the MCP tools:
|
|
2522
2979
|
1. Read mushi://activation for unified setup posture.
|
|
2523
2980
|
2. Read project://integration-health if GitHub or Sentry looks blocked.
|
|
2524
2981
|
3. Call diagnose_setup if the resource is unavailable.
|
|
@@ -2530,8 +2987,9 @@ Then output:
|
|
|
2530
2987
|
- **If still stuck:** one diagnostic command (e.g. mushi doctor --fix)
|
|
2531
2988
|
|
|
2532
2989
|
Be specific. No generic advice.`
|
|
2990
|
+
}
|
|
2533
2991
|
}
|
|
2534
|
-
|
|
2992
|
+
]
|
|
2535
2993
|
})
|
|
2536
2994
|
);
|
|
2537
2995
|
server.registerTool(
|
|
@@ -2552,7 +3010,15 @@ Be specific. No generic advice.`
|
|
|
2552
3010
|
if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
|
|
2553
3011
|
const data = await apiCall(
|
|
2554
3012
|
`/v1/admin/inventory/${projectId2}/map-from-live`,
|
|
2555
|
-
{
|
|
3013
|
+
{
|
|
3014
|
+
method: "POST",
|
|
3015
|
+
body: JSON.stringify({
|
|
3016
|
+
base_url: baseUrl,
|
|
3017
|
+
max_pages: maxPages,
|
|
3018
|
+
provider,
|
|
3019
|
+
cursor_cloud_refine: cursorCloudRefine
|
|
3020
|
+
})
|
|
3021
|
+
}
|
|
2556
3022
|
);
|
|
2557
3023
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2558
3024
|
}
|
|
@@ -2580,7 +3046,9 @@ Be specific. No generic advice.`
|
|
|
2580
3046
|
annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
|
|
2581
3047
|
inputSchema: {
|
|
2582
3048
|
projectId: z.string().describe("Project UUID that owns the inventory story"),
|
|
2583
|
-
storyNodeId: z.string().describe(
|
|
3049
|
+
storyNodeId: z.string().describe(
|
|
3050
|
+
"User story node id/slug from accepted inventory (from map_user_stories or inventory.yaml)"
|
|
3051
|
+
),
|
|
2584
3052
|
automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Approval gate for the generated qa_story (default review \u2192 pending_review)"),
|
|
2585
3053
|
baseUrl: z.string().url().optional().describe("Override the live app URL the Playwright test should target"),
|
|
2586
3054
|
openPr: z.boolean().optional().describe("Open a draft GitHub PR with the generated spec (default true)")
|
|
@@ -2594,10 +3062,14 @@ Be specific. No generic advice.`
|
|
|
2594
3062
|
},
|
|
2595
3063
|
async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
|
|
2596
3064
|
if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
|
|
2597
|
-
const data = await apiCall(
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3065
|
+
const data = await apiCall(`/v1/admin/inventory/${projectId2}/stories/${storyNodeId}/generate-test`, {
|
|
3066
|
+
method: "POST",
|
|
3067
|
+
body: JSON.stringify({
|
|
3068
|
+
automation_mode: automationMode,
|
|
3069
|
+
base_url: baseUrl,
|
|
3070
|
+
open_pr: openPr
|
|
3071
|
+
})
|
|
3072
|
+
});
|
|
2601
3073
|
return jsonResult(data);
|
|
2602
3074
|
}
|
|
2603
3075
|
);
|
|
@@ -2612,10 +3084,10 @@ Be specific. No generic advice.`
|
|
|
2612
3084
|
}
|
|
2613
3085
|
},
|
|
2614
3086
|
async ({ projectId: projectId2 }) => {
|
|
2615
|
-
const data = await apiCall(
|
|
2616
|
-
"
|
|
2617
|
-
|
|
2618
|
-
);
|
|
3087
|
+
const data = await apiCall("/v1/admin/pdca/improve-qa-stories", {
|
|
3088
|
+
method: "POST",
|
|
3089
|
+
body: JSON.stringify({ project_id: projectId2 })
|
|
3090
|
+
});
|
|
2619
3091
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2620
3092
|
}
|
|
2621
3093
|
);
|
|
@@ -2649,7 +3121,9 @@ Be specific. No generic advice.`
|
|
|
2649
3121
|
}
|
|
2650
3122
|
},
|
|
2651
3123
|
async ({ projectId: projectId2 }) => {
|
|
2652
|
-
const data = await apiCall(
|
|
3124
|
+
const data = await apiCall(
|
|
3125
|
+
`/v1/admin/byok/keys?project_id=${encodeURIComponent(projectId2)}`
|
|
3126
|
+
);
|
|
2653
3127
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2654
3128
|
}
|
|
2655
3129
|
);
|
|
@@ -2662,16 +3136,60 @@ Be specific. No generic advice.`
|
|
|
2662
3136
|
inputSchema: {
|
|
2663
3137
|
projectId: z.string().describe("Project id"),
|
|
2664
3138
|
provider: z.enum(["anthropic", "openai", "firecrawl", "browserbase", "cursor"]).describe("Provider slug"),
|
|
2665
|
-
key: z.string().min(
|
|
3139
|
+
key: z.string().min(8).max(4096).describe("The API key value to add"),
|
|
2666
3140
|
label: z.string().optional().describe("Human-readable label for this key"),
|
|
2667
|
-
priority: z.number().int().min(
|
|
3141
|
+
priority: z.number().int().min(0).max(1e4).optional().describe("Priority for ordering (lower = higher priority)"),
|
|
3142
|
+
baseUrl: z.string().url().max(2048).optional().describe("Allow-listed OpenAI-compatible HTTPS base URL")
|
|
2668
3143
|
}
|
|
2669
3144
|
},
|
|
2670
|
-
async ({ projectId: projectId2, provider, key, label, priority }) => {
|
|
2671
|
-
const data = await apiCall(
|
|
2672
|
-
"
|
|
2673
|
-
|
|
2674
|
-
|
|
3145
|
+
async ({ projectId: projectId2, provider, key, label, priority, baseUrl }) => {
|
|
3146
|
+
const data = await apiCall("/v1/admin/byok/keys", {
|
|
3147
|
+
method: "POST",
|
|
3148
|
+
body: JSON.stringify({
|
|
3149
|
+
project_id: projectId2,
|
|
3150
|
+
provider_slug: provider,
|
|
3151
|
+
key,
|
|
3152
|
+
label,
|
|
3153
|
+
priority,
|
|
3154
|
+
base_url: baseUrl
|
|
3155
|
+
})
|
|
3156
|
+
});
|
|
3157
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
3158
|
+
}
|
|
3159
|
+
);
|
|
3160
|
+
server.registerTool(
|
|
3161
|
+
"test_byok_key",
|
|
3162
|
+
{
|
|
3163
|
+
title: titleOf("test_byok_key", TDD_TOOL_CATALOG),
|
|
3164
|
+
description: descOf("test_byok_key", TDD_TOOL_CATALOG),
|
|
3165
|
+
annotations: annotationsFor("test_byok_key", TDD_TOOL_CATALOG),
|
|
3166
|
+
inputSchema: {
|
|
3167
|
+
projectId: z.string().describe("Project id"),
|
|
3168
|
+
keyId: z.string().uuid().describe("Pooled BYOK key id")
|
|
3169
|
+
}
|
|
3170
|
+
},
|
|
3171
|
+
async ({ keyId }) => {
|
|
3172
|
+
const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}/test`, {
|
|
3173
|
+
method: "POST"
|
|
3174
|
+
});
|
|
3175
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
3176
|
+
}
|
|
3177
|
+
);
|
|
3178
|
+
server.registerTool(
|
|
3179
|
+
"remove_byok_key",
|
|
3180
|
+
{
|
|
3181
|
+
title: titleOf("remove_byok_key", TDD_TOOL_CATALOG),
|
|
3182
|
+
description: descOf("remove_byok_key", TDD_TOOL_CATALOG),
|
|
3183
|
+
annotations: annotationsFor("remove_byok_key", TDD_TOOL_CATALOG),
|
|
3184
|
+
inputSchema: {
|
|
3185
|
+
projectId: z.string().describe("Project id"),
|
|
3186
|
+
keyId: z.string().uuid().describe("Pooled BYOK key UUID from list_byok_keys")
|
|
3187
|
+
}
|
|
3188
|
+
},
|
|
3189
|
+
async ({ keyId }) => {
|
|
3190
|
+
const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}`, {
|
|
3191
|
+
method: "DELETE"
|
|
3192
|
+
});
|
|
2675
3193
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2676
3194
|
}
|
|
2677
3195
|
);
|
|
@@ -2686,7 +3204,9 @@ Be specific. No generic advice.`
|
|
|
2686
3204
|
}
|
|
2687
3205
|
},
|
|
2688
3206
|
async ({ projectId: projectId2 }) => {
|
|
2689
|
-
const data = await apiCall(
|
|
3207
|
+
const data = await apiCall(
|
|
3208
|
+
`/v1/admin/inventory/${projectId2}/stories/pending-review`
|
|
3209
|
+
);
|
|
2690
3210
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2691
3211
|
}
|
|
2692
3212
|
);
|
|
@@ -2723,13 +3243,10 @@ Be specific. No generic advice.`
|
|
|
2723
3243
|
}
|
|
2724
3244
|
},
|
|
2725
3245
|
async ({ reportId, message, authorName }) => {
|
|
2726
|
-
const data = await apiCall(
|
|
2727
|
-
|
|
2728
|
-
{
|
|
2729
|
-
|
|
2730
|
-
body: JSON.stringify({ message, author_name: authorName })
|
|
2731
|
-
}
|
|
2732
|
-
);
|
|
3246
|
+
const data = await apiCall(`/v1/sync/reports/${reportId}/reply`, {
|
|
3247
|
+
method: "POST",
|
|
3248
|
+
body: JSON.stringify({ message, author_name: authorName })
|
|
3249
|
+
});
|
|
2733
3250
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2734
3251
|
}
|
|
2735
3252
|
);
|
|
@@ -2808,10 +3325,10 @@ Be specific. No generic advice.`
|
|
|
2808
3325
|
async ({ project_id }) => {
|
|
2809
3326
|
const pid = project_id ?? config.projectId;
|
|
2810
3327
|
if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
|
|
2811
|
-
const data = await apiCall(
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
);
|
|
3328
|
+
const data = await apiCall(`/v1/admin/projects/${pid}/audit`, {
|
|
3329
|
+
method: "POST",
|
|
3330
|
+
body: "{}"
|
|
3331
|
+
});
|
|
2815
3332
|
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2816
3333
|
}
|
|
2817
3334
|
);
|
|
@@ -2907,17 +3424,19 @@ Be specific. No generic advice.`
|
|
|
2907
3424
|
inputSchema: {
|
|
2908
3425
|
root_skill_slug: z.string().describe('Root skill slug to run, e.g. "workflow-fix-and-ship"'),
|
|
2909
3426
|
report_id: z.string().optional().describe("Report UUID to attach the pipeline to"),
|
|
2910
|
-
mode: z.enum(["handoff", "cloud"]).optional().describe(
|
|
3427
|
+
mode: z.enum(["handoff", "cloud"]).optional().describe(
|
|
3428
|
+
"handoff (default): get context packet for local agent. cloud: auto-dispatch via Cursor Cloud."
|
|
3429
|
+
),
|
|
2911
3430
|
project_id: z.string().optional().describe("Project UUID. Falls back to the configured project.")
|
|
2912
3431
|
}
|
|
2913
3432
|
},
|
|
2914
3433
|
async (args) => {
|
|
2915
3434
|
const resolvedProjectId = args.project_id ?? projectId;
|
|
2916
3435
|
if (!resolvedProjectId) return jsonText({ error: "No project_id provided or configured." });
|
|
2917
|
-
const data = await apiCall(
|
|
2918
|
-
"
|
|
2919
|
-
|
|
2920
|
-
);
|
|
3436
|
+
const data = await apiCall("/v1/admin/skills/pipelines", {
|
|
3437
|
+
method: "POST",
|
|
3438
|
+
body: JSON.stringify({ ...args, project_id: resolvedProjectId })
|
|
3439
|
+
});
|
|
2921
3440
|
return jsonText(data);
|
|
2922
3441
|
}
|
|
2923
3442
|
);
|
|
@@ -2953,10 +3472,10 @@ Be specific. No generic advice.`
|
|
|
2953
3472
|
},
|
|
2954
3473
|
async (args) => {
|
|
2955
3474
|
const { run_id, step_index, ...body } = args;
|
|
2956
|
-
await apiCall(
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
);
|
|
3475
|
+
await apiCall(`/v1/admin/skills/pipelines/${run_id}/steps/${step_index}/checkin`, {
|
|
3476
|
+
method: "POST",
|
|
3477
|
+
body: JSON.stringify(body)
|
|
3478
|
+
});
|
|
2960
3479
|
return jsonText({ ok: true, message: `Step ${step_index} \u2192 ${args.status}` });
|
|
2961
3480
|
}
|
|
2962
3481
|
);
|
|
@@ -3124,7 +3643,11 @@ Be specific. No generic advice.`
|
|
|
3124
3643
|
if (args.compare) qs.set("compare", args.compare);
|
|
3125
3644
|
if (args.fix_id) qs.set("fix_id", args.fix_id);
|
|
3126
3645
|
if (!qs.toString()) {
|
|
3127
|
-
throw new MushiApiError(
|
|
3646
|
+
throw new MushiApiError(
|
|
3647
|
+
400,
|
|
3648
|
+
"BAD_REQUEST",
|
|
3649
|
+
"Provide paths, source=last_push, compare, or fix_id"
|
|
3650
|
+
);
|
|
3128
3651
|
}
|
|
3129
3652
|
const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
|
|
3130
3653
|
return jsonResult(data);
|
|
@@ -3176,7 +3699,7 @@ Be specific. No generic advice.`
|
|
|
3176
3699
|
"### First step",
|
|
3177
3700
|
`Call \`${cluster.tools[0]}\` to get started.`,
|
|
3178
3701
|
"",
|
|
3179
|
-
"> Tip: call any tool by name \u2014 `use_mushi` is a read-only helper that never calls other tools itself. All
|
|
3702
|
+
"> Tip: call any tool by name \u2014 `use_mushi` is a read-only helper that never calls other tools itself. All 71 tools remain available."
|
|
3180
3703
|
].join("\n");
|
|
3181
3704
|
return { content: [{ type: "text", text: orientation }] };
|
|
3182
3705
|
}
|
|
@@ -3185,11 +3708,7 @@ Be specific. No generic advice.`
|
|
|
3185
3708
|
const featureFilter = config.features ?? "all";
|
|
3186
3709
|
if (grantedScopes !== void 0 || featureFilter !== "all") {
|
|
3187
3710
|
const toolRegistry = server._registeredTools;
|
|
3188
|
-
const allSpecs = [
|
|
3189
|
-
...TOOL_CATALOG,
|
|
3190
|
-
...TDD_TOOL_CATALOG,
|
|
3191
|
-
...CODEBASE_TOOL_CATALOG
|
|
3192
|
-
];
|
|
3711
|
+
const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG, ...CODEBASE_TOOL_CATALOG];
|
|
3193
3712
|
for (const spec of allSpecs) {
|
|
3194
3713
|
if (grantedScopes !== void 0 && !grantedScopes.includes(spec.scope)) {
|
|
3195
3714
|
toolRegistry[spec.name]?.remove();
|
|
@@ -3263,28 +3782,30 @@ Be specific. No generic advice.`
|
|
|
3263
3782
|
|
|
3264
3783
|
// src/index.ts
|
|
3265
3784
|
import * as Sentry from "@sentry/node";
|
|
3266
|
-
|
|
3267
|
-
args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"
|
|
3268
|
-
);
|
|
3269
|
-
console.log = _writeStderr;
|
|
3270
|
-
console.warn = _writeStderr;
|
|
3785
|
+
installStdoutGuard();
|
|
3271
3786
|
var require2 = createRequire(import.meta.url);
|
|
3272
3787
|
var VERSION = require2("../package.json").version;
|
|
3273
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
|
+
}
|
|
3274
3795
|
function readCliConfig() {
|
|
3796
|
+
const configPath = resolveCliConfigPath();
|
|
3275
3797
|
try {
|
|
3276
|
-
const
|
|
3277
|
-
const appData = process.env.APPDATA;
|
|
3278
|
-
const base = xdg && xdg.length > 0 ? xdg : process.platform === "win32" && appData && appData.length > 0 ? appData : join(homedir(), ".config");
|
|
3279
|
-
const raw = readFileSync(join(base, "mushi", "config.json"), "utf8");
|
|
3798
|
+
const raw = readFileSync(configPath, "utf8");
|
|
3280
3799
|
const parsed = JSON.parse(raw);
|
|
3281
3800
|
return {
|
|
3282
3801
|
apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
|
|
3283
3802
|
projectId: typeof parsed.projectId === "string" ? parsed.projectId : void 0,
|
|
3284
|
-
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
|
|
3285
3806
|
};
|
|
3286
3807
|
} catch {
|
|
3287
|
-
return {};
|
|
3808
|
+
return { path: configPath, found: false };
|
|
3288
3809
|
}
|
|
3289
3810
|
}
|
|
3290
3811
|
var CLI_CONFIG = readCliConfig();
|
|
@@ -3303,11 +3824,49 @@ if (MCP_SENTRY_DSN) {
|
|
|
3303
3824
|
tracesSampleRate: 0
|
|
3304
3825
|
});
|
|
3305
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
|
+
}
|
|
3306
3862
|
async function main() {
|
|
3307
3863
|
if (!API_KEY) {
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
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
|
+
});
|
|
3311
3870
|
process.exit(1);
|
|
3312
3871
|
}
|
|
3313
3872
|
if (!process.env.MUSHI_API_KEY && CLI_CONFIG.apiKey) {
|
|
@@ -3354,6 +3913,7 @@ async function main() {
|
|
|
3354
3913
|
if (pollTimer) clearInterval(pollTimer);
|
|
3355
3914
|
void transport.close().finally(() => process.exit(exitCode));
|
|
3356
3915
|
};
|
|
3916
|
+
setActiveShutdown(shutdown);
|
|
3357
3917
|
process.stdin.on("end", () => shutdown(0));
|
|
3358
3918
|
process.stdin.on("close", () => shutdown(0));
|
|
3359
3919
|
process.on("SIGINT", () => shutdown(0));
|
|
@@ -3400,6 +3960,49 @@ async function main() {
|
|
|
3400
3960
|
pollTimer.unref();
|
|
3401
3961
|
}
|
|
3402
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
|
+
});
|
|
3403
4006
|
main().catch((err) => {
|
|
3404
4007
|
log.fatal("MCP server crashed", { err: String(err) });
|
|
3405
4008
|
process.exit(1);
|