@mushi-mushi/mcp 0.13.1 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -260,6 +260,64 @@ var TOOL_CATALOG = [
260
260
  scope: "mcp:write",
261
261
  hints: { readOnly: false, destructive: false, idempotent: true, openWorld: false },
262
262
  useCase: "Set up this repo for the Mushi evolution loop in one step."
263
+ },
264
+ // ── Sentry-like triage and project context ────────────────────────────────
265
+ {
266
+ name: "list_projects",
267
+ title: "List accessible projects",
268
+ description: "List the Mushi projects accessible to this API key. For project-scoped keys (the typical case), returns a single-item list with the bound project's id, name, slug, plan tier, and ingest status. For org-level tokens, returns all projects in the org. Use this when MUSHI_PROJECT_ID is not configured \u2014 call list_projects first, then pass the id to other tools.",
269
+ scope: "mcp:read",
270
+ hints: { readOnly: true, idempotent: true, openWorld: true },
271
+ useCase: "Which projects can I access with this API key?"
272
+ },
273
+ {
274
+ name: "get_project_context",
275
+ title: "Project context snapshot",
276
+ description: "Return a rich context snapshot for a project: name, repo URL, SDK heartbeat, ingest status, open report count, autofix readiness, BYOK LLM config, plan tier, and active integration health. Equivalent to a merged preflight + activation + settings read. Agents should call this at the start of a triage session to orient themselves.",
277
+ scope: "mcp:read",
278
+ hints: { readOnly: true, idempotent: true, openWorld: true },
279
+ useCase: "Give me a status overview of this project before I start triaging."
280
+ },
281
+ {
282
+ name: "get_pipeline_logs",
283
+ title: "Recent pipeline logs",
284
+ description: "Pull recent log entries from the Mushi pipeline services: fix-worker, qa-story-runner, pipeline, or all. Accepts project_id, service, since (ISO-8601), limit (max 200), and level (info | warn | error | fatal) filters. Returns structured log rows with timestamp, level, service, message, and a trace_id/report_id when available. Use this when a fix failed, a QA story keeps erroring, or an ingest pipeline went silent.",
285
+ scope: "mcp:read",
286
+ hints: { readOnly: true, idempotent: true, openWorld: true },
287
+ useCase: "Why did the last fix attempt fail? Show me recent pipeline errors."
288
+ },
289
+ {
290
+ name: "get_report_evidence",
291
+ title: "Bug report evidence",
292
+ description: "Return the full evidence package for a single bug report: screenshot URL, console logs, network excerpts, browser environment (user agent, URL, viewport, SDK version), user replay link if available, breadcrumb trail, and the reporter's own comments thread. This is the same data an engineer would collect for a root-cause investigation. Faster than calling get_report_detail + report timeline separately.",
293
+ scope: "mcp:read",
294
+ hints: { readOnly: true, idempotent: true, openWorld: true },
295
+ useCase: "I need all the evidence for this bug report to diagnose the root cause."
296
+ },
297
+ {
298
+ name: "triage_issue",
299
+ title: "Triage issue end-to-end",
300
+ description: 'Read-only orchestration tool: combines report detail, evidence, similar bugs, fix context, blast radius, and recent pipeline logs for a report into a single structured triage packet. Returns a prioritised list of recommended next actions (investigate, dispatch_fix, group_with, dismiss). Equivalent to a Sentry "Analyze with Seer" flow grounded in user-felt reports. Pass report_id to kick off triage. Call this before dispatch_fix.',
301
+ scope: "mcp:read",
302
+ hints: { readOnly: true, idempotent: true, openWorld: true },
303
+ useCase: "Analyze this bug report end-to-end and tell me what to do."
304
+ },
305
+ // ── Lessons / evolution loop ─────────────────────────────────────────────
306
+ {
307
+ name: "query_lessons",
308
+ title: "Query lessons for diff context",
309
+ description: 'Token-budget retrieval of relevant learning rules (lessons) for a given code diff or PR context. Returns ranked lessons packed within max_tokens using bi-encoder retrieval + severity-weighted scoring. Use this before opening a PR, writing a fix, or asking "what mistakes should I avoid in this area of code?"',
310
+ scope: "mcp:read",
311
+ hints: { readOnly: true, idempotent: true, openWorld: true },
312
+ useCase: "What past mistakes should I avoid when making this change?"
313
+ },
314
+ {
315
+ name: "list_lessons",
316
+ title: "List project lessons",
317
+ description: "List promoted learning rules (lessons) for the current project. Each lesson represents a named pattern of mistakes that has been encoded from bug reports. Use this to understand what systemic issues have been identified and encoded as heuristics.",
318
+ scope: "mcp:read",
319
+ hints: { readOnly: true, idempotent: true, openWorld: true },
320
+ useCase: "What systemic patterns has Mushi identified for this project?"
263
321
  }
264
322
  ];
265
323
  var TDD_TOOL_CATALOG = [
@@ -1008,6 +1066,175 @@ function createMushiServer(config) {
1008
1066
  });
1009
1067
  }
1010
1068
  );
1069
+ server.registerTool(
1070
+ "list_projects",
1071
+ {
1072
+ title: titleOf("list_projects"),
1073
+ description: descOf("list_projects"),
1074
+ annotations: annotationsFor("list_projects"),
1075
+ inputSchema: {}
1076
+ },
1077
+ async () => {
1078
+ const data = await apiCall("/v1/admin/mcp/projects");
1079
+ return jsonText(data);
1080
+ }
1081
+ );
1082
+ server.registerTool(
1083
+ "get_project_context",
1084
+ {
1085
+ title: titleOf("get_project_context"),
1086
+ description: descOf("get_project_context"),
1087
+ annotations: annotationsFor("get_project_context"),
1088
+ inputSchema: {
1089
+ project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1090
+ }
1091
+ },
1092
+ async (args) => {
1093
+ const pid = args.project_id ?? projectId;
1094
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
1095
+ const [preflightRes, activationRes] = await Promise.allSettled([
1096
+ apiCall(`/v1/admin/projects/${pid}/preflight`),
1097
+ apiCall(`/v1/admin/activation?project_id=${encodeURIComponent(pid)}`)
1098
+ ]);
1099
+ return jsonText({
1100
+ project_id: pid,
1101
+ preflight: preflightRes.status === "fulfilled" ? preflightRes.value : { error: String(preflightRes.reason) },
1102
+ activation: activationRes.status === "fulfilled" ? activationRes.value : { error: String(activationRes.reason) }
1103
+ });
1104
+ }
1105
+ );
1106
+ server.registerTool(
1107
+ "get_pipeline_logs",
1108
+ {
1109
+ title: titleOf("get_pipeline_logs"),
1110
+ description: descOf("get_pipeline_logs"),
1111
+ annotations: annotationsFor("get_pipeline_logs"),
1112
+ inputSchema: {
1113
+ project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1114
+ service: z.enum(["fix-worker", "qa-story-runner", "pipeline", "all"]).optional().describe("Pipeline service to filter (default: all)."),
1115
+ since: z.string().optional().describe("ISO-8601 timestamp \u2014 return only entries after this time."),
1116
+ limit: z.number().optional().describe("Max entries to return (default 50, max 200)."),
1117
+ level: z.enum(["info", "warn", "error", "fatal"]).optional().describe("Minimum severity level (default: warn).")
1118
+ }
1119
+ },
1120
+ async (args) => {
1121
+ const pid = args.project_id ?? projectId;
1122
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT_ID", "project_id is required");
1123
+ const qs = new URLSearchParams();
1124
+ if (args.service && args.service !== "all") qs.set("service", args.service);
1125
+ if (args.since) qs.set("since", args.since);
1126
+ qs.set("limit", String(Math.min(args.limit ?? 50, 200)));
1127
+ if (args.level) qs.set("level", args.level);
1128
+ const data = await apiCall(`/v1/admin/mcp/logs/${pid}?${qs}`);
1129
+ return jsonText(data);
1130
+ }
1131
+ );
1132
+ server.registerTool(
1133
+ "get_report_evidence",
1134
+ {
1135
+ title: titleOf("get_report_evidence"),
1136
+ description: descOf("get_report_evidence"),
1137
+ annotations: annotationsFor("get_report_evidence"),
1138
+ inputSchema: {
1139
+ report_id: z.string().describe("Report UUID.")
1140
+ }
1141
+ },
1142
+ async (args) => {
1143
+ const [reportRes, timelineRes] = await Promise.allSettled([
1144
+ apiCall(`/v1/admin/reports/${args.report_id}`),
1145
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`)
1146
+ ]);
1147
+ const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1148
+ const timeline = timelineRes.status === "fulfilled" ? timelineRes.value : null;
1149
+ const evidence = report ? {
1150
+ report_id: args.report_id,
1151
+ description: report.description,
1152
+ summary: report.summary,
1153
+ screenshot_url: report.screenshot_url ?? null,
1154
+ environment: report.environment ?? null,
1155
+ breadcrumbs: report.breadcrumbs ?? null,
1156
+ sentry_replay_id: report.sentry_replay_id ?? null,
1157
+ sentry_trace_id: report.sentry_trace_id ?? null,
1158
+ sentry_event_id: report.sentry_event_id ?? null,
1159
+ session_id: report.session_id ?? null,
1160
+ created_at: report.created_at,
1161
+ tags: report.tags ?? null
1162
+ } : { error: String(reportRes.reason) };
1163
+ return jsonText({
1164
+ evidence,
1165
+ reporter_thread: timeline ?? { error: String(timelineRes.reason) }
1166
+ });
1167
+ }
1168
+ );
1169
+ server.registerTool(
1170
+ "triage_issue",
1171
+ {
1172
+ title: titleOf("triage_issue"),
1173
+ description: descOf("triage_issue"),
1174
+ annotations: annotationsFor("triage_issue"),
1175
+ inputSchema: {
1176
+ report_id: z.string().describe("Report UUID to triage."),
1177
+ project_id: z.string().optional().describe("Project UUID. Defaults to configured project."),
1178
+ include_logs: z.boolean().optional().describe("Include recent pipeline logs in triage packet (default: true).")
1179
+ }
1180
+ },
1181
+ async (args) => {
1182
+ const pid = args.project_id ?? projectId;
1183
+ const includeLogs = args.include_logs !== false;
1184
+ const [reportRes, evidenceRes, similarRes, fixCtxRes, blastRes, logsRes] = await Promise.allSettled([
1185
+ apiCall(`/v1/admin/reports/${args.report_id}`),
1186
+ apiCall(`/v1/admin/reports/${args.report_id}/timeline`),
1187
+ apiCall(`/v1/admin/reports/similarity`, {
1188
+ method: "POST",
1189
+ body: JSON.stringify({ report_id: args.report_id })
1190
+ }).catch(() => null),
1191
+ pid ? apiCall(`/v1/admin/reports/${args.report_id}/fix-context`).catch(() => null) : Promise.resolve(null),
1192
+ pid ? apiCall(`/v1/admin/reports/${args.report_id}/blast-radius`).catch(() => null) : Promise.resolve(null),
1193
+ includeLogs && pid ? apiCall(`/v1/admin/mcp/logs/${pid}?limit=20&level=warn`).catch(() => null) : Promise.resolve(null)
1194
+ ]);
1195
+ const report = reportRes.status === "fulfilled" ? reportRes.value : null;
1196
+ const severity = report ? report.severity : "unknown";
1197
+ const category = report ? report.category : "unknown";
1198
+ const status = report ? report.status : "unknown";
1199
+ const actions = [];
1200
+ if (status === "new" || status === "classified") {
1201
+ actions.push({
1202
+ action: "dispatch_fix",
1203
+ reason: "Report is classified but no fix has been attempted",
1204
+ tool: "dispatch_fix",
1205
+ args: { reportId: args.report_id, agent: "cursor_cloud" }
1206
+ });
1207
+ } else if (status === "fixing") {
1208
+ actions.push({
1209
+ action: "check_fix_progress",
1210
+ reason: "Fix is in progress \u2014 check the fix timeline for latest status",
1211
+ tool: "get_fix_timeline",
1212
+ args: { reportId: args.report_id }
1213
+ });
1214
+ } else if (status === "fixed") {
1215
+ actions.push({
1216
+ action: "verify_fix",
1217
+ reason: "Fix was applied \u2014 verify it resolved the issue",
1218
+ tool: "get_fix_context",
1219
+ args: { reportId: args.report_id }
1220
+ });
1221
+ }
1222
+ return jsonText({
1223
+ report_id: args.report_id,
1224
+ severity,
1225
+ category,
1226
+ status,
1227
+ report: reportRes.status === "fulfilled" ? reportRes.value : { error: String(reportRes.reason) },
1228
+ reporter_thread: evidenceRes.status === "fulfilled" ? evidenceRes.value : null,
1229
+ similar_bugs: similarRes.status === "fulfilled" ? similarRes.value : null,
1230
+ fix_context: fixCtxRes.status === "fulfilled" ? fixCtxRes.value : null,
1231
+ blast_radius: blastRes.status === "fulfilled" ? blastRes.value : null,
1232
+ recent_logs: logsRes.status === "fulfilled" ? logsRes.value : null,
1233
+ recommended_actions: actions,
1234
+ triage_summary: `[${severity}] ${category} \u2014 status: ${status}. ${actions.length > 0 ? `Recommended: ${actions[0]?.action}.` : "No action required."}`
1235
+ });
1236
+ }
1237
+ );
1011
1238
  server.registerTool(
1012
1239
  "submit_fix_result",
1013
1240
  {
@@ -1171,8 +1398,8 @@ function createMushiServer(config) {
1171
1398
  server.registerTool(
1172
1399
  "reopen_report",
1173
1400
  {
1174
- title: "Reopen report (operator)",
1175
- description: "Operator alias to move a report back to new for regression triage.",
1401
+ title: titleOf("reopen_report"),
1402
+ description: descOf("reopen_report"),
1176
1403
  annotations: annotationsFor("reopen_report"),
1177
1404
  inputSchema: {
1178
1405
  reportId: z.string().describe("Report UUID"),
@@ -1227,6 +1454,55 @@ function createMushiServer(config) {
1227
1454
  return jsonText(data);
1228
1455
  }
1229
1456
  );
1457
+ server.registerTool(
1458
+ "query_lessons",
1459
+ {
1460
+ title: titleOf("query_lessons"),
1461
+ description: descOf("query_lessons"),
1462
+ annotations: annotationsFor("query_lessons"),
1463
+ inputSchema: {
1464
+ diff_text: z.string().describe("The PR diff, code snippet, or description of the change being made."),
1465
+ max_tokens: z.number().optional().describe("Maximum tokens for returned lessons context (default 3000, max 8000)."),
1466
+ top_k: z.number().optional().describe("Max number of lessons to return (default 15, max 50)."),
1467
+ project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1468
+ }
1469
+ },
1470
+ async (args) => {
1471
+ const pid = args.project_id ?? projectId;
1472
+ const data = await apiCall("/v1/admin/lessons/query", {
1473
+ method: "POST",
1474
+ body: JSON.stringify({
1475
+ diff_text: args.diff_text,
1476
+ ...args.max_tokens !== void 0 ? { max_tokens: args.max_tokens } : {},
1477
+ ...args.top_k !== void 0 ? { top_k: args.top_k } : {},
1478
+ ...pid ? { project_id: pid } : {}
1479
+ })
1480
+ });
1481
+ return jsonText(data);
1482
+ }
1483
+ );
1484
+ server.registerTool(
1485
+ "list_lessons",
1486
+ {
1487
+ title: titleOf("list_lessons"),
1488
+ description: descOf("list_lessons"),
1489
+ annotations: annotationsFor("list_lessons"),
1490
+ inputSchema: {
1491
+ severity: z.enum(["info", "warn", "critical"]).optional().describe("Filter by severity level."),
1492
+ limit: z.number().optional().describe("Max number of lessons to return (default 50, max 200)."),
1493
+ project_id: z.string().optional().describe("Project UUID. Defaults to configured project.")
1494
+ }
1495
+ },
1496
+ async (args) => {
1497
+ const pid = args.project_id ?? projectId;
1498
+ const params = new URLSearchParams();
1499
+ if (args.severity) params.set("severity", args.severity);
1500
+ params.set("limit", String(Math.min(args.limit ?? 50, 200)));
1501
+ if (pid) params.set("projectId", pid);
1502
+ const data = await apiCall(`/v1/admin/lessons?${params}`);
1503
+ return jsonText(data);
1504
+ }
1505
+ );
1230
1506
  server.resource(
1231
1507
  "project_stats",
1232
1508
  "project://stats",
@@ -1251,15 +1527,17 @@ function createMushiServer(config) {
1251
1527
  contents: [{ uri: "project://dashboard", mimeType: "application/json", text: JSON.stringify(await apiCall("/v1/admin/dashboard"), null, 2) }]
1252
1528
  })
1253
1529
  );
1254
- const rewardsMeta = (name) => annotationsFor(name);
1255
- server.tool(
1530
+ server.registerTool(
1256
1531
  "list_top_contributors",
1257
- rewardsMeta("list_top_contributors").title,
1258
1532
  {
1259
- limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
1260
- range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
1533
+ title: titleOf("list_top_contributors"),
1534
+ description: descOf("list_top_contributors"),
1535
+ annotations: annotationsFor("list_top_contributors"),
1536
+ inputSchema: {
1537
+ limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
1538
+ range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
1539
+ }
1261
1540
  },
1262
- rewardsMeta("list_top_contributors"),
1263
1541
  async ({ limit, range }) => ({
1264
1542
  content: [{
1265
1543
  type: "text",
@@ -1271,15 +1549,18 @@ function createMushiServer(config) {
1271
1549
  }]
1272
1550
  })
1273
1551
  );
1274
- server.tool(
1552
+ server.registerTool(
1275
1553
  "award_bonus_points",
1276
- rewardsMeta("award_bonus_points").title,
1277
1554
  {
1278
- external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1279
- points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
1280
- reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
1555
+ title: titleOf("award_bonus_points"),
1556
+ description: descOf("award_bonus_points"),
1557
+ annotations: annotationsFor("award_bonus_points"),
1558
+ inputSchema: {
1559
+ external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1560
+ points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
1561
+ reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
1562
+ }
1281
1563
  },
1282
- rewardsMeta("award_bonus_points"),
1283
1564
  async ({ external_user_id, points, reason }) => ({
1284
1565
  content: [{
1285
1566
  type: "text",
@@ -1295,15 +1576,18 @@ function createMushiServer(config) {
1295
1576
  }]
1296
1577
  })
1297
1578
  );
1298
- server.tool(
1579
+ server.registerTool(
1299
1580
  "set_tier",
1300
- rewardsMeta("set_tier").title,
1301
1581
  {
1302
- external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1303
- tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
1304
- reason: z.string().max(200).optional().describe("Optional reason for manual override")
1582
+ title: titleOf("set_tier"),
1583
+ description: descOf("set_tier"),
1584
+ annotations: annotationsFor("set_tier"),
1585
+ inputSchema: {
1586
+ external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
1587
+ tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
1588
+ reason: z.string().max(200).optional().describe("Optional reason for manual override")
1589
+ }
1305
1590
  },
1306
- rewardsMeta("set_tier"),
1307
1591
  async ({ external_user_id, tier_slug, reason }) => ({
1308
1592
  content: [{
1309
1593
  type: "text",
@@ -0,0 +1,54 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { McpScope } from './catalog.js';
3
+
4
+ /**
5
+ * FILE: packages/mcp/src/server.ts
6
+ * PURPOSE: Testable MCP server factory. Exports `createMushiServer()` which
7
+ * builds a fully-configured `McpServer` but does not connect to any
8
+ * transport — allowing the stdio entry (`src/index.ts`) and the
9
+ * in-memory integration tests to share the exact same tool
10
+ * implementations. Keeping the module side-effect-free is critical:
11
+ * the old design called `main()` at import time, which made it
12
+ * impossible to mock `fetch` before the server had wired up its
13
+ * closures.
14
+ *
15
+ * Every tool/resource is a thin, typed wrapper over the Mushi REST
16
+ * API; auth is an API key sent in two headers so the shared
17
+ * `adminOrApiKey` middleware accepts it regardless of which header
18
+ * a proxy strips first.
19
+ */
20
+
21
+ declare class MushiApiError extends Error {
22
+ readonly status: number;
23
+ readonly code: string;
24
+ constructor(status: number, code: string, message: string);
25
+ }
26
+ interface MushiServerConfig {
27
+ /** Server version — surfaced in MCP handshake. Read from package.json by boot. */
28
+ version: string;
29
+ /** Base URL of the Mushi API, e.g. https://xyz.supabase.co/functions/v1/api */
30
+ apiEndpoint: string;
31
+ /** Project API key with `mcp:read` or `mcp:write` scope. */
32
+ apiKey: string;
33
+ /** Optional project hint. Used to scope multi-project tools. */
34
+ projectId?: string;
35
+ /**
36
+ * Granted scopes from the API key. When provided, only tools/resources that
37
+ * require a subset of these scopes are registered. Defaults to ALL_SCOPES.
38
+ */
39
+ scopes?: readonly McpScope[];
40
+ /**
41
+ * Fetch implementation — overridable for tests. Tests pass a spy that
42
+ * asserts request shape and returns canned envelopes without hitting the
43
+ * network. Defaults to the global `fetch`.
44
+ */
45
+ fetch?: typeof fetch;
46
+ }
47
+ /**
48
+ * Build an MCP server instance pre-wired with every Mushi tool, resource,
49
+ * and prompt. Does NOT call `server.connect()` — the caller binds whatever
50
+ * transport they need (stdio for the CLI, InMemoryTransport for tests).
51
+ */
52
+ declare function createMushiServer(config: MushiServerConfig): McpServer;
53
+
54
+ export { MushiApiError, type MushiServerConfig, createMushiServer };