@mushi-mushi/mcp 0.20.1 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,79 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { createRequire } from "module";
6
6
  import { readFileSync } from "fs";
7
7
  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 = [
@@ -149,6 +215,14 @@ var TOOL_CATALOG = [
149
215
  hints: { readOnly: true, idempotent: true, openWorld: true },
150
216
  useCase: "Why is Mushi setup broken \u2014 one call, clear next step."
151
217
  },
218
+ {
219
+ name: "check_sdk_version",
220
+ title: "Check SDK freshness",
221
+ description: "Compare a published @mushi-mushi/* package version against the catalog (GET /v1/sdk/latest-version). Returns { package, current, latest, outdated } and, when outdated, suggestedActions (Sentry-style, max 1) pointing at search_mushi_docs plus the mushi-sdk-upgrade skill. Read-only. Use when Dependabot or mushi upgrade --check reports a drift, or before dispatching a fix that assumes a current SDK. Does not bump the pin \u2014 that stays a human/Dependabot change.",
222
+ scope: "mcp:read",
223
+ hints: { readOnly: true, idempotent: true, openWorld: true },
224
+ useCase: "Is this project on the current Mushi SDK?"
225
+ },
152
226
  {
153
227
  name: "search_mushi_docs",
154
228
  title: "Search Mushi documentation",
@@ -170,7 +244,7 @@ var TOOL_CATALOG = [
170
244
  {
171
245
  name: "dispatch_fix",
172
246
  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 { fixAttemptId }; poll get_fix_timeline for progress and merge_fix when CI is green. Write; NOT idempotent \u2014 each call starts a new attempt. Report must be classified \u2014 run triage_issue if not.',
247
+ 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
248
  scope: "mcp:write",
175
249
  hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
176
250
  useCase: "Let the in-repo agent attempt this fix for me (or: dispatch a Cursor Cloud Agent)."
@@ -305,6 +379,14 @@ var TOOL_CATALOG = [
305
379
  hints: { readOnly: true, idempotent: true, openWorld: true },
306
380
  useCase: "Analyze this bug report end-to-end and tell me what to do."
307
381
  },
382
+ {
383
+ name: "triage_next_steps",
384
+ title: "What should I work on now?",
385
+ 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".',
386
+ scope: "mcp:read",
387
+ hints: { readOnly: true, idempotent: true, openWorld: true },
388
+ useCase: "What should I triage or fix right now?"
389
+ },
308
390
  // ── Lessons / evolution loop ─────────────────────────────────────────────
309
391
  {
310
392
  name: "query_lessons",
@@ -546,24 +628,29 @@ var TDD_TOOL_CATALOG = [
546
628
  }
547
629
  ];
548
630
  var USE_MUSHI_INTENTS = {
631
+ // NOTE: every name below MUST be a registered TOOL (scopes.test.ts guards
632
+ // this). 2026-08-16 audit P0-3 found phantom names (get_report,
633
+ // get_dashboard, list_qa_stories) and prompts listed as tools
634
+ // (summarize_report_for_fix, mushi_setup) — agents routed here then
635
+ // failed the call and gave up.
549
636
  fix: {
550
637
  label: "Fix a bug",
551
638
  tools: [
552
639
  "get_recent_reports",
553
- "get_report",
554
- "summarize_report_for_fix",
640
+ "get_report_detail",
641
+ "triage_issue",
555
642
  "dispatch_fix",
556
643
  "start_skill_pipeline",
557
644
  "checkin_pipeline_step",
558
645
  "get_pipeline_run"
559
646
  ],
560
- hint: "Call get_recent_reports to find the top unresolved bug, then summarize_report_for_fix before dispatching."
647
+ hint: "Call get_recent_reports to find the top unresolved bug, then triage_issue before dispatching."
561
648
  },
562
649
  status: {
563
650
  label: "Check project status",
564
651
  tools: [
565
- "get_dashboard",
566
652
  "triage_next_steps",
653
+ "get_account_overview",
567
654
  "get_usage",
568
655
  "get_backend_health",
569
656
  "activation_status"
@@ -573,7 +660,8 @@ var USE_MUSHI_INTENTS = {
573
660
  setup: {
574
661
  label: "Set up Mushi",
575
662
  tools: [
576
- "mushi_setup",
663
+ "diagnose_setup",
664
+ "check_sdk_version",
577
665
  "activation_status",
578
666
  "get_backend_health",
579
667
  "list_byok_keys",
@@ -581,12 +669,11 @@ var USE_MUSHI_INTENTS = {
581
669
  "test_byok_key",
582
670
  "remove_byok_key"
583
671
  ],
584
- hint: "Call mushi_setup first \u2014 it diagnoses setup gaps and returns the next command to run."
672
+ hint: "Call diagnose_setup first \u2014 it diagnoses setup gaps and returns the next action to take."
585
673
  },
586
674
  qa: {
587
675
  label: "Run / review QA tests",
588
676
  tools: [
589
- "list_qa_stories",
590
677
  "run_qa_story",
591
678
  "list_qa_story_runs",
592
679
  "get_qa_story_run",
@@ -594,7 +681,7 @@ var USE_MUSHI_INTENTS = {
594
681
  "approve_qa_story",
595
682
  "improve_qa_story"
596
683
  ],
597
- hint: "Call list_qa_stories to see what test coverage exists; run_qa_story to trigger a run."
684
+ hint: "Call list_qa_story_runs to see recent coverage and outcomes; run_qa_story to trigger a run."
598
685
  },
599
686
  pipeline: {
600
687
  label: "Run an agent pipeline / skill",
@@ -609,7 +696,7 @@ var USE_MUSHI_INTENTS = {
609
696
  },
610
697
  audit: {
611
698
  label: "Audit / health check",
612
- tools: ["run_fullstack_audit", "get_backend_health", "get_dashboard", "get_usage"],
699
+ tools: ["run_fullstack_audit", "get_backend_health", "get_account_overview", "get_usage"],
613
700
  hint: "Call run_fullstack_audit for a full-stack health scorecard."
614
701
  }
615
702
  };
@@ -725,6 +812,7 @@ var TOOL_FEATURE_MAP = {
725
812
  inventory_current: "admin",
726
813
  // setup
727
814
  diagnose_setup: "setup",
815
+ check_sdk_version: "setup",
728
816
  activation_status: "setup",
729
817
  project_integration_health: "setup",
730
818
  setup_repo_for_mushi: "setup",
@@ -834,7 +922,7 @@ function toolMatchesFeatures(toolName, filter) {
834
922
  }
835
923
 
836
924
  // src/server.ts
837
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
925
+ import { McpServer } from "@modelcontextprotocol/server";
838
926
  import { z } from "zod";
839
927
  import { createLogger } from "@mushi-mushi/core";
840
928
 
@@ -981,7 +1069,40 @@ function wrapUntrustedJson(value, role) {
981
1069
  }
982
1070
 
983
1071
  // src/server.ts
984
- var NO_ARG_INPUT = {};
1072
+ installStdoutGuard();
1073
+ var NO_ARG_INPUT = z.object({});
1074
+ var DEFAULT_API_TIMEOUT_MS = 15e3;
1075
+ var MIN_API_TIMEOUT_MS = 250;
1076
+ var MAX_API_TIMEOUT_MS = 3e5;
1077
+ function resolveTimeoutMsFromEnv() {
1078
+ const raw = process.env?.MUSHI_MCP_TIMEOUT_MS?.trim();
1079
+ if (!raw) return DEFAULT_API_TIMEOUT_MS;
1080
+ const parsed = Number(raw);
1081
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
1082
+ return Math.min(Math.max(parsed, MIN_API_TIMEOUT_MS), MAX_API_TIMEOUT_MS);
1083
+ }
1084
+ function bareMessage(err) {
1085
+ const raw = err instanceof Error ? err.message : String(err);
1086
+ return raw.replace(/^\[[A-Z0-9_]+\]\s*/, "");
1087
+ }
1088
+ var IDEMPOTENCY_NAMESPACE = "mushi.mcp.submit_fix_result.v1";
1089
+ async function stableUuidFrom(...parts) {
1090
+ const input = parts.join("");
1091
+ const digest = new Uint8Array(
1092
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))
1093
+ );
1094
+ const bytes = digest.slice(0, 16);
1095
+ bytes[6] = bytes[6] & 15 | 80;
1096
+ bytes[8] = bytes[8] & 63 | 128;
1097
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1098
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
1099
+ }
1100
+ function withTimeoutSignal(callerSignal, timeoutSignal) {
1101
+ if (!callerSignal) return timeoutSignal;
1102
+ const anyFn = AbortSignal.any;
1103
+ if (typeof anyFn === "function") return anyFn.call(AbortSignal, [callerSignal, timeoutSignal]);
1104
+ return timeoutSignal;
1105
+ }
985
1106
  var MushiApiError = class extends Error {
986
1107
  constructor(status, code, message) {
987
1108
  super(`[${code}] ${message}`);
@@ -995,6 +1116,7 @@ var MushiApiError = class extends Error {
995
1116
  function createMushiServer(config) {
996
1117
  const { version, apiEndpoint, apiKey, projectId } = config;
997
1118
  const doFetch = config.fetch ?? globalThis.fetch;
1119
+ const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : resolveTimeoutMsFromEnv();
998
1120
  const apiLog = createLogger({
999
1121
  scope: "mushi:mcp:api",
1000
1122
  level: process.env.MUSHI_LOG_LEVEL ?? "info",
@@ -1012,22 +1134,66 @@ function createMushiServer(config) {
1012
1134
  async function apiCall(path, options) {
1013
1135
  const requestId = crypto.randomUUID().slice(0, 12);
1014
1136
  const started = Date.now();
1015
- const res = await doFetch(`${apiEndpoint}${path}`, {
1016
- ...options,
1017
- headers: {
1018
- "Content-Type": "application/json",
1019
- // Authorization is accepted by `adminOrApiKey` only as a JWT, but we
1020
- // still send it for endpoints still behind plain `jwtAuth` (legacy)
1021
- // and for transparent proxies that strip X-Mushi-* headers.
1022
- Authorization: `Bearer ${apiKey}`,
1023
- "X-Mushi-Api-Key": apiKey,
1024
- "X-Request-Id": requestId,
1025
- ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
1026
- ...options?.headers ?? {}
1137
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
1138
+ let res;
1139
+ try {
1140
+ res = await doFetch(`${apiEndpoint}${path}`, {
1141
+ ...options,
1142
+ headers: {
1143
+ "Content-Type": "application/json",
1144
+ // Authorization is accepted by `adminOrApiKey` only as a JWT, but we
1145
+ // still send it for endpoints still behind plain `jwtAuth` (legacy)
1146
+ // and for transparent proxies that strip X-Mushi-* headers.
1147
+ Authorization: `Bearer ${apiKey}`,
1148
+ "X-Mushi-Api-Key": apiKey,
1149
+ "X-Request-Id": requestId,
1150
+ ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
1151
+ ...options?.headers ?? {}
1152
+ },
1153
+ // After the spread so a caller-supplied `signal` is merged, not lost.
1154
+ signal: withTimeoutSignal(options?.signal, timeoutSignal)
1155
+ });
1156
+ } catch (err) {
1157
+ throw classifyTransportError(err);
1158
+ }
1159
+ let text;
1160
+ try {
1161
+ text = await res.text();
1162
+ } catch (err) {
1163
+ throw classifyTransportError(err);
1164
+ }
1165
+ function classifyTransportError(err) {
1166
+ const durationMs2 = Date.now() - started;
1167
+ if (timeoutSignal.aborted) {
1168
+ apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
1169
+ return new MushiApiError(
1170
+ 504,
1171
+ "MUSHI_TIMEOUT",
1172
+ `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}`
1173
+ );
1027
1174
  }
1028
- });
1175
+ const name = err instanceof Error ? err.name : "";
1176
+ if (name === "AbortError") {
1177
+ apiLog.warn("api.aborted", { path, requestId, durationMs: durationMs2 });
1178
+ return new MushiApiError(
1179
+ 499,
1180
+ "REQUEST_ABORTED",
1181
+ `The request to ${path} was aborted by the caller after ${durationMs2}ms.`
1182
+ );
1183
+ }
1184
+ apiLog.warn("api.unreachable", {
1185
+ path,
1186
+ requestId,
1187
+ durationMs: durationMs2,
1188
+ err: bareMessage(err)
1189
+ });
1190
+ return new MushiApiError(
1191
+ 503,
1192
+ "NETWORK_ERROR",
1193
+ `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.`
1194
+ );
1195
+ }
1029
1196
  const durationMs = Date.now() - started;
1030
- const text = await res.text();
1031
1197
  let body = null;
1032
1198
  if (text) {
1033
1199
  try {
@@ -1057,6 +1223,7 @@ function createMushiServer(config) {
1057
1223
  apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
1058
1224
  return body;
1059
1225
  }
1226
+ let resolvedAccountProjectId = null;
1060
1227
  async function resolveProjectId(explicitId) {
1061
1228
  if (explicitId) {
1062
1229
  if (projectId && explicitId !== projectId) {
@@ -1069,10 +1236,12 @@ function createMushiServer(config) {
1069
1236
  return explicitId;
1070
1237
  }
1071
1238
  if (projectId) return projectId;
1239
+ if (resolvedAccountProjectId) return resolvedAccountProjectId;
1072
1240
  try {
1073
1241
  const data = await apiCall("/v1/admin/mcp/account-overview");
1074
1242
  if (data.total === 1 && data.projects[0]) {
1075
- return data.projects[0].id;
1243
+ resolvedAccountProjectId = data.projects[0].id;
1244
+ return resolvedAccountProjectId;
1076
1245
  }
1077
1246
  if (data.total === 0) {
1078
1247
  throw new MushiApiError(
@@ -1088,11 +1257,27 @@ function createMushiServer(config) {
1088
1257
  `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
1258
  );
1090
1259
  } catch (err) {
1091
- if (err instanceof MushiApiError) throw err;
1260
+ if (err instanceof MushiApiError) {
1261
+ if (err.code === "MUSHI_TIMEOUT" || err.code === "NETWORK_ERROR") {
1262
+ throw new MushiApiError(
1263
+ err.status,
1264
+ err.code,
1265
+ `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.`
1266
+ );
1267
+ }
1268
+ if (err.status === 401 || err.status === 403) {
1269
+ throw new MushiApiError(
1270
+ err.status,
1271
+ err.code,
1272
+ `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.`
1273
+ );
1274
+ }
1275
+ throw err;
1276
+ }
1092
1277
  throw new MushiApiError(
1093
1278
  400,
1094
1279
  "MISSING_PROJECT_ID",
1095
- "projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly."
1280
+ `projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly. (Project lookup failed with: ${bareMessage(err)})`
1096
1281
  );
1097
1282
  }
1098
1283
  }
@@ -1159,7 +1344,7 @@ function createMushiServer(config) {
1159
1344
  title: titleOf("get_recent_reports"),
1160
1345
  description: descOf("get_recent_reports"),
1161
1346
  annotations: annotationsFor("get_recent_reports"),
1162
- inputSchema: {
1347
+ inputSchema: z.object({
1163
1348
  status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
1164
1349
  category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
1165
1350
  severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
@@ -1167,11 +1352,11 @@ function createMushiServer(config) {
1167
1352
  project_id: z.string().optional().describe(
1168
1353
  "Project UUID \u2014 defaults to the server-configured project. Useful when you have multiple projects and want to query a specific one by its ID. Get IDs by calling list_projects or get_account_overview first."
1169
1354
  )
1170
- },
1171
- outputSchema: {
1355
+ }),
1356
+ outputSchema: z.object({
1172
1357
  reports: z.array(z.unknown()),
1173
1358
  total: z.number()
1174
- }
1359
+ })
1175
1360
  },
1176
1361
  async (args) => {
1177
1362
  const params = new URLSearchParams();
@@ -1196,11 +1381,11 @@ function createMushiServer(config) {
1196
1381
  title: titleOf("get_report_detail"),
1197
1382
  description: descOf("get_report_detail"),
1198
1383
  annotations: annotationsFor("get_report_detail"),
1199
- inputSchema: {
1384
+ inputSchema: z.object({
1200
1385
  reportId: z.string().describe("The report UUID"),
1201
1386
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1202
- },
1203
- outputSchema: { report: z.unknown() }
1387
+ }),
1388
+ outputSchema: z.object({ report: z.unknown() })
1204
1389
  },
1205
1390
  async (args) => {
1206
1391
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -1223,7 +1408,7 @@ function createMushiServer(config) {
1223
1408
  title: titleOf("get_report_timeline"),
1224
1409
  description: descOf("get_report_timeline"),
1225
1410
  annotations: annotationsFor("get_report_timeline"),
1226
- inputSchema: { reportId: z.string().describe("The report UUID") }
1411
+ inputSchema: z.object({ reportId: z.string().describe("The report UUID") })
1227
1412
  },
1228
1413
  async (args) => jsonText(await apiCall(`/v1/sync/reports/${args.reportId}/timeline`))
1229
1414
  );
@@ -1243,14 +1428,14 @@ function createMushiServer(config) {
1243
1428
  title: titleOf("search_reports"),
1244
1429
  description: descOf("search_reports"),
1245
1430
  annotations: annotationsFor("search_reports"),
1246
- inputSchema: {
1431
+ inputSchema: z.object({
1247
1432
  query: z.string().describe("Natural-language search text or component path"),
1248
1433
  limit: z.number().optional().describe("Max results (default 10, max 50)"),
1249
1434
  threshold: z.number().optional().describe("Similarity threshold 0..1, default 0.2")
1250
- },
1251
- outputSchema: {
1435
+ }),
1436
+ outputSchema: z.object({
1252
1437
  results: z.array(z.unknown())
1253
- }
1438
+ })
1254
1439
  },
1255
1440
  async (args) => {
1256
1441
  const data = await apiCall("/v1/admin/reports/similarity", {
@@ -1271,13 +1456,13 @@ function createMushiServer(config) {
1271
1456
  title: titleOf("get_similar_bugs"),
1272
1457
  description: descOf("get_similar_bugs"),
1273
1458
  annotations: annotationsFor("get_similar_bugs"),
1274
- inputSchema: {
1459
+ inputSchema: z.object({
1275
1460
  query: z.string().describe("Component name, page path, or bug description"),
1276
1461
  limit: z.number().optional().describe("Max results (default 5, max 20)")
1277
- },
1278
- outputSchema: {
1462
+ }),
1463
+ outputSchema: z.object({
1279
1464
  results: z.array(z.unknown())
1280
- }
1465
+ })
1281
1466
  },
1282
1467
  async (args) => {
1283
1468
  const data = await apiCall("/v1/admin/reports/similarity", {
@@ -1298,18 +1483,18 @@ function createMushiServer(config) {
1298
1483
  title: titleOf("get_fix_context"),
1299
1484
  description: descOf("get_fix_context"),
1300
1485
  annotations: annotationsFor("get_fix_context"),
1301
- inputSchema: {
1486
+ inputSchema: z.object({
1302
1487
  reportId: z.string().describe("The report UUID to fix"),
1303
1488
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1304
- },
1305
- outputSchema: {
1489
+ }),
1490
+ outputSchema: z.object({
1306
1491
  report: z.unknown(),
1307
1492
  fixPrompt: z.unknown(),
1308
1493
  reproductionSteps: z.unknown(),
1309
1494
  component: z.unknown(),
1310
1495
  rootCause: z.unknown(),
1311
1496
  bugOntologyTags: z.unknown()
1312
- }
1497
+ })
1313
1498
  },
1314
1499
  async (args) => {
1315
1500
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -1338,10 +1523,10 @@ function createMushiServer(config) {
1338
1523
  title: titleOf("get_fix_timeline"),
1339
1524
  description: descOf("get_fix_timeline"),
1340
1525
  annotations: annotationsFor("get_fix_timeline"),
1341
- inputSchema: {
1526
+ inputSchema: z.object({
1342
1527
  fixId: z.string().describe("fix_attempt UUID"),
1343
1528
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
1344
- }
1529
+ })
1345
1530
  },
1346
1531
  async (args) => {
1347
1532
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -1354,7 +1539,7 @@ function createMushiServer(config) {
1354
1539
  title: titleOf("get_blast_radius"),
1355
1540
  description: descOf("get_blast_radius"),
1356
1541
  annotations: annotationsFor("get_blast_radius"),
1357
- inputSchema: { nodeId: z.string().describe("Graph node UUID") }
1542
+ inputSchema: z.object({ nodeId: z.string().describe("Graph node UUID") })
1358
1543
  },
1359
1544
  async (args) => jsonText(await apiCall(`/v1/admin/graph/blast-radius/${args.nodeId}`))
1360
1545
  );
@@ -1364,14 +1549,14 @@ function createMushiServer(config) {
1364
1549
  title: titleOf("get_knowledge_graph"),
1365
1550
  description: descOf("get_knowledge_graph"),
1366
1551
  annotations: annotationsFor("get_knowledge_graph"),
1367
- inputSchema: {
1552
+ inputSchema: z.object({
1368
1553
  seed: z.string().describe("Starting graph node id or human-readable label to traverse from"),
1369
1554
  depth: z.number().optional().describe("BFS hops outward from seed \u2014 clamped to 4 in the handler (default 2)")
1370
- },
1371
- outputSchema: {
1555
+ }),
1556
+ outputSchema: z.object({
1372
1557
  nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
1373
1558
  edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
1374
- }
1559
+ })
1375
1560
  },
1376
1561
  async (args) => {
1377
1562
  const params = new URLSearchParams({
@@ -1390,16 +1575,16 @@ function createMushiServer(config) {
1390
1575
  title: titleOf("get_graph_neighborhood"),
1391
1576
  description: descOf("get_graph_neighborhood"),
1392
1577
  annotations: annotationsFor("get_graph_neighborhood"),
1393
- inputSchema: {
1578
+ inputSchema: z.object({
1394
1579
  seed: z.string().describe(
1395
1580
  "Starting graph node id or label to expand around (e.g. an inventory Action id or component name)"
1396
1581
  ),
1397
1582
  depth: z.number().int().min(1).max(4).optional().describe("BFS hops to traverse outward. Default 2; clamped to a max of 4.")
1398
- },
1399
- outputSchema: {
1583
+ }),
1584
+ outputSchema: z.object({
1400
1585
  nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
1401
1586
  edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
1402
- }
1587
+ })
1403
1588
  },
1404
1589
  async (args) => {
1405
1590
  const params = new URLSearchParams({
@@ -1418,12 +1603,12 @@ function createMushiServer(config) {
1418
1603
  title: titleOf("get_graph_node"),
1419
1604
  description: descOf("get_graph_node"),
1420
1605
  annotations: annotationsFor("get_graph_node"),
1421
- inputSchema: {
1606
+ inputSchema: z.object({
1422
1607
  nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).")
1423
- },
1424
- outputSchema: {
1608
+ }),
1609
+ outputSchema: z.object({
1425
1610
  node: z.unknown().describe("Single graph_nodes row including metadata (Action nodes carry v2 status)")
1426
- }
1611
+ })
1427
1612
  },
1428
1613
  async (args) => {
1429
1614
  const data = await apiCall(`/v1/admin/graph/node/${args.nodeId}`);
@@ -1436,9 +1621,9 @@ function createMushiServer(config) {
1436
1621
  title: titleOf("get_inventory"),
1437
1622
  description: descOf("get_inventory"),
1438
1623
  annotations: annotationsFor("get_inventory"),
1439
- inputSchema: {
1624
+ inputSchema: z.object({
1440
1625
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
1441
- }
1626
+ })
1442
1627
  },
1443
1628
  async (args) => {
1444
1629
  const pid = args.projectId ?? projectId;
@@ -1453,11 +1638,11 @@ function createMushiServer(config) {
1453
1638
  title: titleOf("diff_inventory"),
1454
1639
  description: descOf("diff_inventory"),
1455
1640
  annotations: annotationsFor("diff_inventory"),
1456
- inputSchema: {
1641
+ inputSchema: z.object({
1457
1642
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
1458
1643
  fromSha: z.string().describe("Older commit SHA (the baseline to diff from)"),
1459
1644
  toSha: z.string().describe("Newer commit SHA (the candidate to diff to)")
1460
- }
1645
+ })
1461
1646
  },
1462
1647
  async (args) => {
1463
1648
  const pid = args.projectId ?? projectId;
@@ -1473,15 +1658,15 @@ function createMushiServer(config) {
1473
1658
  title: titleOf("list_gate_findings"),
1474
1659
  description: descOf("list_gate_findings"),
1475
1660
  annotations: annotationsFor("list_gate_findings"),
1476
- inputSchema: {
1661
+ inputSchema: z.object({
1477
1662
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
1478
1663
  gate: z.string().optional().describe("Filter to one gate id: dead-handler | mock-leak | crawl | status-claim"),
1479
1664
  severity: z.string().optional().describe("Minimum severity to include: low | medium | high | critical")
1480
- },
1481
- outputSchema: {
1665
+ }),
1666
+ outputSchema: z.object({
1482
1667
  runs: z.array(z.unknown()).describe("Recent gate_runs rows, newest first"),
1483
1668
  findings: z.array(z.unknown()).describe("gate_findings rows for those runs")
1484
- }
1669
+ })
1485
1670
  },
1486
1671
  async (args) => {
1487
1672
  const pid = args.projectId ?? projectId;
@@ -1507,21 +1692,35 @@ function createMushiServer(config) {
1507
1692
  title: titleOf("suggest_fix"),
1508
1693
  description: descOf("suggest_fix"),
1509
1694
  annotations: annotationsFor("suggest_fix"),
1510
- inputSchema: {
1695
+ inputSchema: z.object({
1511
1696
  reportId: z.string().describe("Report UUID to read the Stage-2 suggested-fix slice for")
1512
- },
1513
- outputSchema: {
1697
+ }),
1698
+ outputSchema: z.object({
1514
1699
  reportId: z.string().describe("The report this slice was read from"),
1515
1700
  rootCause: z.unknown().describe("Stage-2 root-cause hint, or null if not yet classified"),
1516
1701
  suggestedFix: z.unknown().describe("Stage-2 suggested fix, or null if not yet classified"),
1517
1702
  reproductionSteps: z.unknown().describe("Reproduction steps recorded on the report (array; [] if none)"),
1518
1703
  summary: z.unknown().describe("One-line report summary, or null"),
1519
- component: z.unknown().describe("Component/page the bug was attributed to, or null")
1520
- }
1704
+ component: z.unknown().describe("Component/page the bug was attributed to, or null"),
1705
+ note: z.string().optional().describe(
1706
+ "Present when no Stage-2 analysis exists \u2014 explains why and what to call instead"
1707
+ )
1708
+ })
1521
1709
  },
1522
1710
  async (args) => {
1523
1711
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
1524
1712
  const s2 = report.stage2_analysis;
1713
+ if (!s2 || s2.rootCause == null && s2.suggestedFix == null) {
1714
+ return jsonResult({
1715
+ reportId: args.reportId,
1716
+ rootCause: null,
1717
+ suggestedFix: null,
1718
+ reproductionSteps: report.reproduction_steps ?? [],
1719
+ summary: report.summary ?? null,
1720
+ component: report.component ?? null,
1721
+ 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."
1722
+ });
1723
+ }
1525
1724
  return jsonResult({
1526
1725
  reportId: args.reportId,
1527
1726
  rootCause: s2?.rootCause ?? null,
@@ -1538,11 +1737,11 @@ function createMushiServer(config) {
1538
1737
  title: titleOf("run_nl_query"),
1539
1738
  description: descOf("run_nl_query"),
1540
1739
  annotations: annotationsFor("run_nl_query"),
1541
- inputSchema: {
1740
+ inputSchema: z.object({
1542
1741
  question: z.string().describe(
1543
1742
  'Question in plain English, e.g. "Which components had the most critical bugs this week?"'
1544
1743
  )
1545
- }
1744
+ })
1546
1745
  },
1547
1746
  async (args) => {
1548
1747
  const data = await apiCall("/v1/admin/query", {
@@ -1558,14 +1757,14 @@ function createMushiServer(config) {
1558
1757
  title: titleOf("diagnose_setup"),
1559
1758
  description: descOf("diagnose_setup"),
1560
1759
  annotations: annotationsFor("diagnose_setup"),
1561
- inputSchema: {
1760
+ inputSchema: z.object({
1562
1761
  mode: z.enum(["full", "ingest", "dispatch"]).optional().describe(
1563
1762
  "full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."
1564
1763
  ),
1565
1764
  project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
1566
1765
  projectId: z.string().optional().describe("Alias for project_id.")
1567
- },
1568
- outputSchema: {
1766
+ }),
1767
+ outputSchema: z.object({
1569
1768
  mode: z.string(),
1570
1769
  ready: z.boolean(),
1571
1770
  summary: z.string(),
@@ -1573,7 +1772,7 @@ function createMushiServer(config) {
1573
1772
  ingest: z.unknown().optional(),
1574
1773
  dispatch: z.unknown().optional(),
1575
1774
  connection: z.unknown().optional()
1576
- }
1775
+ })
1577
1776
  },
1578
1777
  async (args) => {
1579
1778
  const mode = args.mode ?? "full";
@@ -1712,17 +1911,71 @@ function createMushiServer(config) {
1712
1911
  });
1713
1912
  }
1714
1913
  );
1914
+ server.registerTool(
1915
+ "check_sdk_version",
1916
+ {
1917
+ title: titleOf("check_sdk_version"),
1918
+ description: descOf("check_sdk_version"),
1919
+ annotations: annotationsFor("check_sdk_version"),
1920
+ inputSchema: z.object({
1921
+ package: z.string().optional().describe(
1922
+ "npm package name (default @mushi-mushi/web). Examples: @mushi-mushi/web, @mushi-mushi/node, @mushi-mushi/core."
1923
+ ),
1924
+ current: z.string().optional().describe("Installed version from package.json, if known (e.g. 1.27.0).")
1925
+ }),
1926
+ outputSchema: z.object({
1927
+ package: z.string(),
1928
+ latest: z.string().optional(),
1929
+ current: z.string().optional(),
1930
+ outdated: z.boolean().optional(),
1931
+ suggestedActions: z.array(
1932
+ z.object({
1933
+ type: z.literal("tool_call"),
1934
+ toolName: z.string(),
1935
+ arguments: z.record(z.string(), z.unknown()),
1936
+ reason: z.string()
1937
+ })
1938
+ ).optional()
1939
+ })
1940
+ },
1941
+ async (args) => {
1942
+ const pkg = args.package?.trim() || "@mushi-mushi/web";
1943
+ const query = new URLSearchParams({ package: pkg }).toString();
1944
+ const data = await apiCall(
1945
+ `/v1/sdk/latest-version?${query}`
1946
+ );
1947
+ const latest = data.latest ?? data.version;
1948
+ const current = args.current;
1949
+ const outdated = current && latest ? current !== latest : void 0;
1950
+ return jsonResult({
1951
+ package: data.package ?? pkg,
1952
+ latest,
1953
+ current,
1954
+ outdated,
1955
+ ...outdated ? {
1956
+ suggestedActions: [
1957
+ {
1958
+ type: "tool_call",
1959
+ toolName: "search_mushi_docs",
1960
+ arguments: { query: `${pkg} upgrade mushi-sdk-upgrade` },
1961
+ reason: "Read current upgrade notes, then apply the mushi-sdk-upgrade skill. This tool does not bump the pin."
1962
+ }
1963
+ ]
1964
+ } : {}
1965
+ });
1966
+ }
1967
+ );
1715
1968
  server.registerTool(
1716
1969
  "search_mushi_docs",
1717
1970
  {
1718
1971
  title: titleOf("search_mushi_docs"),
1719
1972
  description: descOf("search_mushi_docs"),
1720
1973
  annotations: annotationsFor("search_mushi_docs"),
1721
- inputSchema: {
1974
+ inputSchema: z.object({
1722
1975
  query: z.string().describe("Keywords to search official Mushi docs (guides, MCP, inventory, QA)."),
1723
1976
  limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8).")
1724
- },
1725
- outputSchema: {
1977
+ }),
1978
+ outputSchema: z.object({
1726
1979
  query: z.string(),
1727
1980
  results: z.array(
1728
1981
  z.object({
@@ -1732,7 +1985,7 @@ function createMushiServer(config) {
1732
1985
  score: z.number()
1733
1986
  })
1734
1987
  )
1735
- }
1988
+ })
1736
1989
  },
1737
1990
  async (args) => {
1738
1991
  const query = args.query ?? "";
@@ -1753,11 +2006,11 @@ function createMushiServer(config) {
1753
2006
  description: descOf("list_projects"),
1754
2007
  annotations: annotationsFor("list_projects"),
1755
2008
  inputSchema: NO_ARG_INPUT,
1756
- outputSchema: {
2009
+ outputSchema: z.object({
1757
2010
  projects: z.array(z.unknown()),
1758
2011
  total: z.number().optional(),
1759
2012
  _multi_project_hint: z.string().optional()
1760
- }
2013
+ })
1761
2014
  },
1762
2015
  async () => {
1763
2016
  const data = await apiCall("/v1/admin/mcp/projects");
@@ -1774,7 +2027,7 @@ function createMushiServer(config) {
1774
2027
  description: descOf("get_account_overview"),
1775
2028
  annotations: annotationsFor("get_account_overview"),
1776
2029
  inputSchema: NO_ARG_INPUT,
1777
- outputSchema: {
2030
+ outputSchema: z.object({
1778
2031
  projects: z.array(z.unknown()),
1779
2032
  total: z.number(),
1780
2033
  active_project_id: z.string().nullable(),
@@ -1782,7 +2035,7 @@ function createMushiServer(config) {
1782
2035
  resourceCount: z.number().optional(),
1783
2036
  promptCount: z.number().optional(),
1784
2037
  multi_project_hint: z.string()
1785
- }
2038
+ })
1786
2039
  },
1787
2040
  async () => {
1788
2041
  const data = await apiCall("/v1/admin/mcp/account-overview");
@@ -1804,9 +2057,9 @@ function createMushiServer(config) {
1804
2057
  title: titleOf("get_project_context"),
1805
2058
  description: descOf("get_project_context"),
1806
2059
  annotations: annotationsFor("get_project_context"),
1807
- inputSchema: {
2060
+ inputSchema: z.object({
1808
2061
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1809
- }
2062
+ })
1810
2063
  },
1811
2064
  async (args) => {
1812
2065
  const pid = args.project_id ?? projectId;
@@ -1828,13 +2081,13 @@ function createMushiServer(config) {
1828
2081
  title: titleOf("get_pipeline_logs"),
1829
2082
  description: descOf("get_pipeline_logs"),
1830
2083
  annotations: annotationsFor("get_pipeline_logs"),
1831
- inputSchema: {
2084
+ inputSchema: z.object({
1832
2085
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1833
2086
  service: z.enum(["fix-worker", "qa-story-runner", "pipeline", "all"]).optional().describe("Pipeline service to filter (default: all)."),
1834
2087
  since: z.string().optional().describe("ISO-8601 timestamp \u2014 return only entries after this time."),
1835
2088
  limit: z.number().optional().describe("Max entries to return (default 50, max 200)."),
1836
2089
  level: z.enum(["info", "warn", "error", "fatal"]).optional().describe("Minimum severity level (default: warn).")
1837
- }
2090
+ })
1838
2091
  },
1839
2092
  async (args) => {
1840
2093
  const pid = args.project_id ?? projectId;
@@ -1854,9 +2107,9 @@ function createMushiServer(config) {
1854
2107
  title: titleOf("get_report_evidence"),
1855
2108
  description: descOf("get_report_evidence"),
1856
2109
  annotations: annotationsFor("get_report_evidence"),
1857
- inputSchema: {
2110
+ inputSchema: z.object({
1858
2111
  report_id: z.string().describe("Report UUID.")
1859
- }
2112
+ })
1860
2113
  },
1861
2114
  async (args) => {
1862
2115
  const [reportRes, timelineRes] = await Promise.allSettled([
@@ -1904,12 +2157,12 @@ function createMushiServer(config) {
1904
2157
  title: titleOf("triage_issue"),
1905
2158
  description: descOf("triage_issue"),
1906
2159
  annotations: annotationsFor("triage_issue"),
1907
- inputSchema: {
2160
+ inputSchema: z.object({
1908
2161
  report_id: z.string().uuid().describe("Report UUID to triage."),
1909
2162
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1910
2163
  include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1911
- },
1912
- outputSchema: {
2164
+ }),
2165
+ outputSchema: z.object({
1913
2166
  report_id: z.string(),
1914
2167
  severity: z.unknown(),
1915
2168
  category: z.unknown(),
@@ -1922,7 +2175,7 @@ function createMushiServer(config) {
1922
2175
  recent_logs: z.unknown().nullable(),
1923
2176
  recommended_actions: z.array(z.unknown()),
1924
2177
  triage_summary: z.string()
1925
- }
2178
+ })
1926
2179
  },
1927
2180
  async (args) => {
1928
2181
  const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
@@ -1952,12 +2205,31 @@ function createMushiServer(config) {
1952
2205
  const category = report ? report.category : "unknown";
1953
2206
  const status = report ? report.status : "unknown";
1954
2207
  const actions = [];
1955
- if (status === "new" || status === "classified") {
2208
+ const processingError = report ? report.processing_error : null;
2209
+ const attempts = report ? report.fix_attempts ?? [] : [];
2210
+ const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
2211
+ const lastAttemptStatus = lastAttempt ? String(lastAttempt.status ?? "") : "";
2212
+ const autofixBlocked = typeof processingError === "string" && processingError.startsWith("autofix_blocked:") || lastAttemptStatus.startsWith("skipped") || lastAttemptStatus === "failed";
2213
+ if (autofixBlocked && status !== "fixing" && status !== "fixed") {
2214
+ const blockReason = typeof processingError === "string" && processingError || String(lastAttempt?.error ?? "previous fix attempt was skipped or failed");
2215
+ actions.push({
2216
+ action: "unblock_autofix",
2217
+ reason: `Auto-fix is blocked: ${blockReason}. Resolve the blocker (settings/integration/budget), then re-dispatch.`,
2218
+ tool: "diagnose_setup",
2219
+ args: { mode: "dispatch" }
2220
+ });
2221
+ actions.push({
2222
+ action: "redispatch_after_unblock",
2223
+ reason: "Once the blocker above is resolved, dispatch a fresh fix attempt.",
2224
+ tool: "dispatch_fix",
2225
+ args: { reportId: args.report_id }
2226
+ });
2227
+ } else if (status === "new" || status === "classified") {
1956
2228
  actions.push({
1957
2229
  action: "dispatch_fix",
1958
- reason: "Report is classified but no fix has been attempted",
2230
+ reason: "Report is classified and no fix has been attempted",
1959
2231
  tool: "dispatch_fix",
1960
- args: { reportId: args.report_id, agent: "cursor_cloud" }
2232
+ args: { reportId: args.report_id }
1961
2233
  });
1962
2234
  } else if (status === "fixing") {
1963
2235
  actions.push({
@@ -2027,7 +2299,7 @@ function createMushiServer(config) {
2027
2299
  title: titleOf("submit_fix_result"),
2028
2300
  description: descOf("submit_fix_result"),
2029
2301
  annotations: annotationsFor("submit_fix_result"),
2030
- inputSchema: {
2302
+ inputSchema: z.object({
2031
2303
  reportId: z.string().describe("Report UUID to attach this external fix attempt to"),
2032
2304
  branch: z.string().describe("Git branch name where the fix was implemented"),
2033
2305
  prUrl: z.string().optional().describe("GitHub pull request URL, if a PR was opened"),
@@ -2035,43 +2307,136 @@ function createMushiServer(config) {
2035
2307
  linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
2036
2308
  summary: z.string().describe("One-line human summary of what the fix changed"),
2037
2309
  idempotencyKey: z.string().uuid().optional().describe(
2038
- "Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows"
2310
+ "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
2311
  )
2040
- },
2041
- outputSchema: {
2312
+ }),
2313
+ outputSchema: z.object({
2042
2314
  ok: z.boolean().describe("True when the fix_attempt row was created and marked completed"),
2043
2315
  fixId: z.string().describe("UUID of the new fix_attempt row")
2044
- }
2316
+ })
2045
2317
  },
2046
2318
  async (args) => {
2047
- const idemKey = args.idempotencyKey ?? crypto.randomUUID();
2319
+ const idemKey = args.idempotencyKey ?? await stableUuidFrom(IDEMPOTENCY_NAMESPACE, args.reportId, args.branch, args.prUrl ?? "");
2048
2320
  const created = await apiCall("/v1/admin/fixes", {
2049
2321
  method: "POST",
2050
2322
  headers: { "Idempotency-Key": idemKey },
2051
2323
  body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
2052
2324
  });
2053
- await apiCall(`/v1/admin/fixes/${created.fixId}`, {
2054
- method: "PATCH",
2055
- body: JSON.stringify({
2056
- status: "completed",
2057
- branch: args.branch,
2058
- pr_url: args.prUrl,
2059
- files_changed: args.filesChanged,
2060
- lines_changed: args.linesChanged,
2061
- summary: args.summary,
2062
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
2063
- })
2064
- });
2325
+ try {
2326
+ await apiCall(`/v1/admin/fixes/${created.fixId}`, {
2327
+ method: "PATCH",
2328
+ body: JSON.stringify({
2329
+ status: "completed",
2330
+ branch: args.branch,
2331
+ pr_url: args.prUrl,
2332
+ files_changed: args.filesChanged,
2333
+ lines_changed: args.linesChanged,
2334
+ summary: args.summary,
2335
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
2336
+ })
2337
+ });
2338
+ } catch (err) {
2339
+ const status = err instanceof MushiApiError ? err.status : 500;
2340
+ throw new MushiApiError(
2341
+ status,
2342
+ "FIX_PATCH_FAILED",
2343
+ `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.`
2344
+ );
2345
+ }
2065
2346
  return jsonResult({ ok: true, fixId: created.fixId });
2066
2347
  }
2067
2348
  );
2349
+ server.registerTool(
2350
+ "triage_next_steps",
2351
+ {
2352
+ title: titleOf("triage_next_steps"),
2353
+ description: descOf("triage_next_steps"),
2354
+ annotations: annotationsFor("triage_next_steps"),
2355
+ inputSchema: z.object({
2356
+ project_id: z.string().uuid().optional().describe("Project UUID (defaults to the configured project).")
2357
+ }),
2358
+ outputSchema: z.object({
2359
+ steps: z.array(
2360
+ z.object({
2361
+ priority: z.number(),
2362
+ action: z.string(),
2363
+ reason: z.string(),
2364
+ tool: z.string().optional(),
2365
+ args: z.record(z.string(), z.unknown()).optional()
2366
+ })
2367
+ ),
2368
+ summary: z.string()
2369
+ })
2370
+ },
2371
+ async (args) => {
2372
+ const pid = await resolveProjectId(args.project_id);
2373
+ const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
2374
+ const data = await apiCall(
2375
+ "/v1/admin/reports?limit=100",
2376
+ { headers: extraHeaders }
2377
+ );
2378
+ const reports = data.reports ?? [];
2379
+ const isCron = (r) => typeof r.reporter_token_hash === "string" && r.reporter_token_hash.startsWith("cron:");
2380
+ const isBlocked = (r) => typeof r.processing_error === "string" && r.processing_error.startsWith("autofix_blocked:");
2381
+ const label = (r) => String(r.title ?? r.summary ?? r.description ?? "").slice(0, 90);
2382
+ const steps = [];
2383
+ let priority = 1;
2384
+ const blocked = reports.filter(isBlocked);
2385
+ if (blocked.length > 0) {
2386
+ steps.push({
2387
+ priority: priority++,
2388
+ action: `Unblock auto-fix (${blocked.length} report${blocked.length === 1 ? "" : "s"} blocked)`,
2389
+ reason: String(blocked[0].processing_error).slice(0, 200),
2390
+ tool: "diagnose_setup",
2391
+ args: { mode: "dispatch" }
2392
+ });
2393
+ }
2394
+ const fixing = reports.filter((r) => r.status === "fixing");
2395
+ for (const r of fixing.slice(0, 2)) {
2396
+ steps.push({
2397
+ priority: priority++,
2398
+ action: `Shepherd in-flight fix: ${label(r)}`,
2399
+ reason: "A fix branch/PR is open \u2014 check CI and merge when green.",
2400
+ tool: "get_report_timeline",
2401
+ args: { report_id: r.id }
2402
+ });
2403
+ }
2404
+ const openUserReports = reports.filter(
2405
+ (r) => (r.status === "new" || r.status === "classified") && !isCron(r) && !isBlocked(r)
2406
+ ).sort((a, b) => {
2407
+ const rank = { critical: 0, high: 1, medium: 2, low: 3 };
2408
+ return (rank[String(a.severity)] ?? 4) - (rank[String(b.severity)] ?? 4);
2409
+ });
2410
+ for (const r of openUserReports.slice(0, Math.max(0, 4 - steps.length))) {
2411
+ steps.push({
2412
+ priority: priority++,
2413
+ action: `Triage [${r.severity}] ${label(r)}`,
2414
+ reason: "User-felt report awaiting triage \u2014 review the packet, then dispatch or dismiss.",
2415
+ tool: "triage_issue",
2416
+ args: { report_id: r.id }
2417
+ });
2418
+ }
2419
+ const chores = reports.filter((r) => r.status === "classified" && isCron(r) && !isBlocked(r));
2420
+ if (chores.length > 0 && steps.length < 5) {
2421
+ steps.push({
2422
+ priority: priority++,
2423
+ action: `Batch ${chores.length} maintenance chore${chores.length === 1 ? "" : "s"} (dependency bumps)`,
2424
+ reason: "Robot-filed modernization reports \u2014 batch-dispatch or dismiss in one sitting; do not let them crowd out user bugs.",
2425
+ tool: "get_recent_reports",
2426
+ args: { status: "classified" }
2427
+ });
2428
+ }
2429
+ 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 ");
2430
+ return jsonResult({ steps, summary });
2431
+ }
2432
+ );
2068
2433
  server.registerTool(
2069
2434
  "dispatch_fix",
2070
2435
  {
2071
2436
  title: titleOf("dispatch_fix"),
2072
2437
  description: descOf("dispatch_fix"),
2073
2438
  annotations: annotationsFor("dispatch_fix"),
2074
- inputSchema: {
2439
+ inputSchema: z.object({
2075
2440
  reportId: z.string().uuid().describe("Report UUID to fix"),
2076
2441
  agent: z.enum(["claude_code", "codex", "rest_worker", "mcp"]).optional().describe("Override the agent adapter"),
2077
2442
  idempotencyKey: z.string().uuid().optional().describe(
@@ -2080,19 +2445,19 @@ function createMushiServer(config) {
2080
2445
  inventoryActionNodeId: z.string().uuid().optional().describe(
2081
2446
  "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."
2082
2447
  )
2083
- },
2084
- outputSchema: {
2448
+ }),
2449
+ outputSchema: z.object({
2085
2450
  fixId: z.string(),
2086
2451
  status: z.string()
2087
- }
2452
+ })
2088
2453
  },
2089
- async (args, extra) => {
2090
- if (extra?.sendNotification && extra?._meta?.progressToken) {
2454
+ async (args, ctx) => {
2455
+ if (ctx?.mcpReq.notify && ctx?.mcpReq._meta?.progressToken) {
2091
2456
  try {
2092
- await extra.sendNotification({
2457
+ await ctx.mcpReq.notify({
2093
2458
  method: "notifications/progress",
2094
2459
  params: {
2095
- progressToken: extra._meta.progressToken,
2460
+ progressToken: ctx.mcpReq._meta.progressToken,
2096
2461
  progress: 0,
2097
2462
  total: 100,
2098
2463
  message: "Dispatching Mushi fix orchestrator\u2026"
@@ -2101,7 +2466,7 @@ function createMushiServer(config) {
2101
2466
  } catch {
2102
2467
  }
2103
2468
  }
2104
- const data = await apiCall("/v1/admin/fixes/dispatch", {
2469
+ const dispatch = await apiCall("/v1/admin/fixes/dispatch", {
2105
2470
  method: "POST",
2106
2471
  headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
2107
2472
  body: JSON.stringify({
@@ -2111,7 +2476,12 @@ function createMushiServer(config) {
2111
2476
  ...projectId ? { projectId } : {}
2112
2477
  })
2113
2478
  });
2114
- return jsonResult(data);
2479
+ if (!dispatch?.dispatchId) {
2480
+ throw new Error(
2481
+ `Dispatch response missing dispatchId \u2014 got: ${JSON.stringify(dispatch).slice(0, 300)}`
2482
+ );
2483
+ }
2484
+ return jsonResult({ fixId: dispatch.dispatchId, status: dispatch.status ?? "queued" });
2115
2485
  }
2116
2486
  );
2117
2487
  server.registerTool(
@@ -2120,13 +2490,13 @@ function createMushiServer(config) {
2120
2490
  title: titleOf("trigger_judge"),
2121
2491
  description: descOf("trigger_judge"),
2122
2492
  annotations: annotationsFor("trigger_judge"),
2123
- inputSchema: {
2493
+ inputSchema: z.object({
2124
2494
  limit: z.number().optional().describe("Max reports to judge in this batch (default 25, max 100)"),
2125
2495
  projectId: z.string().optional().describe("Restrict to one project when the API key owns multiple")
2126
- },
2127
- outputSchema: {
2496
+ }),
2497
+ outputSchema: z.object({
2128
2498
  dispatched: z.number().describe("Number of judge-batch jobs dispatched (one per accessible project)")
2129
- }
2499
+ })
2130
2500
  },
2131
2501
  async (args) => {
2132
2502
  const data = await apiCall("/v1/admin/judge/run", {
@@ -2145,10 +2515,10 @@ function createMushiServer(config) {
2145
2515
  title: titleOf("test_gen_from_report"),
2146
2516
  description: descOf("test_gen_from_report"),
2147
2517
  annotations: annotationsFor("test_gen_from_report"),
2148
- inputSchema: {
2518
+ inputSchema: z.object({
2149
2519
  reportId: z.string().describe("Report UUID to turn into a Playwright PR"),
2150
2520
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
2151
- }
2521
+ })
2152
2522
  },
2153
2523
  async (args) => {
2154
2524
  const pid = args.projectId ?? projectId;
@@ -2174,16 +2544,16 @@ function createMushiServer(config) {
2174
2544
  title: titleOf("merge_fix"),
2175
2545
  description: descOf("merge_fix"),
2176
2546
  annotations: annotationsFor("merge_fix"),
2177
- inputSchema: {
2547
+ inputSchema: z.object({
2178
2548
  fixId: z.string().describe("Fix attempt UUID whose GitHub PR should be squash-merged"),
2179
2549
  mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method (default squash)")
2180
- },
2181
- outputSchema: {
2550
+ }),
2551
+ outputSchema: z.object({
2182
2552
  merged: z.boolean().optional().describe("True when GitHub accepted the merge in this call"),
2183
2553
  alreadyMerged: z.boolean().optional().describe("True when the PR was already merged (idempotent no-op)"),
2184
2554
  reportId: z.string().describe("Report UUID linked to this fix attempt"),
2185
2555
  reportStatus: z.string().describe("Report workflow status after merge bookkeeping")
2186
- }
2556
+ })
2187
2557
  },
2188
2558
  async (args) => {
2189
2559
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/merge`, {
@@ -2199,14 +2569,14 @@ function createMushiServer(config) {
2199
2569
  title: titleOf("refresh_ci"),
2200
2570
  description: descOf("refresh_ci"),
2201
2571
  annotations: annotationsFor("refresh_ci"),
2202
- inputSchema: {
2572
+ inputSchema: z.object({
2203
2573
  fixId: z.string().describe("Fix attempt UUID whose PR check-runs should be re-polled")
2204
- },
2205
- outputSchema: {
2574
+ }),
2575
+ outputSchema: z.object({
2206
2576
  check_run_status: z.string().nullable().describe("GitHub check run status after refresh"),
2207
2577
  check_run_conclusion: z.string().nullable().describe("success | failure | neutral | null while pending"),
2208
2578
  check_run_updated_at: z.string().nullable().describe("ISO timestamp when CI status was last persisted")
2209
- }
2579
+ })
2210
2580
  },
2211
2581
  async (args) => {
2212
2582
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/refresh-ci`, { method: "POST" });
@@ -2219,13 +2589,13 @@ function createMushiServer(config) {
2219
2589
  title: titleOf("reopen_report"),
2220
2590
  description: descOf("reopen_report"),
2221
2591
  annotations: annotationsFor("reopen_report"),
2222
- inputSchema: {
2592
+ inputSchema: z.object({
2223
2593
  reportId: z.string().describe("Report UUID to move back to reopened status"),
2224
2594
  note: z.string().optional().describe("Operator note recorded on the reopen transition")
2225
- },
2226
- outputSchema: {
2595
+ }),
2596
+ outputSchema: z.object({
2227
2597
  report: z.unknown().describe("Updated report row with status=reopened")
2228
- }
2598
+ })
2229
2599
  },
2230
2600
  async (args) => {
2231
2601
  const report = await apiCall(`/v1/sync/reports/${args.reportId}`, {
@@ -2241,11 +2611,16 @@ function createMushiServer(config) {
2241
2611
  title: titleOf("transition_status"),
2242
2612
  description: descOf("transition_status"),
2243
2613
  annotations: annotationsFor("transition_status"),
2244
- inputSchema: {
2614
+ inputSchema: z.object({
2245
2615
  reportId: z.string().describe("Report UUID"),
2246
2616
  status: z.enum([
2247
2617
  "pending",
2248
2618
  "classified",
2619
+ // Workflow states added 20260520600000 but missing from this
2620
+ // enum ever since (2026-08-16 audit P2-2) — agents could not
2621
+ // mark a report as being looked at.
2622
+ "triaged",
2623
+ "in_progress",
2249
2624
  "grouped",
2250
2625
  "fixing",
2251
2626
  "fixed",
@@ -2253,9 +2628,9 @@ function createMushiServer(config) {
2253
2628
  "verified",
2254
2629
  "reopened",
2255
2630
  "dismissed"
2256
- ]).describe("Target status"),
2631
+ ]).describe("Target status (resolved is stored as fixed)"),
2257
2632
  reason: z.string().optional().describe("Reason for the transition (audit trail)")
2258
- }
2633
+ })
2259
2634
  },
2260
2635
  async (args) => {
2261
2636
  const data = await apiCall(`/v1/admin/reports/${args.reportId}`, {
@@ -2271,9 +2646,9 @@ function createMushiServer(config) {
2271
2646
  title: titleOf("setup_repo_for_mushi"),
2272
2647
  description: descOf("setup_repo_for_mushi"),
2273
2648
  annotations: annotationsFor("setup_repo_for_mushi"),
2274
- inputSchema: {
2649
+ inputSchema: z.object({
2275
2650
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
2276
- }
2651
+ })
2277
2652
  },
2278
2653
  async (args) => {
2279
2654
  const pid = args.projectId ?? projectId;
@@ -2296,12 +2671,12 @@ function createMushiServer(config) {
2296
2671
  title: titleOf("query_lessons"),
2297
2672
  description: descOf("query_lessons"),
2298
2673
  annotations: annotationsFor("query_lessons"),
2299
- inputSchema: {
2674
+ inputSchema: z.object({
2300
2675
  diff_text: z.string().describe("The PR diff, code snippet, or description of the change being made."),
2301
2676
  max_tokens: z.number().optional().describe("Maximum tokens for returned lessons context (default 3000, max 8000)."),
2302
2677
  top_k: z.number().optional().describe("Max number of lessons to return (default 15, max 50)."),
2303
2678
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
2304
- }
2679
+ })
2305
2680
  },
2306
2681
  async (args) => {
2307
2682
  const pid = args.project_id ?? projectId;
@@ -2323,14 +2698,14 @@ function createMushiServer(config) {
2323
2698
  title: titleOf("list_lessons"),
2324
2699
  description: descOf("list_lessons"),
2325
2700
  annotations: annotationsFor("list_lessons"),
2326
- inputSchema: {
2701
+ inputSchema: z.object({
2327
2702
  severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter to one severity level"),
2328
2703
  limit: z.number().optional().describe("Max lessons to return (default 50, max 200)"),
2329
2704
  project_id: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
2330
- },
2331
- outputSchema: {
2705
+ }),
2706
+ outputSchema: z.object({
2332
2707
  lessons: z.array(z.unknown()).describe("Promoted lesson rows ordered by frequency")
2333
- }
2708
+ })
2334
2709
  },
2335
2710
  async (args) => {
2336
2711
  const pid = args.project_id ?? projectId;
@@ -2342,7 +2717,7 @@ function createMushiServer(config) {
2342
2717
  return wrappedJsonResult({ lessons }, "lesson text");
2343
2718
  }
2344
2719
  );
2345
- server.resource(
2720
+ server.registerResource(
2346
2721
  "project_stats",
2347
2722
  "project://stats",
2348
2723
  { description: "Report counts, category breakdown, severity distribution" },
@@ -2356,7 +2731,7 @@ function createMushiServer(config) {
2356
2731
  ]
2357
2732
  })
2358
2733
  );
2359
- server.resource(
2734
+ server.registerResource(
2360
2735
  "project_settings",
2361
2736
  "project://settings",
2362
2737
  {
@@ -2372,7 +2747,7 @@ function createMushiServer(config) {
2372
2747
  ]
2373
2748
  })
2374
2749
  );
2375
- server.resource(
2750
+ server.registerResource(
2376
2751
  "project_dashboard",
2377
2752
  "project://dashboard",
2378
2753
  {
@@ -2394,10 +2769,10 @@ function createMushiServer(config) {
2394
2769
  title: titleOf("list_top_contributors"),
2395
2770
  description: descOf("list_top_contributors"),
2396
2771
  annotations: annotationsFor("list_top_contributors"),
2397
- inputSchema: {
2772
+ inputSchema: z.object({
2398
2773
  limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
2399
2774
  range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
2400
- }
2775
+ })
2401
2776
  },
2402
2777
  async ({ limit, range }) => ({
2403
2778
  content: [
@@ -2418,11 +2793,11 @@ function createMushiServer(config) {
2418
2793
  title: titleOf("award_bonus_points"),
2419
2794
  description: descOf("award_bonus_points"),
2420
2795
  annotations: annotationsFor("award_bonus_points"),
2421
- inputSchema: {
2796
+ inputSchema: z.object({
2422
2797
  external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
2423
2798
  points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
2424
2799
  reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
2425
- }
2800
+ })
2426
2801
  },
2427
2802
  async ({ external_user_id, points, reason }) => ({
2428
2803
  content: [
@@ -2447,11 +2822,11 @@ function createMushiServer(config) {
2447
2822
  title: titleOf("set_tier"),
2448
2823
  description: descOf("set_tier"),
2449
2824
  annotations: annotationsFor("set_tier"),
2450
- inputSchema: {
2825
+ inputSchema: z.object({
2451
2826
  external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
2452
2827
  tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
2453
2828
  reason: z.string().max(200).optional().describe("Optional reason for manual override")
2454
- }
2829
+ })
2455
2830
  },
2456
2831
  async ({ external_user_id, tier_slug, reason }) => ({
2457
2832
  content: [
@@ -2470,7 +2845,7 @@ function createMushiServer(config) {
2470
2845
  ]
2471
2846
  })
2472
2847
  );
2473
- server.resource(
2848
+ server.registerResource(
2474
2849
  "privacy_status",
2475
2850
  "privacy://status",
2476
2851
  {
@@ -2486,7 +2861,7 @@ function createMushiServer(config) {
2486
2861
  ]
2487
2862
  })
2488
2863
  );
2489
- server.resource(
2864
+ server.registerResource(
2490
2865
  "evolution_history",
2491
2866
  "evolution://history",
2492
2867
  {
@@ -2508,7 +2883,7 @@ function createMushiServer(config) {
2508
2883
  ]
2509
2884
  })
2510
2885
  );
2511
- server.resource(
2886
+ server.registerResource(
2512
2887
  "project_integration_health",
2513
2888
  "project://integration-health",
2514
2889
  {
@@ -2524,7 +2899,7 @@ function createMushiServer(config) {
2524
2899
  ]
2525
2900
  })
2526
2901
  );
2527
- server.resource(
2902
+ server.registerResource(
2528
2903
  "inventory_current",
2529
2904
  "inventory://current",
2530
2905
  {
@@ -2549,9 +2924,9 @@ function createMushiServer(config) {
2549
2924
  title: titleOf("activation_status"),
2550
2925
  description: descOf("activation_status"),
2551
2926
  annotations: annotationsFor("activation_status"),
2552
- inputSchema: {
2927
+ inputSchema: z.object({
2553
2928
  project_id: z.string().optional().describe("Project UUID (defaults to the configured project).")
2554
- }
2929
+ })
2555
2930
  },
2556
2931
  async (args) => {
2557
2932
  const pid = args.project_id ?? projectId;
@@ -2559,7 +2934,7 @@ function createMushiServer(config) {
2559
2934
  return jsonResult(await apiCall(`/v1/admin/activation${qs}`));
2560
2935
  }
2561
2936
  );
2562
- server.resource(
2937
+ server.registerResource(
2563
2938
  "activation_status",
2564
2939
  "mushi://activation",
2565
2940
  {
@@ -2578,10 +2953,12 @@ function createMushiServer(config) {
2578
2953
  };
2579
2954
  }
2580
2955
  );
2581
- server.prompt(
2956
+ server.registerPrompt(
2582
2957
  "summarize_report_for_fix",
2583
- "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.",
2584
- { reportId: z.string().describe("The report UUID to summarize") },
2958
+ {
2959
+ description: "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.",
2960
+ argsSchema: z.object({ reportId: z.string().describe("The report UUID to summarize") })
2961
+ },
2585
2962
  ({ reportId }) => ({
2586
2963
  messages: [
2587
2964
  {
@@ -2606,10 +2983,12 @@ Lead with the fix. Skip the preamble.`
2606
2983
  ]
2607
2984
  })
2608
2985
  );
2609
- server.prompt(
2986
+ server.registerPrompt(
2610
2987
  "explain_judge_result",
2611
- "Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
2612
- { fixId: z.string().describe("The fix_attempt UUID to explain") },
2988
+ {
2989
+ description: "Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
2990
+ argsSchema: z.object({ fixId: z.string().describe("The fix_attempt UUID to explain") })
2991
+ },
2613
2992
  ({ fixId }) => ({
2614
2993
  messages: [
2615
2994
  {
@@ -2630,10 +3009,12 @@ No preamble, no score-by-score recap \u2014 the numbers are in the tool output.`
2630
3009
  ]
2631
3010
  })
2632
3011
  );
2633
- server.prompt(
3012
+ server.registerPrompt(
2634
3013
  "triage_next_steps",
2635
- 'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
2636
- {},
3014
+ {
3015
+ description: 'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
3016
+ argsSchema: z.object({})
3017
+ },
2637
3018
  () => ({
2638
3019
  messages: [
2639
3020
  {
@@ -2653,10 +3034,12 @@ Prefer items that are bottlenecks or critical severity. Skip filler.`
2653
3034
  ]
2654
3035
  })
2655
3036
  );
2656
- server.prompt(
3037
+ server.registerPrompt(
2657
3038
  "mushi_setup",
2658
- "Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
2659
- {},
3039
+ {
3040
+ description: "Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
3041
+ argsSchema: z.object({})
3042
+ },
2660
3043
  () => ({
2661
3044
  messages: [
2662
3045
  {
@@ -2686,13 +3069,13 @@ Be specific. No generic advice.`
2686
3069
  title: titleOf("map_user_stories", TDD_TOOL_CATALOG),
2687
3070
  description: descOf("map_user_stories", TDD_TOOL_CATALOG),
2688
3071
  annotations: annotationsFor("map_user_stories", TDD_TOOL_CATALOG),
2689
- inputSchema: {
3072
+ inputSchema: z.object({
2690
3073
  projectId: z.string().describe("Project id to map stories for"),
2691
3074
  baseUrl: z.string().url().describe("Live app URL to crawl"),
2692
3075
  maxPages: z.number().int().min(1).max(50).optional().describe("Max pages to crawl (default 20)"),
2693
3076
  provider: z.enum(["firecrawl", "browserbase"]).optional().describe("Crawl provider (default: firecrawl)"),
2694
3077
  cursorCloudRefine: z.boolean().optional().describe("Dispatch Cursor Cloud agent to refine and open a PR")
2695
- }
3078
+ })
2696
3079
  },
2697
3080
  async ({ projectId: projectId2, baseUrl, maxPages, provider, cursorCloudRefine }) => {
2698
3081
  if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
@@ -2717,9 +3100,9 @@ Be specific. No generic advice.`
2717
3100
  title: titleOf("get_map_run_status", TDD_TOOL_CATALOG),
2718
3101
  description: descOf("get_map_run_status", TDD_TOOL_CATALOG),
2719
3102
  annotations: annotationsFor("get_map_run_status", TDD_TOOL_CATALOG),
2720
- inputSchema: {
3103
+ inputSchema: z.object({
2721
3104
  projectId: z.string().describe("Project id")
2722
- }
3105
+ })
2723
3106
  },
2724
3107
  async ({ projectId: projectId2 }) => {
2725
3108
  const data = await apiCall(`/v1/admin/inventory/${projectId2}/map-runs`);
@@ -2732,7 +3115,7 @@ Be specific. No generic advice.`
2732
3115
  title: titleOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
2733
3116
  description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
2734
3117
  annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
2735
- inputSchema: {
3118
+ inputSchema: z.object({
2736
3119
  projectId: z.string().describe("Project UUID that owns the inventory story"),
2737
3120
  storyNodeId: z.string().describe(
2738
3121
  "User story node id/slug from accepted inventory (from map_user_stories or inventory.yaml)"
@@ -2740,13 +3123,13 @@ Be specific. No generic advice.`
2740
3123
  automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Approval gate for the generated qa_story (default review \u2192 pending_review)"),
2741
3124
  baseUrl: z.string().url().optional().describe("Override the live app URL the Playwright test should target"),
2742
3125
  openPr: z.boolean().optional().describe("Open a draft GitHub PR with the generated spec (default true)")
2743
- },
2744
- outputSchema: {
3126
+ }),
3127
+ outputSchema: z.object({
2745
3128
  qaStoryId: z.string().describe("UUID of the inserted qa_stories row"),
2746
3129
  prUrl: z.string().nullable().describe("GitHub PR URL when openPr=true and GitHub is connected"),
2747
3130
  approvalStatus: z.string().describe("pending_review | approved \u2014 follows automationMode and project defaults"),
2748
3131
  needsHumanReview: z.boolean().describe("True when the story requires operator approval before scheduled runs")
2749
- }
3132
+ })
2750
3133
  },
2751
3134
  async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
2752
3135
  if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
@@ -2767,9 +3150,9 @@ Be specific. No generic advice.`
2767
3150
  title: titleOf("improve_qa_story", TDD_TOOL_CATALOG),
2768
3151
  description: descOf("improve_qa_story", TDD_TOOL_CATALOG),
2769
3152
  annotations: annotationsFor("improve_qa_story", TDD_TOOL_CATALOG),
2770
- inputSchema: {
3153
+ inputSchema: z.object({
2771
3154
  projectId: z.string().optional().describe("Project id (omit to run across all projects)")
2772
- }
3155
+ })
2773
3156
  },
2774
3157
  async ({ projectId: projectId2 }) => {
2775
3158
  const data = await apiCall("/v1/admin/pdca/improve-qa-stories", {
@@ -2785,10 +3168,10 @@ Be specific. No generic advice.`
2785
3168
  title: titleOf("run_qa_story", TDD_TOOL_CATALOG),
2786
3169
  description: descOf("run_qa_story", TDD_TOOL_CATALOG),
2787
3170
  annotations: annotationsFor("run_qa_story", TDD_TOOL_CATALOG),
2788
- inputSchema: {
3171
+ inputSchema: z.object({
2789
3172
  projectId: z.string().describe("Project id"),
2790
3173
  qaStoryId: z.string().describe("qa_story id to run")
2791
- }
3174
+ })
2792
3175
  },
2793
3176
  async ({ projectId: projectId2, qaStoryId }) => {
2794
3177
  const data = await apiCall(
@@ -2804,9 +3187,9 @@ Be specific. No generic advice.`
2804
3187
  title: titleOf("list_byok_keys", TDD_TOOL_CATALOG),
2805
3188
  description: descOf("list_byok_keys", TDD_TOOL_CATALOG),
2806
3189
  annotations: annotationsFor("list_byok_keys", TDD_TOOL_CATALOG),
2807
- inputSchema: {
3190
+ inputSchema: z.object({
2808
3191
  projectId: z.string().describe("Project id")
2809
- }
3192
+ })
2810
3193
  },
2811
3194
  async ({ projectId: projectId2 }) => {
2812
3195
  const data = await apiCall(
@@ -2821,14 +3204,14 @@ Be specific. No generic advice.`
2821
3204
  title: titleOf("add_byok_key", TDD_TOOL_CATALOG),
2822
3205
  description: descOf("add_byok_key", TDD_TOOL_CATALOG),
2823
3206
  annotations: annotationsFor("add_byok_key", TDD_TOOL_CATALOG),
2824
- inputSchema: {
3207
+ inputSchema: z.object({
2825
3208
  projectId: z.string().describe("Project id"),
2826
3209
  provider: z.enum(["anthropic", "openai", "firecrawl", "browserbase", "cursor"]).describe("Provider slug"),
2827
3210
  key: z.string().min(8).max(4096).describe("The API key value to add"),
2828
3211
  label: z.string().optional().describe("Human-readable label for this key"),
2829
3212
  priority: z.number().int().min(0).max(1e4).optional().describe("Priority for ordering (lower = higher priority)"),
2830
3213
  baseUrl: z.string().url().max(2048).optional().describe("Allow-listed OpenAI-compatible HTTPS base URL")
2831
- }
3214
+ })
2832
3215
  },
2833
3216
  async ({ projectId: projectId2, provider, key, label, priority, baseUrl }) => {
2834
3217
  const data = await apiCall("/v1/admin/byok/keys", {
@@ -2851,10 +3234,10 @@ Be specific. No generic advice.`
2851
3234
  title: titleOf("test_byok_key", TDD_TOOL_CATALOG),
2852
3235
  description: descOf("test_byok_key", TDD_TOOL_CATALOG),
2853
3236
  annotations: annotationsFor("test_byok_key", TDD_TOOL_CATALOG),
2854
- inputSchema: {
3237
+ inputSchema: z.object({
2855
3238
  projectId: z.string().describe("Project id"),
2856
3239
  keyId: z.string().uuid().describe("Pooled BYOK key id")
2857
- }
3240
+ })
2858
3241
  },
2859
3242
  async ({ keyId }) => {
2860
3243
  const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}/test`, {
@@ -2869,10 +3252,10 @@ Be specific. No generic advice.`
2869
3252
  title: titleOf("remove_byok_key", TDD_TOOL_CATALOG),
2870
3253
  description: descOf("remove_byok_key", TDD_TOOL_CATALOG),
2871
3254
  annotations: annotationsFor("remove_byok_key", TDD_TOOL_CATALOG),
2872
- inputSchema: {
3255
+ inputSchema: z.object({
2873
3256
  projectId: z.string().describe("Project id"),
2874
3257
  keyId: z.string().uuid().describe("Pooled BYOK key UUID from list_byok_keys")
2875
- }
3258
+ })
2876
3259
  },
2877
3260
  async ({ keyId }) => {
2878
3261
  const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}`, {
@@ -2887,9 +3270,9 @@ Be specific. No generic advice.`
2887
3270
  title: titleOf("list_pending_review_stories", TDD_TOOL_CATALOG),
2888
3271
  description: descOf("list_pending_review_stories", TDD_TOOL_CATALOG),
2889
3272
  annotations: annotationsFor("list_pending_review_stories", TDD_TOOL_CATALOG),
2890
- inputSchema: {
3273
+ inputSchema: z.object({
2891
3274
  projectId: z.string().describe("Project id")
2892
- }
3275
+ })
2893
3276
  },
2894
3277
  async ({ projectId: projectId2 }) => {
2895
3278
  const data = await apiCall(
@@ -2904,11 +3287,11 @@ Be specific. No generic advice.`
2904
3287
  title: titleOf("approve_qa_story", TDD_TOOL_CATALOG),
2905
3288
  description: descOf("approve_qa_story", TDD_TOOL_CATALOG),
2906
3289
  annotations: annotationsFor("approve_qa_story", TDD_TOOL_CATALOG),
2907
- inputSchema: {
3290
+ inputSchema: z.object({
2908
3291
  projectId: z.string().describe("Project id"),
2909
3292
  qaStoryId: z.string().describe("QA story id to approve or reject"),
2910
3293
  status: z.enum(["approved", "rejected"]).describe("New approval status")
2911
- }
3294
+ })
2912
3295
  },
2913
3296
  async ({ projectId: projectId2, qaStoryId, status }) => {
2914
3297
  const data = await apiCall(
@@ -2924,11 +3307,11 @@ Be specific. No generic advice.`
2924
3307
  title: titleOf("reply_to_reporter", TDD_TOOL_CATALOG),
2925
3308
  description: descOf("reply_to_reporter", TDD_TOOL_CATALOG),
2926
3309
  annotations: annotationsFor("reply_to_reporter", TDD_TOOL_CATALOG),
2927
- inputSchema: {
3310
+ inputSchema: z.object({
2928
3311
  reportId: z.string().describe("Report id to reply to"),
2929
3312
  message: z.string().min(1).max(1e4).describe("Message text to send to the reporter"),
2930
3313
  authorName: z.string().optional().describe('Display name for the admin sender (default: "Mushi Admin")')
2931
- }
3314
+ })
2932
3315
  },
2933
3316
  async ({ reportId, message, authorName }) => {
2934
3317
  const data = await apiCall(`/v1/sync/reports/${reportId}/reply`, {
@@ -2944,10 +3327,10 @@ Be specific. No generic advice.`
2944
3327
  title: titleOf("list_qa_story_runs", TDD_TOOL_CATALOG),
2945
3328
  description: descOf("list_qa_story_runs", TDD_TOOL_CATALOG),
2946
3329
  annotations: annotationsFor("list_qa_story_runs", TDD_TOOL_CATALOG),
2947
- inputSchema: {
3330
+ inputSchema: z.object({
2948
3331
  storyId: z.string().describe("QA story id (uuid)"),
2949
3332
  limit: z.number().int().min(1).max(50).optional().default(10).describe("Max runs to return (default 10)")
2950
- }
3333
+ })
2951
3334
  },
2952
3335
  async ({ storyId, limit }) => {
2953
3336
  const data = await apiCall(
@@ -2962,10 +3345,10 @@ Be specific. No generic advice.`
2962
3345
  title: titleOf("get_qa_story_run", TDD_TOOL_CATALOG),
2963
3346
  description: descOf("get_qa_story_run", TDD_TOOL_CATALOG),
2964
3347
  annotations: annotationsFor("get_qa_story_run", TDD_TOOL_CATALOG),
2965
- inputSchema: {
3348
+ inputSchema: z.object({
2966
3349
  storyId: z.string().describe("QA story id (uuid)"),
2967
3350
  runId: z.string().describe("Run id (uuid) to fetch detail for")
2968
- }
3351
+ })
2969
3352
  },
2970
3353
  async ({ storyId, runId }) => {
2971
3354
  const data = await apiCall(
@@ -2988,9 +3371,9 @@ Be specific. No generic advice.`
2988
3371
  title: titleOf("test_notification_channel", TDD_TOOL_CATALOG),
2989
3372
  description: descOf("test_notification_channel", TDD_TOOL_CATALOG),
2990
3373
  annotations: annotationsFor("test_notification_channel", TDD_TOOL_CATALOG),
2991
- inputSchema: {
3374
+ inputSchema: z.object({
2992
3375
  kind: z.enum(["slack", "discord"]).describe("Notification channel kind to test")
2993
- }
3376
+ })
2994
3377
  },
2995
3378
  async ({ kind }) => {
2996
3379
  const data = await apiCall(
@@ -3006,9 +3389,9 @@ Be specific. No generic advice.`
3006
3389
  title: titleOf("run_fullstack_audit"),
3007
3390
  description: descOf("run_fullstack_audit"),
3008
3391
  annotations: annotationsFor("run_fullstack_audit"),
3009
- inputSchema: {
3392
+ inputSchema: z.object({
3010
3393
  project_id: z.string().optional().describe("Project ID to audit. Defaults to the configured project.")
3011
- }
3394
+ })
3012
3395
  },
3013
3396
  async ({ project_id }) => {
3014
3397
  const pid = project_id ?? config.projectId;
@@ -3026,10 +3409,10 @@ Be specific. No generic advice.`
3026
3409
  title: titleOf("get_backend_health"),
3027
3410
  description: descOf("get_backend_health"),
3028
3411
  annotations: annotationsFor("get_backend_health"),
3029
- inputSchema: {
3412
+ inputSchema: z.object({
3030
3413
  project_id: z.string().optional().describe("Project ID. Defaults to the configured project."),
3031
3414
  include_logs: z.boolean().optional().describe("Whether to include recent backend error logs (default: true).")
3032
- }
3415
+ })
3033
3416
  },
3034
3417
  async ({ project_id, include_logs = true }) => {
3035
3418
  const pid = project_id ?? config.projectId;
@@ -3053,9 +3436,9 @@ Be specific. No generic advice.`
3053
3436
  title: titleOf("get_usage"),
3054
3437
  description: descOf("get_usage"),
3055
3438
  annotations: annotationsFor("get_usage"),
3056
- inputSchema: {
3439
+ inputSchema: z.object({
3057
3440
  project_id: z.string().optional().describe("Project ID. Defaults to the configured project.")
3058
- }
3441
+ })
3059
3442
  },
3060
3443
  async ({ project_id }) => {
3061
3444
  const pid = project_id ?? config.projectId;
@@ -3070,12 +3453,12 @@ Be specific. No generic advice.`
3070
3453
  title: titleOf("list_skills"),
3071
3454
  description: descOf("list_skills"),
3072
3455
  annotations: annotationsFor("list_skills"),
3073
- inputSchema: {
3456
+ inputSchema: z.object({
3074
3457
  category: z.string().optional().describe("Filter by category: workflow, debug, test, audit, enhance, \u2026"),
3075
3458
  search: z.string().optional().describe("Free-text search across slug, title, description"),
3076
3459
  page: z.number().optional().describe("Page number (default 1)"),
3077
3460
  limit: z.number().optional().describe("Max results per page (default 200, max 200)")
3078
- }
3461
+ })
3079
3462
  },
3080
3463
  async (args) => {
3081
3464
  const qs = new URLSearchParams();
@@ -3094,9 +3477,9 @@ Be specific. No generic advice.`
3094
3477
  title: titleOf("get_skill"),
3095
3478
  description: descOf("get_skill"),
3096
3479
  annotations: annotationsFor("get_skill"),
3097
- inputSchema: {
3480
+ inputSchema: z.object({
3098
3481
  slug: z.string().describe('Skill slug, e.g. "workflow-fix-and-ship"')
3099
- }
3482
+ })
3100
3483
  },
3101
3484
  async (args) => {
3102
3485
  const data = await apiCall(`/v1/admin/skills/${args.slug}`);
@@ -3109,14 +3492,14 @@ Be specific. No generic advice.`
3109
3492
  title: titleOf("start_skill_pipeline"),
3110
3493
  description: descOf("start_skill_pipeline"),
3111
3494
  annotations: annotationsFor("start_skill_pipeline"),
3112
- inputSchema: {
3495
+ inputSchema: z.object({
3113
3496
  root_skill_slug: z.string().describe('Root skill slug to run, e.g. "workflow-fix-and-ship"'),
3114
3497
  report_id: z.string().optional().describe("Report UUID to attach the pipeline to"),
3115
3498
  mode: z.enum(["handoff", "cloud"]).optional().describe(
3116
3499
  "handoff (default): get context packet for local agent. cloud: auto-dispatch via Cursor Cloud."
3117
3500
  ),
3118
3501
  project_id: z.string().optional().describe("Project UUID. Falls back to the configured project.")
3119
- }
3502
+ })
3120
3503
  },
3121
3504
  async (args) => {
3122
3505
  const resolvedProjectId = args.project_id ?? projectId;
@@ -3134,9 +3517,9 @@ Be specific. No generic advice.`
3134
3517
  title: titleOf("get_pipeline_run"),
3135
3518
  description: descOf("get_pipeline_run"),
3136
3519
  annotations: annotationsFor("get_pipeline_run"),
3137
- inputSchema: {
3520
+ inputSchema: z.object({
3138
3521
  run_id: z.string().describe("Pipeline run UUID")
3139
- }
3522
+ })
3140
3523
  },
3141
3524
  async (args) => {
3142
3525
  const data = await apiCall(`/v1/admin/skills/pipelines/${args.run_id}`);
@@ -3149,14 +3532,14 @@ Be specific. No generic advice.`
3149
3532
  title: titleOf("checkin_pipeline_step"),
3150
3533
  description: descOf("checkin_pipeline_step"),
3151
3534
  annotations: annotationsFor("checkin_pipeline_step"),
3152
- inputSchema: {
3535
+ inputSchema: z.object({
3153
3536
  run_id: z.string().describe("Pipeline run UUID"),
3154
3537
  step_index: z.number().describe("Step index (0-based)"),
3155
3538
  status: z.enum(["running", "passed", "failed", "skipped"]).describe("Step status"),
3156
3539
  notes: z.string().optional().describe("Optional notes or output summary"),
3157
3540
  pr_url: z.string().optional().describe("PR URL opened during this step"),
3158
3541
  agent_ref: z.string().optional().describe("Cursor agentId or external agent reference")
3159
- }
3542
+ })
3160
3543
  },
3161
3544
  async (args) => {
3162
3545
  const { run_id, step_index, ...body } = args;
@@ -3173,13 +3556,13 @@ Be specific. No generic advice.`
3173
3556
  title: titleOf("ask_codebase", CODEBASE_TOOL_CATALOG),
3174
3557
  description: descOf("ask_codebase", CODEBASE_TOOL_CATALOG),
3175
3558
  annotations: annotationsFor("ask_codebase", CODEBASE_TOOL_CATALOG),
3176
- inputSchema: {
3559
+ inputSchema: z.object({
3177
3560
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3178
3561
  question: z.string().describe("Plain-English question about the repo"),
3179
3562
  thread_id: z.string().optional().describe("Optional thread UUID to continue a conversation"),
3180
3563
  file_path: z.string().optional().describe("Optional file path focus"),
3181
3564
  symbol_name: z.string().optional().describe("Optional symbol name focus")
3182
- }
3565
+ })
3183
3566
  },
3184
3567
  async (args) => {
3185
3568
  const pid = args.project_id ?? projectId;
@@ -3207,12 +3590,12 @@ Be specific. No generic advice.`
3207
3590
  title: titleOf("get_file_summary", CODEBASE_TOOL_CATALOG),
3208
3591
  description: descOf("get_file_summary", CODEBASE_TOOL_CATALOG),
3209
3592
  annotations: annotationsFor("get_file_summary", CODEBASE_TOOL_CATALOG),
3210
- inputSchema: {
3593
+ inputSchema: z.object({
3211
3594
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3212
3595
  file_path: z.string().describe("Indexed file path"),
3213
3596
  symbol_name: z.string().optional().describe("Optional symbol name within the file"),
3214
3597
  force: z.boolean().optional().describe("Bypass cache and regenerate")
3215
- }
3598
+ })
3216
3599
  },
3217
3600
  async (args) => {
3218
3601
  const pid = args.project_id ?? projectId;
@@ -3230,10 +3613,10 @@ Be specific. No generic advice.`
3230
3613
  title: titleOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
3231
3614
  description: descOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
3232
3615
  annotations: annotationsFor("get_codebase_tour", CODEBASE_TOOL_CATALOG),
3233
- inputSchema: {
3616
+ inputSchema: z.object({
3234
3617
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3235
3618
  force: z.boolean().optional().describe("Bypass cache and regenerate")
3236
- }
3619
+ })
3237
3620
  },
3238
3621
  async (args) => {
3239
3622
  const pid = args.project_id ?? projectId;
@@ -3249,17 +3632,17 @@ Be specific. No generic advice.`
3249
3632
  title: titleOf("search_codebase", CODEBASE_TOOL_CATALOG),
3250
3633
  description: descOf("search_codebase", CODEBASE_TOOL_CATALOG),
3251
3634
  annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
3252
- inputSchema: {
3635
+ inputSchema: z.object({
3253
3636
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3254
3637
  query: z.string().describe("Plain-English search query against indexed source files"),
3255
3638
  k: z.number().int().min(1).max(20).optional().describe("Max hits to return (default 8, max 20)"),
3256
3639
  scope_prefix: z.string().optional().describe("Optional repo subdirectory prefix to limit search scope")
3257
- },
3258
- outputSchema: {
3640
+ }),
3641
+ outputSchema: z.object({
3259
3642
  results: z.array(z.unknown()).describe("Ranked file/symbol hits with paths, line ranges, and similarity"),
3260
3643
  query: z.string().describe("Normalized query string that was searched"),
3261
3644
  mode: z.string().optional().describe("semantic (embeddings) or name (path/symbol match)")
3262
- }
3645
+ })
3263
3646
  },
3264
3647
  async (args) => {
3265
3648
  const pid = args.project_id ?? projectId;
@@ -3284,11 +3667,11 @@ Be specific. No generic advice.`
3284
3667
  title: titleOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
3285
3668
  description: descOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
3286
3669
  annotations: annotationsFor("get_codebase_domains", CODEBASE_TOOL_CATALOG),
3287
- inputSchema: {
3670
+ inputSchema: z.object({
3288
3671
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3289
3672
  force: z.boolean().optional().describe("Bypass cache and regenerate"),
3290
3673
  scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
3291
- }
3674
+ })
3292
3675
  },
3293
3676
  async (args) => {
3294
3677
  const pid = args.project_id ?? projectId;
@@ -3307,20 +3690,20 @@ Be specific. No generic advice.`
3307
3690
  title: titleOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
3308
3691
  description: descOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
3309
3692
  annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
3310
- inputSchema: {
3693
+ inputSchema: z.object({
3311
3694
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
3312
3695
  paths: z.array(z.string()).optional().describe("Changed repo-relative file paths to analyze"),
3313
3696
  source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths instead of manual paths"),
3314
3697
  compare: z.string().optional().describe("GitHub compare range base...head when source=compare"),
3315
3698
  fix_id: z.string().optional().describe("Fix attempt UUID \u2014 uses that PR's changed files when source=fix")
3316
- },
3317
- outputSchema: {
3699
+ }),
3700
+ outputSchema: z.object({
3318
3701
  changed_paths: z.array(z.string()).describe("Resolved set of changed paths analyzed"),
3319
3702
  source: z.string().describe("How changed_paths were resolved (manual, last_push, compare, fix, \u2026)"),
3320
3703
  affected_file_paths: z.array(z.string()).describe("Indexed files that import or depend on changed paths"),
3321
3704
  affected_node_ids: z.array(z.string()).describe("Codebase graph node ids impacted by the change"),
3322
3705
  meta: z.unknown().nullable().optional().describe("Extra resolution metadata when auto-sourced")
3323
- }
3706
+ })
3324
3707
  },
3325
3708
  async (args) => {
3326
3709
  const pid = args.project_id ?? projectId;
@@ -3347,9 +3730,9 @@ Be specific. No generic advice.`
3347
3730
  title: titleOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
3348
3731
  description: descOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
3349
3732
  annotations: annotationsFor("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
3350
- inputSchema: {
3733
+ inputSchema: z.object({
3351
3734
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)")
3352
- }
3735
+ })
3353
3736
  },
3354
3737
  async (args) => {
3355
3738
  const pid = args.project_id ?? projectId;
@@ -3363,11 +3746,11 @@ Be specific. No generic advice.`
3363
3746
  {
3364
3747
  title: titleOf("use_mushi"),
3365
3748
  description: descOf("use_mushi"),
3366
- inputSchema: {
3749
+ inputSchema: z.object({
3367
3750
  intent: z.string().optional().describe(
3368
3751
  'What you are trying to accomplish with Mushi, e.g. "fix the top bug", "check project health", "run QA tests", "set up Mushi". Leave blank for a general orientation.'
3369
3752
  )
3370
- },
3753
+ }),
3371
3754
  annotations: annotationsFor("use_mushi")
3372
3755
  },
3373
3756
  async (args) => {
@@ -3470,28 +3853,30 @@ Be specific. No generic advice.`
3470
3853
 
3471
3854
  // src/index.ts
3472
3855
  import * as Sentry from "@sentry/node";
3473
- var _writeStderr = (...args) => process.stderr.write(
3474
- args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"
3475
- );
3476
- console.log = _writeStderr;
3477
- console.warn = _writeStderr;
3856
+ installStdoutGuard();
3478
3857
  var require2 = createRequire(import.meta.url);
3479
3858
  var VERSION = require2("../package.json").version;
3480
3859
  var log = createLogger2({ scope: "mushi:mcp", level: "info", destination: "stderr" });
3860
+ function resolveCliConfigPath() {
3861
+ const xdg = process.env.XDG_CONFIG_HOME;
3862
+ const appData = process.env.APPDATA;
3863
+ const base = xdg && xdg.length > 0 ? xdg : process.platform === "win32" && appData && appData.length > 0 ? appData : join(homedir(), ".config");
3864
+ return join(base, "mushi", "config.json");
3865
+ }
3481
3866
  function readCliConfig() {
3867
+ const configPath = resolveCliConfigPath();
3482
3868
  try {
3483
- const xdg = process.env.XDG_CONFIG_HOME;
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");
3869
+ const raw = readFileSync(configPath, "utf8");
3487
3870
  const parsed = JSON.parse(raw);
3488
3871
  return {
3489
3872
  apiKey: typeof parsed.apiKey === "string" ? parsed.apiKey : void 0,
3490
3873
  projectId: typeof parsed.projectId === "string" ? parsed.projectId : void 0,
3491
- endpoint: typeof parsed.endpoint === "string" ? parsed.endpoint : void 0
3874
+ endpoint: typeof parsed.endpoint === "string" ? parsed.endpoint : void 0,
3875
+ path: configPath,
3876
+ found: true
3492
3877
  };
3493
3878
  } catch {
3494
- return {};
3879
+ return { path: configPath, found: false };
3495
3880
  }
3496
3881
  }
3497
3882
  var CLI_CONFIG = readCliConfig();
@@ -3510,11 +3895,49 @@ if (MCP_SENTRY_DSN) {
3510
3895
  tracesSampleRate: 0
3511
3896
  });
3512
3897
  }
3898
+ function missingApiKeyReport() {
3899
+ const envState = process.env.MUSHI_API_KEY === void 0 ? "not set" : process.env.MUSHI_API_KEY.trim() === "" ? "set but empty" : "set";
3900
+ const cliState = !CLI_CONFIG.found ? "no file at this path" : CLI_CONFIG.apiKey ? "present" : 'file exists but has no "apiKey" field';
3901
+ return [
3902
+ "",
3903
+ "[mushi-mcp] FATAL: no API key \u2014 the MCP server cannot serve a single tool call.",
3904
+ "",
3905
+ " Sources checked, in precedence order:",
3906
+ ` 1. env MUSHI_API_KEY \u2192 ${envState}`,
3907
+ ` 2. CLI config file \u2192 ${cliState}`,
3908
+ ` ${CLI_CONFIG.path}`,
3909
+ "",
3910
+ " Fix either one:",
3911
+ " \u2022 Run `mushi login` (writes the config file above), or",
3912
+ ' \u2022 Add the key to the "env" block of your MCP client config',
3913
+ " (.cursor/mcp.json \xB7 claude_desktop_config.json \xB7 .vscode/mcp.json):",
3914
+ "",
3915
+ ' { "mcpServers": { "mushi-mushi": {',
3916
+ ' "command": "npx", "args": ["-y", "@mushi-mushi/mcp"],',
3917
+ ' "env": { "MUSHI_API_KEY": "mushi_\u2026", "MUSHI_PROJECT_ID": "<uuid>" } } } }',
3918
+ "",
3919
+ " A key exported in your shell does NOT reach the server: MCP clients",
3920
+ " spawn this process with only the env block they are given.",
3921
+ "",
3922
+ " Other env vars this server reads:",
3923
+ ` MUSHI_API_ENDPOINT ${process.env.MUSHI_API_ENDPOINT?.trim() ? "= " + process.env.MUSHI_API_ENDPOINT.trim() : `unset \u2192 ${API_ENDPOINT}`}`,
3924
+ ` MUSHI_PROJECT_ID ${PROJECT_ID ? "= " + PROJECT_ID : "unset (account mode)"}`,
3925
+ " MUSHI_SCOPES optional CSV: mcp:read,mcp:write",
3926
+ ' MUSHI_FEATURES optional CSV of tool groups, or "all"',
3927
+ " MUSHI_MCP_TIMEOUT_MS optional per-request timeout in ms (default 15000)",
3928
+ "",
3929
+ " Mint a key: Console \u2192 Settings \u2192 API keys.",
3930
+ ""
3931
+ ].join("\n");
3932
+ }
3513
3933
  async function main() {
3514
3934
  if (!API_KEY) {
3515
- log.fatal(
3516
- "No API key found \u2014 set MUSHI_API_KEY, or run `mushi login` so the CLI config can supply it."
3517
- );
3935
+ process.stderr.write(missingApiKeyReport());
3936
+ log.fatal("No API key found \u2014 set MUSHI_API_KEY, or run `mushi login`.", {
3937
+ cliConfigPath: CLI_CONFIG.path,
3938
+ cliConfigFound: CLI_CONFIG.found,
3939
+ endpoint: API_ENDPOINT
3940
+ });
3518
3941
  process.exit(1);
3519
3942
  }
3520
3943
  if (!process.env.MUSHI_API_KEY && CLI_CONFIG.apiKey) {
@@ -3543,24 +3966,33 @@ async function main() {
3543
3966
  hasProjectId: !!PROJECT_ID,
3544
3967
  scopes: SCOPES.join(",")
3545
3968
  });
3546
- const server = createMushiServer({
3547
- version: VERSION,
3548
- apiEndpoint: API_ENDPOINT,
3549
- apiKey: API_KEY,
3550
- projectId: PROJECT_ID || void 0,
3551
- scopes: SCOPES,
3552
- features: FEATURES
3553
- });
3554
- const transport = new StdioServerTransport();
3555
- await server.connect(transport);
3969
+ let server;
3970
+ const handle = serveStdio(
3971
+ (ctx) => {
3972
+ server = createMushiServer({
3973
+ version: VERSION,
3974
+ apiEndpoint: API_ENDPOINT,
3975
+ apiKey: API_KEY,
3976
+ projectId: PROJECT_ID || void 0,
3977
+ scopes: SCOPES,
3978
+ features: FEATURES
3979
+ });
3980
+ log.debug("[mushi-mcp] server instance created", { era: ctx.era });
3981
+ return server;
3982
+ },
3983
+ {
3984
+ onerror: (err) => log.error("stdio transport error", { err: String(err) })
3985
+ }
3986
+ );
3556
3987
  let shuttingDown = false;
3557
3988
  let pollTimer;
3558
3989
  const shutdown = (exitCode) => {
3559
3990
  if (shuttingDown) return;
3560
3991
  shuttingDown = true;
3561
3992
  if (pollTimer) clearInterval(pollTimer);
3562
- void transport.close().finally(() => process.exit(exitCode));
3993
+ void handle.close().finally(() => process.exit(exitCode));
3563
3994
  };
3995
+ setActiveShutdown(shutdown);
3564
3996
  process.stdin.on("end", () => shutdown(0));
3565
3997
  process.stdin.on("close", () => shutdown(0));
3566
3998
  process.on("SIGINT", () => shutdown(0));
@@ -3590,7 +4022,7 @@ async function main() {
3590
4022
  const data = await res.json();
3591
4023
  const updatedAt = data?.data?.updatedAt ?? null;
3592
4024
  if (updatedAt && updatedAt !== lastInventoryAt) {
3593
- if (lastInventoryAt !== null) {
4025
+ if (lastInventoryAt !== null && server) {
3594
4026
  await server.server.sendResourceUpdated({ uri: "inventory://current" });
3595
4027
  log.info("inventory://current updated \u2014 notified subscribers", { updatedAt });
3596
4028
  }
@@ -3607,6 +4039,49 @@ async function main() {
3607
4039
  pollTimer.unref();
3608
4040
  }
3609
4041
  }
4042
+ var activeShutdown;
4043
+ var setActiveShutdown = (fn) => {
4044
+ activeShutdown = fn;
4045
+ };
4046
+ var exitNow = (code) => {
4047
+ if (activeShutdown) activeShutdown(code);
4048
+ else process.exit(code);
4049
+ };
4050
+ var CRASH_WINDOW_MS = 1e4;
4051
+ var CRASH_LIMIT = 20;
4052
+ var crashTimes = [];
4053
+ function isCrashLooping() {
4054
+ const now = Date.now();
4055
+ crashTimes = crashTimes.filter((t) => now - t < CRASH_WINDOW_MS);
4056
+ crashTimes.push(now);
4057
+ return crashTimes.length >= CRASH_LIMIT;
4058
+ }
4059
+ process.on("unhandledRejection", (reason) => {
4060
+ log.error("unhandledRejection \u2014 server kept alive", {
4061
+ err: reason instanceof Error ? `${reason.name}: ${reason.message}` : String(reason),
4062
+ stack: reason instanceof Error ? reason.stack?.split("\n").slice(0, 5).join(" | ") : void 0
4063
+ });
4064
+ if (isCrashLooping()) {
4065
+ log.fatal("too many unhandled rejections in 10s \u2014 exiting so the client can respawn");
4066
+ exitNow(1);
4067
+ }
4068
+ });
4069
+ process.on("uncaughtException", (err) => {
4070
+ if (err?.code === "EPIPE" || err?.code === "ERR_STREAM_DESTROYED" || err?.code === "ERR_STREAM_WRITE_AFTER_END") {
4071
+ log.info("stdio pipe closed by the client \u2014 shutting down", { code: err.code });
4072
+ exitNow(0);
4073
+ return;
4074
+ }
4075
+ log.error("uncaughtException \u2014 server kept alive", {
4076
+ err: `${err?.name ?? "Error"}: ${err?.message ?? String(err)}`,
4077
+ code: err?.code,
4078
+ stack: err?.stack?.split("\n").slice(0, 5).join(" | ")
4079
+ });
4080
+ if (isCrashLooping()) {
4081
+ log.fatal("too many uncaught exceptions in 10s \u2014 exiting so the client can respawn");
4082
+ exitNow(1);
4083
+ }
4084
+ });
3610
4085
  main().catch((err) => {
3611
4086
  log.fatal("MCP server crashed", { err: String(err) });
3612
4087
  process.exit(1);