@mushi-mushi/mcp 0.14.1 → 0.16.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
@@ -1,11 +1,128 @@
1
1
  import {
2
+ CODEBASE_TOOL_CATALOG,
2
3
  TDD_TOOL_CATALOG,
3
4
  TOOL_CATALOG
4
- } from "./chunk-FQ5WR3PS.js";
5
+ } from "./chunk-PZEGN4XD.js";
6
+ import {
7
+ MUSHI_SERVER_METADATA
8
+ } from "./chunk-EHB2BGWX.js";
9
+ import {
10
+ toolMatchesFeatures
11
+ } from "./chunk-B2QEG35D.js";
5
12
 
6
13
  // src/server.ts
7
14
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
15
  import { z } from "zod";
16
+ import { createLogger } from "@mushi-mushi/core";
17
+
18
+ // src/docs-index.ts
19
+ var BASE = "https://kensaur.us/mushi-mushi/docs";
20
+ var MUSHI_DOCS_INDEX = [
21
+ {
22
+ title: "Architecture overview",
23
+ path: `${BASE}/concepts/architecture`,
24
+ keywords: ["architecture", "evolution loop", "pdca", "overview"],
25
+ excerpt: "How Mushi captures user-felt bugs, triages, dispatches fixes, and learns."
26
+ },
27
+ {
28
+ title: "MCP setup",
29
+ path: `${BASE}/guides/mcp-setup`,
30
+ keywords: ["mcp", "cursor", "api key", "stdio", "http", "multi-project", "account mode", "install", "setup", "deeplink"],
31
+ excerpt: "Wire Mushi MCP into Cursor, Claude, or any MCP client. Supports per-project and account-level keys."
32
+ },
33
+ {
34
+ title: "API key scopes",
35
+ path: `${BASE}/guides/api-keys`,
36
+ keywords: ["api key", "mcp:read", "mcp:write", "scopes", "security", "account key", "org-scoped", "multi-project"],
37
+ excerpt: "Separate SDK ingest keys from MCP read/write keys. Mint an org-scoped account key for multi-project access."
38
+ },
39
+ {
40
+ title: "Account key (org-scoped)",
41
+ path: `${BASE}/guides/mcp-setup`,
42
+ keywords: ["account key", "org-scoped", "multi-project", "MUSHI_PROJECT_ID", "all projects", "mint-org-key", "get_account_overview"],
43
+ excerpt: "Mint one org-scoped key that covers all your projects; no MUSHI_PROJECT_ID needed. The agent calls get_account_overview to discover projects."
44
+ },
45
+ {
46
+ title: "Cursor MCP troubleshooting",
47
+ path: `${BASE}/guides/mcp-setup`,
48
+ keywords: ["cursor", "mcp.json", "red badge", "transport error", "stdout", "stderr", "json-rpc", "INVALID_TOKEN", "syntax helper", "diagnose_connection"],
49
+ excerpt: "Cursor stdio MCP requires stdout to contain only JSON-RPC. Use the console syntax helper, restart Cursor MCP, then run diagnose_connection."
50
+ },
51
+ {
52
+ title: "Judge loop",
53
+ path: `${BASE}/concepts/judge-loop`,
54
+ keywords: ["judge", "sonnet", "quality", "fix scoring"],
55
+ excerpt: "Sonnet-as-Judge scores fix quality and feeds the lesson library."
56
+ },
57
+ {
58
+ title: "Inventory v2",
59
+ path: `${BASE}/concepts/inventory`,
60
+ keywords: ["inventory", "user stories", "spec", "yaml", "actions"],
61
+ excerpt: "inventory.yaml maps pages, stories, and expected outcomes for spec traceability."
62
+ },
63
+ {
64
+ title: "QA coverage stories",
65
+ path: `${BASE}/guides/qa-stories`,
66
+ keywords: ["qa", "playwright", "tdd", "cron", "browserbase", "firecrawl"],
67
+ excerpt: "Author, approve, and schedule user-story tests."
68
+ },
69
+ {
70
+ title: "Skill pipelines",
71
+ path: `${BASE}/guides/skill-pipelines`,
72
+ keywords: ["skills", "pipeline", "cursor", "workflow"],
73
+ excerpt: "Attach agent skills to reports and run handoff or cloud pipelines."
74
+ },
75
+ {
76
+ title: "Dispatch fix",
77
+ path: `${BASE}/guides/dispatch-fix`,
78
+ keywords: ["dispatch", "fix", "pr", "cursor cloud", "autofix"],
79
+ excerpt: "Dispatch the Mushi fix agent or Cursor Cloud for a classified report."
80
+ },
81
+ {
82
+ title: "SDK installation",
83
+ path: `${BASE}/guides/sdk`,
84
+ keywords: ["sdk", "install", "react", "next", "ingest"],
85
+ excerpt: "Install @mushi-mushi/web and start capturing user reports."
86
+ },
87
+ {
88
+ title: "Configuration reference",
89
+ path: `${BASE}/reference/config`,
90
+ keywords: ["env", "config", "settings", "byok"],
91
+ excerpt: "Environment variables and project settings reference."
92
+ },
93
+ {
94
+ title: "Evolution loop",
95
+ path: `${BASE}/concepts/evolution`,
96
+ keywords: ["evolution", "lessons", "convergence", "prompt promotion"],
97
+ excerpt: "Closed-loop learning from bugs, fixes, and judge scores."
98
+ },
99
+ {
100
+ title: "Privacy & BYOK",
101
+ path: `${BASE}/guides/privacy`,
102
+ keywords: ["privacy", "byok", "data region", "llm"],
103
+ excerpt: "Data boundaries, BYOK LLM keys, and retention."
104
+ }
105
+ ];
106
+ function searchMushiDocs(query, limit = 8) {
107
+ const q = query.trim().toLowerCase();
108
+ if (!q) {
109
+ return MUSHI_DOCS_INDEX.slice(0, limit).map((e) => ({ ...e, score: 0 }));
110
+ }
111
+ const terms = q.split(/\s+/).filter(Boolean);
112
+ const scored = MUSHI_DOCS_INDEX.map((entry) => {
113
+ const hay = `${entry.title} ${entry.keywords.join(" ")} ${entry.excerpt}`.toLowerCase();
114
+ let score = 0;
115
+ for (const term of terms) {
116
+ if (entry.title.toLowerCase().includes(term)) score += 4;
117
+ if (entry.keywords.some((k) => k.includes(term))) score += 3;
118
+ if (hay.includes(term)) score += 1;
119
+ }
120
+ return { ...entry, score };
121
+ }).filter((e) => e.score > 0).sort((a, b) => b.score - a.score);
122
+ return scored.slice(0, limit);
123
+ }
124
+
125
+ // src/server.ts
9
126
  var MushiApiError = class extends Error {
10
127
  constructor(status, code, message) {
11
128
  super(`[${code}] ${message}`);
@@ -19,10 +136,23 @@ var MushiApiError = class extends Error {
19
136
  function createMushiServer(config) {
20
137
  const { version, apiEndpoint, apiKey, projectId } = config;
21
138
  const doFetch = config.fetch ?? globalThis.fetch;
139
+ const apiLog = createLogger({
140
+ scope: "mushi:mcp:api",
141
+ level: process.env.MUSHI_LOG_LEVEL ?? "info",
142
+ destination: "stderr"
143
+ });
22
144
  if (config.scopes !== void 0 && config.scopes.length === 0) {
23
- return new McpServer({ name: "mushi-mushi", version });
145
+ return new McpServer({
146
+ name: MUSHI_SERVER_METADATA.name,
147
+ version,
148
+ title: MUSHI_SERVER_METADATA.title,
149
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
150
+ icons: [...MUSHI_SERVER_METADATA.icons]
151
+ });
24
152
  }
25
153
  async function apiCall(path, options) {
154
+ const requestId = crypto.randomUUID().slice(0, 12);
155
+ const started = Date.now();
26
156
  const res = await doFetch(`${apiEndpoint}${path}`, {
27
157
  ...options,
28
158
  headers: {
@@ -32,10 +162,12 @@ function createMushiServer(config) {
32
162
  // and for transparent proxies that strip X-Mushi-* headers.
33
163
  "Authorization": `Bearer ${apiKey}`,
34
164
  "X-Mushi-Api-Key": apiKey,
35
- ...projectId ? { "X-Mushi-Project": projectId } : {},
165
+ "X-Request-Id": requestId,
166
+ ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
36
167
  ...options?.headers ?? {}
37
168
  }
38
169
  });
170
+ const durationMs = Date.now() - started;
39
171
  const text = await res.text();
40
172
  let body = null;
41
173
  if (text) {
@@ -49,6 +181,7 @@ function createMushiServer(config) {
49
181
  const envelope2 = body;
50
182
  const code = envelope2?.error?.code ?? `HTTP_${res.status}`;
51
183
  const message = envelope2?.error?.message ?? text.slice(0, 500) ?? `Request failed with ${res.status}`;
184
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
52
185
  throw new MushiApiError(res.status, code, message);
53
186
  }
54
187
  const envelope = body;
@@ -56,12 +189,47 @@ function createMushiServer(config) {
56
189
  if (!envelope.ok) {
57
190
  const code = envelope.error?.code ?? "API_ERROR";
58
191
  const message = envelope.error?.message ?? "API returned ok=false";
192
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
59
193
  throw new MushiApiError(res.status, code, message);
60
194
  }
195
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
61
196
  return envelope.data ?? {};
62
197
  }
198
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
63
199
  return body;
64
200
  }
201
+ async function resolveProjectId(explicitId) {
202
+ if (explicitId) return explicitId;
203
+ if (projectId) return projectId;
204
+ try {
205
+ const data = await apiCall(
206
+ "/v1/admin/mcp/account-overview"
207
+ );
208
+ if (data.total === 1 && data.projects[0]) {
209
+ return data.projects[0].id;
210
+ }
211
+ if (data.total === 0) {
212
+ throw new MushiApiError(
213
+ 400,
214
+ "NO_ACCESSIBLE_PROJECTS",
215
+ "This API key has no accessible projects. Mint an API key on a project in the admin console."
216
+ );
217
+ }
218
+ const projectList = data.projects.map((p) => `${p.name ?? "unnamed"} (${p.id})`).join(", ");
219
+ throw new MushiApiError(
220
+ 400,
221
+ "PROJECT_ID_REQUIRED",
222
+ `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.`
223
+ );
224
+ } catch (err) {
225
+ if (err instanceof MushiApiError) throw err;
226
+ throw new MushiApiError(
227
+ 400,
228
+ "MISSING_PROJECT_ID",
229
+ "projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly."
230
+ );
231
+ }
232
+ }
65
233
  function jsonText(value) {
66
234
  return {
67
235
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -74,10 +242,13 @@ function createMushiServer(config) {
74
242
  };
75
243
  }
76
244
  const server = new McpServer({
77
- name: "mushi-mushi",
78
- version
245
+ name: MUSHI_SERVER_METADATA.name,
246
+ version,
247
+ title: MUSHI_SERVER_METADATA.title,
248
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
249
+ icons: [...MUSHI_SERVER_METADATA.icons]
79
250
  });
80
- const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
251
+ const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG, ...CODEBASE_TOOL_CATALOG];
81
252
  function annotationsFor(name, catalog = ALL_TOOL_CATALOG) {
82
253
  const spec = catalog.find((t) => t.name === name);
83
254
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
@@ -110,7 +281,10 @@ function createMushiServer(config) {
110
281
  status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
111
282
  category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
112
283
  severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
113
- limit: z.number().optional().describe("Max reports to return (default 20, max 100)")
284
+ limit: z.number().optional().describe("Max reports to return (default 20, max 100)"),
285
+ project_id: z.string().optional().describe(
286
+ "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."
287
+ )
114
288
  },
115
289
  outputSchema: {
116
290
  reports: z.array(z.unknown()),
@@ -123,7 +297,11 @@ function createMushiServer(config) {
123
297
  if (args.category) params.set("category", args.category);
124
298
  if (args.severity) params.set("severity", args.severity);
125
299
  params.set("limit", String(Math.min(args.limit ?? 20, 100)));
126
- const data = await apiCall(`/v1/admin/reports?${params}`);
300
+ const pid = await resolveProjectId(args.project_id);
301
+ const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
302
+ const data = await apiCall(`/v1/admin/reports?${params}`, {
303
+ headers: extraHeaders
304
+ });
127
305
  return jsonResult(data);
128
306
  }
129
307
  );
@@ -133,9 +311,19 @@ function createMushiServer(config) {
133
311
  title: titleOf("get_report_detail"),
134
312
  description: descOf("get_report_detail"),
135
313
  annotations: annotationsFor("get_report_detail"),
136
- inputSchema: { reportId: z.string().describe("The report UUID") }
314
+ inputSchema: { reportId: z.string().describe("The report UUID") },
315
+ outputSchema: { report: z.unknown() }
137
316
  },
138
- async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}`))
317
+ async (args) => {
318
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
319
+ const res = jsonResult({ report: data });
320
+ return {
321
+ ...res,
322
+ resource_links: [
323
+ { uri: `project://reports/${args.reportId}`, title: `Report ${args.reportId.slice(0, 8)}\u2026` }
324
+ ]
325
+ };
326
+ }
139
327
  );
140
328
  server.registerTool(
141
329
  "get_report_timeline",
@@ -194,6 +382,9 @@ function createMushiServer(config) {
194
382
  inputSchema: {
195
383
  query: z.string().describe("Component name, page path, or bug description"),
196
384
  limit: z.number().optional().describe("Max results (default 5, max 20)")
385
+ },
386
+ outputSchema: {
387
+ results: z.array(z.unknown())
197
388
  }
198
389
  },
199
390
  async (args) => {
@@ -206,7 +397,7 @@ function createMushiServer(config) {
206
397
  ...projectId ? { projectId } : {}
207
398
  })
208
399
  });
209
- return jsonText(data);
400
+ return jsonResult(data);
210
401
  }
211
402
  );
212
403
  server.registerTool(
@@ -215,11 +406,18 @@ function createMushiServer(config) {
215
406
  title: titleOf("get_fix_context"),
216
407
  description: descOf("get_fix_context"),
217
408
  annotations: annotationsFor("get_fix_context"),
218
- inputSchema: { reportId: z.string().describe("The report UUID to fix") }
409
+ inputSchema: { reportId: z.string().describe("The report UUID to fix") },
410
+ outputSchema: {
411
+ report: z.unknown(),
412
+ reproductionSteps: z.unknown(),
413
+ component: z.unknown(),
414
+ rootCause: z.unknown(),
415
+ bugOntologyTags: z.unknown()
416
+ }
219
417
  },
220
418
  async (args) => {
221
419
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
222
- return jsonText({
420
+ return jsonResult({
223
421
  report,
224
422
  reproductionSteps: report.reproduction_steps ?? [],
225
423
  component: report.component,
@@ -457,13 +655,115 @@ function createMushiServer(config) {
457
655
  });
458
656
  }
459
657
  );
658
+ server.registerTool(
659
+ "diagnose_setup",
660
+ {
661
+ title: titleOf("diagnose_setup"),
662
+ description: descOf("diagnose_setup"),
663
+ annotations: annotationsFor("diagnose_setup"),
664
+ inputSchema: {
665
+ mode: z.enum(["full", "ingest", "dispatch"]).optional().describe("full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."),
666
+ project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
667
+ projectId: z.string().optional().describe("Alias for project_id.")
668
+ },
669
+ outputSchema: {
670
+ mode: z.string(),
671
+ ready: z.boolean(),
672
+ summary: z.string(),
673
+ nextAction: z.string().optional(),
674
+ ingest: z.unknown().optional(),
675
+ dispatch: z.unknown().optional()
676
+ }
677
+ },
678
+ async (args) => {
679
+ const mode = args.mode ?? "full";
680
+ const resolvedId = args.project_id ?? args.projectId ?? projectId;
681
+ if (mode === "ingest") {
682
+ const data = await apiCall("/v1/sync/ingest-setup");
683
+ const failed = data.steps.filter((s) => s.required && !s.complete);
684
+ const summary2 = data.ready ? `Ingest setup complete (${data.required_complete}/${data.required_total}) for ${data.project_name}.` : `Ingest incomplete \u2014 still need: ${failed.map((s) => s.label).join(", ")}.`;
685
+ return jsonResult({
686
+ mode: "ingest",
687
+ ready: data.ready,
688
+ summary: summary2,
689
+ nextAction: failed[0]?.hint,
690
+ ingest: {
691
+ projectId: data.project_id,
692
+ projectName: data.project_name,
693
+ requiredComplete: data.required_complete,
694
+ requiredTotal: data.required_total,
695
+ steps: data.steps,
696
+ diagnostic: data.diagnostic ?? null
697
+ }
698
+ });
699
+ }
700
+ if (mode === "dispatch") {
701
+ if (!resolvedId) {
702
+ throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required for dispatch mode");
703
+ }
704
+ const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
705
+ const failed = data.checks.filter((c) => !c.ready);
706
+ const summary2 = data.ready ? `Project ${resolvedId} is ready to dispatch auto-fixes.` : `Dispatch blocked \u2014 ${failed.map((c) => c.label).join(", ")}.`;
707
+ return jsonResult({
708
+ mode: "dispatch",
709
+ ready: data.ready,
710
+ summary: summary2,
711
+ nextAction: failed[0]?.hint,
712
+ dispatch: { repoUrl: data.repoUrl ?? null, checks: data.checks }
713
+ });
714
+ }
715
+ const ingest = await apiCall("/v1/sync/ingest-setup");
716
+ let dispatchReady = true;
717
+ let dispatchBlock;
718
+ let dispatchChecks = null;
719
+ if (resolvedId) {
720
+ try {
721
+ const preflight = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
722
+ dispatchReady = preflight.ready;
723
+ dispatchChecks = preflight.checks;
724
+ if (!preflight.ready) {
725
+ const failed = preflight.checks.filter((c) => !c.ready);
726
+ dispatchBlock = failed[0]?.hint;
727
+ }
728
+ } catch {
729
+ dispatchReady = false;
730
+ dispatchBlock = "Could not run dispatch preflight \u2014 verify project_id and API key scope.";
731
+ }
732
+ }
733
+ const ingestFailed = ingest.steps.filter((s) => s.required && !s.complete);
734
+ const ingestBlock = ingestFailed[0]?.hint;
735
+ const ready = ingest.ready && dispatchReady;
736
+ const nextAction = !ingest.ready ? ingestBlock ?? "Complete SDK ingest setup." : !dispatchReady ? dispatchBlock ?? "Complete dispatch preflight in Settings \u2192 Integrations." : "All setup checks pass \u2014 submit a test report to confirm end-to-end.";
737
+ const summary = ready ? "Mushi ingest and dispatch setup look healthy." : !ingest.ready ? `Ingest incomplete \u2014 ${ingestFailed.map((s) => s.label).join(", ")}.` : "Ingest OK; dispatch preflight still has blockers.";
738
+ return jsonResult({
739
+ mode: "full",
740
+ ready,
741
+ summary,
742
+ nextAction,
743
+ ingest: { ready: ingest.ready, steps: ingest.steps },
744
+ dispatch: resolvedId ? { ready: dispatchReady, checks: dispatchChecks } : null
745
+ });
746
+ }
747
+ );
460
748
  server.registerTool(
461
749
  "diagnose_connection",
462
750
  {
463
751
  title: titleOf("diagnose_connection"),
464
752
  description: descOf("diagnose_connection"),
465
753
  annotations: annotationsFor("diagnose_connection"),
466
- inputSchema: {}
754
+ inputSchema: {},
755
+ outputSchema: {
756
+ ready: z.boolean(),
757
+ healthOk: z.boolean(),
758
+ ingestReady: z.boolean(),
759
+ dispatchReady: z.boolean(),
760
+ endpoint: z.string().nullable(),
761
+ projectId: z.string().nullable(),
762
+ accessibleProjectCount: z.number().nullable(),
763
+ issues: z.array(z.unknown()),
764
+ nextAction: z.string(),
765
+ summary: z.string()
766
+ }
467
767
  },
468
768
  async () => {
469
769
  const issues = [];
@@ -474,13 +774,6 @@ function createMushiServer(config) {
474
774
  fix: "Run `mushi connect` to write MUSHI_API_KEY into .cursor/mcp.json, then restart the MCP server."
475
775
  });
476
776
  }
477
- if (!projectId) {
478
- issues.push({
479
- check: "mcp_project_id",
480
- detail: "No projectId configured on the MCP server",
481
- fix: "Add MUSHI_PROJECT_ID to .cursor/mcp.json (copy UUID from console \u2192 Projects)."
482
- });
483
- }
484
777
  if (!apiEndpoint) {
485
778
  issues.push({
486
779
  check: "mcp_endpoint",
@@ -512,6 +805,21 @@ function createMushiServer(config) {
512
805
  }
513
806
  let ingestReady = false;
514
807
  let dispatchReady = false;
808
+ let accessibleProjectCount = null;
809
+ if (!projectId && healthOk) {
810
+ try {
811
+ const overview = await apiCall("/v1/admin/mcp/account-overview");
812
+ accessibleProjectCount = overview.total;
813
+ if (overview.total === 0) {
814
+ issues.push({
815
+ check: "no_accessible_projects",
816
+ detail: "API key has no accessible projects",
817
+ fix: "Mint an API key on a project (console \u2192 Projects \u2192 API Keys) or add MUSHI_PROJECT_ID to restrict to a single project."
818
+ });
819
+ }
820
+ } catch {
821
+ }
822
+ }
515
823
  try {
516
824
  const ingest = await apiCall("/v1/sync/ingest-setup");
517
825
  ingestReady = ingest.ready;
@@ -547,30 +855,99 @@ function createMushiServer(config) {
547
855
  }
548
856
  const ready = issues.length === 0 && healthOk && ingestReady;
549
857
  const nextAction = issues[0]?.fix ?? (ready ? "Connection healthy \u2014 SDK ingest is working. Submit a report to confirm end-to-end." : "Run `mushi doctor` in your app repo for a full local checklist.");
550
- return jsonText({
858
+ return jsonResult({
551
859
  ready,
552
860
  healthOk,
553
861
  ingestReady,
554
862
  dispatchReady,
555
863
  endpoint: apiEndpoint ?? null,
556
864
  projectId: projectId ?? null,
865
+ accessibleProjectCount,
557
866
  issues,
558
867
  nextAction,
559
868
  summary: ready ? "MCP credentials valid; ingest pipeline ready." : `Connection issue \u2014 ${issues[0]?.check ?? "unknown"}: ${nextAction}`
560
869
  });
561
870
  }
562
871
  );
872
+ server.registerTool(
873
+ "search_mushi_docs",
874
+ {
875
+ title: titleOf("search_mushi_docs"),
876
+ description: descOf("search_mushi_docs"),
877
+ annotations: annotationsFor("search_mushi_docs"),
878
+ inputSchema: {
879
+ query: z.string().describe("Keywords to search official Mushi docs (guides, MCP, inventory, QA)."),
880
+ limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8).")
881
+ },
882
+ outputSchema: {
883
+ query: z.string(),
884
+ results: z.array(
885
+ z.object({
886
+ title: z.string(),
887
+ path: z.string(),
888
+ excerpt: z.string(),
889
+ score: z.number()
890
+ })
891
+ )
892
+ }
893
+ },
894
+ async (args) => {
895
+ const query = args.query ?? "";
896
+ const hits = searchMushiDocs(query, args.limit ?? 8);
897
+ const results = hits.map(({ title, path, excerpt, score }) => ({ title, path, excerpt, score }));
898
+ return jsonResult({ query, results });
899
+ }
900
+ );
563
901
  server.registerTool(
564
902
  "list_projects",
565
903
  {
566
904
  title: titleOf("list_projects"),
567
905
  description: descOf("list_projects"),
568
906
  annotations: annotationsFor("list_projects"),
569
- inputSchema: {}
907
+ inputSchema: {},
908
+ outputSchema: {
909
+ projects: z.array(z.unknown()),
910
+ total: z.number().optional(),
911
+ _multi_project_hint: z.string().optional()
912
+ }
570
913
  },
571
914
  async () => {
572
915
  const data = await apiCall("/v1/admin/mcp/projects");
573
- return jsonText(data);
916
+ return jsonResult({
917
+ ...data,
918
+ _multi_project_hint: 'Each Mushi project uses a separate API key. To connect another project, open the Mushi console \u2192 MCP \u2192 Setup, pick the project from the switcher, and click "\u26A1 Add to Cursor". Each deeplink adds a uniquely-named server entry (mushi-{project-name}-{id-prefix}) so all your projects appear in your IDE simultaneously.'
919
+ });
920
+ }
921
+ );
922
+ server.registerTool(
923
+ "get_account_overview",
924
+ {
925
+ title: titleOf("get_account_overview"),
926
+ description: descOf("get_account_overview"),
927
+ annotations: annotationsFor("get_account_overview"),
928
+ inputSchema: {},
929
+ outputSchema: {
930
+ projects: z.array(z.unknown()),
931
+ total: z.number(),
932
+ active_project_id: z.string().nullable(),
933
+ toolCount: z.number().optional(),
934
+ resourceCount: z.number().optional(),
935
+ promptCount: z.number().optional(),
936
+ multi_project_hint: z.string()
937
+ }
938
+ },
939
+ async () => {
940
+ const data = await apiCall("/v1/admin/mcp/account-overview");
941
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
942
+ return jsonResult({
943
+ projects,
944
+ total: data?.total ?? projects.length,
945
+ active_project_id: projectId ?? null,
946
+ toolCount: data?.toolCount,
947
+ resourceCount: data?.resourceCount,
948
+ promptCount: data?.promptCount,
949
+ multi_project_hint: projects.length <= 1 ? 'You are currently connected to one project. To connect additional Mushi projects, open the Mushi console \u2192 switch to each project \u2192 MCP \u2192 Setup \u2192 "\u26A1 Add to Cursor". Each click adds a uniquely-named entry (mushi-{name}-{id}) to your global ~/.cursor/mcp.json so you can triage reports across all your apps from a single Cursor session.' : `You have access to ${projects.length} projects. To connect any unconnected project, open the Mushi console \u2192 switch to that project \u2192 MCP \u2192 Setup \u2192 "\u26A1 Add to Cursor".`
950
+ });
574
951
  }
575
952
  );
576
953
  server.registerTool(
@@ -670,6 +1047,20 @@ function createMushiServer(config) {
670
1047
  report_id: z.string().describe("Report UUID to triage."),
671
1048
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
672
1049
  include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1050
+ },
1051
+ outputSchema: {
1052
+ report_id: z.string(),
1053
+ severity: z.unknown(),
1054
+ category: z.unknown(),
1055
+ status: z.unknown(),
1056
+ report: z.unknown(),
1057
+ reporter_thread: z.unknown().nullable(),
1058
+ similar_bugs: z.unknown().nullable(),
1059
+ fix_context: z.unknown().nullable(),
1060
+ blast_radius: z.unknown().nullable(),
1061
+ recent_logs: z.unknown().nullable(),
1062
+ recommended_actions: z.array(z.unknown()),
1063
+ triage_summary: z.string()
673
1064
  }
674
1065
  },
675
1066
  async (args) => {
@@ -713,7 +1104,7 @@ function createMushiServer(config) {
713
1104
  args: { reportId: args.report_id }
714
1105
  });
715
1106
  }
716
- return jsonText({
1107
+ const result = {
717
1108
  report_id: args.report_id,
718
1109
  severity,
719
1110
  category,
@@ -726,7 +1117,15 @@ function createMushiServer(config) {
726
1117
  recent_logs: logsRes.status === "fulfilled" ? logsRes.value : null,
727
1118
  recommended_actions: actions,
728
1119
  triage_summary: `[${severity}] ${category} \u2014 status: ${status}. ${actions.length > 0 ? `Recommended: ${actions[0]?.action}.` : "No action required."}`
729
- });
1120
+ };
1121
+ const res = jsonResult(result);
1122
+ return {
1123
+ ...res,
1124
+ resource_links: [
1125
+ { uri: `project://reports/${args.report_id}`, title: `Report ${args.report_id.slice(0, 8)}\u2026` },
1126
+ ...pid ? [{ uri: `project://dashboard`, title: "Project dashboard" }] : []
1127
+ ]
1128
+ };
730
1129
  }
731
1130
  );
732
1131
  server.registerTool(
@@ -1107,7 +1506,7 @@ function createMushiServer(config) {
1107
1506
  contents: [{
1108
1507
  uri: "privacy://status",
1109
1508
  mimeType: "application/json",
1110
- text: JSON.stringify(await apiCall("/v1/admin/privacy/status"), null, 2)
1509
+ text: JSON.stringify(await apiCall("/v1/admin/privacy-status"), null, 2)
1111
1510
  }]
1112
1511
  })
1113
1512
  );
@@ -1121,7 +1520,11 @@ function createMushiServer(config) {
1121
1520
  contents: [{
1122
1521
  uri: "evolution://history",
1123
1522
  mimeType: "application/json",
1124
- text: JSON.stringify(await apiCall("/v1/admin/evolution/history"), null, 2)
1523
+ text: projectId ? JSON.stringify(
1524
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(projectId)}/evolution-history`),
1525
+ null,
1526
+ 2
1527
+ ) : JSON.stringify({ ok: false, error: "project_id required" })
1125
1528
  }]
1126
1529
  })
1127
1530
  );
@@ -1700,12 +2103,192 @@ Be specific. No generic advice.`
1700
2103
  // admin-authed timeline, which also carries fix / QA / status lanes.
1701
2104
  async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}/timeline`))
1702
2105
  );
2106
+ server.registerTool(
2107
+ "ask_codebase",
2108
+ {
2109
+ title: titleOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2110
+ description: descOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2111
+ annotations: annotationsFor("ask_codebase", CODEBASE_TOOL_CATALOG),
2112
+ inputSchema: {
2113
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2114
+ question: z.string().describe("Plain-English question about the repo"),
2115
+ thread_id: z.string().optional().describe("Optional thread UUID to continue a conversation"),
2116
+ file_path: z.string().optional().describe("Optional file path focus"),
2117
+ symbol_name: z.string().optional().describe("Optional symbol name focus")
2118
+ }
2119
+ },
2120
+ async (args) => {
2121
+ const pid = args.project_id ?? projectId;
2122
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2123
+ const body = {
2124
+ messages: [{ role: "user", content: args.question }]
2125
+ };
2126
+ if (args.thread_id) body.threadId = args.thread_id;
2127
+ if (args.file_path) {
2128
+ body.fileFocus = {
2129
+ file_path: args.file_path,
2130
+ symbol_name: args.symbol_name ?? null
2131
+ };
2132
+ }
2133
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/chat`, {
2134
+ method: "POST",
2135
+ body: JSON.stringify(body)
2136
+ });
2137
+ return jsonText(data);
2138
+ }
2139
+ );
2140
+ server.registerTool(
2141
+ "get_file_summary",
2142
+ {
2143
+ title: titleOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2144
+ description: descOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2145
+ annotations: annotationsFor("get_file_summary", CODEBASE_TOOL_CATALOG),
2146
+ inputSchema: {
2147
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2148
+ file_path: z.string().describe("Indexed file path"),
2149
+ symbol_name: z.string().optional().describe("Optional symbol name within the file"),
2150
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2151
+ }
2152
+ },
2153
+ async (args) => {
2154
+ const pid = args.project_id ?? projectId;
2155
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2156
+ const qs = new URLSearchParams({ file_path: args.file_path });
2157
+ if (args.symbol_name) qs.set("symbol_name", args.symbol_name);
2158
+ if (args.force) qs.set("force", "1");
2159
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/summary?${qs}`);
2160
+ return jsonText(data);
2161
+ }
2162
+ );
2163
+ server.registerTool(
2164
+ "get_codebase_tour",
2165
+ {
2166
+ title: titleOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2167
+ description: descOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2168
+ annotations: annotationsFor("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2169
+ inputSchema: {
2170
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2171
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2172
+ }
2173
+ },
2174
+ async (args) => {
2175
+ const pid = args.project_id ?? projectId;
2176
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2177
+ const qs = args.force ? "?force=1" : "";
2178
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/tour${qs}`);
2179
+ return jsonText(data);
2180
+ }
2181
+ );
2182
+ server.registerTool(
2183
+ "search_codebase",
2184
+ {
2185
+ title: titleOf("search_codebase", CODEBASE_TOOL_CATALOG),
2186
+ description: descOf("search_codebase", CODEBASE_TOOL_CATALOG),
2187
+ annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2188
+ inputSchema: {
2189
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2190
+ query: z.string().describe("Plain-English search query"),
2191
+ k: z.number().optional().describe("Number of results (default 8, max 20)"),
2192
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2193
+ }
2194
+ },
2195
+ async (args) => {
2196
+ const pid = args.project_id ?? projectId;
2197
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2198
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/search`, {
2199
+ method: "POST",
2200
+ body: JSON.stringify({
2201
+ query: args.query,
2202
+ k: args.k ?? 8,
2203
+ scope_prefix: args.scope_prefix ?? void 0
2204
+ })
2205
+ });
2206
+ return jsonText(data);
2207
+ }
2208
+ );
2209
+ server.registerTool(
2210
+ "get_codebase_domains",
2211
+ {
2212
+ title: titleOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2213
+ description: descOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2214
+ annotations: annotationsFor("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2215
+ inputSchema: {
2216
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2217
+ force: z.boolean().optional().describe("Bypass cache and regenerate"),
2218
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2219
+ }
2220
+ },
2221
+ async (args) => {
2222
+ const pid = args.project_id ?? projectId;
2223
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2224
+ const qs = new URLSearchParams();
2225
+ if (args.force) qs.set("force", "1");
2226
+ if (args.scope_prefix) qs.set("scope_prefix", args.scope_prefix);
2227
+ const suffix = qs.toString() ? `?${qs}` : "";
2228
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/domains${suffix}`);
2229
+ return jsonText(data);
2230
+ }
2231
+ );
2232
+ server.registerTool(
2233
+ "analyze_codebase_impact",
2234
+ {
2235
+ title: titleOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2236
+ description: descOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2237
+ annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2238
+ inputSchema: {
2239
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2240
+ paths: z.array(z.string()).optional().describe("Changed file paths"),
2241
+ source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths"),
2242
+ compare: z.string().optional().describe("GitHub compare range base...head"),
2243
+ fix_id: z.string().optional().describe("Fix attempt UUID for PR changed files")
2244
+ }
2245
+ },
2246
+ async (args) => {
2247
+ const pid = args.project_id ?? projectId;
2248
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2249
+ const qs = new URLSearchParams();
2250
+ if (args.paths?.length) qs.set("paths", args.paths.join(","));
2251
+ if (args.source === "last_push") qs.set("ref", "last_push");
2252
+ if (args.compare) qs.set("compare", args.compare);
2253
+ if (args.fix_id) qs.set("fix_id", args.fix_id);
2254
+ if (!qs.toString()) {
2255
+ throw new MushiApiError(400, "BAD_REQUEST", "Provide paths, source=last_push, compare, or fix_id");
2256
+ }
2257
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
2258
+ return jsonText(data);
2259
+ }
2260
+ );
2261
+ server.registerTool(
2262
+ "analyze_wiki_knowledge",
2263
+ {
2264
+ title: titleOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2265
+ description: descOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2266
+ annotations: annotationsFor("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2267
+ inputSchema: {
2268
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)")
2269
+ }
2270
+ },
2271
+ async (args) => {
2272
+ const pid = args.project_id ?? projectId;
2273
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2274
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/knowledge/graph`);
2275
+ return jsonText(data);
2276
+ }
2277
+ );
1703
2278
  const grantedScopes = config.scopes;
1704
- if (grantedScopes !== void 0) {
2279
+ const featureFilter = config.features ?? "all";
2280
+ if (grantedScopes !== void 0 || featureFilter !== "all") {
1705
2281
  const toolRegistry = server._registeredTools;
1706
- const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
2282
+ const allSpecs = [
2283
+ ...TOOL_CATALOG,
2284
+ ...TDD_TOOL_CATALOG,
2285
+ ...CODEBASE_TOOL_CATALOG
2286
+ ];
1707
2287
  for (const spec of allSpecs) {
1708
- if (!grantedScopes.includes(spec.scope)) {
2288
+ if (grantedScopes !== void 0 && !grantedScopes.includes(spec.scope)) {
2289
+ toolRegistry[spec.name]?.remove();
2290
+ }
2291
+ if (featureFilter !== "all" && !toolMatchesFeatures(spec.name, featureFilter)) {
1709
2292
  toolRegistry[spec.name]?.remove();
1710
2293
  }
1711
2294
  }