@mushi-mushi/mcp 0.18.0 → 0.18.2

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/README.md CHANGED
@@ -87,9 +87,9 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
87
87
  Restart Claude Desktop. You should see a hammer icon in the chat input — click it to see the Mushi Mushi tools.
88
88
 
89
89
  > **Where do I find these values?**
90
- > - **`MUSHI_API_KEY`**: [Admin console](https://kensaur.us/mushi-mushi/settings) → **Settings → API Keys**
90
+ > - **`MUSHI_API_KEY`**: [Admin → Connect](https://kensaur.us/mushi-mushi/admin/connect) or **MCP** setup — mint an `mcp:read` / `mcp:write` key. This is **not** the SDK ingest key from **Setup → Verify** (`report:write`) and **not** a BYOK key from **Settings → API Keys**.
91
91
  > - **`MUSHI_PROJECT_ID`**: [Admin console](https://kensaur.us/mushi-mushi/projects) → click your project → copy the UUID below the project name (e.g. `542b34e0-019e-41fe-b900-7b637717bb86`)
92
- > - **`MUSHI_API_ENDPOINT`**: [Admin console](https://kensaur.us/mushi-mushi/settings) → **Settings → API Keys** — shown alongside your key
92
+ > - **`MUSHI_API_ENDPOINT`**: Shown when you mint the MCP key, or use your project's API base (`https://<ref>.supabase.co/functions/v1/api`)
93
93
  >
94
94
  > Or visit **Admin → MCP** in the console for a one-click pre-filled config snippet.
95
95
 
@@ -105,9 +105,9 @@ Then set the same three environment variables:
105
105
 
106
106
  | Variable | Where to find it |
107
107
  |---|---|
108
- | `MUSHI_API_KEY` | Admin console → Settings → API Keys |
108
+ | `MUSHI_API_KEY` | **Connect** or **MCP** tab — mint `mcp:read` / `mcp:write` (not Setup → Verify ingest key; not Settings → BYOK) |
109
109
  | `MUSHI_PROJECT_ID` | Admin console → Projects → click project → UUID below the name |
110
- | `MUSHI_API_ENDPOINT` | Admin console → Settings → API Keys (shown alongside your key) |
110
+ | `MUSHI_API_ENDPOINT` | Shown at key mint, or your Supabase `.../functions/v1/api` base |
111
111
 
112
112
  Without `MUSHI_PROJECT_ID` the server starts but scoped tools return an empty result with a message pointing you here.
113
113
 
package/dist/catalog.js CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  RESOURCE_CATALOG,
6
6
  TDD_TOOL_CATALOG,
7
7
  TOOL_CATALOG
8
- } from "./chunk-KNVB6HA7.js";
8
+ } from "./chunk-DSOPGMDX.js";
9
9
  export {
10
10
  ALL_SCOPES,
11
11
  CODEBASE_TOOL_CATALOG,
@@ -68,7 +68,7 @@ var TOOL_CATALOG = [
68
68
  {
69
69
  name: "get_knowledge_graph",
70
70
  title: "Knowledge graph traversal",
71
- description: "Traverse the knowledge graph from a seed component or page. Returns { nodes: [{ id, label, type }], edges } within a depth budget (default 2, max 4 hops). Read-only. Use to see how a component connects to the rest of the app; use get_blast_radius for a bug's impact area, or get_graph_neighborhood for a tighter BFS around one node.",
71
+ description: "Traverse the knowledge graph from a seed component or page. Returns { nodes: [{ id, label, node_type }], edges: [{ source_node_id, target_node_id, edge_type }] } within a depth budget (default 2, max 4 hops). Read-only. Use to see how a component connects to the rest of the app; use get_blast_radius for a bug's impact area, or get_graph_neighborhood for a tighter BFS around one node.",
72
72
  scope: "mcp:read",
73
73
  hints: { readOnly: true, idempotent: true, openWorld: true },
74
74
  useCase: "Show me how this component connects to the rest of the app."
@@ -101,7 +101,7 @@ var TOOL_CATALOG = [
101
101
  {
102
102
  name: "list_gate_findings",
103
103
  title: "Gate findings",
104
- description: 'List the most recent inventory gate findings for a project, newest run first. Returns rows of { gate, severity, status, message, node_ref }; "latest" = the most recent run per gate by created_at. Filter by gate (dead-handler | mock-leak | crawl | status-claim) or minimum severity (low|medium|high|critical). Read-only. Use to see which CI gates failed on the last crawl; use diff_inventory to compare two commits, or get_inventory for the full snapshot.',
104
+ description: "List recent inventory gate runs and their findings for a project, newest first. Returns { runs: [{ id, gate, status, findings_count, \u2026 }], findings: [{ severity, rule_id, message, file_path, node_id, \u2026 }] }. Filter by gate (dead-handler | mock-leak | crawl | status-claim) or minimum severity (low|medium|high|critical). Read-only. Use to see which CI gates failed on the last crawl; use diff_inventory to compare two commits, or get_inventory for the full snapshot.",
105
105
  scope: "mcp:read",
106
106
  hints: { readOnly: true, idempotent: true, openWorld: true },
107
107
  useCase: "Show me what CI gates failed on the last run."
@@ -109,7 +109,7 @@ var TOOL_CATALOG = [
109
109
  {
110
110
  name: "get_graph_neighborhood",
111
111
  title: "Graph neighborhood",
112
- description: `Return the BFS neighborhood around one graph node by id or label: { nodes: [{ id, label, type }], edges } within a depth budget (default 2, max 4). Read-only. Tuned for "what touches this action?"; use get_knowledge_graph to traverse from a component seed, or get_graph_node for a single node's row.`,
112
+ description: `Return the BFS neighborhood around one graph node by id or label: { nodes: [{ id, label, node_type }], edges: [{ source_node_id, target_node_id, edge_type }] } within a depth budget (default 2, max 4). Read-only. Tuned for "what touches this action?"; use get_knowledge_graph to traverse from a component seed, or get_graph_node for a single node's row.`,
113
113
  scope: "mcp:read",
114
114
  hints: { readOnly: true, idempotent: true, openWorld: true },
115
115
  useCase: "What nodes connect to this inventory Action within 2 hops?"
@@ -117,7 +117,7 @@ var TOOL_CATALOG = [
117
117
  {
118
118
  name: "get_graph_node",
119
119
  title: "Graph node detail",
120
- description: "Fetch one knowledge-graph node row by id: { id, label, type, metadata } including the v2 derived status on Action nodes (ok | stale | broken). Read-only. Use to inspect a single node's status; use get_graph_neighborhood to see what connects to it.",
120
+ description: "Fetch one knowledge-graph node row by id. Returns { node: { id, node_type, label, metadata } } including the v2 derived status on Action nodes (ok | stale | broken). Read-only. Use to inspect a single node's status; use get_graph_neighborhood to see what connects to it.",
121
121
  scope: "mcp:read",
122
122
  hints: { readOnly: true, idempotent: true, openWorld: true },
123
123
  useCase: "What status does the graph store on this node id?"
@@ -168,7 +168,7 @@ var TOOL_CATALOG = [
168
168
  {
169
169
  name: "trigger_judge",
170
170
  title: "Run Sonnet-as-Judge",
171
- description: "Queue the Sonnet-as-Judge to grade the quality of recent fixes across a batch of classified reports. Returns { batchId }; graded scores land asynchronously in the judge_results table (read them back with run_nl_query). Write; consumes LLM budget. Idempotent within a short window \u2014 re-running for the same batch re-grades rather than duplicating. Use before shipping to vet fix quality; use get_fix_timeline to inspect a single attempt instead.",
171
+ description: "Queue the Sonnet-as-Judge to grade recent fix quality across accessible projects. Returns { dispatched: number } \u2014 one judge-batch job per project; scores land asynchronously in judge_results (read back with run_nl_query). Write; consumes LLM budget. Idempotent within a short window. Use before shipping to vet fix quality; use get_fix_timeline to inspect a single attempt instead.",
172
172
  scope: "mcp:write",
173
173
  hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
174
174
  useCase: "Grade the latest batch of fixes before I ship."
@@ -192,7 +192,7 @@ var TOOL_CATALOG = [
192
192
  {
193
193
  name: "merge_fix",
194
194
  title: "Merge fix PR",
195
- description: "Squash-merge the GitHub PR for a fix attempt, mark the linked report fixed, and notify the reporter. Re-readies the PR first if it is still a draft. Returns { merged, reportStatus }. Write; destructive (mutates the target repo's default branch); idempotent \u2014 re-running an already-merged attempt is a safe no-op. Prerequisite: CI green (check with refresh_ci). Use to ship a fix opened by dispatch_fix; use transition_status to change state without merging.",
195
+ description: "Squash-merge the GitHub PR for a fix attempt, mark the linked report fixed, and notify the reporter. Re-readies the PR first if it is still a draft. Returns { merged, reportStatus }. Write; destructive and irreversible from Mushi's side \u2014 once GitHub merges into the target repo's default branch there is no unmerge endpoint, only a manual revert PR outside this tool. Idempotent \u2014 re-running an already-merged attempt is a safe no-op. Prerequisite: CI green (check with refresh_ci); confirm the diff and CI status with the user before calling on a PR you have not reviewed. Use to ship a fix opened by dispatch_fix; use transition_status to change state without merging.",
196
196
  scope: "mcp:write",
197
197
  hints: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
198
198
  useCase: "Merge the draft PR and notify the reporter."
@@ -200,9 +200,9 @@ var TOOL_CATALOG = [
200
200
  {
201
201
  name: "refresh_ci",
202
202
  title: "Refresh fix CI status",
203
- description: "Re-poll GitHub for the latest check-run status of a fix attempt's PR and persist it. Returns { ciStatus: pending|success|failure|neutral, checks: [{ name, conclusion }] }. Read-only (mutates nothing on GitHub). Use right before merge_fix to confirm CI is green; use get_fix_timeline for the full attempt lifecycle.",
204
- scope: "mcp:read",
205
- hints: { readOnly: true, idempotent: true, openWorld: true },
203
+ description: "Re-poll GitHub for the latest check-run status of a fix attempt's PR and persist it on the fix_attempt row (does not merge or mutate GitHub). Returns { check_run_status, check_run_conclusion, check_run_updated_at }. Write; idempotent. Use right before merge_fix to confirm CI is green; use get_fix_timeline for the full attempt lifecycle.",
204
+ scope: "mcp:write",
205
+ hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
206
206
  useCase: "Check whether CI is green before merging."
207
207
  },
208
208
  {
@@ -225,17 +225,17 @@ var TOOL_CATALOG = [
225
225
  {
226
226
  name: "award_bonus_points",
227
227
  title: "Award bonus points",
228
- description: "Award ad-hoc bonus points to a contributor by their external user id (as passed to Mushi.identify()). Points are applied server-side, audit-logged, and trigger tier re-evaluation. Requires mcp:write scope. Use this to thank a contributor who found a critical bug or to run a one-off promotional campaign.",
228
+ description: "Award ad-hoc bonus points (1-50000, positive only \u2014 there is no way to subtract via this tool) to a contributor by their external user id (as passed to Mushi.identify()). Points post immediately to their total and are audit-logged as bonus_manual. Destructive and effectively irreversible: there is no reversal endpoint, so confirm the amount and recipient with the user before calling. Tier re-evaluation (and any host-side reward_webhooks grant tied to crossing a threshold) is NOT immediate \u2014 it only runs on the contributor's next tracked activity, not on this call. Requires mcp:write scope. Use this to thank a contributor who found a critical bug or to run a one-off promotional campaign.",
229
229
  scope: "mcp:write",
230
- hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
230
+ hints: { readOnly: false, destructive: true, idempotent: false, openWorld: true },
231
231
  useCase: "Give this contributor 500 bonus points for the critical bug they found."
232
232
  },
233
233
  {
234
234
  name: "set_tier",
235
235
  title: "Override contributor tier",
236
- description: `Override a contributor's tier by tier slug (e.g. "champion"). This is an admin escape hatch for manual promotions \u2014 normal tier transitions happen automatically via point thresholds. The override is logged in end_user_activity with action=tier_override_manual. Requires mcp:write scope.`,
236
+ description: `Override a contributor's tier by tier slug (e.g. "champion"), bypassing the normal points-threshold logic entirely. This is an admin escape hatch for manual promotions \u2014 normal tier transitions happen automatically via point thresholds instead. Destructive: it is a label-only override that does NOT replay the automatic tier-evaluation path, so any host_credit_payload grant (Stripe perk, badge, etc.) normally tied to reaching that tier via points will NOT fire \u2014 confirm with the user whether they also need the perk granted another way. The override persists until another set_tier call changes it; the prior override stays in the end_user_activity audit trail either way, logged as tier_override_manual. Requires mcp:write scope.`,
237
237
  scope: "mcp:write",
238
- hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
238
+ hints: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
239
239
  useCase: "Manually promote this user to Champion tier as a thank-you."
240
240
  },
241
241
  {
@@ -307,7 +307,7 @@ var TOOL_CATALOG = [
307
307
  {
308
308
  name: "list_lessons",
309
309
  title: "List project lessons",
310
- description: 'List the promoted learning rules ("lessons") for the current project, newest first. Each lesson is a named pattern of mistakes encoded from past bug reports. Returns { lessons: [{ id, title, rule, severity, source_report_ids }] }. Read-only. Use to see the full catalog of encoded heuristics; use query_lessons to retrieve only the lessons relevant to a specific diff or PR within a token budget.',
310
+ description: 'List promoted learning rules ("lessons") for the current project, highest-frequency first. Returns { lessons: [{ id, rule_text, severity, frequency, anti_pattern, \u2026 }] }. Read-only. Use to browse the full catalog of encoded heuristics; use query_lessons to retrieve only lessons relevant to a specific diff or PR within a token budget.',
311
311
  scope: "mcp:read",
312
312
  hints: { readOnly: true, idempotent: true, openWorld: true },
313
313
  useCase: "What systemic patterns has Mushi identified for this project?"
@@ -614,7 +614,7 @@ var CODEBASE_TOOL_CATALOG = [
614
614
  {
615
615
  name: "search_codebase",
616
616
  title: "Semantic codebase search",
617
- description: 'Search the indexed repo by plain-English meaning via embeddings. Returns top-k { results: [{ path, startLine, endLine, snippet, similarity }] } ordered by similarity. Requires codebase indexing enabled. Read-only. Use to locate where something lives ("where do we verify webhooks?"); use ask_codebase for a synthesized answer with citations, or analyze_codebase_impact to find dependents of a file.',
617
+ description: 'Search the indexed repo by plain-English meaning via embeddings. Returns { results: [{ file_path, line_start, line_end, content_preview, similarity, \u2026 }], query, mode } ordered by similarity. Requires codebase indexing enabled. Read-only. Use to locate where something lives ("where do we verify webhooks?"); use ask_codebase for a synthesized answer with citations, or analyze_codebase_impact to find dependents of a file.',
618
618
  scope: "mcp:read",
619
619
  hints: { readOnly: true, idempotent: true, openWorld: true },
620
620
  useCase: "Find files related to authentication or payment handling."
@@ -630,7 +630,7 @@ var CODEBASE_TOOL_CATALOG = [
630
630
  {
631
631
  name: "analyze_codebase_impact",
632
632
  title: "Diff impact analysis",
633
- description: "Find the files that depend on a set of changed paths by walking the reverse import graph. Source the paths from: manual list, the last push, a GitHub compare range, or a fix PR's files. Returns { impacted: [{ path, importedBy }], depth }. Requires codebase indexing. Read-only. Use to gauge a diff's blast radius before merging; use get_blast_radius for a bug's component impact, or search_codebase to locate files.",
633
+ description: "Find files that depend on a set of changed paths by walking the reverse import graph. Source paths from: manual list, last push, a GitHub compare range, or a fix PR's files. Returns { changed_paths, source, affected_file_paths, affected_node_ids, meta }. Requires codebase indexing. Read-only. Use to gauge a diff's blast radius before merging; use get_blast_radius for a bug's component impact, or search_codebase to locate files.",
634
634
  scope: "mcp:read",
635
635
  hints: { readOnly: true, idempotent: true, openWorld: true },
636
636
  useCase: "What breaks if I change lib/auth.ts?"
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ var TOOL_CATALOG = [
75
75
  {
76
76
  name: "get_knowledge_graph",
77
77
  title: "Knowledge graph traversal",
78
- description: "Traverse the knowledge graph from a seed component or page. Returns { nodes: [{ id, label, type }], edges } within a depth budget (default 2, max 4 hops). Read-only. Use to see how a component connects to the rest of the app; use get_blast_radius for a bug's impact area, or get_graph_neighborhood for a tighter BFS around one node.",
78
+ description: "Traverse the knowledge graph from a seed component or page. Returns { nodes: [{ id, label, node_type }], edges: [{ source_node_id, target_node_id, edge_type }] } within a depth budget (default 2, max 4 hops). Read-only. Use to see how a component connects to the rest of the app; use get_blast_radius for a bug's impact area, or get_graph_neighborhood for a tighter BFS around one node.",
79
79
  scope: "mcp:read",
80
80
  hints: { readOnly: true, idempotent: true, openWorld: true },
81
81
  useCase: "Show me how this component connects to the rest of the app."
@@ -108,7 +108,7 @@ var TOOL_CATALOG = [
108
108
  {
109
109
  name: "list_gate_findings",
110
110
  title: "Gate findings",
111
- description: 'List the most recent inventory gate findings for a project, newest run first. Returns rows of { gate, severity, status, message, node_ref }; "latest" = the most recent run per gate by created_at. Filter by gate (dead-handler | mock-leak | crawl | status-claim) or minimum severity (low|medium|high|critical). Read-only. Use to see which CI gates failed on the last crawl; use diff_inventory to compare two commits, or get_inventory for the full snapshot.',
111
+ description: "List recent inventory gate runs and their findings for a project, newest first. Returns { runs: [{ id, gate, status, findings_count, \u2026 }], findings: [{ severity, rule_id, message, file_path, node_id, \u2026 }] }. Filter by gate (dead-handler | mock-leak | crawl | status-claim) or minimum severity (low|medium|high|critical). Read-only. Use to see which CI gates failed on the last crawl; use diff_inventory to compare two commits, or get_inventory for the full snapshot.",
112
112
  scope: "mcp:read",
113
113
  hints: { readOnly: true, idempotent: true, openWorld: true },
114
114
  useCase: "Show me what CI gates failed on the last run."
@@ -116,7 +116,7 @@ var TOOL_CATALOG = [
116
116
  {
117
117
  name: "get_graph_neighborhood",
118
118
  title: "Graph neighborhood",
119
- description: `Return the BFS neighborhood around one graph node by id or label: { nodes: [{ id, label, type }], edges } within a depth budget (default 2, max 4). Read-only. Tuned for "what touches this action?"; use get_knowledge_graph to traverse from a component seed, or get_graph_node for a single node's row.`,
119
+ description: `Return the BFS neighborhood around one graph node by id or label: { nodes: [{ id, label, node_type }], edges: [{ source_node_id, target_node_id, edge_type }] } within a depth budget (default 2, max 4). Read-only. Tuned for "what touches this action?"; use get_knowledge_graph to traverse from a component seed, or get_graph_node for a single node's row.`,
120
120
  scope: "mcp:read",
121
121
  hints: { readOnly: true, idempotent: true, openWorld: true },
122
122
  useCase: "What nodes connect to this inventory Action within 2 hops?"
@@ -124,7 +124,7 @@ var TOOL_CATALOG = [
124
124
  {
125
125
  name: "get_graph_node",
126
126
  title: "Graph node detail",
127
- description: "Fetch one knowledge-graph node row by id: { id, label, type, metadata } including the v2 derived status on Action nodes (ok | stale | broken). Read-only. Use to inspect a single node's status; use get_graph_neighborhood to see what connects to it.",
127
+ description: "Fetch one knowledge-graph node row by id. Returns { node: { id, node_type, label, metadata } } including the v2 derived status on Action nodes (ok | stale | broken). Read-only. Use to inspect a single node's status; use get_graph_neighborhood to see what connects to it.",
128
128
  scope: "mcp:read",
129
129
  hints: { readOnly: true, idempotent: true, openWorld: true },
130
130
  useCase: "What status does the graph store on this node id?"
@@ -175,7 +175,7 @@ var TOOL_CATALOG = [
175
175
  {
176
176
  name: "trigger_judge",
177
177
  title: "Run Sonnet-as-Judge",
178
- description: "Queue the Sonnet-as-Judge to grade the quality of recent fixes across a batch of classified reports. Returns { batchId }; graded scores land asynchronously in the judge_results table (read them back with run_nl_query). Write; consumes LLM budget. Idempotent within a short window \u2014 re-running for the same batch re-grades rather than duplicating. Use before shipping to vet fix quality; use get_fix_timeline to inspect a single attempt instead.",
178
+ description: "Queue the Sonnet-as-Judge to grade recent fix quality across accessible projects. Returns { dispatched: number } \u2014 one judge-batch job per project; scores land asynchronously in judge_results (read back with run_nl_query). Write; consumes LLM budget. Idempotent within a short window. Use before shipping to vet fix quality; use get_fix_timeline to inspect a single attempt instead.",
179
179
  scope: "mcp:write",
180
180
  hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
181
181
  useCase: "Grade the latest batch of fixes before I ship."
@@ -199,7 +199,7 @@ var TOOL_CATALOG = [
199
199
  {
200
200
  name: "merge_fix",
201
201
  title: "Merge fix PR",
202
- description: "Squash-merge the GitHub PR for a fix attempt, mark the linked report fixed, and notify the reporter. Re-readies the PR first if it is still a draft. Returns { merged, reportStatus }. Write; destructive (mutates the target repo's default branch); idempotent \u2014 re-running an already-merged attempt is a safe no-op. Prerequisite: CI green (check with refresh_ci). Use to ship a fix opened by dispatch_fix; use transition_status to change state without merging.",
202
+ description: "Squash-merge the GitHub PR for a fix attempt, mark the linked report fixed, and notify the reporter. Re-readies the PR first if it is still a draft. Returns { merged, reportStatus }. Write; destructive and irreversible from Mushi's side \u2014 once GitHub merges into the target repo's default branch there is no unmerge endpoint, only a manual revert PR outside this tool. Idempotent \u2014 re-running an already-merged attempt is a safe no-op. Prerequisite: CI green (check with refresh_ci); confirm the diff and CI status with the user before calling on a PR you have not reviewed. Use to ship a fix opened by dispatch_fix; use transition_status to change state without merging.",
203
203
  scope: "mcp:write",
204
204
  hints: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
205
205
  useCase: "Merge the draft PR and notify the reporter."
@@ -207,9 +207,9 @@ var TOOL_CATALOG = [
207
207
  {
208
208
  name: "refresh_ci",
209
209
  title: "Refresh fix CI status",
210
- description: "Re-poll GitHub for the latest check-run status of a fix attempt's PR and persist it. Returns { ciStatus: pending|success|failure|neutral, checks: [{ name, conclusion }] }. Read-only (mutates nothing on GitHub). Use right before merge_fix to confirm CI is green; use get_fix_timeline for the full attempt lifecycle.",
211
- scope: "mcp:read",
212
- hints: { readOnly: true, idempotent: true, openWorld: true },
210
+ description: "Re-poll GitHub for the latest check-run status of a fix attempt's PR and persist it on the fix_attempt row (does not merge or mutate GitHub). Returns { check_run_status, check_run_conclusion, check_run_updated_at }. Write; idempotent. Use right before merge_fix to confirm CI is green; use get_fix_timeline for the full attempt lifecycle.",
211
+ scope: "mcp:write",
212
+ hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
213
213
  useCase: "Check whether CI is green before merging."
214
214
  },
215
215
  {
@@ -232,17 +232,17 @@ var TOOL_CATALOG = [
232
232
  {
233
233
  name: "award_bonus_points",
234
234
  title: "Award bonus points",
235
- description: "Award ad-hoc bonus points to a contributor by their external user id (as passed to Mushi.identify()). Points are applied server-side, audit-logged, and trigger tier re-evaluation. Requires mcp:write scope. Use this to thank a contributor who found a critical bug or to run a one-off promotional campaign.",
235
+ description: "Award ad-hoc bonus points (1-50000, positive only \u2014 there is no way to subtract via this tool) to a contributor by their external user id (as passed to Mushi.identify()). Points post immediately to their total and are audit-logged as bonus_manual. Destructive and effectively irreversible: there is no reversal endpoint, so confirm the amount and recipient with the user before calling. Tier re-evaluation (and any host-side reward_webhooks grant tied to crossing a threshold) is NOT immediate \u2014 it only runs on the contributor's next tracked activity, not on this call. Requires mcp:write scope. Use this to thank a contributor who found a critical bug or to run a one-off promotional campaign.",
236
236
  scope: "mcp:write",
237
- hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
237
+ hints: { readOnly: false, destructive: true, idempotent: false, openWorld: true },
238
238
  useCase: "Give this contributor 500 bonus points for the critical bug they found."
239
239
  },
240
240
  {
241
241
  name: "set_tier",
242
242
  title: "Override contributor tier",
243
- description: `Override a contributor's tier by tier slug (e.g. "champion"). This is an admin escape hatch for manual promotions \u2014 normal tier transitions happen automatically via point thresholds. The override is logged in end_user_activity with action=tier_override_manual. Requires mcp:write scope.`,
243
+ description: `Override a contributor's tier by tier slug (e.g. "champion"), bypassing the normal points-threshold logic entirely. This is an admin escape hatch for manual promotions \u2014 normal tier transitions happen automatically via point thresholds instead. Destructive: it is a label-only override that does NOT replay the automatic tier-evaluation path, so any host_credit_payload grant (Stripe perk, badge, etc.) normally tied to reaching that tier via points will NOT fire \u2014 confirm with the user whether they also need the perk granted another way. The override persists until another set_tier call changes it; the prior override stays in the end_user_activity audit trail either way, logged as tier_override_manual. Requires mcp:write scope.`,
244
244
  scope: "mcp:write",
245
- hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
245
+ hints: { readOnly: false, destructive: true, idempotent: true, openWorld: true },
246
246
  useCase: "Manually promote this user to Champion tier as a thank-you."
247
247
  },
248
248
  {
@@ -314,7 +314,7 @@ var TOOL_CATALOG = [
314
314
  {
315
315
  name: "list_lessons",
316
316
  title: "List project lessons",
317
- description: 'List the promoted learning rules ("lessons") for the current project, newest first. Each lesson is a named pattern of mistakes encoded from past bug reports. Returns { lessons: [{ id, title, rule, severity, source_report_ids }] }. Read-only. Use to see the full catalog of encoded heuristics; use query_lessons to retrieve only the lessons relevant to a specific diff or PR within a token budget.',
317
+ description: 'List promoted learning rules ("lessons") for the current project, highest-frequency first. Returns { lessons: [{ id, rule_text, severity, frequency, anti_pattern, \u2026 }] }. Read-only. Use to browse the full catalog of encoded heuristics; use query_lessons to retrieve only lessons relevant to a specific diff or PR within a token budget.',
318
318
  scope: "mcp:read",
319
319
  hints: { readOnly: true, idempotent: true, openWorld: true },
320
320
  useCase: "What systemic patterns has Mushi identified for this project?"
@@ -537,7 +537,7 @@ var CODEBASE_TOOL_CATALOG = [
537
537
  {
538
538
  name: "search_codebase",
539
539
  title: "Semantic codebase search",
540
- description: 'Search the indexed repo by plain-English meaning via embeddings. Returns top-k { results: [{ path, startLine, endLine, snippet, similarity }] } ordered by similarity. Requires codebase indexing enabled. Read-only. Use to locate where something lives ("where do we verify webhooks?"); use ask_codebase for a synthesized answer with citations, or analyze_codebase_impact to find dependents of a file.',
540
+ description: 'Search the indexed repo by plain-English meaning via embeddings. Returns { results: [{ file_path, line_start, line_end, content_preview, similarity, \u2026 }], query, mode } ordered by similarity. Requires codebase indexing enabled. Read-only. Use to locate where something lives ("where do we verify webhooks?"); use ask_codebase for a synthesized answer with citations, or analyze_codebase_impact to find dependents of a file.',
541
541
  scope: "mcp:read",
542
542
  hints: { readOnly: true, idempotent: true, openWorld: true },
543
543
  useCase: "Find files related to authentication or payment handling."
@@ -553,7 +553,7 @@ var CODEBASE_TOOL_CATALOG = [
553
553
  {
554
554
  name: "analyze_codebase_impact",
555
555
  title: "Diff impact analysis",
556
- description: "Find the files that depend on a set of changed paths by walking the reverse import graph. Source the paths from: manual list, the last push, a GitHub compare range, or a fix PR's files. Returns { impacted: [{ path, importedBy }], depth }. Requires codebase indexing. Read-only. Use to gauge a diff's blast radius before merging; use get_blast_radius for a bug's component impact, or search_codebase to locate files.",
556
+ description: "Find files that depend on a set of changed paths by walking the reverse import graph. Source paths from: manual list, last push, a GitHub compare range, or a fix PR's files. Returns { changed_paths, source, affected_file_paths, affected_node_ids, meta }. Requires codebase indexing. Read-only. Use to gauge a diff's blast radius before merging; use get_blast_radius for a bug's component impact, or search_codebase to locate files.",
557
557
  scope: "mcp:read",
558
558
  hints: { readOnly: true, idempotent: true, openWorld: true },
559
559
  useCase: "What breaks if I change lib/auth.ts?"
@@ -1222,8 +1222,12 @@ function createMushiServer(config) {
1222
1222
  description: descOf("get_knowledge_graph"),
1223
1223
  annotations: annotationsFor("get_knowledge_graph"),
1224
1224
  inputSchema: {
1225
- seed: z.string().describe("Starting node id or label"),
1226
- depth: z.number().optional().describe("Traversal depth (default 2, max 4)")
1225
+ seed: z.string().describe("Starting graph node id or human-readable label to traverse from"),
1226
+ depth: z.number().optional().describe("BFS hops outward from seed \u2014 clamped to 4 in the handler (default 2)")
1227
+ },
1228
+ outputSchema: {
1229
+ nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
1230
+ edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
1227
1231
  }
1228
1232
  },
1229
1233
  async (args) => {
@@ -1231,7 +1235,8 @@ function createMushiServer(config) {
1231
1235
  seed: args.seed,
1232
1236
  depth: String(Math.min(args.depth ?? 2, 4))
1233
1237
  });
1234
- return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
1238
+ const data = await apiCall(`/v1/admin/graph/traverse?${params}`);
1239
+ return jsonResult(data);
1235
1240
  }
1236
1241
  );
1237
1242
  server.registerTool(
@@ -1243,6 +1248,10 @@ function createMushiServer(config) {
1243
1248
  inputSchema: {
1244
1249
  seed: z.string().describe("Starting graph node id or label to expand around (e.g. an inventory Action id or component name)"),
1245
1250
  depth: z.number().int().min(1).max(4).optional().describe("BFS hops to traverse outward. Default 2; clamped to a max of 4.")
1251
+ },
1252
+ outputSchema: {
1253
+ nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
1254
+ edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
1246
1255
  }
1247
1256
  },
1248
1257
  async (args) => {
@@ -1250,7 +1259,8 @@ function createMushiServer(config) {
1250
1259
  seed: args.seed,
1251
1260
  depth: String(Math.min(args.depth ?? 2, 4))
1252
1261
  });
1253
- return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
1262
+ const data = await apiCall(`/v1/admin/graph/traverse?${params}`);
1263
+ return jsonResult(data);
1254
1264
  }
1255
1265
  );
1256
1266
  server.registerTool(
@@ -1259,9 +1269,15 @@ function createMushiServer(config) {
1259
1269
  title: titleOf("get_graph_node"),
1260
1270
  description: descOf("get_graph_node"),
1261
1271
  annotations: annotationsFor("get_graph_node"),
1262
- inputSchema: { nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).") }
1272
+ inputSchema: { nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).") },
1273
+ outputSchema: {
1274
+ node: z.unknown().describe("Single graph_nodes row including metadata (Action nodes carry v2 status)")
1275
+ }
1263
1276
  },
1264
- async (args) => jsonText(await apiCall(`/v1/admin/graph/node/${args.nodeId}`))
1277
+ async (args) => {
1278
+ const data = await apiCall(`/v1/admin/graph/node/${args.nodeId}`);
1279
+ return jsonResult(data);
1280
+ }
1265
1281
  );
1266
1282
  server.registerTool(
1267
1283
  "get_inventory",
@@ -1308,6 +1324,10 @@ function createMushiServer(config) {
1308
1324
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
1309
1325
  gate: z.string().optional().describe("Filter to one gate id: dead-handler | mock-leak | crawl | status-claim"),
1310
1326
  severity: z.string().optional().describe("Minimum severity to include: low | medium | high | critical")
1327
+ },
1328
+ outputSchema: {
1329
+ runs: z.array(z.unknown()).describe("Recent gate_runs rows, newest first"),
1330
+ findings: z.array(z.unknown()).describe("gate_findings rows for those runs")
1311
1331
  }
1312
1332
  },
1313
1333
  async (args) => {
@@ -1317,7 +1337,8 @@ function createMushiServer(config) {
1317
1337
  if (args.gate) q.set("gate", args.gate);
1318
1338
  if (args.severity) q.set("severity", args.severity);
1319
1339
  const suffix = q.toString() ? `?${q}` : "";
1320
- return jsonText(await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`));
1340
+ const data = await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`);
1341
+ return jsonResult(data);
1321
1342
  }
1322
1343
  );
1323
1344
  server.registerTool(
@@ -1806,13 +1827,17 @@ function createMushiServer(config) {
1806
1827
  description: descOf("submit_fix_result"),
1807
1828
  annotations: annotationsFor("submit_fix_result"),
1808
1829
  inputSchema: {
1809
- reportId: z.string().describe("The report UUID"),
1810
- branch: z.string().describe("Git branch name"),
1811
- prUrl: z.string().optional().describe("GitHub PR URL"),
1812
- filesChanged: z.array(z.string()).describe("Files modified"),
1813
- linesChanged: z.number().describe("Total lines changed"),
1814
- summary: z.string().describe("Fix summary"),
1830
+ reportId: z.string().describe("Report UUID to attach this external fix attempt to"),
1831
+ branch: z.string().describe("Git branch name where the fix was implemented"),
1832
+ prUrl: z.string().optional().describe("GitHub pull request URL, if a PR was opened"),
1833
+ filesChanged: z.array(z.string()).describe("Repo-relative paths modified by the fix"),
1834
+ linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
1835
+ summary: z.string().describe("One-line human summary of what the fix changed"),
1815
1836
  idempotencyKey: z.string().uuid().optional().describe("Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows")
1837
+ },
1838
+ outputSchema: {
1839
+ ok: z.boolean().describe("True when the fix_attempt row was created and marked completed"),
1840
+ fixId: z.string().describe("UUID of the new fix_attempt row")
1816
1841
  }
1817
1842
  },
1818
1843
  async (args) => {
@@ -1834,7 +1859,7 @@ function createMushiServer(config) {
1834
1859
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
1835
1860
  })
1836
1861
  });
1837
- return jsonText({ ok: true, fixId: created.fixId });
1862
+ return jsonResult({ ok: true, fixId: created.fixId });
1838
1863
  }
1839
1864
  );
1840
1865
  server.registerTool(
@@ -1891,6 +1916,9 @@ function createMushiServer(config) {
1891
1916
  inputSchema: {
1892
1917
  limit: z.number().optional().describe("Max reports to judge in this batch (default 25, max 100)"),
1893
1918
  projectId: z.string().optional().describe("Restrict to one project when the API key owns multiple")
1919
+ },
1920
+ outputSchema: {
1921
+ dispatched: z.number().describe("Number of judge-batch jobs dispatched (one per accessible project)")
1894
1922
  }
1895
1923
  },
1896
1924
  async (args) => {
@@ -1901,7 +1929,7 @@ function createMushiServer(config) {
1901
1929
  projectId: args.projectId ?? projectId ?? void 0
1902
1930
  })
1903
1931
  });
1904
- return jsonText(data);
1932
+ return jsonResult(data);
1905
1933
  }
1906
1934
  );
1907
1935
  server.registerTool(
@@ -1932,8 +1960,14 @@ function createMushiServer(config) {
1932
1960
  description: descOf("merge_fix"),
1933
1961
  annotations: annotationsFor("merge_fix"),
1934
1962
  inputSchema: {
1935
- fixId: z.string().describe("Fix attempt UUID"),
1936
- mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method")
1963
+ fixId: z.string().describe("Fix attempt UUID whose GitHub PR should be squash-merged"),
1964
+ mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method (default squash)")
1965
+ },
1966
+ outputSchema: {
1967
+ merged: z.boolean().optional().describe("True when GitHub accepted the merge in this call"),
1968
+ alreadyMerged: z.boolean().optional().describe("True when the PR was already merged (idempotent no-op)"),
1969
+ reportId: z.string().describe("Report UUID linked to this fix attempt"),
1970
+ reportStatus: z.string().describe("Report workflow status after merge bookkeeping")
1937
1971
  }
1938
1972
  },
1939
1973
  async (args) => {
@@ -1941,7 +1975,7 @@ function createMushiServer(config) {
1941
1975
  method: "POST",
1942
1976
  body: JSON.stringify({ mergeMethod: args.mergeMethod ?? "squash" })
1943
1977
  });
1944
- return jsonText(data);
1978
+ return jsonResult(data);
1945
1979
  }
1946
1980
  );
1947
1981
  server.registerTool(
@@ -1951,12 +1985,17 @@ function createMushiServer(config) {
1951
1985
  description: descOf("refresh_ci"),
1952
1986
  annotations: annotationsFor("refresh_ci"),
1953
1987
  inputSchema: {
1954
- fixId: z.string().describe("Fix attempt UUID")
1988
+ fixId: z.string().describe("Fix attempt UUID whose PR check-runs should be re-polled")
1989
+ },
1990
+ outputSchema: {
1991
+ check_run_status: z.string().nullable().describe("GitHub check run status after refresh"),
1992
+ check_run_conclusion: z.string().nullable().describe("success | failure | neutral | null while pending"),
1993
+ check_run_updated_at: z.string().nullable().describe("ISO timestamp when CI status was last persisted")
1955
1994
  }
1956
1995
  },
1957
1996
  async (args) => {
1958
1997
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/refresh-ci`, { method: "POST" });
1959
- return jsonText(data);
1998
+ return jsonResult(data);
1960
1999
  }
1961
2000
  );
1962
2001
  server.registerTool(
@@ -1966,16 +2005,19 @@ function createMushiServer(config) {
1966
2005
  description: descOf("reopen_report"),
1967
2006
  annotations: annotationsFor("reopen_report"),
1968
2007
  inputSchema: {
1969
- reportId: z.string().describe("Report UUID"),
1970
- note: z.string().optional().describe("Triage note")
2008
+ reportId: z.string().describe("Report UUID to move back to reopened status"),
2009
+ note: z.string().optional().describe("Operator note recorded on the reopen transition")
2010
+ },
2011
+ outputSchema: {
2012
+ report: z.unknown().describe("Updated report row with status=reopened")
1971
2013
  }
1972
2014
  },
1973
2015
  async (args) => {
1974
- const data = await apiCall(`/v1/sync/reports/${args.reportId}`, {
2016
+ const report = await apiCall(`/v1/sync/reports/${args.reportId}`, {
1975
2017
  method: "PATCH",
1976
2018
  body: JSON.stringify({ status: "reopened", note: args.note })
1977
2019
  });
1978
- return jsonText(data);
2020
+ return jsonResult({ report });
1979
2021
  }
1980
2022
  );
1981
2023
  server.registerTool(
@@ -2052,9 +2094,12 @@ function createMushiServer(config) {
2052
2094
  description: descOf("list_lessons"),
2053
2095
  annotations: annotationsFor("list_lessons"),
2054
2096
  inputSchema: {
2055
- severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter by severity level."),
2056
- limit: z.number().optional().describe("Max number of lessons to return (default 50, max 200)."),
2057
- project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
2097
+ severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter to one severity level"),
2098
+ limit: z.number().optional().describe("Max lessons to return (default 50, max 200)"),
2099
+ project_id: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
2100
+ },
2101
+ outputSchema: {
2102
+ lessons: z.array(z.unknown()).describe("Promoted lesson rows ordered by frequency")
2058
2103
  }
2059
2104
  },
2060
2105
  async (args) => {
@@ -2063,8 +2108,8 @@ function createMushiServer(config) {
2063
2108
  if (args.severity) params.set("severity", args.severity);
2064
2109
  params.set("limit", String(Math.min(args.limit ?? 50, 200)));
2065
2110
  if (pid) params.set("projectId", pid);
2066
- const data = await apiCall(`/v1/admin/lessons?${params}`);
2067
- return jsonText(data);
2111
+ const lessons = await apiCall(`/v1/admin/lessons?${params}`);
2112
+ return jsonResult({ lessons });
2068
2113
  }
2069
2114
  );
2070
2115
  server.resource(
@@ -2402,11 +2447,17 @@ Be specific. No generic advice.`
2402
2447
  description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
2403
2448
  annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
2404
2449
  inputSchema: {
2405
- projectId: z.string().describe("Project id"),
2406
- storyNodeId: z.string().describe("User story id slug from the accepted inventory"),
2407
- automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Gate mode for the generated test (default: review)"),
2408
- baseUrl: z.string().url().optional().describe("Override the app base URL"),
2409
- openPr: z.boolean().optional().describe("Open a draft GitHub PR (default: true)")
2450
+ projectId: z.string().describe("Project UUID that owns the inventory story"),
2451
+ storyNodeId: z.string().describe("User story node id/slug from accepted inventory (from map_user_stories or inventory.yaml)"),
2452
+ automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Approval gate for the generated qa_story (default review \u2192 pending_review)"),
2453
+ baseUrl: z.string().url().optional().describe("Override the live app URL the Playwright test should target"),
2454
+ openPr: z.boolean().optional().describe("Open a draft GitHub PR with the generated spec (default true)")
2455
+ },
2456
+ outputSchema: {
2457
+ qaStoryId: z.string().describe("UUID of the inserted qa_stories row"),
2458
+ prUrl: z.string().nullable().describe("GitHub PR URL when openPr=true and GitHub is connected"),
2459
+ approvalStatus: z.string().describe("pending_review | approved \u2014 follows automationMode and project defaults"),
2460
+ needsHumanReview: z.boolean().describe("True when the story requires operator approval before scheduled runs")
2410
2461
  }
2411
2462
  },
2412
2463
  async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
@@ -2415,7 +2466,7 @@ Be specific. No generic advice.`
2415
2466
  `/v1/admin/inventory/${projectId2}/stories/${storyNodeId}/generate-test`,
2416
2467
  { method: "POST", body: JSON.stringify({ automation_mode: automationMode, base_url: baseUrl, open_pr: openPr }) }
2417
2468
  );
2418
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
2469
+ return jsonResult(data);
2419
2470
  }
2420
2471
  );
2421
2472
  server.registerTool(
@@ -2861,23 +2912,31 @@ Be specific. No generic advice.`
2861
2912
  annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2862
2913
  inputSchema: {
2863
2914
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2864
- query: z.string().describe("Plain-English search query"),
2865
- k: z.number().optional().describe("Number of results (default 8, max 20)"),
2866
- scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2915
+ query: z.string().describe("Plain-English search query against indexed source files"),
2916
+ k: z.number().int().min(1).max(20).optional().describe("Max hits to return (default 8, max 20)"),
2917
+ scope_prefix: z.string().optional().describe("Optional repo subdirectory prefix to limit search scope")
2918
+ },
2919
+ outputSchema: {
2920
+ results: z.array(z.unknown()).describe("Ranked file/symbol hits with paths, line ranges, and similarity"),
2921
+ query: z.string().describe("Normalized query string that was searched"),
2922
+ mode: z.string().optional().describe("semantic (embeddings) or name (path/symbol match)")
2867
2923
  }
2868
2924
  },
2869
2925
  async (args) => {
2870
2926
  const pid = args.project_id ?? projectId;
2871
2927
  if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2872
- const data = await apiCall(`/v1/admin/projects/${pid}/codebase/search`, {
2873
- method: "POST",
2874
- body: JSON.stringify({
2875
- query: args.query,
2876
- k: args.k ?? 8,
2877
- scope_prefix: args.scope_prefix ?? void 0
2878
- })
2879
- });
2880
- return jsonText(data);
2928
+ const data = await apiCall(
2929
+ `/v1/admin/projects/${pid}/codebase/search`,
2930
+ {
2931
+ method: "POST",
2932
+ body: JSON.stringify({
2933
+ query: args.query,
2934
+ k: args.k ?? 8,
2935
+ scope_prefix: args.scope_prefix ?? void 0
2936
+ })
2937
+ }
2938
+ );
2939
+ return jsonResult(data);
2881
2940
  }
2882
2941
  );
2883
2942
  server.registerTool(
@@ -2911,10 +2970,17 @@ Be specific. No generic advice.`
2911
2970
  annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2912
2971
  inputSchema: {
2913
2972
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2914
- paths: z.array(z.string()).optional().describe("Changed file paths"),
2915
- source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths"),
2916
- compare: z.string().optional().describe("GitHub compare range base...head"),
2917
- fix_id: z.string().optional().describe("Fix attempt UUID for PR changed files")
2973
+ paths: z.array(z.string()).optional().describe("Changed repo-relative file paths to analyze"),
2974
+ source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths instead of manual paths"),
2975
+ compare: z.string().optional().describe("GitHub compare range base...head when source=compare"),
2976
+ fix_id: z.string().optional().describe("Fix attempt UUID \u2014 uses that PR's changed files when source=fix")
2977
+ },
2978
+ outputSchema: {
2979
+ changed_paths: z.array(z.string()).describe("Resolved set of changed paths analyzed"),
2980
+ source: z.string().describe("How changed_paths were resolved (manual, last_push, compare, fix, \u2026)"),
2981
+ affected_file_paths: z.array(z.string()).describe("Indexed files that import or depend on changed paths"),
2982
+ affected_node_ids: z.array(z.string()).describe("Codebase graph node ids impacted by the change"),
2983
+ meta: z.unknown().nullable().optional().describe("Extra resolution metadata when auto-sourced")
2918
2984
  }
2919
2985
  },
2920
2986
  async (args) => {
@@ -2929,7 +2995,7 @@ Be specific. No generic advice.`
2929
2995
  throw new MushiApiError(400, "BAD_REQUEST", "Provide paths, source=last_push, compare, or fix_id");
2930
2996
  }
2931
2997
  const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
2932
- return jsonText(data);
2998
+ return jsonResult(data);
2933
2999
  }
2934
3000
  );
2935
3001
  server.registerTool(
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  CODEBASE_TOOL_CATALOG,
3
3
  TDD_TOOL_CATALOG,
4
4
  TOOL_CATALOG
5
- } from "./chunk-KNVB6HA7.js";
5
+ } from "./chunk-DSOPGMDX.js";
6
6
  import {
7
7
  MUSHI_SERVER_METADATA
8
8
  } from "./chunk-EANW5YTI.js";
@@ -490,8 +490,12 @@ function createMushiServer(config) {
490
490
  description: descOf("get_knowledge_graph"),
491
491
  annotations: annotationsFor("get_knowledge_graph"),
492
492
  inputSchema: {
493
- seed: z.string().describe("Starting node id or label"),
494
- depth: z.number().optional().describe("Traversal depth (default 2, max 4)")
493
+ seed: z.string().describe("Starting graph node id or human-readable label to traverse from"),
494
+ depth: z.number().optional().describe("BFS hops outward from seed \u2014 clamped to 4 in the handler (default 2)")
495
+ },
496
+ outputSchema: {
497
+ nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
498
+ edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
495
499
  }
496
500
  },
497
501
  async (args) => {
@@ -499,7 +503,8 @@ function createMushiServer(config) {
499
503
  seed: args.seed,
500
504
  depth: String(Math.min(args.depth ?? 2, 4))
501
505
  });
502
- return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
506
+ const data = await apiCall(`/v1/admin/graph/traverse?${params}`);
507
+ return jsonResult(data);
503
508
  }
504
509
  );
505
510
  server.registerTool(
@@ -511,6 +516,10 @@ function createMushiServer(config) {
511
516
  inputSchema: {
512
517
  seed: z.string().describe("Starting graph node id or label to expand around (e.g. an inventory Action id or component name)"),
513
518
  depth: z.number().int().min(1).max(4).optional().describe("BFS hops to traverse outward. Default 2; clamped to a max of 4.")
519
+ },
520
+ outputSchema: {
521
+ nodes: z.array(z.unknown()).describe("Graph nodes within the depth budget"),
522
+ edges: z.array(z.unknown()).describe("Edges connecting the returned nodes")
514
523
  }
515
524
  },
516
525
  async (args) => {
@@ -518,7 +527,8 @@ function createMushiServer(config) {
518
527
  seed: args.seed,
519
528
  depth: String(Math.min(args.depth ?? 2, 4))
520
529
  });
521
- return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
530
+ const data = await apiCall(`/v1/admin/graph/traverse?${params}`);
531
+ return jsonResult(data);
522
532
  }
523
533
  );
524
534
  server.registerTool(
@@ -527,9 +537,15 @@ function createMushiServer(config) {
527
537
  title: titleOf("get_graph_node"),
528
538
  description: descOf("get_graph_node"),
529
539
  annotations: annotationsFor("get_graph_node"),
530
- inputSchema: { nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).") }
540
+ inputSchema: { nodeId: z.string().describe("The graph_nodes.id of the node to fetch (UUID).") },
541
+ outputSchema: {
542
+ node: z.unknown().describe("Single graph_nodes row including metadata (Action nodes carry v2 status)")
543
+ }
531
544
  },
532
- async (args) => jsonText(await apiCall(`/v1/admin/graph/node/${args.nodeId}`))
545
+ async (args) => {
546
+ const data = await apiCall(`/v1/admin/graph/node/${args.nodeId}`);
547
+ return jsonResult(data);
548
+ }
533
549
  );
534
550
  server.registerTool(
535
551
  "get_inventory",
@@ -576,6 +592,10 @@ function createMushiServer(config) {
576
592
  projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project"),
577
593
  gate: z.string().optional().describe("Filter to one gate id: dead-handler | mock-leak | crawl | status-claim"),
578
594
  severity: z.string().optional().describe("Minimum severity to include: low | medium | high | critical")
595
+ },
596
+ outputSchema: {
597
+ runs: z.array(z.unknown()).describe("Recent gate_runs rows, newest first"),
598
+ findings: z.array(z.unknown()).describe("gate_findings rows for those runs")
579
599
  }
580
600
  },
581
601
  async (args) => {
@@ -585,7 +605,8 @@ function createMushiServer(config) {
585
605
  if (args.gate) q.set("gate", args.gate);
586
606
  if (args.severity) q.set("severity", args.severity);
587
607
  const suffix = q.toString() ? `?${q}` : "";
588
- return jsonText(await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`));
608
+ const data = await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`);
609
+ return jsonResult(data);
589
610
  }
590
611
  );
591
612
  server.registerTool(
@@ -1074,13 +1095,17 @@ function createMushiServer(config) {
1074
1095
  description: descOf("submit_fix_result"),
1075
1096
  annotations: annotationsFor("submit_fix_result"),
1076
1097
  inputSchema: {
1077
- reportId: z.string().describe("The report UUID"),
1078
- branch: z.string().describe("Git branch name"),
1079
- prUrl: z.string().optional().describe("GitHub PR URL"),
1080
- filesChanged: z.array(z.string()).describe("Files modified"),
1081
- linesChanged: z.number().describe("Total lines changed"),
1082
- summary: z.string().describe("Fix summary"),
1098
+ reportId: z.string().describe("Report UUID to attach this external fix attempt to"),
1099
+ branch: z.string().describe("Git branch name where the fix was implemented"),
1100
+ prUrl: z.string().optional().describe("GitHub pull request URL, if a PR was opened"),
1101
+ filesChanged: z.array(z.string()).describe("Repo-relative paths modified by the fix"),
1102
+ linesChanged: z.number().describe("Total lines added + removed across filesChanged"),
1103
+ summary: z.string().describe("One-line human summary of what the fix changed"),
1083
1104
  idempotencyKey: z.string().uuid().optional().describe("Optional UUID \u2014 resend the same key to safely retry without creating duplicate fix rows")
1105
+ },
1106
+ outputSchema: {
1107
+ ok: z.boolean().describe("True when the fix_attempt row was created and marked completed"),
1108
+ fixId: z.string().describe("UUID of the new fix_attempt row")
1084
1109
  }
1085
1110
  },
1086
1111
  async (args) => {
@@ -1102,7 +1127,7 @@ function createMushiServer(config) {
1102
1127
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
1103
1128
  })
1104
1129
  });
1105
- return jsonText({ ok: true, fixId: created.fixId });
1130
+ return jsonResult({ ok: true, fixId: created.fixId });
1106
1131
  }
1107
1132
  );
1108
1133
  server.registerTool(
@@ -1159,6 +1184,9 @@ function createMushiServer(config) {
1159
1184
  inputSchema: {
1160
1185
  limit: z.number().optional().describe("Max reports to judge in this batch (default 25, max 100)"),
1161
1186
  projectId: z.string().optional().describe("Restrict to one project when the API key owns multiple")
1187
+ },
1188
+ outputSchema: {
1189
+ dispatched: z.number().describe("Number of judge-batch jobs dispatched (one per accessible project)")
1162
1190
  }
1163
1191
  },
1164
1192
  async (args) => {
@@ -1169,7 +1197,7 @@ function createMushiServer(config) {
1169
1197
  projectId: args.projectId ?? projectId ?? void 0
1170
1198
  })
1171
1199
  });
1172
- return jsonText(data);
1200
+ return jsonResult(data);
1173
1201
  }
1174
1202
  );
1175
1203
  server.registerTool(
@@ -1200,8 +1228,14 @@ function createMushiServer(config) {
1200
1228
  description: descOf("merge_fix"),
1201
1229
  annotations: annotationsFor("merge_fix"),
1202
1230
  inputSchema: {
1203
- fixId: z.string().describe("Fix attempt UUID"),
1204
- mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method")
1231
+ fixId: z.string().describe("Fix attempt UUID whose GitHub PR should be squash-merged"),
1232
+ mergeMethod: z.enum(["squash", "merge", "rebase"]).optional().describe("GitHub merge method (default squash)")
1233
+ },
1234
+ outputSchema: {
1235
+ merged: z.boolean().optional().describe("True when GitHub accepted the merge in this call"),
1236
+ alreadyMerged: z.boolean().optional().describe("True when the PR was already merged (idempotent no-op)"),
1237
+ reportId: z.string().describe("Report UUID linked to this fix attempt"),
1238
+ reportStatus: z.string().describe("Report workflow status after merge bookkeeping")
1205
1239
  }
1206
1240
  },
1207
1241
  async (args) => {
@@ -1209,7 +1243,7 @@ function createMushiServer(config) {
1209
1243
  method: "POST",
1210
1244
  body: JSON.stringify({ mergeMethod: args.mergeMethod ?? "squash" })
1211
1245
  });
1212
- return jsonText(data);
1246
+ return jsonResult(data);
1213
1247
  }
1214
1248
  );
1215
1249
  server.registerTool(
@@ -1219,12 +1253,17 @@ function createMushiServer(config) {
1219
1253
  description: descOf("refresh_ci"),
1220
1254
  annotations: annotationsFor("refresh_ci"),
1221
1255
  inputSchema: {
1222
- fixId: z.string().describe("Fix attempt UUID")
1256
+ fixId: z.string().describe("Fix attempt UUID whose PR check-runs should be re-polled")
1257
+ },
1258
+ outputSchema: {
1259
+ check_run_status: z.string().nullable().describe("GitHub check run status after refresh"),
1260
+ check_run_conclusion: z.string().nullable().describe("success | failure | neutral | null while pending"),
1261
+ check_run_updated_at: z.string().nullable().describe("ISO timestamp when CI status was last persisted")
1223
1262
  }
1224
1263
  },
1225
1264
  async (args) => {
1226
1265
  const data = await apiCall(`/v1/admin/fixes/${args.fixId}/refresh-ci`, { method: "POST" });
1227
- return jsonText(data);
1266
+ return jsonResult(data);
1228
1267
  }
1229
1268
  );
1230
1269
  server.registerTool(
@@ -1234,16 +1273,19 @@ function createMushiServer(config) {
1234
1273
  description: descOf("reopen_report"),
1235
1274
  annotations: annotationsFor("reopen_report"),
1236
1275
  inputSchema: {
1237
- reportId: z.string().describe("Report UUID"),
1238
- note: z.string().optional().describe("Triage note")
1276
+ reportId: z.string().describe("Report UUID to move back to reopened status"),
1277
+ note: z.string().optional().describe("Operator note recorded on the reopen transition")
1278
+ },
1279
+ outputSchema: {
1280
+ report: z.unknown().describe("Updated report row with status=reopened")
1239
1281
  }
1240
1282
  },
1241
1283
  async (args) => {
1242
- const data = await apiCall(`/v1/sync/reports/${args.reportId}`, {
1284
+ const report = await apiCall(`/v1/sync/reports/${args.reportId}`, {
1243
1285
  method: "PATCH",
1244
1286
  body: JSON.stringify({ status: "reopened", note: args.note })
1245
1287
  });
1246
- return jsonText(data);
1288
+ return jsonResult({ report });
1247
1289
  }
1248
1290
  );
1249
1291
  server.registerTool(
@@ -1320,9 +1362,12 @@ function createMushiServer(config) {
1320
1362
  description: descOf("list_lessons"),
1321
1363
  annotations: annotationsFor("list_lessons"),
1322
1364
  inputSchema: {
1323
- severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter by severity level."),
1324
- limit: z.number().optional().describe("Max number of lessons to return (default 50, max 200)."),
1325
- project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1365
+ severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter to one severity level"),
1366
+ limit: z.number().optional().describe("Max lessons to return (default 50, max 200)"),
1367
+ project_id: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
1368
+ },
1369
+ outputSchema: {
1370
+ lessons: z.array(z.unknown()).describe("Promoted lesson rows ordered by frequency")
1326
1371
  }
1327
1372
  },
1328
1373
  async (args) => {
@@ -1331,8 +1376,8 @@ function createMushiServer(config) {
1331
1376
  if (args.severity) params.set("severity", args.severity);
1332
1377
  params.set("limit", String(Math.min(args.limit ?? 50, 200)));
1333
1378
  if (pid) params.set("projectId", pid);
1334
- const data = await apiCall(`/v1/admin/lessons?${params}`);
1335
- return jsonText(data);
1379
+ const lessons = await apiCall(`/v1/admin/lessons?${params}`);
1380
+ return jsonResult({ lessons });
1336
1381
  }
1337
1382
  );
1338
1383
  server.resource(
@@ -1670,11 +1715,17 @@ Be specific. No generic advice.`
1670
1715
  description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
1671
1716
  annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
1672
1717
  inputSchema: {
1673
- projectId: z.string().describe("Project id"),
1674
- storyNodeId: z.string().describe("User story id slug from the accepted inventory"),
1675
- automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Gate mode for the generated test (default: review)"),
1676
- baseUrl: z.string().url().optional().describe("Override the app base URL"),
1677
- openPr: z.boolean().optional().describe("Open a draft GitHub PR (default: true)")
1718
+ projectId: z.string().describe("Project UUID that owns the inventory story"),
1719
+ storyNodeId: z.string().describe("User story node id/slug from accepted inventory (from map_user_stories or inventory.yaml)"),
1720
+ automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Approval gate for the generated qa_story (default review \u2192 pending_review)"),
1721
+ baseUrl: z.string().url().optional().describe("Override the live app URL the Playwright test should target"),
1722
+ openPr: z.boolean().optional().describe("Open a draft GitHub PR with the generated spec (default true)")
1723
+ },
1724
+ outputSchema: {
1725
+ qaStoryId: z.string().describe("UUID of the inserted qa_stories row"),
1726
+ prUrl: z.string().nullable().describe("GitHub PR URL when openPr=true and GitHub is connected"),
1727
+ approvalStatus: z.string().describe("pending_review | approved \u2014 follows automationMode and project defaults"),
1728
+ needsHumanReview: z.boolean().describe("True when the story requires operator approval before scheduled runs")
1678
1729
  }
1679
1730
  },
1680
1731
  async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
@@ -1683,7 +1734,7 @@ Be specific. No generic advice.`
1683
1734
  `/v1/admin/inventory/${projectId2}/stories/${storyNodeId}/generate-test`,
1684
1735
  { method: "POST", body: JSON.stringify({ automation_mode: automationMode, base_url: baseUrl, open_pr: openPr }) }
1685
1736
  );
1686
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1737
+ return jsonResult(data);
1687
1738
  }
1688
1739
  );
1689
1740
  server.registerTool(
@@ -2129,23 +2180,31 @@ Be specific. No generic advice.`
2129
2180
  annotations: annotationsFor("search_codebase", CODEBASE_TOOL_CATALOG),
2130
2181
  inputSchema: {
2131
2182
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2132
- query: z.string().describe("Plain-English search query"),
2133
- k: z.number().optional().describe("Number of results (default 8, max 20)"),
2134
- scope_prefix: z.string().optional().describe("Optional subdirectory scope prefix")
2183
+ query: z.string().describe("Plain-English search query against indexed source files"),
2184
+ k: z.number().int().min(1).max(20).optional().describe("Max hits to return (default 8, max 20)"),
2185
+ scope_prefix: z.string().optional().describe("Optional repo subdirectory prefix to limit search scope")
2186
+ },
2187
+ outputSchema: {
2188
+ results: z.array(z.unknown()).describe("Ranked file/symbol hits with paths, line ranges, and similarity"),
2189
+ query: z.string().describe("Normalized query string that was searched"),
2190
+ mode: z.string().optional().describe("semantic (embeddings) or name (path/symbol match)")
2135
2191
  }
2136
2192
  },
2137
2193
  async (args) => {
2138
2194
  const pid = args.project_id ?? projectId;
2139
2195
  if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "project_id is required");
2140
- const data = await apiCall(`/v1/admin/projects/${pid}/codebase/search`, {
2141
- method: "POST",
2142
- body: JSON.stringify({
2143
- query: args.query,
2144
- k: args.k ?? 8,
2145
- scope_prefix: args.scope_prefix ?? void 0
2146
- })
2147
- });
2148
- return jsonText(data);
2196
+ const data = await apiCall(
2197
+ `/v1/admin/projects/${pid}/codebase/search`,
2198
+ {
2199
+ method: "POST",
2200
+ body: JSON.stringify({
2201
+ query: args.query,
2202
+ k: args.k ?? 8,
2203
+ scope_prefix: args.scope_prefix ?? void 0
2204
+ })
2205
+ }
2206
+ );
2207
+ return jsonResult(data);
2149
2208
  }
2150
2209
  );
2151
2210
  server.registerTool(
@@ -2179,10 +2238,17 @@ Be specific. No generic advice.`
2179
2238
  annotations: annotationsFor("analyze_codebase_impact", CODEBASE_TOOL_CATALOG),
2180
2239
  inputSchema: {
2181
2240
  project_id: z.string().optional().describe("Project UUID (defaults to configured project)"),
2182
- paths: z.array(z.string()).optional().describe("Changed file paths"),
2183
- source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths"),
2184
- compare: z.string().optional().describe("GitHub compare range base...head"),
2185
- fix_id: z.string().optional().describe("Fix attempt UUID for PR changed files")
2241
+ paths: z.array(z.string()).optional().describe("Changed repo-relative file paths to analyze"),
2242
+ source: z.enum(["last_push", "compare", "fix"]).optional().describe("Auto-resolve changed paths instead of manual paths"),
2243
+ compare: z.string().optional().describe("GitHub compare range base...head when source=compare"),
2244
+ fix_id: z.string().optional().describe("Fix attempt UUID \u2014 uses that PR's changed files when source=fix")
2245
+ },
2246
+ outputSchema: {
2247
+ changed_paths: z.array(z.string()).describe("Resolved set of changed paths analyzed"),
2248
+ source: z.string().describe("How changed_paths were resolved (manual, last_push, compare, fix, \u2026)"),
2249
+ affected_file_paths: z.array(z.string()).describe("Indexed files that import or depend on changed paths"),
2250
+ affected_node_ids: z.array(z.string()).describe("Codebase graph node ids impacted by the change"),
2251
+ meta: z.unknown().nullable().optional().describe("Extra resolution metadata when auto-sourced")
2186
2252
  }
2187
2253
  },
2188
2254
  async (args) => {
@@ -2197,7 +2263,7 @@ Be specific. No generic advice.`
2197
2263
  throw new MushiApiError(400, "BAD_REQUEST", "Provide paths, source=last_push, compare, or fix_id");
2198
2264
  }
2199
2265
  const data = await apiCall(`/v1/admin/projects/${pid}/codebase/impact?${qs}`);
2200
- return jsonText(data);
2266
+ return jsonResult(data);
2201
2267
  }
2202
2268
  );
2203
2269
  server.registerTool(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mushi-mushi/mcp",
3
3
  "mcpName": "io.github.kensaurus/mushi-mushi",
4
- "version": "0.18.0",
4
+ "version": "0.18.2",
5
5
  "license": "MIT",
6
6
  "description": "MCP server for Mushi Mushi — triage user-felt bugs, pull evidence (screenshots, console logs, network requests), check blast radius, and dispatch fix PRs, all from your AI coding agent",
7
7
  "type": "module",
@@ -48,7 +48,7 @@
48
48
  "@modelcontextprotocol/sdk": "^1.29.0",
49
49
  "@sentry/node": "^10.57.0",
50
50
  "zod": "^4.4.3",
51
- "@mushi-mushi/core": "^1.22.1"
51
+ "@mushi-mushi/core": "^1.22.5"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "^22.19.21",