@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/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 all Mushi projects accessible to this API key. For project-scoped keys returns a single-item list; for org-scoped keys (account mode) returns every project owned by this account. Call this first when MUSHI_PROJECT_ID is not configured \u2014 use the returned id with subsequent tool calls. If multiple projects are returned, pass the target projectId explicitly to project-scoped tools. Multi-project tip: run `mushi setup --all-projects` in your terminal to create one named MCP server entry per project in .cursor/mcp.json.",
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 an enriched summary of every Mushi project accessible to this API key: id, name, recent report count (last 30 days), number of connected MCP keys, and last-seen heartbeat timestamp. For project-scoped keys this is a one-item list; for org-scoped (account) keys it lists every owned project. Also includes toolCount, resourceCount, promptCount so agents know how many tools are available. 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,319 @@ 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", "multi-project", "account mode", "install", "setup", "deeplink"],
753
+ excerpt: "Wire Mushi MCP into Cursor, Claude, or any MCP client. Supports per-project and account-level keys."
754
+ },
755
+ {
756
+ title: "API key scopes",
757
+ path: `${BASE}/guides/api-keys`,
758
+ keywords: ["api key", "mcp:read", "mcp:write", "scopes", "security", "account key", "org-scoped", "multi-project"],
759
+ excerpt: "Separate SDK ingest keys from MCP read/write keys. Mint an org-scoped account key for multi-project access."
760
+ },
761
+ {
762
+ title: "Account key (org-scoped)",
763
+ path: `${BASE}/guides/mcp-setup`,
764
+ keywords: ["account key", "org-scoped", "multi-project", "MUSHI_PROJECT_ID", "all projects", "mint-org-key", "get_account_overview"],
765
+ 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."
766
+ },
767
+ {
768
+ title: "Cursor MCP troubleshooting",
769
+ path: `${BASE}/guides/mcp-setup`,
770
+ keywords: ["cursor", "mcp.json", "red badge", "transport error", "stdout", "stderr", "json-rpc", "INVALID_TOKEN", "syntax helper", "diagnose_connection"],
771
+ excerpt: "Cursor stdio MCP requires stdout to contain only JSON-RPC. Use the console syntax helper, restart Cursor MCP, then run diagnose_connection."
772
+ },
773
+ {
774
+ title: "Judge loop",
775
+ path: `${BASE}/concepts/judge-loop`,
776
+ keywords: ["judge", "sonnet", "quality", "fix scoring"],
777
+ excerpt: "Sonnet-as-Judge scores fix quality and feeds the lesson library."
778
+ },
779
+ {
780
+ title: "Inventory v2",
781
+ path: `${BASE}/concepts/inventory`,
782
+ keywords: ["inventory", "user stories", "spec", "yaml", "actions"],
783
+ excerpt: "inventory.yaml maps pages, stories, and expected outcomes for spec traceability."
784
+ },
785
+ {
786
+ title: "QA coverage stories",
787
+ path: `${BASE}/guides/qa-stories`,
788
+ keywords: ["qa", "playwright", "tdd", "cron", "browserbase", "firecrawl"],
789
+ excerpt: "Author, approve, and schedule user-story tests."
790
+ },
791
+ {
792
+ title: "Skill pipelines",
793
+ path: `${BASE}/guides/skill-pipelines`,
794
+ keywords: ["skills", "pipeline", "cursor", "workflow"],
795
+ excerpt: "Attach agent skills to reports and run handoff or cloud pipelines."
796
+ },
797
+ {
798
+ title: "Dispatch fix",
799
+ path: `${BASE}/guides/dispatch-fix`,
800
+ keywords: ["dispatch", "fix", "pr", "cursor cloud", "autofix"],
801
+ excerpt: "Dispatch the Mushi fix agent or Cursor Cloud for a classified report."
802
+ },
803
+ {
804
+ title: "SDK installation",
805
+ path: `${BASE}/guides/sdk`,
806
+ keywords: ["sdk", "install", "react", "next", "ingest"],
807
+ excerpt: "Install @mushi-mushi/web and start capturing user reports."
808
+ },
809
+ {
810
+ title: "Configuration reference",
811
+ path: `${BASE}/reference/config`,
812
+ keywords: ["env", "config", "settings", "byok"],
813
+ excerpt: "Environment variables and project settings reference."
814
+ },
815
+ {
816
+ title: "Evolution loop",
817
+ path: `${BASE}/concepts/evolution`,
818
+ keywords: ["evolution", "lessons", "convergence", "prompt promotion"],
819
+ excerpt: "Closed-loop learning from bugs, fixes, and judge scores."
820
+ },
821
+ {
822
+ title: "Privacy & BYOK",
823
+ path: `${BASE}/guides/privacy`,
824
+ keywords: ["privacy", "byok", "data region", "llm"],
825
+ excerpt: "Data boundaries, BYOK LLM keys, and retention."
826
+ }
827
+ ];
828
+ function searchMushiDocs(query, limit = 8) {
829
+ const q = query.trim().toLowerCase();
830
+ if (!q) {
831
+ return MUSHI_DOCS_INDEX.slice(0, limit).map((e) => ({ ...e, score: 0 }));
832
+ }
833
+ const terms = q.split(/\s+/).filter(Boolean);
834
+ const scored = MUSHI_DOCS_INDEX.map((entry) => {
835
+ const hay = `${entry.title} ${entry.keywords.join(" ")} ${entry.excerpt}`.toLowerCase();
836
+ let score = 0;
837
+ for (const term of terms) {
838
+ if (entry.title.toLowerCase().includes(term)) score += 4;
839
+ if (entry.keywords.some((k) => k.includes(term))) score += 3;
840
+ if (hay.includes(term)) score += 1;
841
+ }
842
+ return { ...entry, score };
843
+ }).filter((e) => e.score > 0).sort((a, b) => b.score - a.score);
844
+ return scored.slice(0, limit);
845
+ }
846
+
847
+ // src/server.ts
515
848
  var MushiApiError = class extends Error {
516
849
  constructor(status, code, message) {
517
850
  super(`[${code}] ${message}`);
@@ -525,10 +858,23 @@ var MushiApiError = class extends Error {
525
858
  function createMushiServer(config) {
526
859
  const { version, apiEndpoint, apiKey, projectId } = config;
527
860
  const doFetch = config.fetch ?? globalThis.fetch;
861
+ const apiLog = createLogger({
862
+ scope: "mushi:mcp:api",
863
+ level: process.env.MUSHI_LOG_LEVEL ?? "info",
864
+ destination: "stderr"
865
+ });
528
866
  if (config.scopes !== void 0 && config.scopes.length === 0) {
529
- return new McpServer({ name: "mushi-mushi", version });
867
+ return new McpServer({
868
+ name: MUSHI_SERVER_METADATA.name,
869
+ version,
870
+ title: MUSHI_SERVER_METADATA.title,
871
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
872
+ icons: [...MUSHI_SERVER_METADATA.icons]
873
+ });
530
874
  }
531
875
  async function apiCall(path, options) {
876
+ const requestId = crypto.randomUUID().slice(0, 12);
877
+ const started = Date.now();
532
878
  const res = await doFetch(`${apiEndpoint}${path}`, {
533
879
  ...options,
534
880
  headers: {
@@ -538,10 +884,12 @@ function createMushiServer(config) {
538
884
  // and for transparent proxies that strip X-Mushi-* headers.
539
885
  "Authorization": `Bearer ${apiKey}`,
540
886
  "X-Mushi-Api-Key": apiKey,
541
- ...projectId ? { "X-Mushi-Project": projectId } : {},
887
+ "X-Request-Id": requestId,
888
+ ...projectId ? { "X-Mushi-Project-Id": projectId } : {},
542
889
  ...options?.headers ?? {}
543
890
  }
544
891
  });
892
+ const durationMs = Date.now() - started;
545
893
  const text = await res.text();
546
894
  let body = null;
547
895
  if (text) {
@@ -555,6 +903,7 @@ function createMushiServer(config) {
555
903
  const envelope2 = body;
556
904
  const code = envelope2?.error?.code ?? `HTTP_${res.status}`;
557
905
  const message = envelope2?.error?.message ?? text.slice(0, 500) ?? `Request failed with ${res.status}`;
906
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
558
907
  throw new MushiApiError(res.status, code, message);
559
908
  }
560
909
  const envelope = body;
@@ -562,12 +911,47 @@ function createMushiServer(config) {
562
911
  if (!envelope.ok) {
563
912
  const code = envelope.error?.code ?? "API_ERROR";
564
913
  const message = envelope.error?.message ?? "API returned ok=false";
914
+ apiLog.warn("api.failed", { path, requestId, status: res.status, durationMs, code });
565
915
  throw new MushiApiError(res.status, code, message);
566
916
  }
917
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
567
918
  return envelope.data ?? {};
568
919
  }
920
+ apiLog.info("api.done", { path, requestId, status: res.status, durationMs });
569
921
  return body;
570
922
  }
923
+ async function resolveProjectId(explicitId) {
924
+ if (explicitId) return explicitId;
925
+ if (projectId) return projectId;
926
+ try {
927
+ const data = await apiCall(
928
+ "/v1/admin/mcp/account-overview"
929
+ );
930
+ if (data.total === 1 && data.projects[0]) {
931
+ return data.projects[0].id;
932
+ }
933
+ if (data.total === 0) {
934
+ throw new MushiApiError(
935
+ 400,
936
+ "NO_ACCESSIBLE_PROJECTS",
937
+ "This API key has no accessible projects. Mint an API key on a project in the admin console."
938
+ );
939
+ }
940
+ const projectList = data.projects.map((p) => `${p.name ?? "unnamed"} (${p.id})`).join(", ");
941
+ throw new MushiApiError(
942
+ 400,
943
+ "PROJECT_ID_REQUIRED",
944
+ `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.`
945
+ );
946
+ } catch (err) {
947
+ if (err instanceof MushiApiError) throw err;
948
+ throw new MushiApiError(
949
+ 400,
950
+ "MISSING_PROJECT_ID",
951
+ "projectId is required. Set MUSHI_PROJECT_ID in .cursor/mcp.json or pass it explicitly."
952
+ );
953
+ }
954
+ }
571
955
  function jsonText(value) {
572
956
  return {
573
957
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
@@ -580,10 +964,13 @@ function createMushiServer(config) {
580
964
  };
581
965
  }
582
966
  const server = new McpServer({
583
- name: "mushi-mushi",
584
- version
967
+ name: MUSHI_SERVER_METADATA.name,
968
+ version,
969
+ title: MUSHI_SERVER_METADATA.title,
970
+ websiteUrl: MUSHI_SERVER_METADATA.websiteUrl,
971
+ icons: [...MUSHI_SERVER_METADATA.icons]
585
972
  });
586
- const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
973
+ const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG, ...CODEBASE_TOOL_CATALOG];
587
974
  function annotationsFor(name, catalog = ALL_TOOL_CATALOG) {
588
975
  const spec = catalog.find((t) => t.name === name);
589
976
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
@@ -616,7 +1003,10 @@ function createMushiServer(config) {
616
1003
  status: z.string().optional().describe("Filter by status: new, classified, grouped, fixing, fixed, dismissed"),
617
1004
  category: z.string().optional().describe("Filter by category: bug, slow, visual, confusing, other"),
618
1005
  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)")
1006
+ limit: z.number().optional().describe("Max reports to return (default 20, max 100)"),
1007
+ project_id: z.string().optional().describe(
1008
+ "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."
1009
+ )
620
1010
  },
621
1011
  outputSchema: {
622
1012
  reports: z.array(z.unknown()),
@@ -629,7 +1019,11 @@ function createMushiServer(config) {
629
1019
  if (args.category) params.set("category", args.category);
630
1020
  if (args.severity) params.set("severity", args.severity);
631
1021
  params.set("limit", String(Math.min(args.limit ?? 20, 100)));
632
- const data = await apiCall(`/v1/admin/reports?${params}`);
1022
+ const pid = await resolveProjectId(args.project_id);
1023
+ const extraHeaders = pid !== projectId ? { "X-Mushi-Project-Id": pid } : {};
1024
+ const data = await apiCall(`/v1/admin/reports?${params}`, {
1025
+ headers: extraHeaders
1026
+ });
633
1027
  return jsonResult(data);
634
1028
  }
635
1029
  );
@@ -639,9 +1033,19 @@ function createMushiServer(config) {
639
1033
  title: titleOf("get_report_detail"),
640
1034
  description: descOf("get_report_detail"),
641
1035
  annotations: annotationsFor("get_report_detail"),
642
- inputSchema: { reportId: z.string().describe("The report UUID") }
1036
+ inputSchema: { reportId: z.string().describe("The report UUID") },
1037
+ outputSchema: { report: z.unknown() }
643
1038
  },
644
- async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}`))
1039
+ async (args) => {
1040
+ const data = await apiCall(`/v1/admin/reports/${args.reportId}`);
1041
+ const res = jsonResult({ report: data });
1042
+ return {
1043
+ ...res,
1044
+ resource_links: [
1045
+ { uri: `project://reports/${args.reportId}`, title: `Report ${args.reportId.slice(0, 8)}\u2026` }
1046
+ ]
1047
+ };
1048
+ }
645
1049
  );
646
1050
  server.registerTool(
647
1051
  "get_report_timeline",
@@ -700,6 +1104,9 @@ function createMushiServer(config) {
700
1104
  inputSchema: {
701
1105
  query: z.string().describe("Component name, page path, or bug description"),
702
1106
  limit: z.number().optional().describe("Max results (default 5, max 20)")
1107
+ },
1108
+ outputSchema: {
1109
+ results: z.array(z.unknown())
703
1110
  }
704
1111
  },
705
1112
  async (args) => {
@@ -712,7 +1119,7 @@ function createMushiServer(config) {
712
1119
  ...projectId ? { projectId } : {}
713
1120
  })
714
1121
  });
715
- return jsonText(data);
1122
+ return jsonResult(data);
716
1123
  }
717
1124
  );
718
1125
  server.registerTool(
@@ -721,11 +1128,18 @@ function createMushiServer(config) {
721
1128
  title: titleOf("get_fix_context"),
722
1129
  description: descOf("get_fix_context"),
723
1130
  annotations: annotationsFor("get_fix_context"),
724
- inputSchema: { reportId: z.string().describe("The report UUID to fix") }
1131
+ inputSchema: { reportId: z.string().describe("The report UUID to fix") },
1132
+ outputSchema: {
1133
+ report: z.unknown(),
1134
+ reproductionSteps: z.unknown(),
1135
+ component: z.unknown(),
1136
+ rootCause: z.unknown(),
1137
+ bugOntologyTags: z.unknown()
1138
+ }
725
1139
  },
726
1140
  async (args) => {
727
1141
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`);
728
- return jsonText({
1142
+ return jsonResult({
729
1143
  report,
730
1144
  reproductionSteps: report.reproduction_steps ?? [],
731
1145
  component: report.component,
@@ -963,13 +1377,115 @@ function createMushiServer(config) {
963
1377
  });
964
1378
  }
965
1379
  );
1380
+ server.registerTool(
1381
+ "diagnose_setup",
1382
+ {
1383
+ title: titleOf("diagnose_setup"),
1384
+ description: descOf("diagnose_setup"),
1385
+ annotations: annotationsFor("diagnose_setup"),
1386
+ inputSchema: {
1387
+ mode: z.enum(["full", "ingest", "dispatch"]).optional().describe("full (default) = ingest + dispatch; ingest = SDK pipeline only; dispatch = fix preflight only."),
1388
+ project_id: z.string().optional().describe("Project UUID for dispatch checks (defaults to configured project)."),
1389
+ projectId: z.string().optional().describe("Alias for project_id.")
1390
+ },
1391
+ outputSchema: {
1392
+ mode: z.string(),
1393
+ ready: z.boolean(),
1394
+ summary: z.string(),
1395
+ nextAction: z.string().optional(),
1396
+ ingest: z.unknown().optional(),
1397
+ dispatch: z.unknown().optional()
1398
+ }
1399
+ },
1400
+ async (args) => {
1401
+ const mode = args.mode ?? "full";
1402
+ const resolvedId = args.project_id ?? args.projectId ?? projectId;
1403
+ if (mode === "ingest") {
1404
+ const data = await apiCall("/v1/sync/ingest-setup");
1405
+ const failed = data.steps.filter((s) => s.required && !s.complete);
1406
+ 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(", ")}.`;
1407
+ return jsonResult({
1408
+ mode: "ingest",
1409
+ ready: data.ready,
1410
+ summary: summary2,
1411
+ nextAction: failed[0]?.hint,
1412
+ ingest: {
1413
+ projectId: data.project_id,
1414
+ projectName: data.project_name,
1415
+ requiredComplete: data.required_complete,
1416
+ requiredTotal: data.required_total,
1417
+ steps: data.steps,
1418
+ diagnostic: data.diagnostic ?? null
1419
+ }
1420
+ });
1421
+ }
1422
+ if (mode === "dispatch") {
1423
+ if (!resolvedId) {
1424
+ throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required for dispatch mode");
1425
+ }
1426
+ const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
1427
+ const failed = data.checks.filter((c) => !c.ready);
1428
+ const summary2 = data.ready ? `Project ${resolvedId} is ready to dispatch auto-fixes.` : `Dispatch blocked \u2014 ${failed.map((c) => c.label).join(", ")}.`;
1429
+ return jsonResult({
1430
+ mode: "dispatch",
1431
+ ready: data.ready,
1432
+ summary: summary2,
1433
+ nextAction: failed[0]?.hint,
1434
+ dispatch: { repoUrl: data.repoUrl ?? null, checks: data.checks }
1435
+ });
1436
+ }
1437
+ const ingest = await apiCall("/v1/sync/ingest-setup");
1438
+ let dispatchReady = true;
1439
+ let dispatchBlock;
1440
+ let dispatchChecks = null;
1441
+ if (resolvedId) {
1442
+ try {
1443
+ const preflight = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
1444
+ dispatchReady = preflight.ready;
1445
+ dispatchChecks = preflight.checks;
1446
+ if (!preflight.ready) {
1447
+ const failed = preflight.checks.filter((c) => !c.ready);
1448
+ dispatchBlock = failed[0]?.hint;
1449
+ }
1450
+ } catch {
1451
+ dispatchReady = false;
1452
+ dispatchBlock = "Could not run dispatch preflight \u2014 verify project_id and API key scope.";
1453
+ }
1454
+ }
1455
+ const ingestFailed = ingest.steps.filter((s) => s.required && !s.complete);
1456
+ const ingestBlock = ingestFailed[0]?.hint;
1457
+ const ready = ingest.ready && dispatchReady;
1458
+ 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.";
1459
+ 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.";
1460
+ return jsonResult({
1461
+ mode: "full",
1462
+ ready,
1463
+ summary,
1464
+ nextAction,
1465
+ ingest: { ready: ingest.ready, steps: ingest.steps },
1466
+ dispatch: resolvedId ? { ready: dispatchReady, checks: dispatchChecks } : null
1467
+ });
1468
+ }
1469
+ );
966
1470
  server.registerTool(
967
1471
  "diagnose_connection",
968
1472
  {
969
1473
  title: titleOf("diagnose_connection"),
970
1474
  description: descOf("diagnose_connection"),
971
1475
  annotations: annotationsFor("diagnose_connection"),
972
- inputSchema: {}
1476
+ inputSchema: {},
1477
+ outputSchema: {
1478
+ ready: z.boolean(),
1479
+ healthOk: z.boolean(),
1480
+ ingestReady: z.boolean(),
1481
+ dispatchReady: z.boolean(),
1482
+ endpoint: z.string().nullable(),
1483
+ projectId: z.string().nullable(),
1484
+ accessibleProjectCount: z.number().nullable(),
1485
+ issues: z.array(z.unknown()),
1486
+ nextAction: z.string(),
1487
+ summary: z.string()
1488
+ }
973
1489
  },
974
1490
  async () => {
975
1491
  const issues = [];
@@ -980,13 +1496,6 @@ function createMushiServer(config) {
980
1496
  fix: "Run `mushi connect` to write MUSHI_API_KEY into .cursor/mcp.json, then restart the MCP server."
981
1497
  });
982
1498
  }
983
- if (!projectId) {
984
- issues.push({
985
- check: "mcp_project_id",
986
- detail: "No projectId configured on the MCP server",
987
- fix: "Add MUSHI_PROJECT_ID to .cursor/mcp.json (copy UUID from console \u2192 Projects)."
988
- });
989
- }
990
1499
  if (!apiEndpoint) {
991
1500
  issues.push({
992
1501
  check: "mcp_endpoint",
@@ -1018,6 +1527,21 @@ function createMushiServer(config) {
1018
1527
  }
1019
1528
  let ingestReady = false;
1020
1529
  let dispatchReady = false;
1530
+ let accessibleProjectCount = null;
1531
+ if (!projectId && healthOk) {
1532
+ try {
1533
+ const overview = await apiCall("/v1/admin/mcp/account-overview");
1534
+ accessibleProjectCount = overview.total;
1535
+ if (overview.total === 0) {
1536
+ issues.push({
1537
+ check: "no_accessible_projects",
1538
+ detail: "API key has no accessible projects",
1539
+ 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."
1540
+ });
1541
+ }
1542
+ } catch {
1543
+ }
1544
+ }
1021
1545
  try {
1022
1546
  const ingest = await apiCall("/v1/sync/ingest-setup");
1023
1547
  ingestReady = ingest.ready;
@@ -1053,30 +1577,99 @@ function createMushiServer(config) {
1053
1577
  }
1054
1578
  const ready = issues.length === 0 && healthOk && ingestReady;
1055
1579
  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({
1580
+ return jsonResult({
1057
1581
  ready,
1058
1582
  healthOk,
1059
1583
  ingestReady,
1060
1584
  dispatchReady,
1061
1585
  endpoint: apiEndpoint ?? null,
1062
1586
  projectId: projectId ?? null,
1587
+ accessibleProjectCount,
1063
1588
  issues,
1064
1589
  nextAction,
1065
1590
  summary: ready ? "MCP credentials valid; ingest pipeline ready." : `Connection issue \u2014 ${issues[0]?.check ?? "unknown"}: ${nextAction}`
1066
1591
  });
1067
1592
  }
1068
1593
  );
1594
+ server.registerTool(
1595
+ "search_mushi_docs",
1596
+ {
1597
+ title: titleOf("search_mushi_docs"),
1598
+ description: descOf("search_mushi_docs"),
1599
+ annotations: annotationsFor("search_mushi_docs"),
1600
+ inputSchema: {
1601
+ query: z.string().describe("Keywords to search official Mushi docs (guides, MCP, inventory, QA)."),
1602
+ limit: z.number().int().min(1).max(20).optional().describe("Max results (default 8).")
1603
+ },
1604
+ outputSchema: {
1605
+ query: z.string(),
1606
+ results: z.array(
1607
+ z.object({
1608
+ title: z.string(),
1609
+ path: z.string(),
1610
+ excerpt: z.string(),
1611
+ score: z.number()
1612
+ })
1613
+ )
1614
+ }
1615
+ },
1616
+ async (args) => {
1617
+ const query = args.query ?? "";
1618
+ const hits = searchMushiDocs(query, args.limit ?? 8);
1619
+ const results = hits.map(({ title, path, excerpt, score }) => ({ title, path, excerpt, score }));
1620
+ return jsonResult({ query, results });
1621
+ }
1622
+ );
1069
1623
  server.registerTool(
1070
1624
  "list_projects",
1071
1625
  {
1072
1626
  title: titleOf("list_projects"),
1073
1627
  description: descOf("list_projects"),
1074
1628
  annotations: annotationsFor("list_projects"),
1075
- inputSchema: {}
1629
+ inputSchema: {},
1630
+ outputSchema: {
1631
+ projects: z.array(z.unknown()),
1632
+ total: z.number().optional(),
1633
+ _multi_project_hint: z.string().optional()
1634
+ }
1076
1635
  },
1077
1636
  async () => {
1078
1637
  const data = await apiCall("/v1/admin/mcp/projects");
1079
- return jsonText(data);
1638
+ return jsonResult({
1639
+ ...data,
1640
+ _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.'
1641
+ });
1642
+ }
1643
+ );
1644
+ server.registerTool(
1645
+ "get_account_overview",
1646
+ {
1647
+ title: titleOf("get_account_overview"),
1648
+ description: descOf("get_account_overview"),
1649
+ annotations: annotationsFor("get_account_overview"),
1650
+ inputSchema: {},
1651
+ outputSchema: {
1652
+ projects: z.array(z.unknown()),
1653
+ total: z.number(),
1654
+ active_project_id: z.string().nullable(),
1655
+ toolCount: z.number().optional(),
1656
+ resourceCount: z.number().optional(),
1657
+ promptCount: z.number().optional(),
1658
+ multi_project_hint: z.string()
1659
+ }
1660
+ },
1661
+ async () => {
1662
+ const data = await apiCall("/v1/admin/mcp/account-overview");
1663
+ const projects = Array.isArray(data?.projects) ? data.projects : [];
1664
+ return jsonResult({
1665
+ projects,
1666
+ total: data?.total ?? projects.length,
1667
+ active_project_id: projectId ?? null,
1668
+ toolCount: data?.toolCount,
1669
+ resourceCount: data?.resourceCount,
1670
+ promptCount: data?.promptCount,
1671
+ 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".`
1672
+ });
1080
1673
  }
1081
1674
  );
1082
1675
  server.registerTool(
@@ -1176,6 +1769,20 @@ function createMushiServer(config) {
1176
1769
  report_id: z.string().describe("Report UUID to triage."),
1177
1770
  project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1178
1771
  include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1772
+ },
1773
+ outputSchema: {
1774
+ report_id: z.string(),
1775
+ severity: z.unknown(),
1776
+ category: z.unknown(),
1777
+ status: z.unknown(),
1778
+ report: z.unknown(),
1779
+ reporter_thread: z.unknown().nullable(),
1780
+ similar_bugs: z.unknown().nullable(),
1781
+ fix_context: z.unknown().nullable(),
1782
+ blast_radius: z.unknown().nullable(),
1783
+ recent_logs: z.unknown().nullable(),
1784
+ recommended_actions: z.array(z.unknown()),
1785
+ triage_summary: z.string()
1179
1786
  }
1180
1787
  },
1181
1788
  async (args) => {
@@ -1219,7 +1826,7 @@ function createMushiServer(config) {
1219
1826
  args: { reportId: args.report_id }
1220
1827
  });
1221
1828
  }
1222
- return jsonText({
1829
+ const result = {
1223
1830
  report_id: args.report_id,
1224
1831
  severity,
1225
1832
  category,
@@ -1232,7 +1839,15 @@ function createMushiServer(config) {
1232
1839
  recent_logs: logsRes.status === "fulfilled" ? logsRes.value : null,
1233
1840
  recommended_actions: actions,
1234
1841
  triage_summary: `[${severity}] ${category} \u2014 status: ${status}. ${actions.length > 0 ? `Recommended: ${actions[0]?.action}.` : "No action required."}`
1235
- });
1842
+ };
1843
+ const res = jsonResult(result);
1844
+ return {
1845
+ ...res,
1846
+ resource_links: [
1847
+ { uri: `project://reports/${args.report_id}`, title: `Report ${args.report_id.slice(0, 8)}\u2026` },
1848
+ ...pid ? [{ uri: `project://dashboard`, title: "Project dashboard" }] : []
1849
+ ]
1850
+ };
1236
1851
  }
1237
1852
  );
1238
1853
  server.registerTool(
@@ -1613,7 +2228,7 @@ function createMushiServer(config) {
1613
2228
  contents: [{
1614
2229
  uri: "privacy://status",
1615
2230
  mimeType: "application/json",
1616
- text: JSON.stringify(await apiCall("/v1/admin/privacy/status"), null, 2)
2231
+ text: JSON.stringify(await apiCall("/v1/admin/privacy-status"), null, 2)
1617
2232
  }]
1618
2233
  })
1619
2234
  );
@@ -1627,7 +2242,11 @@ function createMushiServer(config) {
1627
2242
  contents: [{
1628
2243
  uri: "evolution://history",
1629
2244
  mimeType: "application/json",
1630
- text: JSON.stringify(await apiCall("/v1/admin/evolution/history"), null, 2)
2245
+ text: projectId ? JSON.stringify(
2246
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(projectId)}/evolution-history`),
2247
+ null,
2248
+ 2
2249
+ ) : JSON.stringify({ ok: false, error: "project_id required" })
1631
2250
  }]
1632
2251
  })
1633
2252
  );
@@ -2206,12 +2825,192 @@ Be specific. No generic advice.`
2206
2825
  // admin-authed timeline, which also carries fix / QA / status lanes.
2207
2826
  async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}/timeline`))
2208
2827
  );
2828
+ server.registerTool(
2829
+ "ask_codebase",
2830
+ {
2831
+ title: titleOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2832
+ description: descOf("ask_codebase", CODEBASE_TOOL_CATALOG),
2833
+ annotations: annotationsFor("ask_codebase", CODEBASE_TOOL_CATALOG),
2834
+ inputSchema: {
2835
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2836
+ question: z.string().describe("Plain-English question about the repo"),
2837
+ thread_id: z.string().optional().describe("Optional thread UUID to continue a conversation"),
2838
+ file_path: z.string().optional().describe("Optional file path focus"),
2839
+ symbol_name: z.string().optional().describe("Optional symbol name focus")
2840
+ }
2841
+ },
2842
+ async (args) => {
2843
+ const pid = args.project_id ?? projectId;
2844
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2845
+ const body = {
2846
+ messages: [{ role: "user", content: args.question }]
2847
+ };
2848
+ if (args.thread_id) body.threadId = args.thread_id;
2849
+ if (args.file_path) {
2850
+ body.fileFocus = {
2851
+ file_path: args.file_path,
2852
+ symbol_name: args.symbol_name ?? null
2853
+ };
2854
+ }
2855
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/chat`, {
2856
+ method: "POST",
2857
+ body: JSON.stringify(body)
2858
+ });
2859
+ return jsonText(data);
2860
+ }
2861
+ );
2862
+ server.registerTool(
2863
+ "get_file_summary",
2864
+ {
2865
+ title: titleOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2866
+ description: descOf("get_file_summary", CODEBASE_TOOL_CATALOG),
2867
+ annotations: annotationsFor("get_file_summary", CODEBASE_TOOL_CATALOG),
2868
+ inputSchema: {
2869
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2870
+ file_path: z.string().describe("Indexed file path"),
2871
+ symbol_name: z.string().optional().describe("Optional symbol name within the file"),
2872
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2873
+ }
2874
+ },
2875
+ async (args) => {
2876
+ const pid = args.project_id ?? projectId;
2877
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2878
+ const qs = new URLSearchParams({ file_path: args.file_path });
2879
+ if (args.symbol_name) qs.set("symbol_name", args.symbol_name);
2880
+ if (args.force) qs.set("force", "1");
2881
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/summary?${qs}`);
2882
+ return jsonText(data);
2883
+ }
2884
+ );
2885
+ server.registerTool(
2886
+ "get_codebase_tour",
2887
+ {
2888
+ title: titleOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2889
+ description: descOf("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2890
+ annotations: annotationsFor("get_codebase_tour", CODEBASE_TOOL_CATALOG),
2891
+ inputSchema: {
2892
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2893
+ force: z.boolean().optional().describe("Bypass cache and regenerate")
2894
+ }
2895
+ },
2896
+ async (args) => {
2897
+ const pid = args.project_id ?? projectId;
2898
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2899
+ const qs = args.force ? "?force=1" : "";
2900
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/tour${qs}`);
2901
+ return jsonText(data);
2902
+ }
2903
+ );
2904
+ server.registerTool(
2905
+ "search_codebase",
2906
+ {
2907
+ title: titleOf("search_codebase", CODEBASE_TOOL_CATALOG),
2908
+ description: descOf("search_codebase", CODEBASE_TOOL_CATALOG),
2909
+ annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2910
+ inputSchema: {
2911
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2912
+ query: z.string().describe("Plain-English search query"),
2913
+ k: z.number().optional().describe("Number of results (default 8, max 20)"),
2914
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2915
+ }
2916
+ },
2917
+ async (args) => {
2918
+ const pid = args.project_id ?? projectId;
2919
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2920
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/search`, {
2921
+ method: "POST",
2922
+ body: JSON.stringify({
2923
+ query: args.query,
2924
+ k: args.k ?? 8,
2925
+ scope_prefix: args.scope_prefix ?? void 0
2926
+ })
2927
+ });
2928
+ return jsonText(data);
2929
+ }
2930
+ );
2931
+ server.registerTool(
2932
+ "get_codebase_domains",
2933
+ {
2934
+ title: titleOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2935
+ description: descOf("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2936
+ annotations: annotationsFor("get_codebase_domains", CODEBASE_TOOL_CATALOG),
2937
+ inputSchema: {
2938
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2939
+ force: z.boolean().optional().describe("Bypass cache and regenerate"),
2940
+ scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2941
+ }
2942
+ },
2943
+ async (args) => {
2944
+ const pid = args.project_id ?? projectId;
2945
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2946
+ const qs = new URLSearchParams();
2947
+ if (args.force) qs.set("force", "1");
2948
+ if (args.scope_prefix) qs.set("scope_prefix", args.scope_prefix);
2949
+ const suffix = qs.toString() ? `?${qs}` : "";
2950
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/domains${suffix}`);
2951
+ return jsonText(data);
2952
+ }
2953
+ );
2954
+ server.registerTool(
2955
+ "analyze_codebase_impact",
2956
+ {
2957
+ title: titleOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2958
+ description: descOf("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2959
+ annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2960
+ inputSchema: {
2961
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2962
+ paths: z.array(z.string()).optional().describe("Changed file paths"),
2963
+ source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths"),
2964
+ compare: z.string().optional().describe("GitHub compare range base...head"),
2965
+ fix_id: z.string().optional().describe("Fix attempt UUID for PR changed files")
2966
+ }
2967
+ },
2968
+ async (args) => {
2969
+ const pid = args.project_id ?? projectId;
2970
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2971
+ const qs = new URLSearchParams();
2972
+ if (args.paths?.length) qs.set("paths", args.paths.join(","));
2973
+ if (args.source === "last_push") qs.set("ref", "last_push");
2974
+ if (args.compare) qs.set("compare", args.compare);
2975
+ if (args.fix_id) qs.set("fix_id", args.fix_id);
2976
+ if (!qs.toString()) {
2977
+ throw new MushiApiError(400, "BAD_REQUEST", "Provide paths, source=last_push, compare, or fix_id");
2978
+ }
2979
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
2980
+ return jsonText(data);
2981
+ }
2982
+ );
2983
+ server.registerTool(
2984
+ "analyze_wiki_knowledge",
2985
+ {
2986
+ title: titleOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2987
+ description: descOf("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2988
+ annotations: annotationsFor("analyze_wiki_knowledge", CODEBASE_TOOL_CATALOG),
2989
+ inputSchema: {
2990
+ project_id: z.string().optional().describe("Project UUID (defaults to configured project)")
2991
+ }
2992
+ },
2993
+ async (args) => {
2994
+ const pid = args.project_id ?? projectId;
2995
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2996
+ const data = await apiCall(`/v1/admin/projects/${pid}/codebase/knowledge/graph`);
2997
+ return jsonText(data);
2998
+ }
2999
+ );
2209
3000
  const grantedScopes = config.scopes;
2210
- if (grantedScopes !== void 0) {
3001
+ const featureFilter = config.features ?? "all";
3002
+ if (grantedScopes !== void 0 || featureFilter !== "all") {
2211
3003
  const toolRegistry = server._registeredTools;
2212
- const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
3004
+ const allSpecs = [
3005
+ ...TOOL_CATALOG,
3006
+ ...TDD_TOOL_CATALOG,
3007
+ ...CODEBASE_TOOL_CATALOG
3008
+ ];
2213
3009
  for (const spec of allSpecs) {
2214
- if (!grantedScopes.includes(spec.scope)) {
3010
+ if (grantedScopes !== void 0 && !grantedScopes.includes(spec.scope)) {
3011
+ toolRegistry[spec.name]?.remove();
3012
+ }
3013
+ if (featureFilter !== "all" && !toolMatchesFeatures(spec.name, featureFilter)) {
2215
3014
  toolRegistry[spec.name]?.remove();
2216
3015
  }
2217
3016
  }
@@ -2220,15 +3019,21 @@ Be specific. No generic advice.`
2220
3019
  }
2221
3020
 
2222
3021
  // src/index.ts
3022
+ var _writeStderr = (...args) => process.stderr.write(
3023
+ args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ") + "\n"
3024
+ );
3025
+ console.log = _writeStderr;
3026
+ console.warn = _writeStderr;
2223
3027
  var require2 = createRequire(import.meta.url);
2224
3028
  var VERSION = require2("../package.json").version;
2225
- var log = createLogger({ scope: "mushi:mcp", level: "info" });
3029
+ var log = createLogger2({ scope: "mushi:mcp", level: "info", destination: "stderr" });
2226
3030
  var API_ENDPOINT = process.env.MUSHI_API_ENDPOINT ?? "";
2227
3031
  var API_KEY = process.env.MUSHI_API_KEY ?? "";
2228
3032
  var PROJECT_ID = process.env.MUSHI_PROJECT_ID ?? "";
2229
3033
  var SCOPES_RAW = process.env.MUSHI_SCOPES ?? "";
2230
3034
  var parsedScopes = SCOPES_RAW ? SCOPES_RAW.split(",").map((s) => s.trim()).filter((s) => s === "mcp:read" || s === "mcp:write") : ALL_SCOPES;
2231
3035
  var SCOPES = SCOPES_RAW && parsedScopes.length === 0 ? ALL_SCOPES : parsedScopes;
3036
+ var FEATURES = parseFeaturesCsv(process.env.MUSHI_FEATURES);
2232
3037
  async function main() {
2233
3038
  if (!API_KEY) {
2234
3039
  log.fatal("MUSHI_API_KEY environment variable is required");
@@ -2239,13 +3044,15 @@ async function main() {
2239
3044
  "[mushi-mcp] MUSHI_API_ENDPOINT is not set. All tool calls will fail.\nSet MUSHI_API_ENDPOINT to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
2240
3045
  );
2241
3046
  }
3047
+ const mode = PROJECT_ID ? "single-project" : "account";
2242
3048
  if (!PROJECT_ID) {
2243
- console.error(
2244
- "[mushi-mcp] MUSHI_PROJECT_ID is not set.\n\nTools that scope to a project (get_recent_reports, get_report_detail,\nsearch_reports, etc.) will require you to pass projectId explicitly on\nevery call. To set it once and never pass it again:\n\n 1. Open the Mushi admin console \u2192 Projects\n https://kensaur.us/mushi-mushi/projects\n 2. Click your project \u2014 copy the UUID below the project name.\n 3. Add it to your MCP env config:\n MUSHI_PROJECT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\n\nOr visit Admin \u2192 MCP for a pre-filled config snippet with your actual UUID."
3049
+ log.info(
3050
+ "[mushi-mcp] Running in account mode (no MUSHI_PROJECT_ID set). Project-scoped tools accept an explicit projectId argument. Run `get_account_overview` to see accessible projects."
2245
3051
  );
2246
3052
  }
2247
3053
  log.info("Starting Mushi MCP server", {
2248
3054
  version: VERSION,
3055
+ mode,
2249
3056
  endpoint: API_ENDPOINT || "(unset)",
2250
3057
  hasProjectId: !!PROJECT_ID,
2251
3058
  scopes: SCOPES.join(",")
@@ -2255,7 +3062,8 @@ async function main() {
2255
3062
  apiEndpoint: API_ENDPOINT,
2256
3063
  apiKey: API_KEY,
2257
3064
  projectId: PROJECT_ID || void 0,
2258
- scopes: SCOPES
3065
+ scopes: SCOPES,
3066
+ features: FEATURES
2259
3067
  });
2260
3068
  const transport = new StdioServerTransport();
2261
3069
  await server.connect(transport);
@@ -2267,7 +3075,7 @@ async function main() {
2267
3075
  const res = await fetch(`${API_ENDPOINT}/v1/admin/inventory/${PROJECT_ID}`, {
2268
3076
  headers: {
2269
3077
  "X-Mushi-Api-Key": API_KEY,
2270
- "X-Mushi-Project": PROJECT_ID
3078
+ "X-Mushi-Project-Id": PROJECT_ID
2271
3079
  },
2272
3080
  signal: AbortSignal.timeout(1e4)
2273
3081
  });