@mushi-mushi/mcp 0.22.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  TDD_TOOL_CATALOG,
5
5
  TOOL_CATALOG,
6
6
  routeUseMushiIntent
7
- } from "./chunk-RPSPV4FC.js";
7
+ } from "./chunk-2NW2VVWV.js";
8
8
  import {
9
9
  MUSHI_SERVER_METADATA
10
10
  } from "./chunk-FJRPC4VR.js";
@@ -12,7 +12,7 @@ import {
12
12
  DEPRECATED_TOOL_ALIASES,
13
13
  TOOL_FEATURE_MAP,
14
14
  toolMatchesFeatures
15
- } from "./chunk-LFSAKI6Z.js";
15
+ } from "./chunk-F65T4IGT.js";
16
16
 
17
17
  // src/server.ts
18
18
  import { AsyncLocalStorage } from "async_hooks";
@@ -32,7 +32,7 @@ var MUSHI_DOCS_INDEX = [
32
32
  route: "/",
33
33
  markdown: "index.md",
34
34
  keywords: ["know", "ai-built", "app", "broke", "fix", "ready"],
35
- headings: ["See it in action", "What this is (and is not)", "Get started with the SDK", "Install in your framework", "One queue. Sentry flows in, fixes flow out."],
35
+ headings: ["This is what a diagnosis looks like", "See it in action", "What this is (and is not)", "Get started with the SDK", "Install in your framework", "One queue. Sentry flows in, fixes flow out."],
36
36
  excerpt: "Your AI shipped it. Mushi tells you why it broke: a plain-English diagnosis and a ready fix in your editor. Open source, Sentry optional."
37
37
  },
38
38
  {
@@ -185,7 +185,7 @@ var MUSHI_DOCS_INDEX = [
185
185
  route: "/admin/explore",
186
186
  markdown: "admin/explore.md",
187
187
  keywords: ["admin", "explore", "codebase", "atlas"],
188
- headings: ["Tab groups", "Map views", "Graph", "Layers", "Search", "The detail panel", "Layer filter chips", "Stats bar"],
188
+ headings: ["Tab groups", "Copy digest", "Map views", "Diagram", "Graph", "Layers", "Search", "The detail panel"],
189
189
  excerpt: "Visual, searchable map of every indexed source file \u2014 explore architectural layers, import dependencies, and find code by meaning."
190
190
  },
191
191
  {
@@ -221,7 +221,7 @@ var MUSHI_DOCS_INDEX = [
221
221
  route: "/admin/fixes",
222
222
  markdown: "admin/fixes.md",
223
223
  keywords: ["admin", "fixes", "fix", "drafts", "prs"],
224
- headings: ["The fixes list", "Streaming view", "Spec-validation warnings", "Cursor Cloud Agent runs", "Dispatching a fix", "Webhooks", "CI feedback on open PRs", "Merging from the console"],
224
+ headings: ["The fixes list", "Streaming view", "Spec-validation warnings", "Cursor Cloud Agent runs", "Notes-only pull requests", "Dispatching a fix", "Webhooks", "CI feedback on open PRs"],
225
225
  excerpt: "The Fixes page tracks every draft pull request Mushi opened from a bug report, from the moment a fix run starts until the PR is ready or the run fails."
226
226
  },
227
227
  {
@@ -230,7 +230,7 @@ var MUSHI_DOCS_INDEX = [
230
230
  route: "/admin/fullstack-audit",
231
231
  markdown: "admin/fullstack-audit.md",
232
232
  keywords: ["admin", "fullstack-audit", "fullstack", "audit", "full-stack"],
233
- headings: ["Scorecard sections", "Prerequisites", "CLI", "Related pages"],
233
+ headings: ["Scorecard sections", "Open findings by check", 'Mushi setup checks and "Apply suggested caps"', "Prerequisites", "CLI", "Related pages"],
234
234
  excerpt: "The Full-stack audit runs one health check across RLS gaps, recent backend errors and API contract drift, and returns a readable scorecard."
235
235
  },
236
236
  {
@@ -368,6 +368,15 @@ var MUSHI_DOCS_INDEX = [
368
368
  headings: ["What you see", "Data source", "Related"],
369
369
  excerpt: "The Overview page is the organization-wide portfolio view \u2014 seven-day activity and open tickets for every connected project at a glance."
370
370
  },
371
+ {
372
+ title: "Portfolio",
373
+ url: docUrl("/admin/portfolio"),
374
+ route: "/admin/portfolio",
375
+ markdown: "admin/portfolio.md",
376
+ keywords: ["admin", "portfolio"],
377
+ headings: ["What you see", "When something could not be read", "Spend per app", "Daily digest", "Accounts and resilience", "From your editor", "Who can see it"],
378
+ excerpt: "The Portfolio page shows every app in a team at once, with each app's worst recipe state, hole checks and SDK version, plus fixes that apply across apps."
379
+ },
371
380
  {
372
381
  title: "Projects",
373
382
  url: docUrl("/admin/projects"),
@@ -428,7 +437,7 @@ var MUSHI_DOCS_INDEX = [
428
437
  route: "/admin/releases",
429
438
  markdown: "admin/releases.md",
430
439
  keywords: ["admin", "releases"],
431
- headings: ["What it generates", "Creating a release", "Release entry fields", "SDK surface", "Related pages"],
440
+ headings: ["What it generates", "Creating a release", "Release automatically when you ship", "Release entry fields", "SDK surface", "Related pages"],
432
441
  excerpt: "The Releases page drafts a changelog for each version from the fixes that shipped, crediting the users whose reports led to each fix."
433
442
  },
434
443
  {
@@ -683,6 +692,15 @@ var MUSHI_DOCS_INDEX = [
683
692
  headings: ["Reporter reputation", "Anti-gaming detection", "Report fatigue prevention"],
684
693
  excerpt: "How Mushi keeps bug reports honest \u2014 reporter reputation against spam and point farming, and prompt-fatigue limits so real users are not nagged."
685
694
  },
695
+ {
696
+ title: "App recipe and design system",
697
+ url: docUrl("/concepts/app-recipe"),
698
+ route: "/concepts/app-recipe",
699
+ markdown: "concepts/app-recipe.md",
700
+ keywords: ["concepts", "app-recipe", "app", "recipe", "design", "system"],
701
+ headings: ["The five states", "mushi.recipe.json", "The design system page", "Directions", "The deviance check", "The deviance score", "When the score is too high", "Hole checks"],
702
+ excerpt: "The App Recipe records what your app is made of, so a diagnosis can say what changed and a fix can respect your design system."
703
+ },
686
704
  {
687
705
  title: "Architecture",
688
706
  url: docUrl("/concepts/architecture"),
@@ -827,6 +845,15 @@ var MUSHI_DOCS_INDEX = [
827
845
  headings: ["Setup default", "When host init wins", "Console tuning without rebuild", "Widget draft persistence", "Troubleshooting", "Related"],
828
846
  excerpt: "How the Mushi SDK fetches widget and capture settings from the console at startup, how they merge with your init options, and when to turn it off."
829
847
  },
848
+ {
849
+ title: "One login and one purchase across your apps",
850
+ url: docUrl("/concepts/shared-login"),
851
+ route: "/concepts/shared-login",
852
+ markdown: "concepts/shared-login.md",
853
+ keywords: ["concepts", "shared-login", "shared", "login", "one", "purchase", "across", "apps"],
854
+ headings: ["Signing in once", "What Mushi checks", "Keeping a purchase in every app", "Sending users between your apps", "Comparing apps"],
855
+ excerpt: "How to let people sign in once and keep what they bought across several of your apps, and what Mushi checks so it keeps working."
856
+ },
830
857
  {
831
858
  title: "Where the report button lives",
832
859
  url: docUrl("/concepts/trigger-modes"),
@@ -1043,6 +1070,15 @@ var MUSHI_DOCS_INDEX = [
1043
1070
  headings: ["What changes (high level)", "API mapping (Mushi-specific)", "Migration checklist", "Common gotchas", "Mushi behaviour during the migration", "References"],
1044
1071
  excerpt: "Upgrade a Vue 2 app to Vue 3 and move from the vanilla Mushi web SDK to the @mushi-mushi/vue plugin, with an interactive migration checklist."
1045
1072
  },
1073
+ {
1074
+ title: "Open metrics",
1075
+ url: docUrl("/open"),
1076
+ route: "/open",
1077
+ markdown: "open.md",
1078
+ keywords: ["open", "metrics"],
1079
+ headings: ["What is not on this page, and why", "Where the numbers come from"],
1080
+ excerpt: "Mushi Mushi's public numbers (releases, commits, contributors, npm downloads), fetched from public APIs at build time, with what each one does not mean."
1081
+ },
1046
1082
  {
1047
1083
  title: "Operating (maintainers)",
1048
1084
  url: docUrl("/operating"),
@@ -1202,7 +1238,7 @@ var MUSHI_DOCS_INDEX = [
1202
1238
  route: "/plugins/slack",
1203
1239
  markdown: "plugins/slack.md",
1204
1240
  keywords: ["plugins", "slack", "app"],
1205
- headings: ["Setup", "What's on the card", "Acting from the channel", "Package", "Events", "Troubleshooting"],
1241
+ headings: ["Setup", "What's on the card", "Acting from the channel", "Slash commands", "Package", "Events", "Troubleshooting"],
1206
1242
  excerpt: "Triage Mushi bug reports from Slack \u2014 each classified report posts as a card with its root cause and buttons to dispatch a fix, resolve or dismiss."
1207
1243
  },
1208
1244
  {
@@ -1328,7 +1364,7 @@ var MUSHI_DOCS_INDEX = [
1328
1364
  route: "/quickstart/react-native",
1329
1365
  markdown: "quickstart/react-native.md",
1330
1366
  keywords: ["quickstart", "react-native", "react", "native"],
1331
- headings: ["1. Install", "2. Mount the provider", "3. Verify"],
1367
+ headings: ["1. Install", "2. Mount the provider", "Optional native modules", "3. Verify"],
1332
1368
  excerpt: "Add shake-to-report bug capture to a React Native or Expo app with @mushi-mushi/react-native \u2014 install, mount the provider, and verify the first report."
1333
1369
  },
1334
1370
  {
@@ -1445,7 +1481,7 @@ var MUSHI_DOCS_INDEX = [
1445
1481
  route: "/sdks/cli",
1446
1482
  markdown: "sdks/cli.md",
1447
1483
  keywords: ["sdks", "cli", "@mushi-mushi/cli"],
1448
- headings: ["Console \u2194 CLI setup loop", "Setup & health", "mushi doctor", "mushi nudge", "Reports & lessons", "Agentic fixes", "mushi fix", "mushi fixes"],
1484
+ headings: ["Console \u2194 CLI setup loop", "Setup & health", "mushi doctor", "mushi nudge", "Reports & lessons", "Sentry issues already open", "Reporter updates held for review", "Agentic fixes"],
1449
1485
  excerpt: "Reference for @mushi-mushi/cli \u2014 log in, connect a project, run doctor, list reports, dispatch agent fixes and merge their pull requests from the terminal."
1450
1486
  },
1451
1487
  {
@@ -1607,7 +1643,7 @@ var MUSHI_DOCS_INDEX = [
1607
1643
  route: "/sdks/react-native",
1608
1644
  markdown: "sdks/react-native.md",
1609
1645
  keywords: ["sdks", "react-native", "react", "native", "@mushi-mushi/react-native"],
1610
- headings: ["Install", "Mount the provider", "Submit a report from a screen", "Offline queue", "What gets captured (v0.19+)", "Screenshots (react-native-view-shot)", "Manual breadcrumbs", "Reporter identity contract"],
1646
+ headings: ["Install", "Mount the provider", "Submit a report from a screen", "Offline queue", "What gets captured (v0.19+)", "Screenshots (react-native-view-shot)", "Theming the report sheet", "Manual breadcrumbs"],
1611
1647
  excerpt: "Reference for @mushi-mushi/react-native \u2014 MushiProvider, hooks, shake-to-report, screenshots, identity and the offline queue for React Native and Expo."
1612
1648
  },
1613
1649
  {
@@ -2131,6 +2167,43 @@ function fixContextOf(report) {
2131
2167
  bugOntologyTags: report.bug_ontology_tags ?? null
2132
2168
  };
2133
2169
  }
2170
+ var RECIPE_STATES = ["ok", "drift", "unknown", "not_connected", "error"];
2171
+ var DESIGN_EXCERPT_MAX_FILES = 20;
2172
+ function designExcerptFilesOf(report) {
2173
+ const out = [];
2174
+ const add = (path) => {
2175
+ if (typeof path !== "string") return;
2176
+ const p = path.trim();
2177
+ if (!p || p.includes(",") || out.includes(p) || out.length >= DESIGN_EXCERPT_MAX_FILES) return;
2178
+ out.push(p);
2179
+ };
2180
+ const attempts = Array.isArray(report.fix_attempts) ? report.fix_attempts : [];
2181
+ for (const attempt of attempts) {
2182
+ if (Array.isArray(attempt?.files_changed)) attempt.files_changed.forEach(add);
2183
+ }
2184
+ if (typeof report.fix_packet === "string") {
2185
+ for (const m of report.fix_packet.matchAll(/^### `([^`\n]+)`$/gm)) add(m[1]);
2186
+ }
2187
+ return out;
2188
+ }
2189
+ function designExcerptQueryOf(report) {
2190
+ const params = new URLSearchParams();
2191
+ const files = designExcerptFilesOf(report);
2192
+ if (files.length > 0) params.set("files", files.join(","));
2193
+ if (typeof report.id === "string" && /^[0-9a-f-]{36}$/i.test(report.id)) params.set("reportId", report.id);
2194
+ const query = params.toString();
2195
+ return query ? `?${query}` : "";
2196
+ }
2197
+ function recipeFromExcerpt(excerpt) {
2198
+ const row = excerpt && typeof excerpt === "object" ? excerpt : null;
2199
+ if (!row || !RECIPE_STATES.includes(row.state)) {
2200
+ return recipeError("The design excerpt came back without a state.");
2201
+ }
2202
+ return typeof row.note === "string" ? row : { ...row, note: "" };
2203
+ }
2204
+ function recipeError(note) {
2205
+ return { state: "error", note };
2206
+ }
2134
2207
  function similarityQueryOf(report) {
2135
2208
  for (const field of ["summary", "description"]) {
2136
2209
  const value = report[field];
@@ -2327,7 +2400,15 @@ var GATE_IDS = [
2327
2400
  "orphan_endpoint",
2328
2401
  "unknown_call",
2329
2402
  "schema_drift",
2330
- "code_health"
2403
+ "code_health",
2404
+ "design_drift",
2405
+ "ci_drift",
2406
+ "deploy_drift",
2407
+ "env_drift",
2408
+ "radar",
2409
+ "portfolio_radar",
2410
+ "portfolio_radar_ci",
2411
+ "store_review"
2331
2412
  ];
2332
2413
  var GATE_FINDING_SEVERITIES = ["info", "warn", "error"];
2333
2414
  var SKILL_CATEGORIES = [
@@ -2392,6 +2473,22 @@ var FIX_CONTEXT_SHAPE = {
2392
2473
  rootCause: z.unknown().describe("Stage-2 root cause, or null before Stage 2 runs"),
2393
2474
  bugOntologyTags: z.unknown().describe("Bug ontology tags, or null")
2394
2475
  };
2476
+ var RECIPE_EXCERPT_OUTPUT = z.looseObject({
2477
+ state: z.enum(RECIPE_STATES).describe("ok | drift | unknown | not_connected | error; unknown never means healthy"),
2478
+ note: z.string().describe("Why this state, in plain English"),
2479
+ set: z.string().nullable().optional().describe("The token set the excerpt was taken from"),
2480
+ tokens: z.array(z.unknown()).optional().describe("Most useful tokens first: { path, value, cssVar, ts }. Use these names, not literals."),
2481
+ findings: z.array(z.unknown()).optional().describe("Open design deviance findings in the files this fix touches"),
2482
+ score: z.number().nullable().optional().describe("Design deviance score 0\u2013100, lower is better; null = not scored"),
2483
+ truncated: z.boolean().optional().describe("True when the excerpt was cut to stay under 4 KB"),
2484
+ context: z.looseObject({
2485
+ schema: z.unknown().describe("{ state, note, snapshotAt, tables: [{ name, columns }], missing }: tables the stack trace names, from the latest schema snapshot"),
2486
+ deploy: z.unknown().describe("{ state: live | deployed_since_merge | not_live | probe_failed | unknown | no_merged_fix | error, note, lastFix, targets }"),
2487
+ radar: z.unknown().describe("{ state, note, checkedAt, findings: [{ rule, severity, message, fix }] }: open hole checks")
2488
+ }).optional().describe("Backend and release context for this bug; each section has its own state, and unknown never means healthy")
2489
+ }).describe(
2490
+ "Design excerpt (\u2264 4 KB) for the report's project: tokens with CSS var / TS names, the deviance score, findings in the files this fix touches, and context (tables the stack trace names, whether the last fix is live, open hole checks). Never null; when it could not be read, { state: 'error', note }."
2491
+ );
2395
2492
  function isWrappedUntrusted(text) {
2396
2493
  return text.startsWith('<mushi-data role="');
2397
2494
  }
@@ -2414,10 +2511,11 @@ function createMushiServer(config) {
2414
2511
  icons: [...MUSHI_SERVER_METADATA.icons]
2415
2512
  });
2416
2513
  }
2417
- async function apiCall(path, options) {
2514
+ async function apiCall(path, options, callOptions) {
2418
2515
  const requestId = crypto.randomUUID().slice(0, 12);
2419
2516
  const started = Date.now();
2420
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
2517
+ const effectiveTimeoutMs = Math.max(timeoutMs, callOptions?.minTimeoutMs ?? 0);
2518
+ const timeoutSignal = AbortSignal.timeout(effectiveTimeoutMs);
2421
2519
  const toolCall = toolCallContext.getStore();
2422
2520
  let res;
2423
2521
  try {
@@ -2454,11 +2552,11 @@ function createMushiServer(config) {
2454
2552
  function classifyTransportError(err) {
2455
2553
  const durationMs2 = Date.now() - started;
2456
2554
  if (timeoutSignal.aborted) {
2457
- apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs });
2555
+ apiLog.warn("api.timeout", { path, requestId, durationMs: durationMs2, timeoutMs: effectiveTimeoutMs });
2458
2556
  return new MushiApiError(
2459
2557
  504,
2460
2558
  "MUSHI_TIMEOUT",
2461
- `Timed out after ${timeoutMs}ms waiting for ${path}. The Mushi API accepted the connection but did not answer in time \u2014 retry, or raise the budget with MUSHI_MCP_TIMEOUT_MS (ms) in your MCP env block if you self-host and cold starts are slow. Endpoint: ${apiEndpoint}`
2559
+ `Timed out after ${effectiveTimeoutMs}ms waiting for ${path}. The Mushi API accepted the connection but did not answer in time \u2014 retry, or raise the budget with MUSHI_MCP_TIMEOUT_MS (ms) in your MCP env block if you self-host and cold starts are slow. Endpoint: ${apiEndpoint}`
2462
2560
  );
2463
2561
  }
2464
2562
  const name = err instanceof Error ? err.name : "";
@@ -2597,6 +2695,19 @@ function createMushiServer(config) {
2597
2695
  structuredContent: value
2598
2696
  };
2599
2697
  }
2698
+ async function designRecipeFor(report, fallbackProjectId, headers) {
2699
+ const pid = typeof report.project_id === "string" && report.project_id ? report.project_id : fallbackProjectId;
2700
+ const query = designExcerptQueryOf(report);
2701
+ try {
2702
+ return recipeFromExcerpt(
2703
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/excerpt${query}`, {
2704
+ headers
2705
+ })
2706
+ );
2707
+ } catch (err) {
2708
+ return recipeError(`Could not read the design excerpt: ${bareMessage(err)}`);
2709
+ }
2710
+ }
2600
2711
  const server = new McpServer(
2601
2712
  {
2602
2713
  name: MUSHI_SERVER_METADATA.name,
@@ -2802,19 +2913,22 @@ function createMushiServer(config) {
2802
2913
  outputSchema: z.object({
2803
2914
  report: REPORT_DETAIL_OUTPUT,
2804
2915
  ...FIX_CONTEXT_SHAPE,
2805
- inventoryAction: z.unknown().describe("The inventory action (with its expected_outcome contract) the report is filed against, or null")
2916
+ inventoryAction: z.unknown().describe("The inventory action (with its expected_outcome contract) the report is filed against, or null"),
2917
+ recipe: RECIPE_EXCERPT_OUTPUT
2806
2918
  })
2807
2919
  },
2808
2920
  async (args) => {
2809
- const { headers } = await projectScopeHeaders(args.projectId);
2921
+ const { projectId: scopedProjectId, headers } = await projectScopeHeaders(args.projectId);
2810
2922
  const report = await apiCall(`/v1/admin/reports/${args.reportId}`, {
2811
2923
  headers
2812
2924
  });
2925
+ const recipe = await designRecipeFor(report, scopedProjectId, headers);
2813
2926
  return wrappedJsonResult(
2814
2927
  {
2815
2928
  report: projectReportDetail(report, false),
2816
2929
  ...fixContextOf(report),
2817
- inventoryAction: report.inventory_action ?? null
2930
+ inventoryAction: report.inventory_action ?? null,
2931
+ recipe
2818
2932
  },
2819
2933
  "fix context"
2820
2934
  );
@@ -2989,6 +3103,598 @@ function createMushiServer(config) {
2989
3103
  return jsonResult(data);
2990
3104
  }
2991
3105
  );
3106
+ server.registerTool(
3107
+ "get_app_recipe",
3108
+ {
3109
+ title: titleOf("get_app_recipe"),
3110
+ description: descOf("get_app_recipe"),
3111
+ annotations: annotationsFor("get_app_recipe"),
3112
+ inputSchema: z.object({
3113
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
3114
+ })
3115
+ },
3116
+ async (args) => {
3117
+ const pid = await resolveProjectId(args.projectId);
3118
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe`));
3119
+ }
3120
+ );
3121
+ server.registerTool(
3122
+ "get_design_tokens",
3123
+ {
3124
+ title: titleOf("get_design_tokens"),
3125
+ description: descOf("get_design_tokens"),
3126
+ annotations: annotationsFor("get_design_tokens"),
3127
+ inputSchema: z.object({
3128
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3129
+ group: z.string().max(80).optional().describe("Only tokens in this group (first path segment, e.g. color, space, radius)."),
3130
+ type: z.string().max(40).optional().describe("Only tokens of this DTCG $type (color, dimension, fontFamily, \u2026)."),
3131
+ direction: z.string().max(80).optional().describe("A named token set (directions/<name>/) instead of the active one.")
3132
+ })
3133
+ },
3134
+ async (args) => {
3135
+ const pid = await resolveProjectId(args.projectId);
3136
+ const q = new URLSearchParams();
3137
+ if (args.group) q.set("group", args.group);
3138
+ if (args.type) q.set("type", args.type);
3139
+ if (args.direction) q.set("direction", args.direction);
3140
+ const suffix = q.toString() ? `?${q}` : "";
3141
+ return jsonText(
3142
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/tokens${suffix}`)
3143
+ );
3144
+ }
3145
+ );
3146
+ server.registerTool(
3147
+ "get_design_deviance",
3148
+ {
3149
+ title: titleOf("get_design_deviance"),
3150
+ description: descOf("get_design_deviance"),
3151
+ annotations: annotationsFor("get_design_deviance"),
3152
+ inputSchema: z.object({
3153
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3154
+ limit: z.number().int().min(1).max(200).optional().describe("Findings to return, 1\u2013200 (default 25).")
3155
+ })
3156
+ },
3157
+ async (args) => {
3158
+ const pid = await resolveProjectId(args.projectId);
3159
+ const suffix = args.limit !== void 0 ? `?${new URLSearchParams({ limit: String(args.limit) })}` : "";
3160
+ return jsonText(
3161
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/deviance${suffix}`)
3162
+ );
3163
+ }
3164
+ );
3165
+ const ORG_ID_INPUT = z.object({
3166
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization when omitted")
3167
+ });
3168
+ server.registerTool(
3169
+ "get_portfolio",
3170
+ {
3171
+ title: titleOf("get_portfolio"),
3172
+ description: descOf("get_portfolio"),
3173
+ annotations: annotationsFor("get_portfolio"),
3174
+ inputSchema: ORG_ID_INPUT
3175
+ },
3176
+ async (args) => {
3177
+ const org = encodeURIComponent(args.organizationId ?? "current");
3178
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio`));
3179
+ }
3180
+ );
3181
+ server.registerTool(
3182
+ "list_portfolio_findings",
3183
+ {
3184
+ title: titleOf("list_portfolio_findings"),
3185
+ description: descOf("list_portfolio_findings"),
3186
+ annotations: annotationsFor("list_portfolio_findings"),
3187
+ inputSchema: ORG_ID_INPUT
3188
+ },
3189
+ async (args) => {
3190
+ const org = encodeURIComponent(args.organizationId ?? "current");
3191
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio/findings`));
3192
+ }
3193
+ );
3194
+ server.registerTool(
3195
+ "get_recipe_drift",
3196
+ {
3197
+ title: titleOf("get_recipe_drift"),
3198
+ description: descOf("get_recipe_drift"),
3199
+ annotations: annotationsFor("get_recipe_drift"),
3200
+ inputSchema: z.object({
3201
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
3202
+ })
3203
+ },
3204
+ async (args) => {
3205
+ const pid = await resolveProjectId(args.projectId);
3206
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/drift`));
3207
+ }
3208
+ );
3209
+ server.registerTool(
3210
+ "list_connectors",
3211
+ {
3212
+ title: titleOf("list_connectors"),
3213
+ description: descOf("list_connectors"),
3214
+ annotations: annotationsFor("list_connectors"),
3215
+ inputSchema: ORG_ID_INPUT
3216
+ },
3217
+ async (args) => {
3218
+ const org = encodeURIComponent(args.organizationId ?? "current");
3219
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/connectors`));
3220
+ }
3221
+ );
3222
+ const EDIT_INPUT = z.object({
3223
+ path: z.string().min(1).max(400).describe("Repo-relative path the recipe allows"),
3224
+ content: z.string().max(512 * 1024).describe("The full new file content"),
3225
+ reason: z.string().max(200).optional(),
3226
+ baseSha: z.string().min(1).max(80).nullable().optional().describe("The file's baseSha from the dry run (null = it did not exist). The confirm is refused if the file changed since, so it never reverts someone else's edit")
3227
+ });
3228
+ const ELEMENT_INPUT = z.enum(["design", "gates", "env", "routes", "store", "release"]).describe("Which part of the recipe the edit belongs to");
3229
+ server.registerTool(
3230
+ "propose_recipe_change",
3231
+ {
3232
+ title: titleOf("propose_recipe_change"),
3233
+ description: descOf("propose_recipe_change"),
3234
+ annotations: annotationsFor("propose_recipe_change"),
3235
+ inputSchema: z.object({
3236
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3237
+ element: ELEMENT_INPUT,
3238
+ edits: z.array(EDIT_INPUT).min(1).max(30),
3239
+ title: z.string().min(1).max(120).optional(),
3240
+ confirm: z.boolean().optional().describe("true opens the draft PR; omitted or false is a dry run")
3241
+ })
3242
+ },
3243
+ async (args) => {
3244
+ const pid = await resolveProjectId(args.projectId);
3245
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/changes`, {
3246
+ method: "POST",
3247
+ body: JSON.stringify({ element: args.element, edits: args.edits, title: args.title, dryRun: args.confirm !== true })
3248
+ }));
3249
+ }
3250
+ );
3251
+ server.registerTool(
3252
+ "propose_portfolio_change",
3253
+ {
3254
+ title: titleOf("propose_portfolio_change"),
3255
+ description: descOf("propose_portfolio_change"),
3256
+ annotations: annotationsFor("propose_portfolio_change"),
3257
+ inputSchema: z.object({
3258
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3259
+ element: ELEMENT_INPUT,
3260
+ changes: z.array(z.object({ projectId: z.string().uuid(), edits: z.array(EDIT_INPUT).min(1).max(30) })).min(1).max(10),
3261
+ title: z.string().min(1).max(120).optional(),
3262
+ confirm: z.boolean().optional().describe("true opens one draft PR per repo; omitted or false is a dry run")
3263
+ })
3264
+ },
3265
+ async (args) => {
3266
+ const org = encodeURIComponent(args.organizationId ?? "current");
3267
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/portfolio/changes`, {
3268
+ method: "POST",
3269
+ body: JSON.stringify({ element: args.element, changes: args.changes, title: args.title, dryRun: args.confirm !== true })
3270
+ }));
3271
+ }
3272
+ );
3273
+ server.registerTool(
3274
+ "request_connector_action",
3275
+ {
3276
+ title: titleOf("request_connector_action"),
3277
+ description: descOf("request_connector_action"),
3278
+ annotations: annotationsFor("request_connector_action"),
3279
+ inputSchema: z.object({
3280
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3281
+ connectorId: z.string().uuid().describe("Connector instance id (list_connectors)"),
3282
+ action: z.string().min(1).max(60).describe("For example set_rollout or promote_track"),
3283
+ payload: z.record(z.string(), z.unknown()).describe("The exact action input; the approval binds its hash"),
3284
+ projectId: z.string().uuid().optional(),
3285
+ reason: z.string().max(500).optional().describe("Why, shown to the approver")
3286
+ })
3287
+ },
3288
+ async (args) => {
3289
+ const org = encodeURIComponent(args.organizationId ?? "current");
3290
+ const body = { connectorId: args.connectorId, action: args.action, payload: args.payload, projectId: args.projectId, reason: args.reason };
3291
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/connector-actions`, { method: "POST", body: JSON.stringify(body) }));
3292
+ }
3293
+ );
3294
+ server.registerTool(
3295
+ "get_store_status",
3296
+ {
3297
+ title: titleOf("get_store_status"),
3298
+ description: descOf("get_store_status"),
3299
+ annotations: annotationsFor("get_store_status"),
3300
+ inputSchema: z.object({
3301
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
3302
+ })
3303
+ },
3304
+ async (args) => {
3305
+ const pid = await resolveProjectId(args.projectId);
3306
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store`));
3307
+ }
3308
+ );
3309
+ server.registerTool(
3310
+ "get_radar",
3311
+ {
3312
+ title: titleOf("get_radar"),
3313
+ description: descOf("get_radar"),
3314
+ annotations: annotationsFor("get_radar"),
3315
+ inputSchema: z.object({
3316
+ scope: z.enum(["project", "organization"]).optional().describe("project (default) or organization"),
3317
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project (scope project)"),
3318
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization (scope organization)")
3319
+ })
3320
+ },
3321
+ async (args) => {
3322
+ if (args.scope === "organization") {
3323
+ const org = encodeURIComponent(args.organizationId ?? "current");
3324
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/radar`));
3325
+ }
3326
+ const pid = await resolveProjectId(args.projectId);
3327
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/radar`));
3328
+ }
3329
+ );
3330
+ const PROJECT_ID_INPUT = z.object({
3331
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted")
3332
+ });
3333
+ server.registerTool(
3334
+ "run_radar",
3335
+ {
3336
+ title: titleOf("run_radar"),
3337
+ description: descOf("run_radar"),
3338
+ annotations: annotationsFor("run_radar"),
3339
+ inputSchema: PROJECT_ID_INPUT
3340
+ },
3341
+ async (args) => {
3342
+ const pid = await resolveProjectId(args.projectId);
3343
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/radar/run`, { method: "POST" }));
3344
+ }
3345
+ );
3346
+ server.registerTool(
3347
+ "refresh_recipe",
3348
+ {
3349
+ title: titleOf("refresh_recipe"),
3350
+ description: descOf("refresh_recipe"),
3351
+ annotations: annotationsFor("refresh_recipe"),
3352
+ inputSchema: PROJECT_ID_INPUT
3353
+ },
3354
+ async (args) => {
3355
+ const pid = await resolveProjectId(args.projectId);
3356
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/refresh`, { method: "POST" }));
3357
+ }
3358
+ );
3359
+ server.registerTool(
3360
+ "run_design_deviance",
3361
+ {
3362
+ title: titleOf("run_design_deviance"),
3363
+ description: descOf("run_design_deviance"),
3364
+ annotations: annotationsFor("run_design_deviance"),
3365
+ inputSchema: PROJECT_ID_INPUT
3366
+ },
3367
+ async (args) => {
3368
+ const pid = await resolveProjectId(args.projectId);
3369
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/deviance/run`, { method: "POST" }));
3370
+ }
3371
+ );
3372
+ server.registerTool(
3373
+ "run_store_review",
3374
+ {
3375
+ title: titleOf("run_store_review"),
3376
+ description: descOf("run_store_review"),
3377
+ annotations: annotationsFor("run_store_review"),
3378
+ inputSchema: PROJECT_ID_INPUT
3379
+ },
3380
+ async (args) => {
3381
+ const pid = await resolveProjectId(args.projectId);
3382
+ return jsonText(
3383
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/review`, { method: "POST" }, { minTimeoutMs: 9e4 })
3384
+ );
3385
+ }
3386
+ );
3387
+ server.registerTool(
3388
+ "get_release_calendar",
3389
+ {
3390
+ title: titleOf("get_release_calendar"),
3391
+ description: descOf("get_release_calendar"),
3392
+ annotations: annotationsFor("get_release_calendar"),
3393
+ inputSchema: ORG_ID_INPUT
3394
+ },
3395
+ async (args) => {
3396
+ const org = encodeURIComponent(args.organizationId ?? "current");
3397
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/releases`));
3398
+ }
3399
+ );
3400
+ server.registerTool(
3401
+ "get_code_health",
3402
+ {
3403
+ title: titleOf("get_code_health"),
3404
+ description: descOf("get_code_health"),
3405
+ annotations: annotationsFor("get_code_health"),
3406
+ inputSchema: z.object({
3407
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3408
+ days: z.number().int().min(1).max(365).optional().describe("Trend window in days (default 30)")
3409
+ })
3410
+ },
3411
+ async (args) => {
3412
+ const pid = await resolveProjectId(args.projectId);
3413
+ const qs = new URLSearchParams({ project_id: pid, days: String(args.days ?? 30) });
3414
+ return jsonText(await apiCall(`/v1/admin/code-health?${qs}`));
3415
+ }
3416
+ );
3417
+ server.registerTool(
3418
+ "explain_finding",
3419
+ {
3420
+ title: titleOf("explain_finding"),
3421
+ description: descOf("explain_finding"),
3422
+ annotations: annotationsFor("explain_finding"),
3423
+ inputSchema: z.object({
3424
+ findingId: z.string().uuid().describe("Gate finding id (the id field of a finding in list_gate_findings, get_radar, get_code_health\u2026)")
3425
+ })
3426
+ },
3427
+ async (args) => {
3428
+ return jsonText(await apiCall(`/v1/admin/findings/${encodeURIComponent(args.findingId)}`));
3429
+ }
3430
+ );
3431
+ server.registerTool(
3432
+ "get_auto_release_status",
3433
+ {
3434
+ title: titleOf("get_auto_release_status"),
3435
+ description: descOf("get_auto_release_status"),
3436
+ annotations: annotationsFor("get_auto_release_status"),
3437
+ inputSchema: PROJECT_ID_INPUT
3438
+ },
3439
+ async (args) => {
3440
+ const pid = await resolveProjectId(args.projectId);
3441
+ return jsonText(await apiCall(`/v1/admin/releases/auto-release?${new URLSearchParams({ project_id: pid })}`));
3442
+ }
3443
+ );
3444
+ server.registerTool(
3445
+ "get_design_settings",
3446
+ {
3447
+ title: titleOf("get_design_settings"),
3448
+ description: descOf("get_design_settings"),
3449
+ annotations: annotationsFor("get_design_settings"),
3450
+ inputSchema: PROJECT_ID_INPUT
3451
+ },
3452
+ async (args) => {
3453
+ const pid = await resolveProjectId(args.projectId);
3454
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/settings`));
3455
+ }
3456
+ );
3457
+ server.registerTool(
3458
+ "set_design_settings",
3459
+ {
3460
+ title: titleOf("set_design_settings"),
3461
+ description: descOf("set_design_settings"),
3462
+ annotations: annotationsFor("set_design_settings"),
3463
+ inputSchema: z.object({
3464
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3465
+ threshold: z.number().int().min(0).max(100).optional().describe("0\u2013100; the actions fire when the score is above it"),
3466
+ failCi: z.boolean().optional().describe("true: `mushi recipe check --push` fails CI above the threshold"),
3467
+ autofix: z.boolean().optional().describe("false turns the design auto-fix off; an API key cannot turn it on")
3468
+ })
3469
+ },
3470
+ async (args) => {
3471
+ const pid = await resolveProjectId(args.projectId);
3472
+ const body = { threshold: args.threshold, failCi: args.failCi, autofix: args.autofix };
3473
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/design/settings`, { method: "PUT", body: JSON.stringify(body) }));
3474
+ }
3475
+ );
3476
+ server.registerTool(
3477
+ "get_recipe_sources",
3478
+ {
3479
+ title: titleOf("get_recipe_sources"),
3480
+ description: descOf("get_recipe_sources"),
3481
+ annotations: annotationsFor("get_recipe_sources"),
3482
+ inputSchema: z.object({
3483
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3484
+ element: z.enum(["gates", "env", "routes"]).describe("gates (mushi.recipe.json budgets), env (.env.example) or routes (the inventory file)")
3485
+ })
3486
+ },
3487
+ async (args) => {
3488
+ const pid = await resolveProjectId(args.projectId);
3489
+ const qs = new URLSearchParams({ element: args.element });
3490
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/sources?${qs}`));
3491
+ }
3492
+ );
3493
+ server.registerTool(
3494
+ "get_recipe_change",
3495
+ {
3496
+ title: titleOf("get_recipe_change"),
3497
+ description: descOf("get_recipe_change"),
3498
+ annotations: annotationsFor("get_recipe_change"),
3499
+ inputSchema: z.object({
3500
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3501
+ jobId: z.string().uuid().describe("The jobId propose_recipe_change returned")
3502
+ })
3503
+ },
3504
+ async (args) => {
3505
+ const pid = await resolveProjectId(args.projectId);
3506
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/recipe/changes/${encodeURIComponent(args.jobId)}`));
3507
+ }
3508
+ );
3509
+ server.registerTool(
3510
+ "get_store_reviews",
3511
+ {
3512
+ title: titleOf("get_store_reviews"),
3513
+ description: descOf("get_store_reviews"),
3514
+ annotations: annotationsFor("get_store_reviews"),
3515
+ inputSchema: PROJECT_ID_INPUT
3516
+ },
3517
+ async (args) => {
3518
+ const pid = await resolveProjectId(args.projectId);
3519
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews`));
3520
+ }
3521
+ );
3522
+ server.registerTool(
3523
+ "pull_store_reviews",
3524
+ {
3525
+ title: titleOf("pull_store_reviews"),
3526
+ description: descOf("pull_store_reviews"),
3527
+ annotations: annotationsFor("pull_store_reviews"),
3528
+ inputSchema: PROJECT_ID_INPUT
3529
+ },
3530
+ async (args) => {
3531
+ const pid = await resolveProjectId(args.projectId);
3532
+ return jsonText(
3533
+ await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews/pull`, { method: "POST" }, { minTimeoutMs: 9e4 })
3534
+ );
3535
+ }
3536
+ );
3537
+ server.registerTool(
3538
+ "set_store_review_intake",
3539
+ {
3540
+ title: titleOf("set_store_review_intake"),
3541
+ description: descOf("set_store_review_intake"),
3542
+ annotations: annotationsFor("set_store_review_intake"),
3543
+ inputSchema: z.object({
3544
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to the server-configured project when omitted"),
3545
+ enabled: z.boolean().describe("false turns intake off; true keeps it on (only the console can turn it on)"),
3546
+ maxRating: z.number().int().min(1).max(5).optional().describe("Reviews at or under this many stars are filed (1\u20135)")
3547
+ })
3548
+ },
3549
+ async (args) => {
3550
+ const pid = await resolveProjectId(args.projectId);
3551
+ const body = { enabled: args.enabled, maxRating: args.maxRating };
3552
+ return jsonText(await apiCall(`/v1/admin/projects/${encodeURIComponent(pid)}/store/reviews/settings`, { method: "PUT", body: JSON.stringify(body) }));
3553
+ }
3554
+ );
3555
+ const ACCOUNT_PROVIDER = z.enum(["apple", "google_play", "aws", "supabase", "vercel", "registrar", "stripe", "github", "cloudflare", "other"]);
3556
+ server.registerTool(
3557
+ "get_accounts_register",
3558
+ {
3559
+ title: titleOf("get_accounts_register"),
3560
+ description: descOf("get_accounts_register"),
3561
+ annotations: annotationsFor("get_accounts_register"),
3562
+ inputSchema: ORG_ID_INPUT
3563
+ },
3564
+ async (args) => {
3565
+ const org = encodeURIComponent(args.organizationId ?? "current");
3566
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/accounts`));
3567
+ }
3568
+ );
3569
+ server.registerTool(
3570
+ "save_register_account",
3571
+ {
3572
+ title: titleOf("save_register_account"),
3573
+ description: descOf("save_register_account"),
3574
+ annotations: annotationsFor("save_register_account"),
3575
+ inputSchema: z.object({
3576
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3577
+ id: z.string().uuid().optional().describe("Account id to change (get_accounts_register); omit to record a new account"),
3578
+ provider: ACCOUNT_PROVIDER.optional().describe("Required when recording a new account"),
3579
+ displayName: z.string().min(1).max(120).optional().describe("Required when recording a new account"),
3580
+ ownerEmail: z.string().email().max(254).nullable().optional(),
3581
+ twoFactorDeclared: z.boolean().nullable().optional(),
3582
+ recoveryContact: z.string().max(200).nullable().optional().describe("A person who can recover the account (never a recovery code)"),
3583
+ adminCount: z.number().int().min(1).max(100).optional(),
3584
+ autoRenew: z.boolean().nullable().optional()
3585
+ })
3586
+ },
3587
+ async (args) => {
3588
+ const org = encodeURIComponent(args.organizationId ?? "current");
3589
+ const { organizationId: _org, id, ...fields } = args;
3590
+ if (!id && (!fields.provider || !fields.displayName)) {
3591
+ throw new MushiApiError(400, "VALIDATION_ERROR", "provider and displayName are required to record a new account (pass id to change one).");
3592
+ }
3593
+ const path = id ? `/v1/admin/orgs/${org}/accounts/${encodeURIComponent(id)}` : `/v1/admin/orgs/${org}/accounts`;
3594
+ return jsonText(await apiCall(path, { method: id ? "PATCH" : "POST", body: JSON.stringify(fields) }));
3595
+ }
3596
+ );
3597
+ server.registerTool(
3598
+ "remove_register_account",
3599
+ {
3600
+ title: titleOf("remove_register_account"),
3601
+ description: descOf("remove_register_account"),
3602
+ annotations: annotationsFor("remove_register_account"),
3603
+ inputSchema: z.object({
3604
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3605
+ id: z.string().uuid().describe("Account id (get_accounts_register)")
3606
+ })
3607
+ },
3608
+ async (args) => {
3609
+ const org = encodeURIComponent(args.organizationId ?? "current");
3610
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/accounts/${encodeURIComponent(args.id)}`, { method: "DELETE" }));
3611
+ }
3612
+ );
3613
+ server.registerTool(
3614
+ "set_domain_auto_renew",
3615
+ {
3616
+ title: titleOf("set_domain_auto_renew"),
3617
+ description: descOf("set_domain_auto_renew"),
3618
+ annotations: annotationsFor("set_domain_auto_renew"),
3619
+ inputSchema: z.object({
3620
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3621
+ domainId: z.string().uuid().describe("Domain id (the id of a domain in get_accounts_register)"),
3622
+ autoRenew: z.boolean().nullable().describe("true, false, or null for not known")
3623
+ })
3624
+ },
3625
+ async (args) => {
3626
+ const org = encodeURIComponent(args.organizationId ?? "current");
3627
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/domains/${encodeURIComponent(args.domainId)}`, {
3628
+ method: "PATCH",
3629
+ body: JSON.stringify({ autoRenew: args.autoRenew })
3630
+ }));
3631
+ }
3632
+ );
3633
+ server.registerTool(
3634
+ "get_spend_ledger",
3635
+ {
3636
+ title: titleOf("get_spend_ledger"),
3637
+ description: descOf("get_spend_ledger"),
3638
+ annotations: annotationsFor("get_spend_ledger"),
3639
+ inputSchema: ORG_ID_INPUT
3640
+ },
3641
+ async (args) => {
3642
+ const org = encodeURIComponent(args.organizationId ?? "current");
3643
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend`));
3644
+ }
3645
+ );
3646
+ server.registerTool(
3647
+ "import_spend_bill",
3648
+ {
3649
+ title: titleOf("import_spend_bill"),
3650
+ description: descOf("import_spend_bill"),
3651
+ annotations: annotationsFor("import_spend_bill"),
3652
+ inputSchema: z.object({
3653
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3654
+ vendor: z.enum(["vercel", "aws", "supabase", "other"]),
3655
+ csv: z.string().min(1).describe("The bill CSV content (at most 5 MB)"),
3656
+ projectId: z.string().uuid().optional().describe("Put every row on this app; omit to match an app column by name or slug"),
3657
+ filename: z.string().max(200).optional()
3658
+ })
3659
+ },
3660
+ async (args) => {
3661
+ const org = encodeURIComponent(args.organizationId ?? "current");
3662
+ const body = { vendor: args.vendor, csv: args.csv, projectId: args.projectId, filename: args.filename };
3663
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend/imports`, { method: "POST", body: JSON.stringify(body) }, { minTimeoutMs: 6e4 }));
3664
+ }
3665
+ );
3666
+ server.registerTool(
3667
+ "remove_spend_import",
3668
+ {
3669
+ title: titleOf("remove_spend_import"),
3670
+ description: descOf("remove_spend_import"),
3671
+ annotations: annotationsFor("remove_spend_import"),
3672
+ inputSchema: z.object({
3673
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3674
+ importId: z.string().uuid().describe("Import id (get_spend_ledger imports)")
3675
+ })
3676
+ },
3677
+ async (args) => {
3678
+ const org = encodeURIComponent(args.organizationId ?? "current");
3679
+ return jsonText(await apiCall(`/v1/admin/orgs/${org}/spend/imports/${encodeURIComponent(args.importId)}`, { method: "DELETE" }));
3680
+ }
3681
+ );
3682
+ server.registerTool(
3683
+ "import_portfolio_resources",
3684
+ {
3685
+ title: titleOf("import_portfolio_resources"),
3686
+ description: descOf("import_portfolio_resources"),
3687
+ annotations: annotationsFor("import_portfolio_resources"),
3688
+ inputSchema: z.object({
3689
+ organizationId: z.string().uuid().optional().describe("Organization UUID \u2014 defaults to the key owner's only organization"),
3690
+ csv: z.string().min(1).max(256 * 1024).describe("CSV with the columns kind, external_id, project and optionally role")
3691
+ })
3692
+ },
3693
+ async (args) => {
3694
+ const body = { organizationId: args.organizationId ?? "current", csv: args.csv };
3695
+ return jsonText(await apiCall("/v1/ingest/recipe/csv", { method: "POST", body: JSON.stringify(body) }));
3696
+ }
3697
+ );
2992
3698
  server.registerTool(
2993
3699
  "suggest_fix",
2994
3700
  {
@@ -3701,6 +4407,18 @@ function createMushiServer(config) {
3701
4407
  args: { mode: "dispatch" }
3702
4408
  });
3703
4409
  }
4410
+ const waiting = reports.filter(
4411
+ (r) => typeof r.last_reporter_reply_at === "string" && (typeof r.admin_seen_at !== "string" || Date.parse(r.last_reporter_reply_at) > Date.parse(r.admin_seen_at))
4412
+ );
4413
+ if (waiting.length > 0) {
4414
+ steps.push({
4415
+ priority: priority++,
4416
+ action: `Answer ${waiting.length} reporter${waiting.length === 1 ? "" : "s"} waiting for a reply`,
4417
+ reason: `They replied in your app's "Your reports" thread. Latest: ${label(waiting[0])}. Read it, then answer with reply_to_reporter.`,
4418
+ tool: "get_report_timeline",
4419
+ args: { reportId: waiting[0].id }
4420
+ });
4421
+ }
3704
4422
  const fixing = reports.filter((r) => r.status === "fixing");
3705
4423
  for (const r of fixing.slice(0, 2)) {
3706
4424
  steps.push({
@@ -3736,7 +4454,7 @@ function createMushiServer(config) {
3736
4454
  args: { status: "classified" }
3737
4455
  });
3738
4456
  }
3739
- const summary = steps.length === 0 ? "Inbox is clear \u2014 no blocked fixes, no open user reports, no chores. Nothing needs your attention." : `${steps.length} prioritised step${steps.length === 1 ? "" : "s"}: ` + steps.map((s) => s.action).join(" \u2192 ");
4457
+ const summary = steps.length === 0 ? "Inbox is clear \u2014 no blocked fixes, no reporters waiting, no open user reports, no chores. Nothing needs your attention." : `${steps.length} prioritised step${steps.length === 1 ? "" : "s"}: ` + steps.map((s) => s.action).join(" \u2192 ");
3740
4458
  return jsonResult({ steps, summary });
3741
4459
  }
3742
4460
  );
@@ -3921,6 +4639,57 @@ function createMushiServer(config) {
3921
4639
  return jsonResult({ report });
3922
4640
  }
3923
4641
  );
4642
+ server.registerTool(
4643
+ "import_sentry_issues",
4644
+ {
4645
+ title: titleOf("import_sentry_issues"),
4646
+ description: descOf("import_sentry_issues"),
4647
+ annotations: annotationsFor("import_sentry_issues"),
4648
+ inputSchema: z.object({
4649
+ issueIds: z.array(z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/)).max(10).optional().describe('Sentry issue ids or short ids (e.g. "4501", "WEB-12"), at most 10'),
4650
+ query: z.string().max(200).optional().describe("Sentry search query within the configured Sentry project (default is:unresolved). Not with issueIds."),
4651
+ limit: z.number().int().min(1).max(10).optional().describe("How many issues a query imports (1-10, default 5)"),
4652
+ sinceDays: z.number().int().min(1).max(90).optional().describe("Only issues seen in the last N days (1-90). Search mode only."),
4653
+ cursor: z.string().regex(/^[0-9][0-9.:-]{0,63}$/).optional().describe("nextCursor from the previous import, to fetch the next page of a backlog. Search mode only."),
4654
+ sentryProject: z.string().regex(/^[a-z0-9][a-z0-9_-]{0,49}$/).optional().describe("Which of the project's Sentry projects to search (default: the primary one). Search mode only."),
4655
+ projectId: z.string().optional().describe("Project UUID. Defaults to configured project.")
4656
+ }),
4657
+ outputSchema: z.object({
4658
+ items: z.array(
4659
+ z.looseObject({
4660
+ input: z.string(),
4661
+ issueId: z.string().nullable(),
4662
+ shortId: z.string().nullable(),
4663
+ outcome: z.string().describe("created | linked | deduped | ignored | error"),
4664
+ reportId: z.string().nullable(),
4665
+ error: z.string().optional()
4666
+ })
4667
+ ).describe("One entry per requested or matched issue"),
4668
+ created: z.array(z.string().nullable()).describe("Report ids created by this import"),
4669
+ linked: z.array(z.string().nullable()).describe("Report ids that already existed for these issues"),
4670
+ failed: z.number().describe("Issues that could not be imported"),
4671
+ indexing: z.looseObject({ queued: z.boolean(), paths: z.number() }).describe("Whether stack-frame files were queued for codebase indexing"),
4672
+ sentryProject: z.string().nullable().optional().describe("The Sentry project a search ran in (null for issueIds)"),
4673
+ sentryProjects: z.array(z.string()).optional().describe("This project's Sentry projects, primary first"),
4674
+ nextCursor: z.string().nullable().optional().describe("Pass back as cursor to import the next page; null when the backlog is done")
4675
+ })
4676
+ },
4677
+ async (args) => {
4678
+ const pid = await resolveProjectId(args.projectId);
4679
+ const data = await apiCall(`/v1/admin/projects/${pid}/sentry/import`, {
4680
+ method: "POST",
4681
+ body: JSON.stringify({
4682
+ issueIds: args.issueIds,
4683
+ query: args.query,
4684
+ limit: args.limit,
4685
+ sinceDays: args.sinceDays,
4686
+ cursor: args.cursor,
4687
+ sentryProject: args.sentryProject
4688
+ })
4689
+ });
4690
+ return jsonResult(data);
4691
+ }
4692
+ );
3924
4693
  server.registerTool(
3925
4694
  "transition_status",
3926
4695
  {
@@ -3945,13 +4714,20 @@ function createMushiServer(config) {
3945
4714
  "reopened",
3946
4715
  "dismissed"
3947
4716
  ]).describe("Target status (resolved is stored as fixed)"),
3948
- reason: z.string().optional().describe("Reason for the transition (audit trail)")
4717
+ reason: z.string().optional().describe("Reason for the transition (audit trail)"),
4718
+ closedReason: z.enum(["duplicate", "not_reproducible", "wont_fix", "working_as_intended", "spam"]).optional().describe("When dismissing: what the reporter is told (spam closes silently)"),
4719
+ reporterMessage: z.string().max(1e4).optional().describe("Optional note posted to the reporter verbatim")
3949
4720
  })
3950
4721
  },
3951
4722
  async (args) => {
3952
4723
  const data = await apiCall(`/v1/admin/reports/${args.reportId}`, {
3953
4724
  method: "PATCH",
3954
- body: JSON.stringify({ status: args.status, reason: args.reason })
4725
+ body: JSON.stringify({
4726
+ status: args.status,
4727
+ reason: args.reason,
4728
+ closed_reason: args.closedReason,
4729
+ reporter_message: args.reporterMessage
4730
+ })
3955
4731
  });
3956
4732
  return jsonText(data);
3957
4733
  }
@@ -4695,6 +5471,65 @@ Be specific. No generic advice.`
4695
5471
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
4696
5472
  }
4697
5473
  );
5474
+ server.registerTool(
5475
+ "request_reporter_info",
5476
+ {
5477
+ title: titleOf("request_reporter_info", TDD_TOOL_CATALOG),
5478
+ description: descOf("request_reporter_info", TDD_TOOL_CATALOG),
5479
+ annotations: annotationsFor("request_reporter_info", TDD_TOOL_CATALOG),
5480
+ inputSchema: z.object({
5481
+ reportId: z.string().describe("Report id to ask about"),
5482
+ question: z.string().min(1).max(2e3).describe("The question, shown to the reporter verbatim"),
5483
+ authorName: z.string().optional().describe('Display name for the sender (default: "Developer")')
5484
+ })
5485
+ },
5486
+ async ({ reportId, question, authorName }) => {
5487
+ const data = await apiCall(`/v1/admin/reports/${reportId}/request-info`, {
5488
+ method: "POST",
5489
+ body: JSON.stringify({ question, author_name: authorName })
5490
+ });
5491
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
5492
+ }
5493
+ );
5494
+ server.registerTool(
5495
+ "list_reporter_outbox",
5496
+ {
5497
+ title: titleOf("list_reporter_outbox", TDD_TOOL_CATALOG),
5498
+ description: descOf("list_reporter_outbox", TDD_TOOL_CATALOG),
5499
+ annotations: annotationsFor("list_reporter_outbox", TDD_TOOL_CATALOG),
5500
+ inputSchema: z.object({
5501
+ projectId: z.string().optional().describe("Project UUID. Defaults to configured project.")
5502
+ })
5503
+ },
5504
+ async ({ projectId: pid }) => {
5505
+ const scoped = pid ?? config.projectId;
5506
+ const data = await apiCall(
5507
+ `/v1/admin/reporter-outbox${scoped ? `?project_id=${encodeURIComponent(scoped)}` : ""}`
5508
+ );
5509
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
5510
+ }
5511
+ );
5512
+ server.registerTool(
5513
+ "release_reporter_update",
5514
+ {
5515
+ title: titleOf("release_reporter_update", TDD_TOOL_CATALOG),
5516
+ description: descOf("release_reporter_update", TDD_TOOL_CATALOG),
5517
+ annotations: annotationsFor("release_reporter_update", TDD_TOOL_CATALOG),
5518
+ inputSchema: z.object({
5519
+ messageId: z.string().describe("Held message id from list_reporter_outbox"),
5520
+ action: z.enum(["release", "discard"]).optional().describe("release (default) sends it; discard drops it"),
5521
+ bodyOverride: z.string().min(1).max(1e3).optional().describe("Replacement text to send instead")
5522
+ })
5523
+ },
5524
+ async ({ messageId, action, bodyOverride }) => {
5525
+ const verb = action === "discard" ? "discard" : "release";
5526
+ const data = await apiCall(`/v1/admin/reporter-outbox/${messageId}/${verb}`, {
5527
+ method: "POST",
5528
+ body: JSON.stringify(verb === "release" && bodyOverride ? { body_override: bodyOverride } : {})
5529
+ });
5530
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
5531
+ }
5532
+ );
4698
5533
  server.registerTool(
4699
5534
  "list_qa_story_runs",
4700
5535
  {
@@ -5135,6 +5970,66 @@ Be specific. No generic advice.`
5135
5970
  return jsonText(data);
5136
5971
  }
5137
5972
  );
5973
+ server.registerTool(
5974
+ "get_repo_digest",
5975
+ {
5976
+ title: titleOf("get_repo_digest", CODEBASE_TOOL_CATALOG),
5977
+ description: descOf("get_repo_digest", CODEBASE_TOOL_CATALOG),
5978
+ annotations: annotationsFor("get_repo_digest", CODEBASE_TOOL_CATALOG),
5979
+ inputSchema: z.object({
5980
+ projectId: z.string().optional().describe("Project UUID (defaults to configured project)"),
5981
+ reportId: z.string().optional().describe("Scope to one bug: the report's files come first, then the rest of the repo by priority"),
5982
+ path: z.string().optional().describe('Scope to one folder, e.g. "apps/web/src"'),
5983
+ budgetTokens: z.number().int().min(2e3).max(2e5).optional().describe("Estimated token budget for the whole digest (default 50,000)"),
5984
+ include: z.array(z.string()).max(50).optional().describe('Glob patterns a file must match, e.g. ["src/**/*.ts", "*.md"]'),
5985
+ exclude: z.array(z.string()).max(50).optional().describe('Glob patterns to leave out, e.g. ["tests/"]'),
5986
+ ref: z.string().optional().describe("Branch, tag or commit SHA (default: the repo default branch)")
5987
+ })
5988
+ },
5989
+ async (args) => {
5990
+ const pid = args.projectId ?? projectId;
5991
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
5992
+ const qs = new URLSearchParams();
5993
+ if (args.budgetTokens) qs.set("budget", String(args.budgetTokens));
5994
+ if (args.reportId) qs.set("report_id", args.reportId);
5995
+ if (args.path) qs.set("path", args.path);
5996
+ if (args.include?.length) qs.set("include", args.include.join(","));
5997
+ if (args.exclude?.length) qs.set("exclude", args.exclude.join(","));
5998
+ if (args.ref) qs.set("ref", args.ref);
5999
+ const query = qs.toString();
6000
+ const data = await apiCall(
6001
+ `/v1/admin/projects/${pid}/codebase/digest${query ? `?${query}` : ""}`,
6002
+ void 0,
6003
+ // The server reads up to a few hundred files from GitHub (60 s deadline).
6004
+ { minTimeoutMs: 9e4 }
6005
+ );
6006
+ return jsonText(data);
6007
+ }
6008
+ );
6009
+ server.registerTool(
6010
+ "get_repo_diagram",
6011
+ {
6012
+ title: titleOf("get_repo_diagram", CODEBASE_TOOL_CATALOG),
6013
+ description: descOf("get_repo_diagram", CODEBASE_TOOL_CATALOG),
6014
+ annotations: annotationsFor("get_repo_diagram", CODEBASE_TOOL_CATALOG),
6015
+ inputSchema: z.object({
6016
+ projectId: z.string().optional().describe("Project UUID (defaults to configured project)"),
6017
+ overlay: z.boolean().optional().describe("Also place the open bug reports and code findings on each part of the diagram")
6018
+ })
6019
+ },
6020
+ async (args) => {
6021
+ const pid = encodeURIComponent(await resolveProjectId(args.projectId));
6022
+ const data = await apiCall(
6023
+ `/v1/admin/projects/${pid}/codebase/diagram`
6024
+ );
6025
+ if (args.overlay !== true) return jsonText(data);
6026
+ const overlay = data.diagram ? await apiCall(`/v1/admin/projects/${pid}/codebase/diagram/overlay`, void 0, {
6027
+ // One GitHub tree read plus up to a few hundred reports.
6028
+ minTimeoutMs: 6e4
6029
+ }) : null;
6030
+ return jsonText({ ...data, overlay });
6031
+ }
6032
+ );
5138
6033
  server.registerTool(
5139
6034
  "use_mushi",
5140
6035
  {