@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/server.js CHANGED
@@ -3,17 +3,17 @@ import {
3
3
  TDD_TOOL_CATALOG,
4
4
  TOOL_CATALOG,
5
5
  USE_MUSHI_INTENTS
6
- } from "./chunk-TP2QQWDJ.js";
6
+ } from "./chunk-FNDGCFSJ.js";
7
7
  import {
8
8
  MUSHI_SERVER_METADATA
9
9
  } from "./chunk-EANW5YTI.js";
10
10
  import {
11
11
  DEPRECATED_TOOL_ALIASES,
12
12
  toolMatchesFeatures
13
- } from "./chunk-ZSVRLSW5.js";
13
+ } from "./chunk-XSL2FPLV.js";
14
14
 
15
15
  // src/server.ts
16
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { McpServer } from "@modelcontextprotocol/server";
17
17
  import { z } from "zod";
18
18
  import { createLogger } from "@mushi-mushi/core";
19
19
 
@@ -143,8 +143,107 @@ function wrapUntrustedJson(value, role) {
143
143
  return wrapUntrusted(serialised, role);
144
144
  }
145
145
 
146
+ // src/stdout-guard.ts
147
+ import { Console } from "console";
148
+ var GUARDED_CONSOLE_METHODS = [
149
+ "log",
150
+ "info",
151
+ "debug",
152
+ "dir",
153
+ "dirxml",
154
+ "table",
155
+ "group",
156
+ "groupCollapsed",
157
+ "groupEnd",
158
+ "count",
159
+ "countReset",
160
+ "time",
161
+ "timeEnd",
162
+ "timeLog",
163
+ "trace",
164
+ "assert",
165
+ "warn",
166
+ "error"
167
+ ];
168
+ var installed = false;
169
+ function installStdoutGuard() {
170
+ if (installed) return false;
171
+ if (typeof process !== "undefined" && process.env?.MUSHI_MCP_STDOUT_GUARD === "off") {
172
+ return false;
173
+ }
174
+ if (typeof process === "undefined" || typeof process.stderr?.write !== "function" || typeof console === "undefined") {
175
+ return false;
176
+ }
177
+ try {
178
+ const stderrConsole = new Console({
179
+ stdout: process.stderr,
180
+ stderr: process.stderr,
181
+ // Colour codes would land in the client's log pane as escape noise.
182
+ colorMode: false
183
+ });
184
+ const target = console;
185
+ const source = stderrConsole;
186
+ for (const method of GUARDED_CONSOLE_METHODS) {
187
+ const fn = source[method];
188
+ if (typeof fn === "function") {
189
+ target[method] = fn.bind(stderrConsole);
190
+ }
191
+ }
192
+ } catch {
193
+ const write = (...args) => process.stderr.write(
194
+ args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ") + "\n"
195
+ );
196
+ const target = console;
197
+ for (const method of GUARDED_CONSOLE_METHODS) {
198
+ target[method] = write;
199
+ }
200
+ }
201
+ installed = true;
202
+ return true;
203
+ }
204
+ function safeStringify(value) {
205
+ try {
206
+ return JSON.stringify(value) ?? String(value);
207
+ } catch {
208
+ return String(value);
209
+ }
210
+ }
211
+
146
212
  // src/server.ts
147
- var NO_ARG_INPUT = {};
213
+ installStdoutGuard();
214
+ var NO_ARG_INPUT = z.object({});
215
+ var DEFAULT_API_TIMEOUT_MS = 15e3;
216
+ var MIN_API_TIMEOUT_MS = 250;
217
+ var MAX_API_TIMEOUT_MS = 3e5;
218
+ function resolveTimeoutMsFromEnv() {
219
+ const raw = process.env?.MUSHI_MCP_TIMEOUT_MS?.trim();
220
+ if (!raw) return DEFAULT_API_TIMEOUT_MS;
221
+ const parsed = Number(raw);
222
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
223
+ return Math.min(Math.max(parsed, MIN_API_TIMEOUT_MS), MAX_API_TIMEOUT_MS);
224
+ }
225
+ function bareMessage(err) {
226
+ const raw = err instanceof Error ? err.message : String(err);
227
+ return raw.replace(/^\[[A-Z0-9_]+\]\s*/, "");
228
+ }
229
+ var IDEMPOTENCY_NAMESPACE = "mushi.mcp.submit_fix_result.v1";
230
+ async function stableUuidFrom(...parts) {
231
+ const input = parts.join("");
232
+ const digest = new Uint8Array(
233
+ await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input))
234
+ );
235
+ const bytes = digest.slice(0, 16);
236
+ bytes[6] = bytes[6] & 15 | 80;
237
+ bytes[8] = bytes[8] & 63 | 128;
238
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
239
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
240
+ }
241
+ function withTimeoutSignal(callerSignal, timeoutSignal) {
242
+ if (!callerSignal) return timeoutSignal;
243
+ const anyFn = AbortSignal.any;
244
+ if (typeof anyFn === "function") return anyFn.call(AbortSignal, [callerSignal, timeoutSignal]);
245
+ return timeoutSignal;
246
+ }
148
247
  var MushiApiError = class extends Error {
149
248
  constructor(status, code, message) {
150
249
  super(`[${code}] ${message}`);
@@ -158,6 +257,7 @@ var MushiApiError = class extends Error {
158
257
  function createMushiServer(config) {
159
258
  const { version, apiEndpoint, apiKey, projectId } = config;
160
259
  const doFetch = config.fetch ?? globalThis.fetch;
260
+ const timeoutMs = typeof config.timeoutMs === "number" && Number.isFinite(config.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : resolveTimeoutMsFromEnv();
161
261
  const apiLog = createLogger({
162
262
  scope: "mushi:mcp:api",
163
263
  level: process.env.MUSHI_LOG_LEVEL ?? "info",
@@ -175,22 +275,66 @@ function createMushiServer(config) {
175
275
  async function apiCall(path, options) {
176
276
  const requestId = crypto.randomUUID().slice(0, 12);
177
277
  const started = Date.now();
178
- const res = await doFetch(`${apiEndpoint}${path}`, {
179
- ...options,
180
- headers: {
181
- "Content-Type": "application/json",
182
- // Authorization is accepted by `adminOrApiKey` only as a JWT, but we
183
- // still send it for endpoints still behind plain `jwtAuth` (legacy)
184
- // and for transparent proxies that strip X-Mushi-* headers.
185
- Authorization: `Bearer ${apiKey}`,
186
- "X-Mushi-Api-Key": apiKey,
187
- "X-Request-Id": requestId,
188
- ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
189
- ...options?.headers ?? {}
278
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
279
+ let res;
280
+ try {
281
+ res = await doFetch(`${apiEndpoint}${path}`, {
282
+ ...options,
283
+ headers: {
284
+ "Content-Type": "application/json",
285
+ // Authorization is accepted by `adminOrApiKey` only as a JWT, but we
286
+ // still send it for endpoints still behind plain `jwtAuth` (legacy)
287
+ // and for transparent proxies that strip X-Mushi-* headers.
288
+ Authorization: `Bearer ${apiKey}`,
289
+ "X-Mushi-Api-Key": apiKey,
290
+ "X-Request-Id": requestId,
291
+ ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
292
+ ...options?.headers ?? {}
293
+ },
294
+ // After the spread so a caller-supplied `signal` is merged, not lost.
295
+ signal: withTimeoutSignal(options?.signal, timeoutSignal)
296
+ });
297
+ } catch (err) {
298
+ throw classifyTransportError(err);
299
+ }
300
+ let text;
301
+ try {
302
+ text = await res.text();
303
+ } catch (err) {
304
+ throw classifyTransportError(err);
305
+ }
306
+ function classifyTransportError(err) {
307
+ const durationMs2 = Date.now() - started;
308
+ if (timeoutSignal.aborted) {
309
+ apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
310
+ return new MushiApiError(
311
+ 504,
312
+ "MUSHI_TIMEOUT",
313
+ `Timed out after ${timeoutMs}ms waiting for ${path}. The Mushi API accepted the connection but did not answer in time \u2014 retry, or raise the budget with MUSHI_MCP_TIMEOUT_MS (ms) in your MCP env block if you self-host and cold starts are slow. Endpoint: ${apiEndpoint}`
314
+ );
190
315
  }
191
- });
316
+ const name = err instanceof Error ? err.name : "";
317
+ if (name === "AbortError") {
318
+ apiLog.warn("api.aborted", { path, requestId, durationMs: durationMs2 });
319
+ return new MushiApiError(
320
+ 499,
321
+ "REQUEST_ABORTED",
322
+ `The request to ${path} was aborted by the caller after ${durationMs2}ms.`
323
+ );
324
+ }
325
+ apiLog.warn("api.unreachable", {
326
+ path,
327
+ requestId,
328
+ durationMs: durationMs2,
329
+ err: bareMessage(err)
330
+ });
331
+ return new MushiApiError(
332
+ 503,
333
+ "NETWORK_ERROR",
334
+ `Could not reach the Mushi API at ${apiEndpoint}${path} \u2014 ${bareMessage(err)}. This is a connectivity failure, not an API rejection: check that you are online, that MUSHI_API_ENDPOINT points at your \`.../functions/v1/api\` URL, and that no proxy/VPN is blocking it.`
335
+ );
336
+ }
192
337
  const durationMs = Date.now() - started;
193
- const text = await res.text();
194
338
  let body = null;
195
339
  if (text) {
196
340
  try {
@@ -220,6 +364,7 @@ function createMushiServer(config) {
220
364
  apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
221
365
  return body;
222
366
  }
367
+ let resolvedAccountProjectId = null;
223
368
  async function resolveProjectId(explicitId) {
224
369
  if (explicitId) {
225
370
  if (projectId && explicitId !== projectId) {
@@ -232,10 +377,12 @@ function createMushiServer(config) {
232
377
  return explicitId;
233
378
  }
234
379
  if (projectId) return projectId;
380
+ if (resolvedAccountProjectId) return resolvedAccountProjectId;
235
381
  try {
236
382
  const data = await apiCall("/v1/admin/mcp/account-overview");
237
383
  if (data.total === 1 && data.projects[0]) {
238
- return data.projects[0].id;
384
+ resolvedAccountProjectId = data.projects[0].id;
385
+ return resolvedAccountProjectId;
239
386
  }
240
387
  if (data.total === 0) {
241
388
  throw new MushiApiError(
@@ -251,11 +398,27 @@ function createMushiServer(config) {
251
398
  `Multiple projects are accessible with this key: ${projectList}. Pass projectId explicitly to target a specific project, or set MUSHI_PROJECT_ID in your MCP env config.`
252
399
  );
253
400
  } catch (err) {
254
- if (err instanceof MushiApiError) throw err;
401
+ if (err instanceof MushiApiError) {
402
+ if (err.code === "MUSHI_TIMEOUT" || err.code === "NETWORK_ERROR") {
403
+ throw new MushiApiError(
404
+ err.status,
405
+ err.code,
406
+ `Could not resolve which project to use because the Mushi API is unreachable: ${bareMessage(err)} This is a connectivity problem \u2014 setting MUSHI_PROJECT_ID would only skip the lookup, not fix the tool call that follows it.`
407
+ );
408
+ }
409
+ if (err.status === 401 || err.status === 403) {
410
+ throw new MushiApiError(
411
+ err.status,
412
+ err.code,
413
+ `The API key was rejected while resolving the project: ${bareMessage(err)} Mint a fresh key in the console (Settings \u2192 API keys) and update MUSHI_API_KEY, or re-run \`mushi login\`. This is an authentication problem, not a missing project id.`
414
+ );
415
+ }
416
+ throw err;
417
+ }
255
418
  throw new MushiApiError(
256
419
  400,
257
420
  "MISSING_PROJECT_ID",
258
- "projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly."
421
+ `projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly. (Project lookup failed with: ${bareMessage(err)})`
259
422
  );
260
423
  }
261
424
  }
@@ -322,7 +485,7 @@ function createMushiServer(config) {
322
485
  title: titleOf("get_recent_reports"),
323
486
  description: descOf("get_recent_reports"),
324
487
  annotations: annotationsFor("get_recent_reports"),
325
- inputSchema: {
488
+ inputSchema: z.object({
326
489
  status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
327
490
  category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
328
491
  severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
@@ -330,11 +493,11 @@ function createMushiServer(config) {
330
493
  project_id: z.string().optional().describe(
331
494
  "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."
332
495
  )
333
- },
334
- outputSchema: {
496
+ }),
497
+ outputSchema: z.object({
335
498
  reports: z.array(z.unknown()),
336
499
  total: z.number()
337
- }
500
+ })
338
501
  },
339
502
  async (args) => {
340
503
  const params = new URLSearchParams();
@@ -359,11 +522,11 @@ function createMushiServer(config) {
359
522
  title: titleOf("get_report_detail"),
360
523
  description: descOf("get_report_detail"),
361
524
  annotations: annotationsFor("get_report_detail"),
362
- inputSchema: {
525
+ inputSchema: z.object({
363
526
  reportId: z.string().describe("The report UUID"),
364
527
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
365
- },
366
- outputSchema: { report: z.unknown() }
528
+ }),
529
+ outputSchema: z.object({ report: z.unknown() })
367
530
  },
368
531
  async (args) => {
369
532
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -386,7 +549,7 @@ function createMushiServer(config) {
386
549
  title: titleOf("get_report_timeline"),
387
550
  description: descOf("get_report_timeline"),
388
551
  annotations: annotationsFor("get_report_timeline"),
389
- inputSchema: { reportId: z.string().describe("The report UUID") }
552
+ inputSchema: z.object({ reportId: z.string().describe("The report UUID") })
390
553
  },
391
554
  async (args) => jsonText(await apiCall(`/v1/sync/reports/${args.reportId}/timeline`))
392
555
  );
@@ -406,14 +569,14 @@ function createMushiServer(config) {
406
569
  title: titleOf("search_reports"),
407
570
  description: descOf("search_reports"),
408
571
  annotations: annotationsFor("search_reports"),
409
- inputSchema: {
572
+ inputSchema: z.object({
410
573
  query: z.string().describe("Natural-language search text or component path"),
411
574
  limit: z.number().optional().describe("Max results (default 10, max 50)"),
412
575
  threshold: z.number().optional().describe("Similarity threshold 0..1, default 0.2")
413
- },
414
- outputSchema: {
576
+ }),
577
+ outputSchema: z.object({
415
578
  results: z.array(z.unknown())
416
- }
579
+ })
417
580
  },
418
581
  async (args) => {
419
582
  const data = await apiCall("/v1/admin/reports/similarity", {
@@ -434,13 +597,13 @@ function createMushiServer(config) {
434
597
  title: titleOf("get_similar_bugs"),
435
598
  description: descOf("get_similar_bugs"),
436
599
  annotations: annotationsFor("get_similar_bugs"),
437
- inputSchema: {
600
+ inputSchema: z.object({
438
601
  query: z.string().describe("Component name, page path, or bug description"),
439
602
  limit: z.number().optional().describe("Max results (default 5, max 20)")
440
- },
441
- outputSchema: {
603
+ }),
604
+ outputSchema: z.object({
442
605
  results: z.array(z.unknown())
443
- }
606
+ })
444
607
  },
445
608
  async (args) => {
446
609
  const data = await apiCall("/v1/admin/reports/similarity", {
@@ -461,18 +624,18 @@ function createMushiServer(config) {
461
624
  title: titleOf("get_fix_context"),
462
625
  description: descOf("get_fix_context"),
463
626
  annotations: annotationsFor("get_fix_context"),
464
- inputSchema: {
627
+ inputSchema: z.object({
465
628
  reportId: z.string().describe("The report UUID to fix"),
466
629
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
467
- },
468
- outputSchema: {
630
+ }),
631
+ outputSchema: z.object({
469
632
  report: z.unknown(),
470
633
  fixPrompt: z.unknown(),
471
634
  reproductionSteps: z.unknown(),
472
635
  component: z.unknown(),
473
636
  rootCause: z.unknown(),
474
637
  bugOntologyTags: z.unknown()
475
- }
638
+ })
476
639
  },
477
640
  async (args) => {
478
641
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -501,10 +664,10 @@ function createMushiServer(config) {
501
664
  title: titleOf("get_fix_timeline"),
502
665
  description: descOf("get_fix_timeline"),
503
666
  annotations: annotationsFor("get_fix_timeline"),
504
- inputSchema: {
667
+ inputSchema: z.object({
505
668
  fixId: z.string().describe("fix_attempt UUID"),
506
669
  project_id: z.string().optional().describe("Project UUID \u2014 required for org-scoped keys with multiple projects.")
507
- }
670
+ })
508
671
  },
509
672
  async (args) => {
510
673
  const { headers } = await projectScopeHeaders(args.project_id);
@@ -517,7 +680,7 @@ function createMushiServer(config) {
517
680
  title: titleOf("get_blast_radius"),
518
681
  description: descOf("get_blast_radius"),
519
682
  annotations: annotationsFor("get_blast_radius"),
520
- inputSchema: { nodeId: z.string().describe("Graph node UUID") }
683
+ inputSchema: z.object({ nodeId: z.string().describe("Graph node UUID") })
521
684
  },
522
685
  async (args) => jsonText(await apiCall(`/v1/admin/graph/blast-radius/${args.nodeId}`))
523
686
  );
@@ -527,14 +690,14 @@ function createMushiServer(config) {
527
690
  title: titleOf("get_knowledge_graph"),
528
691
  description: descOf("get_knowledge_graph"),
529
692
  annotations: annotationsFor("get_knowledge_graph"),
530
- inputSchema: {
693
+ inputSchema: z.object({
531
694
  seed: z.string().describe("Starting graph node id or human-readable label to traverse from"),
532
695
  depth: z.number().optional().describe("BFS hops outward from seed \u2014 clamped to 4 in the handler (default 2)")
533
- },
534
- outputSchema: {
696
+ }),
697
+ outputSchema: z.object({
535
698
  nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
536
699
  edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
537
- }
700
+ })
538
701
  },
539
702
  async (args) => {
540
703
  const params = new URLSearchParams({
@@ -553,16 +716,16 @@ function createMushiServer(config) {
553
716
  title: titleOf("get_graph_neighborhood"),
554
717
  description: descOf("get_graph_neighborhood"),
555
718
  annotations: annotationsFor("get_graph_neighborhood"),
556
- inputSchema: {
719
+ inputSchema: z.object({
557
720
  seed: z.string().describe(
558
721
  "Starting graph node id or label to expand around (e.g. an inventory Action id or component name)"
559
722
  ),
560
723
  depth: z.number().int().min(1).max(4).optional().describe("BFS hops to traverse outward. Default 2; clamped to a max of 4.")
561
- },
562
- outputSchema: {
724
+ }),
725
+ outputSchema: z.object({
563
726
  nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
564
727
  edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
565
- }
728
+ })
566
729
  },
567
730
  async (args) => {
568
731
  const params = new URLSearchParams({
@@ -581,12 +744,12 @@ function createMushiServer(config) {
581
744
  title: titleOf("get_graph_node"),
582
745
  description: descOf("get_graph_node"),
583
746
  annotations: annotationsFor("get_graph_node"),
584
- inputSchema: {
747
+ inputSchema: z.object({
585
748
  nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).")
586
- },
587
- outputSchema: {
749
+ }),
750
+ outputSchema: z.object({
588
751
  node: z.unknown().describe("Single graph_nodes row including metadata (Action nodes carry v2 status)")
589
- }
752
+ })
590
753
  },
591
754
  async (args) => {
592
755
  const data = await apiCall(`/v1/admin/graph/node/${args.nodeId}`);
@@ -599,9 +762,9 @@ function createMushiServer(config) {
599
762
  title: titleOf("get_inventory"),
600
763
  description: descOf("get_inventory"),
601
764
  annotations: annotationsFor("get_inventory"),
602
- inputSchema: {
765
+ inputSchema: z.object({
603
766
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
604
- }
767
+ })
605
768
  },
606
769
  async (args) => {
607
770
  const pid = args.projectId ?? projectId;
@@ -616,11 +779,11 @@ function createMushiServer(config) {
616
779
  title: titleOf("diff_inventory"),
617
780
  description: descOf("diff_inventory"),
618
781
  annotations: annotationsFor("diff_inventory"),
619
- inputSchema: {
782
+ inputSchema: z.object({
620
783
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
621
784
  fromSha: z.string().describe("Older commit SHA (the baseline to diff from)"),
622
785
  toSha: z.string().describe("Newer commit SHA (the candidate to diff to)")
623
- }
786
+ })
624
787
  },
625
788
  async (args) => {
626
789
  const pid = args.projectId ?? projectId;
@@ -636,15 +799,15 @@ function createMushiServer(config) {
636
799
  title: titleOf("list_gate_findings"),
637
800
  description: descOf("list_gate_findings"),
638
801
  annotations: annotationsFor("list_gate_findings"),
639
- inputSchema: {
802
+ inputSchema: z.object({
640
803
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
641
804
  gate: z.string().optional().describe("Filter to one gate id: dead-handler | mock-leak | crawl | status-claim"),
642
805
  severity: z.string().optional().describe("Minimum severity to include: low | medium | high | critical")
643
- },
644
- outputSchema: {
806
+ }),
807
+ outputSchema: z.object({
645
808
  runs: z.array(z.unknown()).describe("Recent gate_runs rows, newest first"),
646
809
  findings: z.array(z.unknown()).describe("gate_findings rows for those runs")
647
- }
810
+ })
648
811
  },
649
812
  async (args) => {
650
813
  const pid = args.projectId ?? projectId;
@@ -670,21 +833,35 @@ function createMushiServer(config) {
670
833
  title: titleOf("suggest_fix"),
671
834
  description: descOf("suggest_fix"),
672
835
  annotations: annotationsFor("suggest_fix"),
673
- inputSchema: {
836
+ inputSchema: z.object({
674
837
  reportId: z.string().describe("Report UUID to read the Stage-2 suggested-fix slice for")
675
- },
676
- outputSchema: {
838
+ }),
839
+ outputSchema: z.object({
677
840
  reportId: z.string().describe("The report this slice was read from"),
678
841
  rootCause: z.unknown().describe("Stage-2 root-cause hint, or null if not yet classified"),
679
842
  suggestedFix: z.unknown().describe("Stage-2 suggested fix, or null if not yet classified"),
680
843
  reproductionSteps: z.unknown().describe("Reproduction steps recorded on the report (array; [] if none)"),
681
844
  summary: z.unknown().describe("One-line report summary, or null"),
682
- component: z.unknown().describe("Component/page the bug was attributed to, or null")
683
- }
845
+ component: z.unknown().describe("Component/page the bug was attributed to, or null"),
846
+ note: z.string().optional().describe(
847
+ "Present when no Stage-2 analysis exists \u2014 explains why and what to call instead"
848
+ )
849
+ })
684
850
  },
685
851
  async (args) => {
686
852
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
687
853
  const s2 = report.stage2_analysis;
854
+ if (!s2 || s2.rootCause == null && s2.suggestedFix == null) {
855
+ return jsonResult({
856
+ reportId: args.reportId,
857
+ rootCause: null,
858
+ suggestedFix: null,
859
+ reproductionSteps: report.reproduction_steps ?? [],
860
+ summary: report.summary ?? null,
861
+ component: report.component ?? null,
862
+ 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."
863
+ });
864
+ }
688
865
  return jsonResult({
689
866
  reportId: args.reportId,
690
867
  rootCause: s2?.rootCause ?? null,
@@ -701,11 +878,11 @@ function createMushiServer(config) {
701
878
  title: titleOf("run_nl_query"),
702
879
  description: descOf("run_nl_query"),
703
880
  annotations: annotationsFor("run_nl_query"),
704
- inputSchema: {
881
+ inputSchema: z.object({
705
882
  question: z.string().describe(
706
883
  'Question in plain English, e.g. "Which components had the most critical bugs this week?"'
707
884
  )
708
- }
885
+ })
709
886
  },
710
887
  async (args) => {
711
888
  const data = await apiCall("/v1/admin/query", {
@@ -721,14 +898,14 @@ function createMushiServer(config) {
721
898
  title: titleOf("diagnose_setup"),
722
899
  description: descOf("diagnose_setup"),
723
900
  annotations: annotationsFor("diagnose_setup"),
724
- inputSchema: {
901
+ inputSchema: z.object({
725
902
  mode: z.enum(["full", "ingest", "dispatch"]).optional().describe(
726
903
  "full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."
727
904
  ),
728
905
  project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
729
906
  projectId: z.string().optional().describe("Alias for project_id.")
730
- },
731
- outputSchema: {
907
+ }),
908
+ outputSchema: z.object({
732
909
  mode: z.string(),
733
910
  ready: z.boolean(),
734
911
  summary: z.string(),
@@ -736,7 +913,7 @@ function createMushiServer(config) {
736
913
  ingest: z.unknown().optional(),
737
914
  dispatch: z.unknown().optional(),
738
915
  connection: z.unknown().optional()
739
- }
916
+ })
740
917
  },
741
918
  async (args) => {
742
919
  const mode = args.mode ?? "full";
@@ -875,17 +1052,71 @@ function createMushiServer(config) {
875
1052
  });
876
1053
  }
877
1054
  );
1055
+ server.registerTool(
1056
+ "check_sdk_version",
1057
+ {
1058
+ title: titleOf("check_sdk_version"),
1059
+ description: descOf("check_sdk_version"),
1060
+ annotations: annotationsFor("check_sdk_version"),
1061
+ inputSchema: z.object({
1062
+ package: z.string().optional().describe(
1063
+ "npm package name (default @mushi-mushi/web). Examples: @mushi-mushi/web, @mushi-mushi/node, @mushi-mushi/core."
1064
+ ),
1065
+ current: z.string().optional().describe("Installed version from package.json, if known (e.g. 1.27.0).")
1066
+ }),
1067
+ outputSchema: z.object({
1068
+ package: z.string(),
1069
+ latest: z.string().optional(),
1070
+ current: z.string().optional(),
1071
+ outdated: z.boolean().optional(),
1072
+ suggestedActions: z.array(
1073
+ z.object({
1074
+ type: z.literal("tool_call"),
1075
+ toolName: z.string(),
1076
+ arguments: z.record(z.string(), z.unknown()),
1077
+ reason: z.string()
1078
+ })
1079
+ ).optional()
1080
+ })
1081
+ },
1082
+ async (args) => {
1083
+ const pkg = args.package?.trim() || "@mushi-mushi/web";
1084
+ const query = new URLSearchParams({ package: pkg }).toString();
1085
+ const data = await apiCall(
1086
+ `/v1/sdk/latest-version?${query}`
1087
+ );
1088
+ const latest = data.latest ?? data.version;
1089
+ const current = args.current;
1090
+ const outdated = current && latest ? current !== latest : void 0;
1091
+ return jsonResult({
1092
+ package: data.package ?? pkg,
1093
+ latest,
1094
+ current,
1095
+ outdated,
1096
+ ...outdated ? {
1097
+ suggestedActions: [
1098
+ {
1099
+ type: "tool_call",
1100
+ toolName: "search_mushi_docs",
1101
+ arguments: { query: `${pkg} upgrade mushi-sdk-upgrade` },
1102
+ reason: "Read current upgrade notes, then apply the mushi-sdk-upgrade skill. This tool does not bump the pin."
1103
+ }
1104
+ ]
1105
+ } : {}
1106
+ });
1107
+ }
1108
+ );
878
1109
  server.registerTool(
879
1110
  "search_mushi_docs",
880
1111
  {
881
1112
  title: titleOf("search_mushi_docs"),
882
1113
  description: descOf("search_mushi_docs"),
883
1114
  annotations: annotationsFor("search_mushi_docs"),
884
- inputSchema: {
1115
+ inputSchema: z.object({
885
1116
  query: z.string().describe("Keywords to search official Mushi docs (guides, MCP, inventory, QA)."),
886
1117
  limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8).")
887
- },
888
- outputSchema: {
1118
+ }),
1119
+ outputSchema: z.object({
889
1120
  query: z.string(),
890
1121
  results: z.array(
891
1122
  z.object({
@@ -895,7 +1126,7 @@ function createMushiServer(config) {
895
1126
  score: z.number()
896
1127
  })
897
1128
  )
898
- }
1129
+ })
899
1130
  },
900
1131
  async (args) => {
901
1132
  const query = args.query ?? "";
@@ -916,11 +1147,11 @@ function createMushiServer(config) {
916
1147
  description: descOf("list_projects"),
917
1148
  annotations: annotationsFor("list_projects"),
918
1149
  inputSchema: NO_ARG_INPUT,
919
- outputSchema: {
1150
+ outputSchema: z.object({
920
1151
  projects: z.array(z.unknown()),
921
1152
  total: z.number().optional(),
922
1153
  _multi_project_hint: z.string().optional()
923
- }
1154
+ })
924
1155
  },
925
1156
  async () => {
926
1157
  const data = await apiCall("/v1/admin/mcp/projects");
@@ -937,7 +1168,7 @@ function createMushiServer(config) {
937
1168
  description: descOf("get_account_overview"),
938
1169
  annotations: annotationsFor("get_account_overview"),
939
1170
  inputSchema: NO_ARG_INPUT,
940
- outputSchema: {
1171
+ outputSchema: z.object({
941
1172
  projects: z.array(z.unknown()),
942
1173
  total: z.number(),
943
1174
  active_project_id: z.string().nullable(),
@@ -945,7 +1176,7 @@ function createMushiServer(config) {
945
1176
  resourceCount: z.number().optional(),
946
1177
  promptCount: z.number().optional(),
947
1178
  multi_project_hint: z.string()
948
- }
1179
+ })
949
1180
  },
950
1181
  async () => {
951
1182
  const data = await apiCall("/v1/admin/mcp/account-overview");
@@ -967,9 +1198,9 @@ function createMushiServer(config) {
967
1198
  title: titleOf("get_project_context"),
968
1199
  description: descOf("get_project_context"),
969
1200
  annotations: annotationsFor("get_project_context"),
970
- inputSchema: {
1201
+ inputSchema: z.object({
971
1202
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
972
- }
1203
+ })
973
1204
  },
974
1205
  async (args) => {
975
1206
  const pid = args.project_id ?? projectId;
@@ -991,13 +1222,13 @@ function createMushiServer(config) {
991
1222
  title: titleOf("get_pipeline_logs"),
992
1223
  description: descOf("get_pipeline_logs"),
993
1224
  annotations: annotationsFor("get_pipeline_logs"),
994
- inputSchema: {
1225
+ inputSchema: z.object({
995
1226
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
996
1227
  service: z.enum(["fix-worker", "qa-story-runner", "pipeline", "all"]).optional().describe("Pipeline service to filter (default: all)."),
997
1228
  since: z.string().optional().describe("ISO-8601 timestamp \u2014 return only entries after this time."),
998
1229
  limit: z.number().optional().describe("Max entries to return (default 50, max 200)."),
999
1230
  level: z.enum(["info", "warn", "error", "fatal"]).optional().describe("Minimum severity level (default: warn).")
1000
- }
1231
+ })
1001
1232
  },
1002
1233
  async (args) => {
1003
1234
  const pid = args.project_id ?? projectId;
@@ -1017,9 +1248,9 @@ function createMushiServer(config) {
1017
1248
  title: titleOf("get_report_evidence"),
1018
1249
  description: descOf("get_report_evidence"),
1019
1250
  annotations: annotationsFor("get_report_evidence"),
1020
- inputSchema: {
1251
+ inputSchema: z.object({
1021
1252
  report_id: z.string().describe("Report UUID.")
1022
- }
1253
+ })
1023
1254
  },
1024
1255
  async (args) => {
1025
1256
  const [reportRes, timelineRes] = await Promise.allSettled([
@@ -1067,12 +1298,12 @@ function createMushiServer(config) {
1067
1298
  title: titleOf("triage_issue"),
1068
1299
  description: descOf("triage_issue"),
1069
1300
  annotations: annotationsFor("triage_issue"),
1070
- inputSchema: {
1301
+ inputSchema: z.object({
1071
1302
  report_id: z.string().uuid().describe("Report UUID to triage."),
1072
1303
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1073
1304
  include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1074
- },
1075
- outputSchema: {
1305
+ }),
1306
+ outputSchema: z.object({
1076
1307
  report_id: z.string(),
1077
1308
  severity: z.unknown(),
1078
1309
  category: z.unknown(),
@@ -1085,7 +1316,7 @@ function createMushiServer(config) {
1085
1316
  recent_logs: z.unknown().nullable(),
1086
1317
  recommended_actions: z.array(z.unknown()),
1087
1318
  triage_summary: z.string()
1088
- }
1319
+ })
1089
1320
  },
1090
1321
  async (args) => {
1091
1322
  const { projectId: pid, headers } = await projectScopeHeaders(args.project_id);
@@ -1115,12 +1346,31 @@ function createMushiServer(config) {
1115
1346
  const category = report ? report.category : "unknown";
1116
1347
  const status = report ? report.status : "unknown";
1117
1348
  const actions = [];
1118
- if (status === "new" || status === "classified") {
1349
+ const processingError = report ? report.processing_error : null;
1350
+ const attempts = report ? report.fix_attempts ?? [] : [];
1351
+ const lastAttempt = attempts.length > 0 ? attempts[attempts.length - 1] : null;
1352
+ const lastAttemptStatus = lastAttempt ? String(lastAttempt.status ?? "") : "";
1353
+ const autofixBlocked = typeof processingError === "string" && processingError.startsWith("autofix_blocked:") || lastAttemptStatus.startsWith("skipped") || lastAttemptStatus === "failed";
1354
+ if (autofixBlocked && status !== "fixing" && status !== "fixed") {
1355
+ const blockReason = typeof processingError === "string" && processingError || String(lastAttempt?.error ?? "previous fix attempt was skipped or failed");
1356
+ actions.push({
1357
+ action: "unblock_autofix",
1358
+ reason: `Auto-fix is blocked: ${blockReason}. Resolve the blocker (settings/integration/budget), then re-dispatch.`,
1359
+ tool: "diagnose_setup",
1360
+ args: { mode: "dispatch" }
1361
+ });
1362
+ actions.push({
1363
+ action: "redispatch_after_unblock",
1364
+ reason: "Once the blocker above is resolved, dispatch a fresh fix attempt.",
1365
+ tool: "dispatch_fix",
1366
+ args: { reportId: args.report_id }
1367
+ });
1368
+ } else if (status === "new" || status === "classified") {
1119
1369
  actions.push({
1120
1370
  action: "dispatch_fix",
1121
- reason: "Report is classified but no fix has been attempted",
1371
+ reason: "Report is classified and no fix has been attempted",
1122
1372
  tool: "dispatch_fix",
1123
- args: { reportId: args.report_id, agent: "cursor_cloud" }
1373
+ args: { reportId: args.report_id }
1124
1374
  });
1125
1375
  } else if (status === "fixing") {
1126
1376
  actions.push({
@@ -1190,7 +1440,7 @@ function createMushiServer(config) {
1190
1440
  title: titleOf("submit_fix_result"),
1191
1441
  description: descOf("submit_fix_result"),
1192
1442
  annotations: annotationsFor("submit_fix_result"),
1193
- inputSchema: {
1443
+ inputSchema: z.object({
1194
1444
  reportId: z.string().describe("Report UUID to attach this external fix attempt to"),
1195
1445
  branch: z.string().describe("Git branch name where the fix was implemented"),
1196
1446
  prUrl: z.string().optional().describe("GitHub pull request URL, if a PR was opened"),
@@ -1198,43 +1448,136 @@ function createMushiServer(config) {
1198
1448
  linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
1199
1449
  summary: z.string().describe("One-line human summary of what the fix changed"),
1200
1450
  idempotencyKey: z.string().uuid().optional().describe(
1201
- "Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows"
1451
+ "Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows. Omit it and a stable key is derived from reportId + branch + prUrl, so a retried submission carries the identical key instead of a fresh random one."
1202
1452
  )
1203
- },
1204
- outputSchema: {
1453
+ }),
1454
+ outputSchema: z.object({
1205
1455
  ok: z.boolean().describe("True when the fix_attempt row was created and marked completed"),
1206
1456
  fixId: z.string().describe("UUID of the new fix_attempt row")
1207
- }
1457
+ })
1208
1458
  },
1209
1459
  async (args) => {
1210
- const idemKey = args.idempotencyKey ?? crypto.randomUUID();
1460
+ const idemKey = args.idempotencyKey ?? await stableUuidFrom(IDEMPOTENCY_NAMESPACE, args.reportId, args.branch, args.prUrl ?? "");
1211
1461
  const created = await apiCall("/v1/admin/fixes", {
1212
1462
  method: "POST",
1213
1463
  headers: { "Idempotency-Key": idemKey },
1214
1464
  body: JSON.stringify({ reportId: args.reportId, agent: "mcp" })
1215
1465
  });
1216
- await apiCall(`/v1/admin/fixes/${created.fixId}`, {
1217
- method: "PATCH",
1218
- body: JSON.stringify({
1219
- status: "completed",
1220
- branch: args.branch,
1221
- pr_url: args.prUrl,
1222
- files_changed: args.filesChanged,
1223
- lines_changed: args.linesChanged,
1224
- summary: args.summary,
1225
- completed_at: (/* @__PURE__ */ new Date()).toISOString()
1226
- })
1227
- });
1466
+ try {
1467
+ await apiCall(`/v1/admin/fixes/${created.fixId}`, {
1468
+ method: "PATCH",
1469
+ body: JSON.stringify({
1470
+ status: "completed",
1471
+ branch: args.branch,
1472
+ pr_url: args.prUrl,
1473
+ files_changed: args.filesChanged,
1474
+ lines_changed: args.linesChanged,
1475
+ summary: args.summary,
1476
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
1477
+ })
1478
+ });
1479
+ } catch (err) {
1480
+ const status = err instanceof MushiApiError ? err.status : 500;
1481
+ throw new MushiApiError(
1482
+ status,
1483
+ "FIX_PATCH_FAILED",
1484
+ `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.`
1485
+ );
1486
+ }
1228
1487
  return jsonResult({ ok: true, fixId: created.fixId });
1229
1488
  }
1230
1489
  );
1490
+ server.registerTool(
1491
+ "triage_next_steps",
1492
+ {
1493
+ title: titleOf("triage_next_steps"),
1494
+ description: descOf("triage_next_steps"),
1495
+ annotations: annotationsFor("triage_next_steps"),
1496
+ inputSchema: z.object({
1497
+ project_id: z.string().uuid().optional().describe("Project UUID (defaults to the configured project).")
1498
+ }),
1499
+ outputSchema: z.object({
1500
+ steps: z.array(
1501
+ z.object({
1502
+ priority: z.number(),
1503
+ action: z.string(),
1504
+ reason: z.string(),
1505
+ tool: z.string().optional(),
1506
+ args: z.record(z.string(), z.unknown()).optional()
1507
+ })
1508
+ ),
1509
+ summary: z.string()
1510
+ })
1511
+ },
1512
+ async (args) => {
1513
+ const pid = await resolveProjectId(args.project_id);
1514
+ const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
1515
+ const data = await apiCall(
1516
+ "/v1/admin/reports?limit=100",
1517
+ { headers: extraHeaders }
1518
+ );
1519
+ const reports = data.reports ?? [];
1520
+ const isCron = (r) => typeof r.reporter_token_hash === "string" && r.reporter_token_hash.startsWith("cron:");
1521
+ const isBlocked = (r) => typeof r.processing_error === "string" && r.processing_error.startsWith("autofix_blocked:");
1522
+ const label = (r) => String(r.title ?? r.summary ?? r.description ?? "").slice(0, 90);
1523
+ const steps = [];
1524
+ let priority = 1;
1525
+ const blocked = reports.filter(isBlocked);
1526
+ if (blocked.length > 0) {
1527
+ steps.push({
1528
+ priority: priority++,
1529
+ action: `Unblock auto-fix (${blocked.length} report${blocked.length === 1 ? "" : "s"} blocked)`,
1530
+ reason: String(blocked[0].processing_error).slice(0, 200),
1531
+ tool: "diagnose_setup",
1532
+ args: { mode: "dispatch" }
1533
+ });
1534
+ }
1535
+ const fixing = reports.filter((r) => r.status === "fixing");
1536
+ for (const r of fixing.slice(0, 2)) {
1537
+ steps.push({
1538
+ priority: priority++,
1539
+ action: `Shepherd in-flight fix: ${label(r)}`,
1540
+ reason: "A fix branch/PR is open \u2014 check CI and merge when green.",
1541
+ tool: "get_report_timeline",
1542
+ args: { report_id: r.id }
1543
+ });
1544
+ }
1545
+ const openUserReports = reports.filter(
1546
+ (r) => (r.status === "new" || r.status === "classified") && !isCron(r) && !isBlocked(r)
1547
+ ).sort((a, b) => {
1548
+ const rank = { critical: 0, high: 1, medium: 2, low: 3 };
1549
+ return (rank[String(a.severity)] ?? 4) - (rank[String(b.severity)] ?? 4);
1550
+ });
1551
+ for (const r of openUserReports.slice(0, Math.max(0, 4 - steps.length))) {
1552
+ steps.push({
1553
+ priority: priority++,
1554
+ action: `Triage [${r.severity}] ${label(r)}`,
1555
+ reason: "User-felt report awaiting triage \u2014 review the packet, then dispatch or dismiss.",
1556
+ tool: "triage_issue",
1557
+ args: { report_id: r.id }
1558
+ });
1559
+ }
1560
+ const chores = reports.filter((r) => r.status === "classified" && isCron(r) && !isBlocked(r));
1561
+ if (chores.length > 0 && steps.length < 5) {
1562
+ steps.push({
1563
+ priority: priority++,
1564
+ action: `Batch ${chores.length} maintenance chore${chores.length === 1 ? "" : "s"} (dependency bumps)`,
1565
+ reason: "Robot-filed modernization reports \u2014 batch-dispatch or dismiss in one sitting; do not let them crowd out user bugs.",
1566
+ tool: "get_recent_reports",
1567
+ args: { status: "classified" }
1568
+ });
1569
+ }
1570
+ 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 ");
1571
+ return jsonResult({ steps, summary });
1572
+ }
1573
+ );
1231
1574
  server.registerTool(
1232
1575
  "dispatch_fix",
1233
1576
  {
1234
1577
  title: titleOf("dispatch_fix"),
1235
1578
  description: descOf("dispatch_fix"),
1236
1579
  annotations: annotationsFor("dispatch_fix"),
1237
- inputSchema: {
1580
+ inputSchema: z.object({
1238
1581
  reportId: z.string().uuid().describe("Report UUID to fix"),
1239
1582
  agent: z.enum(["claude_code", "codex", "rest_worker", "mcp"]).optional().describe("Override the agent adapter"),
1240
1583
  idempotencyKey: z.string().uuid().optional().describe(
@@ -1243,19 +1586,19 @@ function createMushiServer(config) {
1243
1586
  inventoryActionNodeId: z.string().uuid().optional().describe(
1244
1587
  "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."
1245
1588
  )
1246
- },
1247
- outputSchema: {
1589
+ }),
1590
+ outputSchema: z.object({
1248
1591
  fixId: z.string(),
1249
1592
  status: z.string()
1250
- }
1593
+ })
1251
1594
  },
1252
- async (args, extra) => {
1253
- if (extra?.sendNotification && extra?._meta?.progressToken) {
1595
+ async (args, ctx) => {
1596
+ if (ctx?.mcpReq.notify && ctx?.mcpReq._meta?.progressToken) {
1254
1597
  try {
1255
- await extra.sendNotification({
1598
+ await ctx.mcpReq.notify({
1256
1599
  method: "notifications/progress",
1257
1600
  params: {
1258
- progressToken: extra._meta.progressToken,
1601
+ progressToken: ctx.mcpReq._meta.progressToken,
1259
1602
  progress: 0,
1260
1603
  total: 100,
1261
1604
  message: "Dispatching Mushi fix orchestrator\u2026"
@@ -1264,7 +1607,7 @@ function createMushiServer(config) {
1264
1607
  } catch {
1265
1608
  }
1266
1609
  }
1267
- const data = await apiCall("/v1/admin/fixes/dispatch", {
1610
+ const dispatch = await apiCall("/v1/admin/fixes/dispatch", {
1268
1611
  method: "POST",
1269
1612
  headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
1270
1613
  body: JSON.stringify({
@@ -1274,7 +1617,12 @@ function createMushiServer(config) {
1274
1617
  ...projectId ? { projectId } : {}
1275
1618
  })
1276
1619
  });
1277
- return jsonResult(data);
1620
+ if (!dispatch?.dispatchId) {
1621
+ throw new Error(
1622
+ `Dispatch response missing dispatchId \u2014 got: ${JSON.stringify(dispatch).slice(0, 300)}`
1623
+ );
1624
+ }
1625
+ return jsonResult({ fixId: dispatch.dispatchId, status: dispatch.status ?? "queued" });
1278
1626
  }
1279
1627
  );
1280
1628
  server.registerTool(
@@ -1283,13 +1631,13 @@ function createMushiServer(config) {
1283
1631
  title: titleOf("trigger_judge"),
1284
1632
  description: descOf("trigger_judge"),
1285
1633
  annotations: annotationsFor("trigger_judge"),
1286
- inputSchema: {
1634
+ inputSchema: z.object({
1287
1635
  limit: z.number().optional().describe("Max reports to judge in this batch (default 25, max 100)"),
1288
1636
  projectId: z.string().optional().describe("Restrict to one project when the API key owns multiple")
1289
- },
1290
- outputSchema: {
1637
+ }),
1638
+ outputSchema: z.object({
1291
1639
  dispatched: z.number().describe("Number of judge-batch jobs dispatched (one per accessible project)")
1292
- }
1640
+ })
1293
1641
  },
1294
1642
  async (args) => {
1295
1643
  const data = await apiCall("/v1/admin/judge/run", {
@@ -1308,10 +1656,10 @@ function createMushiServer(config) {
1308
1656
  title: titleOf("test_gen_from_report"),
1309
1657
  description: descOf("test_gen_from_report"),
1310
1658
  annotations: annotationsFor("test_gen_from_report"),
1311
- inputSchema: {
1659
+ inputSchema: z.object({
1312
1660
  reportId: z.string().describe("Report UUID to turn into a Playwright PR"),
1313
1661
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
1314
- }
1662
+ })
1315
1663
  },
1316
1664
  async (args) => {
1317
1665
  const pid = args.projectId ?? projectId;
@@ -1337,16 +1685,16 @@ function createMushiServer(config) {
1337
1685
  title: titleOf("merge_fix"),
1338
1686
  description: descOf("merge_fix"),
1339
1687
  annotations: annotationsFor("merge_fix"),
1340
- inputSchema: {
1688
+ inputSchema: z.object({
1341
1689
  fixId: z.string().describe("Fix attempt UUID whose GitHub PR should be squash-merged"),
1342
1690
  mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method (default squash)")
1343
- },
1344
- outputSchema: {
1691
+ }),
1692
+ outputSchema: z.object({
1345
1693
  merged: z.boolean().optional().describe("True when GitHub accepted the merge in this call"),
1346
1694
  alreadyMerged: z.boolean().optional().describe("True when the PR was already merged (idempotent no-op)"),
1347
1695
  reportId: z.string().describe("Report UUID linked to this fix attempt"),
1348
1696
  reportStatus: z.string().describe("Report workflow status after merge bookkeeping")
1349
- }
1697
+ })
1350
1698
  },
1351
1699
  async (args) => {
1352
1700
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/merge`, {
@@ -1362,14 +1710,14 @@ function createMushiServer(config) {
1362
1710
  title: titleOf("refresh_ci"),
1363
1711
  description: descOf("refresh_ci"),
1364
1712
  annotations: annotationsFor("refresh_ci"),
1365
- inputSchema: {
1713
+ inputSchema: z.object({
1366
1714
  fixId: z.string().describe("Fix attempt UUID whose PR check-runs should be re-polled")
1367
- },
1368
- outputSchema: {
1715
+ }),
1716
+ outputSchema: z.object({
1369
1717
  check_run_status: z.string().nullable().describe("GitHub check run status after refresh"),
1370
1718
  check_run_conclusion: z.string().nullable().describe("success | failure | neutral | null while pending"),
1371
1719
  check_run_updated_at: z.string().nullable().describe("ISO timestamp when CI status was last persisted")
1372
- }
1720
+ })
1373
1721
  },
1374
1722
  async (args) => {
1375
1723
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/refresh-ci`, { method: "POST" });
@@ -1382,13 +1730,13 @@ function createMushiServer(config) {
1382
1730
  title: titleOf("reopen_report"),
1383
1731
  description: descOf("reopen_report"),
1384
1732
  annotations: annotationsFor("reopen_report"),
1385
- inputSchema: {
1733
+ inputSchema: z.object({
1386
1734
  reportId: z.string().describe("Report UUID to move back to reopened status"),
1387
1735
  note: z.string().optional().describe("Operator note recorded on the reopen transition")
1388
- },
1389
- outputSchema: {
1736
+ }),
1737
+ outputSchema: z.object({
1390
1738
  report: z.unknown().describe("Updated report row with status=reopened")
1391
- }
1739
+ })
1392
1740
  },
1393
1741
  async (args) => {
1394
1742
  const report = await apiCall(`/v1/sync/reports/${args.reportId}`, {
@@ -1404,11 +1752,16 @@ function createMushiServer(config) {
1404
1752
  title: titleOf("transition_status"),
1405
1753
  description: descOf("transition_status"),
1406
1754
  annotations: annotationsFor("transition_status"),
1407
- inputSchema: {
1755
+ inputSchema: z.object({
1408
1756
  reportId: z.string().describe("Report UUID"),
1409
1757
  status: z.enum([
1410
1758
  "pending",
1411
1759
  "classified",
1760
+ // Workflow states added 20260520600000 but missing from this
1761
+ // enum ever since (2026-08-16 audit P2-2) — agents could not
1762
+ // mark a report as being looked at.
1763
+ "triaged",
1764
+ "in_progress",
1412
1765
  "grouped",
1413
1766
  "fixing",
1414
1767
  "fixed",
@@ -1416,9 +1769,9 @@ function createMushiServer(config) {
1416
1769
  "verified",
1417
1770
  "reopened",
1418
1771
  "dismissed"
1419
- ]).describe("Target status"),
1772
+ ]).describe("Target status (resolved is stored as fixed)"),
1420
1773
  reason: z.string().optional().describe("Reason for the transition (audit trail)")
1421
- }
1774
+ })
1422
1775
  },
1423
1776
  async (args) => {
1424
1777
  const data = await apiCall(`/v1/admin/reports/${args.reportId}`, {
@@ -1434,9 +1787,9 @@ function createMushiServer(config) {
1434
1787
  title: titleOf("setup_repo_for_mushi"),
1435
1788
  description: descOf("setup_repo_for_mushi"),
1436
1789
  annotations: annotationsFor("setup_repo_for_mushi"),
1437
- inputSchema: {
1790
+ inputSchema: z.object({
1438
1791
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
1439
- }
1792
+ })
1440
1793
  },
1441
1794
  async (args) => {
1442
1795
  const pid = args.projectId ?? projectId;
@@ -1459,12 +1812,12 @@ function createMushiServer(config) {
1459
1812
  title: titleOf("query_lessons"),
1460
1813
  description: descOf("query_lessons"),
1461
1814
  annotations: annotationsFor("query_lessons"),
1462
- inputSchema: {
1815
+ inputSchema: z.object({
1463
1816
  diff_text: z.string().describe("The PR diff, code snippet, or description of the change being made."),
1464
1817
  max_tokens: z.number().optional().describe("Maximum tokens for returned lessons context (default 3000, max 8000)."),
1465
1818
  top_k: z.number().optional().describe("Max number of lessons to return (default 15, max 50)."),
1466
1819
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1467
- }
1820
+ })
1468
1821
  },
1469
1822
  async (args) => {
1470
1823
  const pid = args.project_id ?? projectId;
@@ -1486,14 +1839,14 @@ function createMushiServer(config) {
1486
1839
  title: titleOf("list_lessons"),
1487
1840
  description: descOf("list_lessons"),
1488
1841
  annotations: annotationsFor("list_lessons"),
1489
- inputSchema: {
1842
+ inputSchema: z.object({
1490
1843
  severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter to one severity level"),
1491
1844
  limit: z.number().optional().describe("Max lessons to return (default 50, max 200)"),
1492
1845
  project_id: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
1493
- },
1494
- outputSchema: {
1846
+ }),
1847
+ outputSchema: z.object({
1495
1848
  lessons: z.array(z.unknown()).describe("Promoted lesson rows ordered by frequency")
1496
- }
1849
+ })
1497
1850
  },
1498
1851
  async (args) => {
1499
1852
  const pid = args.project_id ?? projectId;
@@ -1505,7 +1858,7 @@ function createMushiServer(config) {
1505
1858
  return wrappedJsonResult({ lessons }, "lesson text");
1506
1859
  }
1507
1860
  );
1508
- server.resource(
1861
+ server.registerResource(
1509
1862
  "project_stats",
1510
1863
  "project://stats",
1511
1864
  { description: "Report counts, category breakdown, severity distribution" },
@@ -1519,7 +1872,7 @@ function createMushiServer(config) {
1519
1872
  ]
1520
1873
  })
1521
1874
  );
1522
- server.resource(
1875
+ server.registerResource(
1523
1876
  "project_settings",
1524
1877
  "project://settings",
1525
1878
  {
@@ -1535,7 +1888,7 @@ function createMushiServer(config) {
1535
1888
  ]
1536
1889
  })
1537
1890
  );
1538
- server.resource(
1891
+ server.registerResource(
1539
1892
  "project_dashboard",
1540
1893
  "project://dashboard",
1541
1894
  {
@@ -1557,10 +1910,10 @@ function createMushiServer(config) {
1557
1910
  title: titleOf("list_top_contributors"),
1558
1911
  description: descOf("list_top_contributors"),
1559
1912
  annotations: annotationsFor("list_top_contributors"),
1560
- inputSchema: {
1913
+ inputSchema: z.object({
1561
1914
  limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
1562
1915
  range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
1563
- }
1916
+ })
1564
1917
  },
1565
1918
  async ({ limit, range }) => ({
1566
1919
  content: [
@@ -1581,11 +1934,11 @@ function createMushiServer(config) {
1581
1934
  title: titleOf("award_bonus_points"),
1582
1935
  description: descOf("award_bonus_points"),
1583
1936
  annotations: annotationsFor("award_bonus_points"),
1584
- inputSchema: {
1937
+ inputSchema: z.object({
1585
1938
  external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1586
1939
  points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
1587
1940
  reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
1588
- }
1941
+ })
1589
1942
  },
1590
1943
  async ({ external_user_id, points, reason }) => ({
1591
1944
  content: [
@@ -1610,11 +1963,11 @@ function createMushiServer(config) {
1610
1963
  title: titleOf("set_tier"),
1611
1964
  description: descOf("set_tier"),
1612
1965
  annotations: annotationsFor("set_tier"),
1613
- inputSchema: {
1966
+ inputSchema: z.object({
1614
1967
  external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1615
1968
  tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
1616
1969
  reason: z.string().max(200).optional().describe("Optional reason for manual override")
1617
- }
1970
+ })
1618
1971
  },
1619
1972
  async ({ external_user_id, tier_slug, reason }) => ({
1620
1973
  content: [
@@ -1633,7 +1986,7 @@ function createMushiServer(config) {
1633
1986
  ]
1634
1987
  })
1635
1988
  );
1636
- server.resource(
1989
+ server.registerResource(
1637
1990
  "privacy_status",
1638
1991
  "privacy://status",
1639
1992
  {
@@ -1649,7 +2002,7 @@ function createMushiServer(config) {
1649
2002
  ]
1650
2003
  })
1651
2004
  );
1652
- server.resource(
2005
+ server.registerResource(
1653
2006
  "evolution_history",
1654
2007
  "evolution://history",
1655
2008
  {
@@ -1671,7 +2024,7 @@ function createMushiServer(config) {
1671
2024
  ]
1672
2025
  })
1673
2026
  );
1674
- server.resource(
2027
+ server.registerResource(
1675
2028
  "project_integration_health",
1676
2029
  "project://integration-health",
1677
2030
  {
@@ -1687,7 +2040,7 @@ function createMushiServer(config) {
1687
2040
  ]
1688
2041
  })
1689
2042
  );
1690
- server.resource(
2043
+ server.registerResource(
1691
2044
  "inventory_current",
1692
2045
  "inventory://current",
1693
2046
  {
@@ -1712,9 +2065,9 @@ function createMushiServer(config) {
1712
2065
  title: titleOf("activation_status"),
1713
2066
  description: descOf("activation_status"),
1714
2067
  annotations: annotationsFor("activation_status"),
1715
- inputSchema: {
2068
+ inputSchema: z.object({
1716
2069
  project_id: z.string().optional().describe("Project UUID (defaults to the configured project).")
1717
- }
2070
+ })
1718
2071
  },
1719
2072
  async (args) => {
1720
2073
  const pid = args.project_id ?? projectId;
@@ -1722,7 +2075,7 @@ function createMushiServer(config) {
1722
2075
  return jsonResult(await apiCall(`/v1/admin/activation${qs}`));
1723
2076
  }
1724
2077
  );
1725
- server.resource(
2078
+ server.registerResource(
1726
2079
  "activation_status",
1727
2080
  "mushi://activation",
1728
2081
  {
@@ -1741,10 +2094,12 @@ function createMushiServer(config) {
1741
2094
  };
1742
2095
  }
1743
2096
  );
1744
- server.prompt(
2097
+ server.registerPrompt(
1745
2098
  "summarize_report_for_fix",
1746
- "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.",
1747
- { reportId: z.string().describe("The report UUID to summarize") },
2099
+ {
2100
+ 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.",
2101
+ argsSchema: z.object({ reportId: z.string().describe("The report UUID to summarize") })
2102
+ },
1748
2103
  ({ reportId }) => ({
1749
2104
  messages: [
1750
2105
  {
@@ -1769,10 +2124,12 @@ Lead with the fix. Skip the preamble.`
1769
2124
  ]
1770
2125
  })
1771
2126
  );
1772
- server.prompt(
2127
+ server.registerPrompt(
1773
2128
  "explain_judge_result",
1774
- "Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
1775
- { fixId: z.string().describe("The fix_attempt UUID to explain") },
2129
+ {
2130
+ description: "Turn raw Sonnet-as-Judge scores into ship / iterate / dismiss guidance. Use after a fix attempt has been judged.",
2131
+ argsSchema: z.object({ fixId: z.string().describe("The fix_attempt UUID to explain") })
2132
+ },
1776
2133
  ({ fixId }) => ({
1777
2134
  messages: [
1778
2135
  {
@@ -1793,10 +2150,12 @@ No preamble, no score-by-score recap \u2014 the numbers are in the tool output.`
1793
2150
  ]
1794
2151
  })
1795
2152
  );
1796
- server.prompt(
2153
+ server.registerPrompt(
1797
2154
  "triage_next_steps",
1798
- 'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
1799
- {},
2155
+ {
2156
+ description: 'Answer "what should I focus on right now?" \u2014 five-item markdown list drawn from the dashboard + recent classified queue.',
2157
+ argsSchema: z.object({})
2158
+ },
1800
2159
  () => ({
1801
2160
  messages: [
1802
2161
  {
@@ -1816,10 +2175,12 @@ Prefer items that are bottlenecks or critical severity. Skip filler.`
1816
2175
  ]
1817
2176
  })
1818
2177
  );
1819
- server.prompt(
2178
+ server.registerPrompt(
1820
2179
  "mushi_setup",
1821
- "Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
1822
- {},
2180
+ {
2181
+ description: "Diagnose why Mushi setup is stuck and return the single next command or console step to unblock it.",
2182
+ argsSchema: z.object({})
2183
+ },
1823
2184
  () => ({
1824
2185
  messages: [
1825
2186
  {
@@ -1849,13 +2210,13 @@ Be specific. No generic advice.`
1849
2210
  title: titleOf("map_user_stories", TDD_TOOL_CATALOG),
1850
2211
  description: descOf("map_user_stories", TDD_TOOL_CATALOG),
1851
2212
  annotations: annotationsFor("map_user_stories", TDD_TOOL_CATALOG),
1852
- inputSchema: {
2213
+ inputSchema: z.object({
1853
2214
  projectId: z.string().describe("Project id to map stories for"),
1854
2215
  baseUrl: z.string().url().describe("Live app URL to crawl"),
1855
2216
  maxPages: z.number().int().min(1).max(50).optional().describe("Max pages to crawl (default 20)"),
1856
2217
  provider: z.enum(["firecrawl", "browserbase"]).optional().describe("Crawl provider (default: firecrawl)"),
1857
2218
  cursorCloudRefine: z.boolean().optional().describe("Dispatch Cursor Cloud agent to refine and open a PR")
1858
- }
2219
+ })
1859
2220
  },
1860
2221
  async ({ projectId: projectId2, baseUrl, maxPages, provider, cursorCloudRefine }) => {
1861
2222
  if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
@@ -1880,9 +2241,9 @@ Be specific. No generic advice.`
1880
2241
  title: titleOf("get_map_run_status", TDD_TOOL_CATALOG),
1881
2242
  description: descOf("get_map_run_status", TDD_TOOL_CATALOG),
1882
2243
  annotations: annotationsFor("get_map_run_status", TDD_TOOL_CATALOG),
1883
- inputSchema: {
2244
+ inputSchema: z.object({
1884
2245
  projectId: z.string().describe("Project id")
1885
- }
2246
+ })
1886
2247
  },
1887
2248
  async ({ projectId: projectId2 }) => {
1888
2249
  const data = await apiCall(`/v1/admin/inventory/${projectId2}/map-runs`);
@@ -1895,7 +2256,7 @@ Be specific. No generic advice.`
1895
2256
  title: titleOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
1896
2257
  description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
1897
2258
  annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
1898
- inputSchema: {
2259
+ inputSchema: z.object({
1899
2260
  projectId: z.string().describe("Project UUID that owns the inventory story"),
1900
2261
  storyNodeId: z.string().describe(
1901
2262
  "User story node id/slug from accepted inventory (from map_user_stories or inventory.yaml)"
@@ -1903,13 +2264,13 @@ Be specific. No generic advice.`
1903
2264
  automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Approval gate for the generated qa_story (default review \u2192 pending_review)"),
1904
2265
  baseUrl: z.string().url().optional().describe("Override the live app URL the Playwright test should target"),
1905
2266
  openPr: z.boolean().optional().describe("Open a draft GitHub PR with the generated spec (default true)")
1906
- },
1907
- outputSchema: {
2267
+ }),
2268
+ outputSchema: z.object({
1908
2269
  qaStoryId: z.string().describe("UUID of the inserted qa_stories row"),
1909
2270
  prUrl: z.string().nullable().describe("GitHub PR URL when openPr=true and GitHub is connected"),
1910
2271
  approvalStatus: z.string().describe("pending_review | approved \u2014 follows automationMode and project defaults"),
1911
2272
  needsHumanReview: z.boolean().describe("True when the story requires operator approval before scheduled runs")
1912
- }
2273
+ })
1913
2274
  },
1914
2275
  async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
1915
2276
  if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
@@ -1930,9 +2291,9 @@ Be specific. No generic advice.`
1930
2291
  title: titleOf("improve_qa_story", TDD_TOOL_CATALOG),
1931
2292
  description: descOf("improve_qa_story", TDD_TOOL_CATALOG),
1932
2293
  annotations: annotationsFor("improve_qa_story", TDD_TOOL_CATALOG),
1933
- inputSchema: {
2294
+ inputSchema: z.object({
1934
2295
  projectId: z.string().optional().describe("Project id (omit to run across all projects)")
1935
- }
2296
+ })
1936
2297
  },
1937
2298
  async ({ projectId: projectId2 }) => {
1938
2299
  const data = await apiCall("/v1/admin/pdca/improve-qa-stories", {
@@ -1948,10 +2309,10 @@ Be specific. No generic advice.`
1948
2309
  title: titleOf("run_qa_story", TDD_TOOL_CATALOG),
1949
2310
  description: descOf("run_qa_story", TDD_TOOL_CATALOG),
1950
2311
  annotations: annotationsFor("run_qa_story", TDD_TOOL_CATALOG),
1951
- inputSchema: {
2312
+ inputSchema: z.object({
1952
2313
  projectId: z.string().describe("Project id"),
1953
2314
  qaStoryId: z.string().describe("qa_story id to run")
1954
- }
2315
+ })
1955
2316
  },
1956
2317
  async ({ projectId: projectId2, qaStoryId }) => {
1957
2318
  const data = await apiCall(
@@ -1967,9 +2328,9 @@ Be specific. No generic advice.`
1967
2328
  title: titleOf("list_byok_keys", TDD_TOOL_CATALOG),
1968
2329
  description: descOf("list_byok_keys", TDD_TOOL_CATALOG),
1969
2330
  annotations: annotationsFor("list_byok_keys", TDD_TOOL_CATALOG),
1970
- inputSchema: {
2331
+ inputSchema: z.object({
1971
2332
  projectId: z.string().describe("Project id")
1972
- }
2333
+ })
1973
2334
  },
1974
2335
  async ({ projectId: projectId2 }) => {
1975
2336
  const data = await apiCall(
@@ -1984,14 +2345,14 @@ Be specific. No generic advice.`
1984
2345
  title: titleOf("add_byok_key", TDD_TOOL_CATALOG),
1985
2346
  description: descOf("add_byok_key", TDD_TOOL_CATALOG),
1986
2347
  annotations: annotationsFor("add_byok_key", TDD_TOOL_CATALOG),
1987
- inputSchema: {
2348
+ inputSchema: z.object({
1988
2349
  projectId: z.string().describe("Project id"),
1989
2350
  provider: z.enum(["anthropic", "openai", "firecrawl", "browserbase", "cursor"]).describe("Provider slug"),
1990
2351
  key: z.string().min(8).max(4096).describe("The API key value to add"),
1991
2352
  label: z.string().optional().describe("Human-readable label for this key"),
1992
2353
  priority: z.number().int().min(0).max(1e4).optional().describe("Priority for ordering (lower = higher priority)"),
1993
2354
  baseUrl: z.string().url().max(2048).optional().describe("Allow-listed OpenAI-compatible HTTPS base URL")
1994
- }
2355
+ })
1995
2356
  },
1996
2357
  async ({ projectId: projectId2, provider, key, label, priority, baseUrl }) => {
1997
2358
  const data = await apiCall("/v1/admin/byok/keys", {
@@ -2014,10 +2375,10 @@ Be specific. No generic advice.`
2014
2375
  title: titleOf("test_byok_key", TDD_TOOL_CATALOG),
2015
2376
  description: descOf("test_byok_key", TDD_TOOL_CATALOG),
2016
2377
  annotations: annotationsFor("test_byok_key", TDD_TOOL_CATALOG),
2017
- inputSchema: {
2378
+ inputSchema: z.object({
2018
2379
  projectId: z.string().describe("Project id"),
2019
2380
  keyId: z.string().uuid().describe("Pooled BYOK key id")
2020
- }
2381
+ })
2021
2382
  },
2022
2383
  async ({ keyId }) => {
2023
2384
  const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}/test`, {
@@ -2032,10 +2393,10 @@ Be specific. No generic advice.`
2032
2393
  title: titleOf("remove_byok_key", TDD_TOOL_CATALOG),
2033
2394
  description: descOf("remove_byok_key", TDD_TOOL_CATALOG),
2034
2395
  annotations: annotationsFor("remove_byok_key", TDD_TOOL_CATALOG),
2035
- inputSchema: {
2396
+ inputSchema: z.object({
2036
2397
  projectId: z.string().describe("Project id"),
2037
2398
  keyId: z.string().uuid().describe("Pooled BYOK key UUID from list_byok_keys")
2038
- }
2399
+ })
2039
2400
  },
2040
2401
  async ({ keyId }) => {
2041
2402
  const data = await apiCall(`/v1/admin/byok/keys/${encodeURIComponent(keyId)}`, {
@@ -2050,9 +2411,9 @@ Be specific. No generic advice.`
2050
2411
  title: titleOf("list_pending_review_stories", TDD_TOOL_CATALOG),
2051
2412
  description: descOf("list_pending_review_stories", TDD_TOOL_CATALOG),
2052
2413
  annotations: annotationsFor("list_pending_review_stories", TDD_TOOL_CATALOG),
2053
- inputSchema: {
2414
+ inputSchema: z.object({
2054
2415
  projectId: z.string().describe("Project id")
2055
- }
2416
+ })
2056
2417
  },
2057
2418
  async ({ projectId: projectId2 }) => {
2058
2419
  const data = await apiCall(
@@ -2067,11 +2428,11 @@ Be specific. No generic advice.`
2067
2428
  title: titleOf("approve_qa_story", TDD_TOOL_CATALOG),
2068
2429
  description: descOf("approve_qa_story", TDD_TOOL_CATALOG),
2069
2430
  annotations: annotationsFor("approve_qa_story", TDD_TOOL_CATALOG),
2070
- inputSchema: {
2431
+ inputSchema: z.object({
2071
2432
  projectId: z.string().describe("Project id"),
2072
2433
  qaStoryId: z.string().describe("QA story id to approve or reject"),
2073
2434
  status: z.enum(["approved", "rejected"]).describe("New approval status")
2074
- }
2435
+ })
2075
2436
  },
2076
2437
  async ({ projectId: projectId2, qaStoryId, status }) => {
2077
2438
  const data = await apiCall(
@@ -2087,11 +2448,11 @@ Be specific. No generic advice.`
2087
2448
  title: titleOf("reply_to_reporter", TDD_TOOL_CATALOG),
2088
2449
  description: descOf("reply_to_reporter", TDD_TOOL_CATALOG),
2089
2450
  annotations: annotationsFor("reply_to_reporter", TDD_TOOL_CATALOG),
2090
- inputSchema: {
2451
+ inputSchema: z.object({
2091
2452
  reportId: z.string().describe("Report id to reply to"),
2092
2453
  message: z.string().min(1).max(1e4).describe("Message text to send to the reporter"),
2093
2454
  authorName: z.string().optional().describe('Display name for the admin sender (default: "Mushi Admin")')
2094
- }
2455
+ })
2095
2456
  },
2096
2457
  async ({ reportId, message, authorName }) => {
2097
2458
  const data = await apiCall(`/v1/sync/reports/${reportId}/reply`, {
@@ -2107,10 +2468,10 @@ Be specific. No generic advice.`
2107
2468
  title: titleOf("list_qa_story_runs", TDD_TOOL_CATALOG),
2108
2469
  description: descOf("list_qa_story_runs", TDD_TOOL_CATALOG),
2109
2470
  annotations: annotationsFor("list_qa_story_runs", TDD_TOOL_CATALOG),
2110
- inputSchema: {
2471
+ inputSchema: z.object({
2111
2472
  storyId: z.string().describe("QA story id (uuid)"),
2112
2473
  limit: z.number().int().min(1).max(50).optional().default(10).describe("Max runs to return (default 10)")
2113
- }
2474
+ })
2114
2475
  },
2115
2476
  async ({ storyId, limit }) => {
2116
2477
  const data = await apiCall(
@@ -2125,10 +2486,10 @@ Be specific. No generic advice.`
2125
2486
  title: titleOf("get_qa_story_run", TDD_TOOL_CATALOG),
2126
2487
  description: descOf("get_qa_story_run", TDD_TOOL_CATALOG),
2127
2488
  annotations: annotationsFor("get_qa_story_run", TDD_TOOL_CATALOG),
2128
- inputSchema: {
2489
+ inputSchema: z.object({
2129
2490
  storyId: z.string().describe("QA story id (uuid)"),
2130
2491
  runId: z.string().describe("Run id (uuid) to fetch detail for")
2131
- }
2492
+ })
2132
2493
  },
2133
2494
  async ({ storyId, runId }) => {
2134
2495
  const data = await apiCall(
@@ -2151,9 +2512,9 @@ Be specific. No generic advice.`
2151
2512
  title: titleOf("test_notification_channel", TDD_TOOL_CATALOG),
2152
2513
  description: descOf("test_notification_channel", TDD_TOOL_CATALOG),
2153
2514
  annotations: annotationsFor("test_notification_channel", TDD_TOOL_CATALOG),
2154
- inputSchema: {
2515
+ inputSchema: z.object({
2155
2516
  kind: z.enum(["slack", "discord"]).describe("Notification channel kind to test")
2156
- }
2517
+ })
2157
2518
  },
2158
2519
  async ({ kind }) => {
2159
2520
  const data = await apiCall(
@@ -2169,9 +2530,9 @@ Be specific. No generic advice.`
2169
2530
  title: titleOf("run_fullstack_audit"),
2170
2531
  description: descOf("run_fullstack_audit"),
2171
2532
  annotations: annotationsFor("run_fullstack_audit"),
2172
- inputSchema: {
2533
+ inputSchema: z.object({
2173
2534
  project_id: z.string().optional().describe("Project ID to audit. Defaults to the configured project.")
2174
- }
2535
+ })
2175
2536
  },
2176
2537
  async ({ project_id }) => {
2177
2538
  const pid = project_id ?? config.projectId;
@@ -2189,10 +2550,10 @@ Be specific. No generic advice.`
2189
2550
  title: titleOf("get_backend_health"),
2190
2551
  description: descOf("get_backend_health"),
2191
2552
  annotations: annotationsFor("get_backend_health"),
2192
- inputSchema: {
2553
+ inputSchema: z.object({
2193
2554
  project_id: z.string().optional().describe("Project ID. Defaults to the configured project."),
2194
2555
  include_logs: z.boolean().optional().describe("Whether to include recent backend error logs (default: true).")
2195
- }
2556
+ })
2196
2557
  },
2197
2558
  async ({ project_id, include_logs = true }) => {
2198
2559
  const pid = project_id ?? config.projectId;
@@ -2216,9 +2577,9 @@ Be specific. No generic advice.`
2216
2577
  title: titleOf("get_usage"),
2217
2578
  description: descOf("get_usage"),
2218
2579
  annotations: annotationsFor("get_usage"),
2219
- inputSchema: {
2580
+ inputSchema: z.object({
2220
2581
  project_id: z.string().optional().describe("Project ID. Defaults to the configured project.")
2221
- }
2582
+ })
2222
2583
  },
2223
2584
  async ({ project_id }) => {
2224
2585
  const pid = project_id ?? config.projectId;
@@ -2233,12 +2594,12 @@ Be specific. No generic advice.`
2233
2594
  title: titleOf("list_skills"),
2234
2595
  description: descOf("list_skills"),
2235
2596
  annotations: annotationsFor("list_skills"),
2236
- inputSchema: {
2597
+ inputSchema: z.object({
2237
2598
  category: z.string().optional().describe("Filter by category: workflow, debug, test, audit, enhance, \u2026"),
2238
2599
  search: z.string().optional().describe("Free-text search across slug, title, description"),
2239
2600
  page: z.number().optional().describe("Page number (default 1)"),
2240
2601
  limit: z.number().optional().describe("Max results per page (default 200, max 200)")
2241
- }
2602
+ })
2242
2603
  },
2243
2604
  async (args) => {
2244
2605
  const qs = new URLSearchParams();
@@ -2257,9 +2618,9 @@ Be specific. No generic advice.`
2257
2618
  title: titleOf("get_skill"),
2258
2619
  description: descOf("get_skill"),
2259
2620
  annotations: annotationsFor("get_skill"),
2260
- inputSchema: {
2621
+ inputSchema: z.object({
2261
2622
  slug: z.string().describe('Skill slug, e.g. "workflow-fix-and-ship"')
2262
- }
2623
+ })
2263
2624
  },
2264
2625
  async (args) => {
2265
2626
  const data = await apiCall(`/v1/admin/skills/${args.slug}`);
@@ -2272,14 +2633,14 @@ Be specific. No generic advice.`
2272
2633
  title: titleOf("start_skill_pipeline"),
2273
2634
  description: descOf("start_skill_pipeline"),
2274
2635
  annotations: annotationsFor("start_skill_pipeline"),
2275
- inputSchema: {
2636
+ inputSchema: z.object({
2276
2637
  root_skill_slug: z.string().describe('Root skill slug to run, e.g. "workflow-fix-and-ship"'),
2277
2638
  report_id: z.string().optional().describe("Report UUID to attach the pipeline to"),
2278
2639
  mode: z.enum(["handoff", "cloud"]).optional().describe(
2279
2640
  "handoff (default): get context packet for local agent. cloud: auto-dispatch via Cursor Cloud."
2280
2641
  ),
2281
2642
  project_id: z.string().optional().describe("Project UUID. Falls back to the configured project.")
2282
- }
2643
+ })
2283
2644
  },
2284
2645
  async (args) => {
2285
2646
  const resolvedProjectId = args.project_id ?? projectId;
@@ -2297,9 +2658,9 @@ Be specific. No generic advice.`
2297
2658
  title: titleOf("get_pipeline_run"),
2298
2659
  description: descOf("get_pipeline_run"),
2299
2660
  annotations: annotationsFor("get_pipeline_run"),
2300
- inputSchema: {
2661
+ inputSchema: z.object({
2301
2662
  run_id: z.string().describe("Pipeline run UUID")
2302
- }
2663
+ })
2303
2664
  },
2304
2665
  async (args) => {
2305
2666
  const data = await apiCall(`/v1/admin/skills/pipelines/${args.run_id}`);
@@ -2312,14 +2673,14 @@ Be specific. No generic advice.`
2312
2673
  title: titleOf("checkin_pipeline_step"),
2313
2674
  description: descOf("checkin_pipeline_step"),
2314
2675
  annotations: annotationsFor("checkin_pipeline_step"),
2315
- inputSchema: {
2676
+ inputSchema: z.object({
2316
2677
  run_id: z.string().describe("Pipeline run UUID"),
2317
2678
  step_index: z.number().describe("Step index (0-based)"),
2318
2679
  status: z.enum(["running", "passed", "failed", "skipped"]).describe("Step status"),
2319
2680
  notes: z.string().optional().describe("Optional notes or output summary"),
2320
2681
  pr_url: z.string().optional().describe("PR URL opened during this step"),
2321
2682
  agent_ref: z.string().optional().describe("Cursor agentId or external agent reference")
2322
- }
2683
+ })
2323
2684
  },
2324
2685
  async (args) => {
2325
2686
  const { run_id, step_index, ...body } = args;
@@ -2336,13 +2697,13 @@ Be specific. No generic advice.`
2336
2697
  title: titleOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2337
2698
  description: descOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2338
2699
  annotations: annotationsFor("ask_codebase", CODEBASE_TOOL_CATALOG),
2339
- inputSchema: {
2700
+ inputSchema: z.object({
2340
2701
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2341
2702
  question: z.string().describe("Plain-English question about the repo"),
2342
2703
  thread_id: z.string().optional().describe("Optional thread UUID to continue a conversation"),
2343
2704
  file_path: z.string().optional().describe("Optional file path focus"),
2344
2705
  symbol_name: z.string().optional().describe("Optional symbol name focus")
2345
- }
2706
+ })
2346
2707
  },
2347
2708
  async (args) => {
2348
2709
  const pid = args.project_id ?? projectId;
@@ -2370,12 +2731,12 @@ Be specific. No generic advice.`
2370
2731
  title: titleOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2371
2732
  description: descOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2372
2733
  annotations: annotationsFor("get_file_summary", CODEBASE_TOOL_CATALOG),
2373
- inputSchema: {
2734
+ inputSchema: z.object({
2374
2735
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2375
2736
  file_path: z.string().describe("Indexed file path"),
2376
2737
  symbol_name: z.string().optional().describe("Optional symbol name within the file"),
2377
2738
  force: z.boolean().optional().describe("Bypass cache and regenerate")
2378
- }
2739
+ })
2379
2740
  },
2380
2741
  async (args) => {
2381
2742
  const pid = args.project_id ?? projectId;
@@ -2393,10 +2754,10 @@ Be specific. No generic advice.`
2393
2754
  title: titleOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2394
2755
  description: descOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2395
2756
  annotations: annotationsFor("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2396
- inputSchema: {
2757
+ inputSchema: z.object({
2397
2758
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2398
2759
  force: z.boolean().optional().describe("Bypass cache and regenerate")
2399
- }
2760
+ })
2400
2761
  },
2401
2762
  async (args) => {
2402
2763
  const pid = args.project_id ?? projectId;
@@ -2412,17 +2773,17 @@ Be specific. No generic advice.`
2412
2773
  title: titleOf("search_codebase", CODEBASE_TOOL_CATALOG),
2413
2774
  description: descOf("search_codebase", CODEBASE_TOOL_CATALOG),
2414
2775
  annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2415
- inputSchema: {
2776
+ inputSchema: z.object({
2416
2777
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2417
2778
  query: z.string().describe("Plain-English search query against indexed source files"),
2418
2779
  k: z.number().int().min(1).max(20).optional().describe("Max hits to return (default 8, max 20)"),
2419
2780
  scope_prefix: z.string().optional().describe("Optional repo subdirectory prefix to limit search scope")
2420
- },
2421
- outputSchema: {
2781
+ }),
2782
+ outputSchema: z.object({
2422
2783
  results: z.array(z.unknown()).describe("Ranked file/symbol hits with paths, line ranges, and similarity"),
2423
2784
  query: z.string().describe("Normalized query string that was searched"),
2424
2785
  mode: z.string().optional().describe("semantic (embeddings) or name (path/symbol match)")
2425
- }
2786
+ })
2426
2787
  },
2427
2788
  async (args) => {
2428
2789
  const pid = args.project_id ?? projectId;
@@ -2447,11 +2808,11 @@ Be specific. No generic advice.`
2447
2808
  title: titleOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2448
2809
  description: descOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2449
2810
  annotations: annotationsFor("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2450
- inputSchema: {
2811
+ inputSchema: z.object({
2451
2812
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2452
2813
  force: z.boolean().optional().describe("Bypass cache and regenerate"),
2453
2814
  scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2454
- }
2815
+ })
2455
2816
  },
2456
2817
  async (args) => {
2457
2818
  const pid = args.project_id ?? projectId;
@@ -2470,20 +2831,20 @@ Be specific. No generic advice.`
2470
2831
  title: titleOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2471
2832
  description: descOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2472
2833
  annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2473
- inputSchema: {
2834
+ inputSchema: z.object({
2474
2835
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2475
2836
  paths: z.array(z.string()).optional().describe("Changed repo-relative file paths to analyze"),
2476
2837
  source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths instead of manual paths"),
2477
2838
  compare: z.string().optional().describe("GitHub compare range base...head when source=compare"),
2478
2839
  fix_id: z.string().optional().describe("Fix attempt UUID \u2014 uses that PR's changed files when source=fix")
2479
- },
2480
- outputSchema: {
2840
+ }),
2841
+ outputSchema: z.object({
2481
2842
  changed_paths: z.array(z.string()).describe("Resolved set of changed paths analyzed"),
2482
2843
  source: z.string().describe("How changed_paths were resolved (manual, last_push, compare, fix, \u2026)"),
2483
2844
  affected_file_paths: z.array(z.string()).describe("Indexed files that import or depend on changed paths"),
2484
2845
  affected_node_ids: z.array(z.string()).describe("Codebase graph node ids impacted by the change"),
2485
2846
  meta: z.unknown().nullable().optional().describe("Extra resolution metadata when auto-sourced")
2486
- }
2847
+ })
2487
2848
  },
2488
2849
  async (args) => {
2489
2850
  const pid = args.project_id ?? projectId;
@@ -2510,9 +2871,9 @@ Be specific. No generic advice.`
2510
2871
  title: titleOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2511
2872
  description: descOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2512
2873
  annotations: annotationsFor("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2513
- inputSchema: {
2874
+ inputSchema: z.object({
2514
2875
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)")
2515
- }
2876
+ })
2516
2877
  },
2517
2878
  async (args) => {
2518
2879
  const pid = args.project_id ?? projectId;
@@ -2526,11 +2887,11 @@ Be specific. No generic advice.`
2526
2887
  {
2527
2888
  title: titleOf("use_mushi"),
2528
2889
  description: descOf("use_mushi"),
2529
- inputSchema: {
2890
+ inputSchema: z.object({
2530
2891
  intent: z.string().optional().describe(
2531
2892
  '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.'
2532
2893
  )
2533
- },
2894
+ }),
2534
2895
  annotations: annotationsFor("use_mushi")
2535
2896
  },
2536
2897
  async (args) => {
@@ -2632,5 +2993,6 @@ Be specific. No generic advice.`
2632
2993
  }
2633
2994
  export {
2634
2995
  MushiApiError,
2635
- createMushiServer
2996
+ createMushiServer,
2997
+ resolveTimeoutMsFromEnv
2636
2998
  };