@mushi-mushi/mcp 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // src/index.ts
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { createRequire } from "module";
6
- import { createLogger } from "@mushi-mushi/core";
6
+ import { createLogger as createLogger2 } from "@mushi-mushi/core";
7
7
 
8
8
  // src/catalog.ts
9
9
  var ALL_SCOPES = ["mcp:read", "mcp:write"];
@@ -141,7 +141,7 @@ var TOOL_CATALOG = [
141
141
  {
142
142
  name: "setup_check",
143
143
  title: "Dispatch preflight check",
144
- description: "Run the 4 **dispatch-readiness** checks for a project and return their pass/fail status (GitHub repo connected, codebase indexed, Anthropic BYOK key present, autofix enabled). Also returns the target repo URL when GitHub is connected. Use this before calling dispatch_fix to understand why a dispatch might fail. For SDK ingest health (API key \u2192 heartbeat \u2192 first report), call ingest_setup_check instead.",
144
+ description: "[Deprecated \u2014 use diagnose_setup with mode=dispatch or diagnose_connection.] Run the 4 **dispatch-readiness** checks for a project and return their pass/fail status (GitHub repo connected, codebase indexed, Anthropic BYOK key present, autofix enabled). Also returns the target repo URL when GitHub is connected.",
145
145
  scope: "mcp:read",
146
146
  hints: { readOnly: true, idempotent: true, openWorld: true },
147
147
  useCase: "Is this project ready to auto-fix bugs? What is blocking dispatch?"
@@ -149,19 +149,35 @@ var TOOL_CATALOG = [
149
149
  {
150
150
  name: "ingest_setup_check",
151
151
  title: "Ingest setup check",
152
- description: "Run the 4 **required ingest** checks for the project tied to this API key: project exists, active API key, SDK heartbeat (or real report), and at least one ingested report. Returns per-step pass/fail plus last_sdk_seen_at and endpoint host diagnostics. Use after wiring env vars or pasting the SDK snippet to confirm the banner will work.",
152
+ description: "[Deprecated \u2014 use diagnose_setup with mode=ingest or diagnose_connection.] Run the 4 **required ingest** checks for the project tied to this API key: project exists, active API key, SDK heartbeat (or real report), and at least one ingested report.",
153
153
  scope: "mcp:read",
154
154
  hints: { readOnly: true, idempotent: true, openWorld: true },
155
155
  useCase: "Is the SDK installed and ingesting reports? Why is my banner still missing?"
156
156
  },
157
+ {
158
+ name: "diagnose_setup",
159
+ title: "Unified setup diagnose",
160
+ description: "Single entry point for setup health. mode=full (default) runs ingest + dispatch checks and returns the best next action; mode=ingest runs SDK ingest checks only; mode=dispatch runs fix-dispatch preflight only. Prefer this over setup_check, ingest_setup_check, or diagnose_connection alone.",
161
+ scope: "mcp:read",
162
+ hints: { readOnly: true, idempotent: true, openWorld: true },
163
+ useCase: "Why is Mushi setup broken \u2014 one call, clear next step."
164
+ },
157
165
  {
158
166
  name: "diagnose_connection",
159
167
  title: "Connection diagnose (CLI + MCP + SDK)",
160
- description: `Validate the MCP server credentials (endpoint, API key, projectId), ping /health, run ingest-setup and dispatch preflight, and return the single best next action when anything fails. Use when the user asks "why aren't my reports showing up?".`,
168
+ description: `Validate MCP credentials, ping /health, run ingest-setup and dispatch preflight, return the single best next action. For mode-specific checks prefer diagnose_setup. Use when the user asks "why aren't my reports showing up?".`,
161
169
  scope: "mcp:read",
162
170
  hints: { readOnly: true, idempotent: true, openWorld: true },
163
171
  useCase: "Why is my Mushi setup broken \u2014 what exact step should I fix next?"
164
172
  },
173
+ {
174
+ name: "search_mushi_docs",
175
+ title: "Search Mushi documentation",
176
+ description: "Search official Mushi docs (guides, MCP setup, inventory, QA, skills) by keyword. Returns ranked page titles, URLs, and excerpts \u2014 use before guessing API shapes or RPC names.",
177
+ scope: "mcp:read",
178
+ hints: { readOnly: true, idempotent: true, openWorld: false },
179
+ useCase: "How do I configure MCP scopes / dispatch a fix / wire QA stories?"
180
+ },
165
181
  // --- Write / agentic ----------------------------------------------------
166
182
  {
167
183
  name: "submit_fix_result",
@@ -265,11 +281,19 @@ var TOOL_CATALOG = [
265
281
  {
266
282
  name: "list_projects",
267
283
  title: "List accessible projects",
268
- description: "List the Mushi projects accessible to this API key. For project-scoped keys (the typical case), returns a single-item list with the bound project's id, name, slug, plan tier, and ingest status. For org-level tokens, returns all projects in the org. Use this when MUSHI_PROJECT_ID is not configured \u2014 call list_projects first, then pass the id to other tools.",
284
+ description: `List the Mushi projects accessible to this API key. For project-scoped keys (the typical case), returns a single-item list with the bound project's id, name, created_at. Use this when MUSHI_PROJECT_ID is not configured \u2014 call list_projects first, then pass the id to other tools. Multi-project note: each Mushi project uses its own API key. To connect multiple projects, run "Add to Cursor" in the Mushi console for each project \u2014 each deeplink creates a uniquely-named MCP server entry (mushi-{slug}-{id}). You can then call list_projects on each named server to confirm which project it serves.`,
269
285
  scope: "mcp:read",
270
286
  hints: { readOnly: true, idempotent: true, openWorld: true },
271
287
  useCase: "Which projects can I access with this API key?"
272
288
  },
289
+ {
290
+ name: "get_account_overview",
291
+ title: "Account overview \u2014 all projects",
292
+ description: "Return a summary of every Mushi project accessible to this API key, including report counts, SDK heartbeat status, and the last ingest timestamp for each. For project-scoped keys this is a one-item list; for future org-level keys it lists every project. Also includes a hint explaining how to connect additional projects to your IDE. Call this at the start of a multi-repo or multi-app triage session to orient yourself.",
293
+ scope: "mcp:read",
294
+ hints: { readOnly: true, idempotent: true, openWorld: true },
295
+ useCase: "Show me all my Mushi projects and their health at a glance."
296
+ },
273
297
  {
274
298
  name: "get_project_context",
275
299
  title: "Project context snapshot",
@@ -477,7 +501,7 @@ var TDD_TOOL_CATALOG = [
477
501
  {
478
502
  name: "get_activation_status",
479
503
  title: "Activation cockpit status",
480
- description: "Unified setup posture for the active project: required steps, SDK heartbeat, dispatch preflight, and the next best action. Use this first when the user says setup is broken or they cannot connect.",
504
+ description: "[Deprecated \u2014 use activation_status resource or tool on hosted MCP.] Unified setup posture for the active project: required steps, SDK heartbeat, dispatch preflight, and the next best action.",
481
505
  scope: "mcp:read",
482
506
  hints: { readOnly: true, idempotent: true, openWorld: true },
483
507
  useCase: "What is blocking this project from going live?"
@@ -485,7 +509,7 @@ var TDD_TOOL_CATALOG = [
485
509
  {
486
510
  name: "get_reporter_thread",
487
511
  title: "Reporter feedback thread",
488
- description: "Fetch the unified timeline for a report \u2014 the reporter/admin comment thread (including verify/reopen signals) plus fix, QA, and status lanes. Use when triaging whether an end user still sees a bug as unfixed.",
512
+ description: "[Deprecated \u2014 use get_report_timeline instead.] Fetch the unified timeline for a report \u2014 reporter comments plus fix, QA, and status lanes.",
489
513
  scope: "mcp:read",
490
514
  hints: { readOnly: true, idempotent: true, openWorld: true },
491
515
  useCase: "What did the reporter say after we marked this fixed?"
@@ -508,10 +532,307 @@ var TDD_TOOL_CATALOG = [
508
532
  useCase: "Are there any tables without RLS? Show me recent backend errors for this project."
509
533
  }
510
534
  ];
535
+ var CODEBASE_TOOL_CATALOG = [
536
+ {
537
+ name: "ask_codebase",
538
+ title: "Ask about the indexed codebase",
539
+ description: "Ask a plain-English question about the connected repo. Grounds on pgvector retrieval over project_codebase_files and returns an answer with file:line citations. Requires codebase indexing enabled and an Anthropic or OpenAI BYOK key.",
540
+ scope: "mcp:write",
541
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
542
+ useCase: "How does authentication work in this repo?"
543
+ },
544
+ {
545
+ name: "get_file_summary",
546
+ title: "Plain-English file summary",
547
+ description: "Lazy-generate (or return cached) plain-English summary for an indexed file or symbol. Cache invalidates when content_hash changes after re-index.",
548
+ scope: "mcp:read",
549
+ hints: { readOnly: true, idempotent: true, openWorld: true },
550
+ useCase: "Explain what lib/auth.ts does in plain English."
551
+ },
552
+ {
553
+ name: "get_codebase_tour",
554
+ title: "Guided codebase tour",
555
+ description: "Return a dependency-ordered guided tour (~6\u201310 stops) for onboarding \u2014 each stop lists node ids, file paths, layer, and rationale. Cached per index fingerprint.",
556
+ scope: "mcp:read",
557
+ hints: { readOnly: true, idempotent: true, openWorld: true },
558
+ useCase: "Give me an onboarding walkthrough of this codebase."
559
+ },
560
+ {
561
+ name: "search_codebase",
562
+ title: "Semantic codebase search",
563
+ description: "Search indexed files by plain-English meaning via embeddings. Returns top-k file chunks with similarity scores.",
564
+ scope: "mcp:read",
565
+ hints: { readOnly: true, idempotent: true, openWorld: true },
566
+ useCase: "Find files related to authentication or payment handling."
567
+ },
568
+ {
569
+ name: "get_codebase_domains",
570
+ title: "Business domain map",
571
+ description: "Extract business domains, flows, and steps mapped to file paths. Cached per index fingerprint.",
572
+ scope: "mcp:read",
573
+ hints: { readOnly: true, idempotent: true, openWorld: true },
574
+ useCase: "What business domains does this codebase implement?"
575
+ },
576
+ {
577
+ name: "analyze_codebase_impact",
578
+ title: "Diff impact analysis",
579
+ description: "Find files that depend on changed paths (reverse import graph). Supports manual paths, last push, GitHub compare, or fix PR files.",
580
+ scope: "mcp:read",
581
+ hints: { readOnly: true, idempotent: true, openWorld: true },
582
+ useCase: "What breaks if I change lib/auth.ts?"
583
+ },
584
+ {
585
+ name: "analyze_wiki_knowledge",
586
+ title: "Wiki knowledge graph",
587
+ description: "Return wiki/docs knowledge graph nodes and sources for the project. Use include_wiki on ask_codebase for RAG merge.",
588
+ scope: "mcp:read",
589
+ hints: { readOnly: true, idempotent: true, openWorld: true },
590
+ useCase: "What docs entities exist for onboarding?"
591
+ }
592
+ ];
593
+
594
+ // src/feature-groups.ts
595
+ var FEATURE_GROUPS = [
596
+ "triage",
597
+ "fixes",
598
+ "inventory",
599
+ "setup",
600
+ "qa",
601
+ "skills",
602
+ "rewards",
603
+ "admin",
604
+ "audit",
605
+ "docs",
606
+ /** Deprecated aliases kept for one release — hidden unless `legacy` or `all`. */
607
+ "legacy"
608
+ ];
609
+ var ALL_FEATURE_GROUPS = FEATURE_GROUPS.filter(
610
+ (g) => g !== "legacy"
611
+ );
612
+ var TOOL_FEATURE_MAP = {
613
+ // triage
614
+ get_recent_reports: "triage",
615
+ get_report_detail: "triage",
616
+ get_report_timeline: "triage",
617
+ search_reports: "triage",
618
+ get_similar_bugs: "triage",
619
+ get_fix_context: "triage",
620
+ get_fix_timeline: "triage",
621
+ get_blast_radius: "triage",
622
+ get_knowledge_graph: "triage",
623
+ run_nl_query: "triage",
624
+ get_report_evidence: "triage",
625
+ triage_issue: "triage",
626
+ query_lessons: "triage",
627
+ list_lessons: "triage",
628
+ fix_suggest: "triage",
629
+ graph_neighborhood: "triage",
630
+ graph_node_status: "triage",
631
+ get_reporter_thread: "legacy",
632
+ // inventory
633
+ inventory_get: "inventory",
634
+ inventory_diff: "inventory",
635
+ inventory_findings: "inventory",
636
+ inventory_current: "admin",
637
+ // setup
638
+ diagnose_connection: "setup",
639
+ diagnose_setup: "setup",
640
+ setup_check: "legacy",
641
+ ingest_setup_check: "legacy",
642
+ get_activation_status: "legacy",
643
+ activation_status: "setup",
644
+ project_integration_health: "setup",
645
+ setup_repo_for_mushi: "setup",
646
+ get_two_way_comms_health: "setup",
647
+ // fixes
648
+ submit_fix_result: "fixes",
649
+ dispatch_fix: "fixes",
650
+ trigger_judge: "fixes",
651
+ test_gen_from_report: "fixes",
652
+ transition_status: "fixes",
653
+ merge_fix: "fixes",
654
+ refresh_ci: "fixes",
655
+ reopen_report: "fixes",
656
+ reply_to_reporter: "fixes",
657
+ // rewards
658
+ list_top_contributors: "rewards",
659
+ award_bonus_points: "rewards",
660
+ set_tier: "rewards",
661
+ // admin / project context
662
+ list_projects: "admin",
663
+ get_account_overview: "admin",
664
+ get_project_context: "admin",
665
+ get_pipeline_logs: "admin",
666
+ project_dashboard: "admin",
667
+ project_stats: "admin",
668
+ project_settings: "admin",
669
+ privacy_status: "admin",
670
+ evolution_history: "admin",
671
+ // qa / tdd
672
+ map_user_stories: "qa",
673
+ get_map_run_status: "qa",
674
+ generate_tdd_from_story: "qa",
675
+ improve_qa_story: "qa",
676
+ run_qa_story: "qa",
677
+ list_pending_review_stories: "qa",
678
+ approve_qa_story: "qa",
679
+ list_qa_story_runs: "qa",
680
+ get_qa_story_run: "qa",
681
+ test_notification_channel: "qa",
682
+ // skills
683
+ list_skills: "skills",
684
+ get_skill: "skills",
685
+ start_skill_pipeline: "skills",
686
+ get_pipeline_run: "skills",
687
+ checkin_pipeline_step: "skills",
688
+ list_byok_keys: "skills",
689
+ add_byok_key: "skills",
690
+ // audit
691
+ run_fullstack_audit: "audit",
692
+ get_backend_health: "audit",
693
+ // docs
694
+ search_mushi_docs: "docs"
695
+ };
696
+ function parseFeaturesParam(raw) {
697
+ if (raw == null || raw.trim() === "") return "all";
698
+ const normalized = raw.trim().toLowerCase();
699
+ if (normalized === "all" || normalized === "*") return "all";
700
+ const groups = normalized.split(",").map((s) => s.trim()).filter(Boolean);
701
+ const valid = groups.filter((g) => FEATURE_GROUPS.includes(g));
702
+ if (valid.length === 0) return "all";
703
+ return valid;
704
+ }
705
+ function parseFeaturesCsv(raw) {
706
+ if (!raw?.trim()) return "all";
707
+ return parseFeaturesParam(raw);
708
+ }
709
+ function toolMatchesFeatures(toolName, filter) {
710
+ if (filter === "all") return true;
711
+ const group = TOOL_FEATURE_MAP[toolName];
712
+ if (!group) return true;
713
+ if (group === "legacy") {
714
+ return filter.includes("legacy");
715
+ }
716
+ return filter.includes(group);
717
+ }
511
718
 
512
719
  // src/server.ts
513
720
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
514
721
  import { z } from "zod";
722
+ import { createLogger } from "@mushi-mushi/core";
723
+
724
+ // src/branding.ts
725
+ var MUSHI_WEBSITE_URL = "https://kensaur.us/mushi-mushi";
726
+ var MUSHI_ICON_PNG_URL = `${MUSHI_WEBSITE_URL}/integrations/mushi-mark-512.png`;
727
+ var MUSHI_ICON_SVG_URL = `${MUSHI_WEBSITE_URL}/favicon.svg`;
728
+ var MUSHI_SERVER_ICONS = [
729
+ { src: MUSHI_ICON_PNG_URL, mimeType: "image/png", sizes: ["512x512"] },
730
+ { src: MUSHI_ICON_SVG_URL, mimeType: "image/svg+xml", sizes: ["any"] }
731
+ ];
732
+ var MUSHI_SERVER_METADATA = {
733
+ name: "mushi-mushi",
734
+ title: "Mushi Mushi",
735
+ description: "User-felt bug triage, fix dispatch, QA stories, and inventory spec traceability for AI coding agents.",
736
+ websiteUrl: MUSHI_WEBSITE_URL,
737
+ icons: MUSHI_SERVER_ICONS
738
+ };
739
+
740
+ // src/docs-index.ts
741
+ var BASE = "https://kensaur.us/mushi-mushi/docs";
742
+ var MUSHI_DOCS_INDEX = [
743
+ {
744
+ title: "Architecture overview",
745
+ path: `${BASE}/concepts/architecture`,
746
+ keywords: ["architecture", "evolution loop", "pdca", "overview"],
747
+ excerpt: "How Mushi captures user-felt bugs, triages, dispatches fixes, and learns."
748
+ },
749
+ {
750
+ title: "MCP setup",
751
+ path: `${BASE}/guides/mcp-setup`,
752
+ keywords: ["mcp", "cursor", "api key", "stdio", "http"],
753
+ excerpt: "Wire Mushi MCP into Cursor, Claude, or any MCP client."
754
+ },
755
+ {
756
+ title: "API key scopes",
757
+ path: `${BASE}/guides/api-keys`,
758
+ keywords: ["api key", "mcp:read", "mcp:write", "scopes", "security"],
759
+ excerpt: "Separate SDK ingest keys from MCP read/write keys."
760
+ },
761
+ {
762
+ title: "Judge loop",
763
+ path: `${BASE}/concepts/judge-loop`,
764
+ keywords: ["judge", "sonnet", "quality", "fix scoring"],
765
+ excerpt: "Sonnet-as-Judge scores fix quality and feeds the lesson library."
766
+ },
767
+ {
768
+ title: "Inventory v2",
769
+ path: `${BASE}/concepts/inventory`,
770
+ keywords: ["inventory", "user stories", "spec", "yaml", "actions"],
771
+ excerpt: "inventory.yaml maps pages, stories, and expected outcomes for spec traceability."
772
+ },
773
+ {
774
+ title: "QA coverage stories",
775
+ path: `${BASE}/guides/qa-stories`,
776
+ keywords: ["qa", "playwright", "tdd", "cron", "browserbase", "firecrawl"],
777
+ excerpt: "Author, approve, and schedule user-story tests."
778
+ },
779
+ {
780
+ title: "Skill pipelines",
781
+ path: `${BASE}/guides/skill-pipelines`,
782
+ keywords: ["skills", "pipeline", "cursor", "workflow"],
783
+ excerpt: "Attach agent skills to reports and run handoff or cloud pipelines."
784
+ },
785
+ {
786
+ title: "Dispatch fix",
787
+ path: `${BASE}/guides/dispatch-fix`,
788
+ keywords: ["dispatch", "fix", "pr", "cursor cloud", "autofix"],
789
+ excerpt: "Dispatch the Mushi fix agent or Cursor Cloud for a classified report."
790
+ },
791
+ {
792
+ title: "SDK installation",
793
+ path: `${BASE}/guides/sdk`,
794
+ keywords: ["sdk", "install", "react", "next", "ingest"],
795
+ excerpt: "Install @mushi-mushi/web and start capturing user reports."
796
+ },
797
+ {
798
+ title: "Configuration reference",
799
+ path: `${BASE}/reference/config`,
800
+ keywords: ["env", "config", "settings", "byok"],
801
+ excerpt: "Environment variables and project settings reference."
802
+ },
803
+ {
804
+ title: "Evolution loop",
805
+ path: `${BASE}/concepts/evolution`,
806
+ keywords: ["evolution", "lessons", "convergence", "prompt promotion"],
807
+ excerpt: "Closed-loop learning from bugs, fixes, and judge scores."
808
+ },
809
+ {
810
+ title: "Privacy & BYOK",
811
+ path: `${BASE}/guides/privacy`,
812
+ keywords: ["privacy", "byok", "data region", "llm"],
813
+ excerpt: "Data boundaries, BYOK LLM keys, and retention."
814
+ }
815
+ ];
816
+ function searchMushiDocs(query, limit = 8) {
817
+ const q = query.trim().toLowerCase();
818
+ if (!q) {
819
+ return MUSHI_DOCS_INDEX.slice(0, limit).map((e) => ({ ...e, score: 0 }));
820
+ }
821
+ const terms = q.split(/\s+/).filter(Boolean);
822
+ const scored = MUSHI_DOCS_INDEX.map((entry) => {
823
+ const hay = `${entry.title} ${entry.keywords.join(" ")} ${entry.excerpt}`.toLowerCase();
824
+ let score = 0;
825
+ for (const term of terms) {
826
+ if (entry.title.toLowerCase().includes(term)) score += 4;
827
+ if (entry.keywords.some((k) => k.includes(term))) score += 3;
828
+ if (hay.includes(term)) score += 1;
829
+ }
830
+ return { ...entry, score };
831
+ }).filter((e) => e.score > 0).sort((a, b) => b.score - a.score);
832
+ return scored.slice(0, limit);
833
+ }
834
+
835
+ // src/server.ts
515
836
  var MushiApiError = class extends Error {
516
837
  constructor(status, code, message) {
517
838
  super(`[${code}] ${message}`);
@@ -525,10 +846,22 @@ var MushiApiError = class extends Error {
525
846
  function createMushiServer(config) {
526
847
  const { version, apiEndpoint, apiKey, projectId } = config;
527
848
  const doFetch = config.fetch ?? globalThis.fetch;
849
+ const apiLog = createLogger({
850
+ scope: "mushi:mcp:api",
851
+ level: process.env.MUSHI_LOG_LEVEL ?? "info"
852
+ });
528
853
  if (config.scopes !== void 0 && config.scopes.length === 0) {
529
- return new McpServer({ name: "mushi-mushi", version });
854
+ return new McpServer({
855
+ name: MUSHI_SERVER_METADATA.name,
856
+ version,
857
+ title: MUSHI_SERVER_METADATA.title,
858
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
859
+ icons: [...MUSHI_SERVER_METADATA.icons]
860
+ });
530
861
  }
531
862
  async function apiCall(path, options) {
863
+ const requestId = crypto.randomUUID().slice(0, 12);
864
+ const started = Date.now();
532
865
  const res = await doFetch(`${apiEndpoint}${path}`, {
533
866
  ...options,
534
867
  headers: {
@@ -538,10 +871,12 @@ function createMushiServer(config) {
538
871
  // and for transparent proxies that strip X-Mushi-* headers.
539
872
  "Authorization": `Bearer ${apiKey}`,
540
873
  "X-Mushi-Api-Key": apiKey,
541
- ...projectId ? { "X-Mushi-Project": projectId } : {},
874
+ "X-Request-Id": requestId,
875
+ ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
542
876
  ...options?.headers ?? {}
543
877
  }
544
878
  });
879
+ const durationMs = Date.now() - started;
545
880
  const text = await res.text();
546
881
  let body = null;
547
882
  if (text) {
@@ -555,6 +890,7 @@ function createMushiServer(config) {
555
890
  const envelope2 = body;
556
891
  const code = envelope2?.error?.code ?? `HTTP_${res.status}`;
557
892
  const message = envelope2?.error?.message ?? text.slice(0, 500) ?? `Request failed with ${res.status}`;
893
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
558
894
  throw new MushiApiError(res.status, code, message);
559
895
  }
560
896
  const envelope = body;
@@ -562,10 +898,13 @@ function createMushiServer(config) {
562
898
  if (!envelope.ok) {
563
899
  const code = envelope.error?.code ?? "API_ERROR";
564
900
  const message = envelope.error?.message ?? "API returned ok=false";
901
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
565
902
  throw new MushiApiError(res.status, code, message);
566
903
  }
904
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
567
905
  return envelope.data ?? {};
568
906
  }
907
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
569
908
  return body;
570
909
  }
571
910
  function jsonText(value) {
@@ -580,10 +919,13 @@ function createMushiServer(config) {
580
919
  };
581
920
  }
582
921
  const server = new McpServer({
583
- name: "mushi-mushi",
584
- version
922
+ name: MUSHI_SERVER_METADATA.name,
923
+ version,
924
+ title: MUSHI_SERVER_METADATA.title,
925
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
926
+ icons: [...MUSHI_SERVER_METADATA.icons]
585
927
  });
586
- const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
928
+ const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG, ...CODEBASE_TOOL_CATALOG];
587
929
  function annotationsFor(name, catalog = ALL_TOOL_CATALOG) {
588
930
  const spec = catalog.find((t) => t.name === name);
589
931
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
@@ -616,7 +958,10 @@ function createMushiServer(config) {
616
958
  status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
617
959
  category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
618
960
  severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
619
- limit: z.number().optional().describe("Max reports to return (default 20, max 100)")
961
+ limit: z.number().optional().describe("Max reports to return (default 20, max 100)"),
962
+ project_id: z.string().optional().describe(
963
+ "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."
964
+ )
620
965
  },
621
966
  outputSchema: {
622
967
  reports: z.array(z.unknown()),
@@ -629,7 +974,11 @@ function createMushiServer(config) {
629
974
  if (args.category) params.set("category", args.category);
630
975
  if (args.severity) params.set("severity", args.severity);
631
976
  params.set("limit", String(Math.min(args.limit ?? 20, 100)));
632
- const data = await apiCall(`/v1/admin/reports?${params}`);
977
+ const pid = args.project_id ?? projectId;
978
+ const extraHeaders = pid && pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
979
+ const data = await apiCall(`/v1/admin/reports?${params}`, {
980
+ headers: extraHeaders
981
+ });
633
982
  return jsonResult(data);
634
983
  }
635
984
  );
@@ -639,9 +988,19 @@ function createMushiServer(config) {
639
988
  title: titleOf("get_report_detail"),
640
989
  description: descOf("get_report_detail"),
641
990
  annotations: annotationsFor("get_report_detail"),
642
- inputSchema: { reportId: z.string().describe("The report UUID") }
991
+ inputSchema: { reportId: z.string().describe("The report UUID") },
992
+ outputSchema: { report: z.unknown() }
643
993
  },
644
- async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}`))
994
+ async (args) => {
995
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
996
+ const res = jsonResult({ report: data });
997
+ return {
998
+ ...res,
999
+ resource_links: [
1000
+ { uri: `project://reports/${args.reportId}`, title: `Report ${args.reportId.slice(0, 8)}\u2026` }
1001
+ ]
1002
+ };
1003
+ }
645
1004
  );
646
1005
  server.registerTool(
647
1006
  "get_report_timeline",
@@ -700,6 +1059,9 @@ function createMushiServer(config) {
700
1059
  inputSchema: {
701
1060
  query: z.string().describe("Component name, page path, or bug description"),
702
1061
  limit: z.number().optional().describe("Max results (default 5, max 20)")
1062
+ },
1063
+ outputSchema: {
1064
+ results: z.array(z.unknown())
703
1065
  }
704
1066
  },
705
1067
  async (args) => {
@@ -712,7 +1074,7 @@ function createMushiServer(config) {
712
1074
  ...projectId ? { projectId } : {}
713
1075
  })
714
1076
  });
715
- return jsonText(data);
1077
+ return jsonResult(data);
716
1078
  }
717
1079
  );
718
1080
  server.registerTool(
@@ -721,11 +1083,18 @@ function createMushiServer(config) {
721
1083
  title: titleOf("get_fix_context"),
722
1084
  description: descOf("get_fix_context"),
723
1085
  annotations: annotationsFor("get_fix_context"),
724
- inputSchema: { reportId: z.string().describe("The report UUID to fix") }
1086
+ inputSchema: { reportId: z.string().describe("The report UUID to fix") },
1087
+ outputSchema: {
1088
+ report: z.unknown(),
1089
+ reproductionSteps: z.unknown(),
1090
+ component: z.unknown(),
1091
+ rootCause: z.unknown(),
1092
+ bugOntologyTags: z.unknown()
1093
+ }
725
1094
  },
726
1095
  async (args) => {
727
1096
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
728
- return jsonText({
1097
+ return jsonResult({
729
1098
  report,
730
1099
  reproductionSteps: report.reproduction_steps ?? [],
731
1100
  component: report.component,
@@ -963,13 +1332,114 @@ function createMushiServer(config) {
963
1332
  });
964
1333
  }
965
1334
  );
1335
+ server.registerTool(
1336
+ "diagnose_setup",
1337
+ {
1338
+ title: titleOf("diagnose_setup"),
1339
+ description: descOf("diagnose_setup"),
1340
+ annotations: annotationsFor("diagnose_setup"),
1341
+ inputSchema: {
1342
+ mode: z.enum(["full", "ingest", "dispatch"]).optional().describe("full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."),
1343
+ project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
1344
+ projectId: z.string().optional().describe("Alias for project_id.")
1345
+ },
1346
+ outputSchema: {
1347
+ mode: z.string(),
1348
+ ready: z.boolean(),
1349
+ summary: z.string(),
1350
+ nextAction: z.string().optional(),
1351
+ ingest: z.unknown().optional(),
1352
+ dispatch: z.unknown().optional()
1353
+ }
1354
+ },
1355
+ async (args) => {
1356
+ const mode = args.mode ?? "full";
1357
+ const resolvedId = args.project_id ?? args.projectId ?? projectId;
1358
+ if (mode === "ingest") {
1359
+ const data = await apiCall("/v1/sync/ingest-setup");
1360
+ const failed = data.steps.filter((s) => s.required && !s.complete);
1361
+ 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(", ")}.`;
1362
+ return jsonResult({
1363
+ mode: "ingest",
1364
+ ready: data.ready,
1365
+ summary: summary2,
1366
+ nextAction: failed[0]?.hint,
1367
+ ingest: {
1368
+ projectId: data.project_id,
1369
+ projectName: data.project_name,
1370
+ requiredComplete: data.required_complete,
1371
+ requiredTotal: data.required_total,
1372
+ steps: data.steps,
1373
+ diagnostic: data.diagnostic ?? null
1374
+ }
1375
+ });
1376
+ }
1377
+ if (mode === "dispatch") {
1378
+ if (!resolvedId) {
1379
+ throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required for dispatch mode");
1380
+ }
1381
+ const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
1382
+ const failed = data.checks.filter((c) => !c.ready);
1383
+ const summary2 = data.ready ? `Project ${resolvedId} is ready to dispatch auto-fixes.` : `Dispatch blocked \u2014 ${failed.map((c) => c.label).join(", ")}.`;
1384
+ return jsonResult({
1385
+ mode: "dispatch",
1386
+ ready: data.ready,
1387
+ summary: summary2,
1388
+ nextAction: failed[0]?.hint,
1389
+ dispatch: { repoUrl: data.repoUrl ?? null, checks: data.checks }
1390
+ });
1391
+ }
1392
+ const ingest = await apiCall("/v1/sync/ingest-setup");
1393
+ let dispatchReady = true;
1394
+ let dispatchBlock;
1395
+ let dispatchChecks = null;
1396
+ if (resolvedId) {
1397
+ try {
1398
+ const preflight = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
1399
+ dispatchReady = preflight.ready;
1400
+ dispatchChecks = preflight.checks;
1401
+ if (!preflight.ready) {
1402
+ const failed = preflight.checks.filter((c) => !c.ready);
1403
+ dispatchBlock = failed[0]?.hint;
1404
+ }
1405
+ } catch {
1406
+ dispatchReady = false;
1407
+ dispatchBlock = "Could not run dispatch preflight \u2014 verify project_id and API key scope.";
1408
+ }
1409
+ }
1410
+ const ingestFailed = ingest.steps.filter((s) => s.required && !s.complete);
1411
+ const ingestBlock = ingestFailed[0]?.hint;
1412
+ const ready = ingest.ready && dispatchReady;
1413
+ 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.";
1414
+ 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.";
1415
+ return jsonResult({
1416
+ mode: "full",
1417
+ ready,
1418
+ summary,
1419
+ nextAction,
1420
+ ingest: { ready: ingest.ready, steps: ingest.steps },
1421
+ dispatch: resolvedId ? { ready: dispatchReady, checks: dispatchChecks } : null
1422
+ });
1423
+ }
1424
+ );
966
1425
  server.registerTool(
967
1426
  "diagnose_connection",
968
1427
  {
969
1428
  title: titleOf("diagnose_connection"),
970
1429
  description: descOf("diagnose_connection"),
971
1430
  annotations: annotationsFor("diagnose_connection"),
972
- inputSchema: {}
1431
+ inputSchema: {},
1432
+ outputSchema: {
1433
+ ready: z.boolean(),
1434
+ healthOk: z.boolean(),
1435
+ ingestReady: z.boolean(),
1436
+ dispatchReady: z.boolean(),
1437
+ endpoint: z.string().nullable(),
1438
+ projectId: z.string().nullable(),
1439
+ issues: z.array(z.unknown()),
1440
+ nextAction: z.string(),
1441
+ summary: z.string()
1442
+ }
973
1443
  },
974
1444
  async () => {
975
1445
  const issues = [];
@@ -1053,7 +1523,7 @@ function createMushiServer(config) {
1053
1523
  }
1054
1524
  const ready = issues.length === 0 && healthOk && ingestReady;
1055
1525
  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.");
1056
- return jsonText({
1526
+ return jsonResult({
1057
1527
  ready,
1058
1528
  healthOk,
1059
1529
  ingestReady,
@@ -1066,17 +1536,77 @@ function createMushiServer(config) {
1066
1536
  });
1067
1537
  }
1068
1538
  );
1539
+ server.registerTool(
1540
+ "search_mushi_docs",
1541
+ {
1542
+ title: titleOf("search_mushi_docs"),
1543
+ description: descOf("search_mushi_docs"),
1544
+ annotations: annotationsFor("search_mushi_docs"),
1545
+ inputSchema: {
1546
+ query: z.string().describe("Keywords to search official Mushi docs (guides, MCP, inventory, QA)."),
1547
+ limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8).")
1548
+ },
1549
+ outputSchema: {
1550
+ query: z.string(),
1551
+ results: z.array(
1552
+ z.object({
1553
+ title: z.string(),
1554
+ path: z.string(),
1555
+ excerpt: z.string(),
1556
+ score: z.number()
1557
+ })
1558
+ )
1559
+ }
1560
+ },
1561
+ async (args) => {
1562
+ const query = args.query ?? "";
1563
+ const hits = searchMushiDocs(query, args.limit ?? 8);
1564
+ const results = hits.map(({ title, path, excerpt, score }) => ({ title, path, excerpt, score }));
1565
+ return jsonResult({ query, results });
1566
+ }
1567
+ );
1069
1568
  server.registerTool(
1070
1569
  "list_projects",
1071
1570
  {
1072
1571
  title: titleOf("list_projects"),
1073
1572
  description: descOf("list_projects"),
1074
1573
  annotations: annotationsFor("list_projects"),
1075
- inputSchema: {}
1574
+ inputSchema: {},
1575
+ outputSchema: {
1576
+ projects: z.array(z.unknown()),
1577
+ total: z.number().optional(),
1578
+ _multi_project_hint: z.string().optional()
1579
+ }
1076
1580
  },
1077
1581
  async () => {
1078
1582
  const data = await apiCall("/v1/admin/mcp/projects");
1079
- return jsonText(data);
1583
+ return jsonResult({
1584
+ ...data,
1585
+ _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.'
1586
+ });
1587
+ }
1588
+ );
1589
+ server.registerTool(
1590
+ "get_account_overview",
1591
+ {
1592
+ title: titleOf("get_account_overview"),
1593
+ description: descOf("get_account_overview"),
1594
+ annotations: annotationsFor("get_account_overview"),
1595
+ inputSchema: {},
1596
+ outputSchema: {
1597
+ projects: z.array(z.unknown()),
1598
+ active_project_id: z.string().nullable(),
1599
+ multi_project_hint: z.string()
1600
+ }
1601
+ },
1602
+ async () => {
1603
+ const data = await apiCall("/v1/admin/mcp/projects");
1604
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
1605
+ return jsonResult({
1606
+ projects,
1607
+ active_project_id: projectId ?? null,
1608
+ 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".`
1609
+ });
1080
1610
  }
1081
1611
  );
1082
1612
  server.registerTool(
@@ -1176,6 +1706,20 @@ function createMushiServer(config) {
1176
1706
  report_id: z.string().describe("Report UUID to triage."),
1177
1707
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1178
1708
  include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1709
+ },
1710
+ outputSchema: {
1711
+ report_id: z.string(),
1712
+ severity: z.unknown(),
1713
+ category: z.unknown(),
1714
+ status: z.unknown(),
1715
+ report: z.unknown(),
1716
+ reporter_thread: z.unknown().nullable(),
1717
+ similar_bugs: z.unknown().nullable(),
1718
+ fix_context: z.unknown().nullable(),
1719
+ blast_radius: z.unknown().nullable(),
1720
+ recent_logs: z.unknown().nullable(),
1721
+ recommended_actions: z.array(z.unknown()),
1722
+ triage_summary: z.string()
1179
1723
  }
1180
1724
  },
1181
1725
  async (args) => {
@@ -1219,7 +1763,7 @@ function createMushiServer(config) {
1219
1763
  args: { reportId: args.report_id }
1220
1764
  });
1221
1765
  }
1222
- return jsonText({
1766
+ const result = {
1223
1767
  report_id: args.report_id,
1224
1768
  severity,
1225
1769
  category,
@@ -1232,7 +1776,15 @@ function createMushiServer(config) {
1232
1776
  recent_logs: logsRes.status === "fulfilled" ? logsRes.value : null,
1233
1777
  recommended_actions: actions,
1234
1778
  triage_summary: `[${severity}] ${category} \u2014 status: ${status}. ${actions.length > 0 ? `Recommended: ${actions[0]?.action}.` : "No action required."}`
1235
- });
1779
+ };
1780
+ const res = jsonResult(result);
1781
+ return {
1782
+ ...res,
1783
+ resource_links: [
1784
+ { uri: `project://reports/${args.report_id}`, title: `Report ${args.report_id.slice(0, 8)}\u2026` },
1785
+ ...pid ? [{ uri: `project://dashboard`, title: "Project dashboard" }] : []
1786
+ ]
1787
+ };
1236
1788
  }
1237
1789
  );
1238
1790
  server.registerTool(
@@ -1613,7 +2165,7 @@ function createMushiServer(config) {
1613
2165
  contents: [{
1614
2166
  uri: "privacy://status",
1615
2167
  mimeType: "application/json",
1616
- text: JSON.stringify(await apiCall("/v1/admin/privacy/status"), null, 2)
2168
+ text: JSON.stringify(await apiCall("/v1/admin/privacy-status"), null, 2)
1617
2169
  }]
1618
2170
  })
1619
2171
  );
@@ -1627,7 +2179,11 @@ function createMushiServer(config) {
1627
2179
  contents: [{
1628
2180
  uri: "evolution://history",
1629
2181
  mimeType: "application/json",
1630
- text: JSON.stringify(await apiCall("/v1/admin/evolution/history"), null, 2)
2182
+ text: projectId ? JSON.stringify(
2183
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(projectId)}/evolution-history`),
2184
+ null,
2185
+ 2
2186
+ ) : JSON.stringify({ ok: false, error: "project_id required" })
1631
2187
  }]
1632
2188
  })
1633
2189
  );
@@ -2206,12 +2762,192 @@ Be specific. No generic advice.`
2206
2762
  // admin-authed timeline, which also carries fix / QA / status lanes.
2207
2763
  async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}/timeline`))
2208
2764
  );
2765
+ server.registerTool(
2766
+ "ask_codebase",
2767
+ {
2768
+ title: titleOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2769
+ description: descOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2770
+ annotations: annotationsFor("ask_codebase", CODEBASE_TOOL_CATALOG),
2771
+ inputSchema: {
2772
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2773
+ question: z.string().describe("Plain-English question about the repo"),
2774
+ thread_id: z.string().optional().describe("Optional thread UUID to continue a conversation"),
2775
+ file_path: z.string().optional().describe("Optional file path focus"),
2776
+ symbol_name: z.string().optional().describe("Optional symbol name focus")
2777
+ }
2778
+ },
2779
+ async (args) => {
2780
+ const pid = args.project_id ?? projectId;
2781
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2782
+ const body = {
2783
+ messages: [{ role: "user", content: args.question }]
2784
+ };
2785
+ if (args.thread_id) body.threadId = args.thread_id;
2786
+ if (args.file_path) {
2787
+ body.fileFocus = {
2788
+ file_path: args.file_path,
2789
+ symbol_name: args.symbol_name ?? null
2790
+ };
2791
+ }
2792
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/chat`, {
2793
+ method: "POST",
2794
+ body: JSON.stringify(body)
2795
+ });
2796
+ return jsonText(data);
2797
+ }
2798
+ );
2799
+ server.registerTool(
2800
+ "get_file_summary",
2801
+ {
2802
+ title: titleOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2803
+ description: descOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2804
+ annotations: annotationsFor("get_file_summary", CODEBASE_TOOL_CATALOG),
2805
+ inputSchema: {
2806
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2807
+ file_path: z.string().describe("Indexed file path"),
2808
+ symbol_name: z.string().optional().describe("Optional symbol name within the file"),
2809
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2810
+ }
2811
+ },
2812
+ async (args) => {
2813
+ const pid = args.project_id ?? projectId;
2814
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2815
+ const qs = new URLSearchParams({ file_path: args.file_path });
2816
+ if (args.symbol_name) qs.set("symbol_name", args.symbol_name);
2817
+ if (args.force) qs.set("force", "1");
2818
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/summary?${qs}`);
2819
+ return jsonText(data);
2820
+ }
2821
+ );
2822
+ server.registerTool(
2823
+ "get_codebase_tour",
2824
+ {
2825
+ title: titleOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2826
+ description: descOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2827
+ annotations: annotationsFor("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2828
+ inputSchema: {
2829
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2830
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2831
+ }
2832
+ },
2833
+ async (args) => {
2834
+ const pid = args.project_id ?? projectId;
2835
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2836
+ const qs = args.force ? "?force=1" : "";
2837
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/tour${qs}`);
2838
+ return jsonText(data);
2839
+ }
2840
+ );
2841
+ server.registerTool(
2842
+ "search_codebase",
2843
+ {
2844
+ title: titleOf("search_codebase", CODEBASE_TOOL_CATALOG),
2845
+ description: descOf("search_codebase", CODEBASE_TOOL_CATALOG),
2846
+ annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2847
+ inputSchema: {
2848
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2849
+ query: z.string().describe("Plain-English search query"),
2850
+ k: z.number().optional().describe("Number of results (default 8, max 20)"),
2851
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2852
+ }
2853
+ },
2854
+ async (args) => {
2855
+ const pid = args.project_id ?? projectId;
2856
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2857
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/search`, {
2858
+ method: "POST",
2859
+ body: JSON.stringify({
2860
+ query: args.query,
2861
+ k: args.k ?? 8,
2862
+ scope_prefix: args.scope_prefix ?? void 0
2863
+ })
2864
+ });
2865
+ return jsonText(data);
2866
+ }
2867
+ );
2868
+ server.registerTool(
2869
+ "get_codebase_domains",
2870
+ {
2871
+ title: titleOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2872
+ description: descOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2873
+ annotations: annotationsFor("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2874
+ inputSchema: {
2875
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2876
+ force: z.boolean().optional().describe("Bypass cache and regenerate"),
2877
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2878
+ }
2879
+ },
2880
+ async (args) => {
2881
+ const pid = args.project_id ?? projectId;
2882
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2883
+ const qs = new URLSearchParams();
2884
+ if (args.force) qs.set("force", "1");
2885
+ if (args.scope_prefix) qs.set("scope_prefix", args.scope_prefix);
2886
+ const suffix = qs.toString() ? `?${qs}` : "";
2887
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/domains${suffix}`);
2888
+ return jsonText(data);
2889
+ }
2890
+ );
2891
+ server.registerTool(
2892
+ "analyze_codebase_impact",
2893
+ {
2894
+ title: titleOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2895
+ description: descOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2896
+ annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2897
+ inputSchema: {
2898
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2899
+ paths: z.array(z.string()).optional().describe("Changed file paths"),
2900
+ source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths"),
2901
+ compare: z.string().optional().describe("GitHub compare range base...head"),
2902
+ fix_id: z.string().optional().describe("Fix attempt UUID for PR changed files")
2903
+ }
2904
+ },
2905
+ async (args) => {
2906
+ const pid = args.project_id ?? projectId;
2907
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2908
+ const qs = new URLSearchParams();
2909
+ if (args.paths?.length) qs.set("paths", args.paths.join(","));
2910
+ if (args.source === "last_push") qs.set("ref", "last_push");
2911
+ if (args.compare) qs.set("compare", args.compare);
2912
+ if (args.fix_id) qs.set("fix_id", args.fix_id);
2913
+ if (!qs.toString()) {
2914
+ throw new MushiApiError(400, "BAD_REQUEST", "Provide paths, source=last_push, compare, or fix_id");
2915
+ }
2916
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
2917
+ return jsonText(data);
2918
+ }
2919
+ );
2920
+ server.registerTool(
2921
+ "analyze_wiki_knowledge",
2922
+ {
2923
+ title: titleOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2924
+ description: descOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2925
+ annotations: annotationsFor("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2926
+ inputSchema: {
2927
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)")
2928
+ }
2929
+ },
2930
+ async (args) => {
2931
+ const pid = args.project_id ?? projectId;
2932
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2933
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/knowledge/graph`);
2934
+ return jsonText(data);
2935
+ }
2936
+ );
2209
2937
  const grantedScopes = config.scopes;
2210
- if (grantedScopes !== void 0) {
2938
+ const featureFilter = config.features ?? "all";
2939
+ if (grantedScopes !== void 0 || featureFilter !== "all") {
2211
2940
  const toolRegistry = server._registeredTools;
2212
- const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
2941
+ const allSpecs = [
2942
+ ...TOOL_CATALOG,
2943
+ ...TDD_TOOL_CATALOG,
2944
+ ...CODEBASE_TOOL_CATALOG
2945
+ ];
2213
2946
  for (const spec of allSpecs) {
2214
- if (!grantedScopes.includes(spec.scope)) {
2947
+ if (grantedScopes !== void 0 && !grantedScopes.includes(spec.scope)) {
2948
+ toolRegistry[spec.name]?.remove();
2949
+ }
2950
+ if (featureFilter !== "all" && !toolMatchesFeatures(spec.name, featureFilter)) {
2215
2951
  toolRegistry[spec.name]?.remove();
2216
2952
  }
2217
2953
  }
@@ -2222,13 +2958,14 @@ Be specific. No generic advice.`
2222
2958
  // src/index.ts
2223
2959
  var require2 = createRequire(import.meta.url);
2224
2960
  var VERSION = require2("../package.json").version;
2225
- var log = createLogger({ scope: "mushi:mcp", level: "info" });
2961
+ var log = createLogger2({ scope: "mushi:mcp", level: "info" });
2226
2962
  var API_ENDPOINT = process.env.MUSHI_API_ENDPOINT ?? "";
2227
2963
  var API_KEY = process.env.MUSHI_API_KEY ?? "";
2228
2964
  var PROJECT_ID = process.env.MUSHI_PROJECT_ID ?? "";
2229
2965
  var SCOPES_RAW = process.env.MUSHI_SCOPES ?? "";
2230
2966
  var parsedScopes = SCOPES_RAW ? SCOPES_RAW.split(",").map((s) => s.trim()).filter((s) => s === "mcp:read" || s === "mcp:write") : ALL_SCOPES;
2231
2967
  var SCOPES = SCOPES_RAW && parsedScopes.length === 0 ? ALL_SCOPES : parsedScopes;
2968
+ var FEATURES = parseFeaturesCsv(process.env.MUSHI_FEATURES);
2232
2969
  async function main() {
2233
2970
  if (!API_KEY) {
2234
2971
  log.fatal("MUSHI_API_KEY environment variable is required");
@@ -2255,7 +2992,8 @@ async function main() {
2255
2992
  apiEndpoint: API_ENDPOINT,
2256
2993
  apiKey: API_KEY,
2257
2994
  projectId: PROJECT_ID || void 0,
2258
- scopes: SCOPES
2995
+ scopes: SCOPES,
2996
+ features: FEATURES
2259
2997
  });
2260
2998
  const transport = new StdioServerTransport();
2261
2999
  await server.connect(transport);
@@ -2267,7 +3005,7 @@ async function main() {
2267
3005
  const res = await fetch(`${API_ENDPOINT}/v1/admin/inventory/${PROJECT_ID}`, {
2268
3006
  headers: {
2269
3007
  "X-Mushi-Api-Key": API_KEY,
2270
- "X-Mushi-Project": PROJECT_ID
3008
+ "X-Mushi-Project-Id": PROJECT_ID
2271
3009
  },
2272
3010
  signal: AbortSignal.timeout(1e4)
2273
3011
  });