@mushi-mushi/mcp 0.8.0 → 0.10.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/CONTRIBUTING.md CHANGED
@@ -101,17 +101,6 @@ Releases are fully automated. Maintainers don't run `npm publish` by hand.
101
101
 
102
102
  If GitHub's anti-loop protection suppresses the auto re-fire (the squash merge can be attributed to `github-actions[bot]`), trigger the workflow manually: **Actions → release → Run workflow → master**.
103
103
 
104
- ### Known CI/CD quirks and their automatic safeguards
105
-
106
- A handful of GitHub-Actions × Changesets edge cases have caused release-pipeline stalls in the past. Each is now caught automatically — keep these in mind when you see the symptom:
107
-
108
- | Symptom | Root cause | Automatic safeguard |
109
- | --- | --- | --- |
110
- | The `Build & Test` required check never registers on the `chore: version packages` PR — the PR stays "Required check missing" forever | `changeset-release/master` is pushed by `github-actions[bot]`. GitHub silently drops the downstream `pull_request` event to prevent bot loops (observed on PR #45, #102, #124). | `ci.yml` now also triggers on `push` to `changeset-release/master`, so `Build & Test` reports against the head commit directly. No empty-commit nudge needed. |
111
- | Release workflow fails with `No commits between master and changeset-release/master` after merging a PR with a new changeset. | A `.changeset/*.md` file whose YAML frontmatter only targets packages listed in `.changeset/config.json#ignore` (e.g. `@mushi-mushi/server`, `@mushi-mushi/admin`). `changeset version` produces no bumps, the version PR is empty, the next push errors (PR #102 / #121, 2026-05-19). | `pnpm check:changeset-orphans` runs in both `ci.yml` and `release.yml`. PR CI fails with an actionable message naming the orphan file *before* it can reach master. If you legitimately need to record an internal-only change, omit the changeset entirely — the diff lives in git history. |
112
- | Release workflow's `Audit signatures of installed dependencies` step fails with `npm ETARGET / No matching version found for @mushi-mushi/<pkg>@<ver>` seconds after the publish step succeeded. | npm's registry CDN can take up to ~30s to propagate a freshly-published manifest. The audit step shells out to `npm install` against the just-published version and races the CDN (observed 2026-05-20, run 26149167393). | The audit step retries with exponential backoff (1, 2, 4, 8, 16, 32s — 63s total) before failing. Sigstore signatures are written at publish time, so a one-off audit failure never indicates a corrupted package — `pnpm view <pkg> version` is the ground truth. |
113
- | Push to `master` after merging a PR doesn't fire the `Release` workflow. | Same anti-loop protection: when a squash merge is attributed to `github-actions[bot]`, GitHub may suppress the downstream `push` trigger. Sporadic — observed twice in the last 60 days. | `release.yml` keeps `workflow_dispatch` as the manual fallback. Recovery: **Actions → Release → Run workflow → master**. The `Build & Verify` job re-runs identically to the auto-fired path. |
114
-
115
104
  ### Adding a brand-new publishable package
116
105
 
117
106
  Trusted Publisher bindings are configured **per package** on `npmjs.com` and require the package to already exist on the registry. New packages therefore need a one-time bootstrap before OIDC can take over.
package/README.md CHANGED
@@ -1,18 +1,44 @@
1
- # @mushi-mushi/mcp
1
+ # mushi-mcp
2
2
 
3
- [Model Context Protocol](https://spec.modelcontextprotocol.io/) server that wires Mushi Mushi's closed-loop bug intelligence into your AI coding agent so Cursor, Claude Code, Copilot, and any other MCP-compatible client can read classified reports, query the lesson library, and dispatch AI-generated fixes directly from the editor.
3
+ > **Sentry sees what code throws. Mushi sees what users feel — and closes the loop with AI.**
4
4
 
5
- **The evolutionary angle:** the lesson library (`lessons.query` tool) is the agent's institutional memory. Every bug cluster your team has ever named is available to the agent before it touches a single line of code — so it doesn't repeat the same class of mistake the last agent made. That's the "cumulative selection" loop the [main README](https://www.npmjs.com/package/mushi-mushi) describes, delivered to your IDE.
5
+ [Model Context Protocol](https://spec.modelcontextprotocol.io/) server that wires Mushi's **evolution loop** into your AI coding agent. The loop is already running in your Mushi project:
6
+
7
+ ```
8
+ User feels a bug → Mushi captures it → AI triages → AI opens a PR
9
+ → QA verifies → Judge scores → Lesson library remembers → next agent is smarter
10
+ ```
11
+
12
+ Wire it into Cursor, Claude Code, or any MCP client in one command:
13
+
14
+ ```bash
15
+ npx mushi-mushi setup --ide cursor
16
+ # or: npx mushi-mushi setup --ide claude
17
+ ```
18
+
19
+ That command reads `~/.mushirc`, writes `.cursor/mcp.json` with the `mushi` server block, and prints "Done — restart Cursor and ask: `list mushi tools`". No copy-pasting environment variables.
6
20
 
7
21
  > **What this is, and what it isn't**
8
22
  >
9
- > - **This package** is the MCP **server** — runs locally next to your editor, talks to the Mushi Mushi API, and presents bug reports as MCP tools/resources to your coding agent.
23
+ > - **This package** (`mushi-mcp`) is the MCP **server** — runs locally next to your editor, talks to the Mushi API, and presents bug reports as MCP tools/resources to your coding agent. The npm package name is `mushi-mcp`; the scoped alias `@mushi-mushi/mcp` still works.
10
24
  > - **`@mushi-mushi/agents`** ships the MCP **client adapter** — used by the autofix orchestrator when your project's `autofix_agent = 'mcp'`. See `packages/agents/src/adapters/mcp.ts`.
11
25
  > - The `generic_mcp` adapter shipped before V5.3 was a misnomer (it spoke plain REST). It is now `RestFixWorkerAgent`; the old export is kept as a deprecated alias for one more minor.
12
26
 
13
27
  ## Quick start
14
28
 
15
- ### 1. With Claude Desktop
29
+ ### 0. One-liner (recommended)
30
+
31
+ ```bash
32
+ # First: make sure you've logged in
33
+ npx mushi-mushi login --api-key mushi_xxx --endpoint https://<ref>.supabase.co/functions/v1/api
34
+
35
+ # Then wire your IDE
36
+ npx mushi-mushi setup --ide cursor # Cursor
37
+ npx mushi-mushi setup --ide claude # Claude Code / Claude Desktop
38
+ npx mushi-mushi setup --ide cursor --with-rules # also write .cursorrules
39
+ ```
40
+
41
+ ### 1. With Claude Desktop (manual)
16
42
 
17
43
  Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
18
44
 
package/dist/index.js CHANGED
@@ -129,6 +129,15 @@ var TOOL_CATALOG = [
129
129
  hints: { readOnly: true, idempotent: true, openWorld: true },
130
130
  useCase: "What did Stage 2 say we should try for this report?"
131
131
  },
132
+ // --- Setup / admin -------------------------------------------------------
133
+ {
134
+ name: "setup_check",
135
+ title: "Dispatch preflight check",
136
+ description: "Run the 4 dispatch-readiness checks for a project and return their pass/fail status (GitHub repo connected, codebase indexed, Anthropic BYOK key present, autofix enabled). Also returns the target repo URL when GitHub is connected. Use this before calling dispatch_fix to understand why a dispatch might fail \u2014 or to validate that the onboarding wizard is complete.",
137
+ scope: "mcp:read",
138
+ hints: { readOnly: true, idempotent: true, openWorld: true },
139
+ useCase: "Is this project ready to auto-fix bugs? What is blocking me?"
140
+ },
132
141
  // --- Write / agentic ----------------------------------------------------
133
142
  {
134
143
  name: "submit_fix_result",
@@ -198,6 +207,88 @@ var TOOL_CATALOG = [
198
207
  scope: "mcp:write",
199
208
  hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
200
209
  useCase: "Manually promote this user to Champion tier as a thank-you."
210
+ },
211
+ {
212
+ name: "setup_repo_for_mushi",
213
+ title: "Bootstrap repo for Mushi",
214
+ description: "Writes the three Mushi bootstrap files into the current repo root: `.cursorrules` (Cursor evolution-loop coding rules), `.mushi/lessons.json` (initial empty lesson cache), and `MUSHI.md` (one-page project contract for agents). Idempotent \u2014 safe to re-run after lessons sync. Requires mcp:write scope. Call this once after connecting the repo; subsequently use `mushi sync-lessons` from CI to keep lessons current.",
215
+ scope: "mcp:write",
216
+ hints: { readOnly: false, destructive: false, idempotent: true, openWorld: false },
217
+ useCase: "Set up this repo for the Mushi evolution loop in one step."
218
+ }
219
+ ];
220
+ var TDD_TOOL_CATALOG = [
221
+ {
222
+ name: "map_user_stories",
223
+ title: "Map user stories from live app",
224
+ description: 'Crawl a live application URL with Firecrawl/Browserbase and ask Claude to draft an inventory.yaml with pages and user stories. Creates a story_map_run row for progress tracking, then writes an inventory_proposals row (source=live_crawl). Optionally dispatches a Cursor Cloud agent to refine the draft and open a PR. Returns { runId, status: "pending" } immediately \u2014 poll get_map_run_status for progress.',
225
+ scope: "mcp:write",
226
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
227
+ useCase: "Map the user stories in my live app automatically without writing YAML by hand."
228
+ },
229
+ {
230
+ name: "get_map_run_status",
231
+ title: "Story map run status",
232
+ description: "Get the status and results of a story_map_run (pending \u2192 running \u2192 completed/failed). Returns pages_crawled, proposal_id (once done), and cursor_pr_url if Cursor Cloud refined the draft.",
233
+ scope: "mcp:read",
234
+ hints: { readOnly: true, idempotent: true, openWorld: true },
235
+ useCase: "Is my story mapping crawl done yet?"
236
+ },
237
+ {
238
+ name: "generate_tdd_from_story",
239
+ title: "Generate TDD test from user story",
240
+ description: "Given a user story id (from the accepted inventory), ask Claude to write a full Playwright TypeScript test. Inserts a qa_stories row (source=test_gen_from_story) with approval_status driven by automation_mode. Optionally opens a draft GitHub PR. Returns { qaStoryId, prUrl, approvalStatus, needsHumanReview }.",
241
+ scope: "mcp:write",
242
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
243
+ useCase: "Generate a Playwright test for this user story."
244
+ },
245
+ {
246
+ name: "improve_qa_story",
247
+ title: "PDCA auto-improve a failing QA story",
248
+ description: "Trigger the PDCA improver for a specific project. Finds recently failed qa_story_runs and uses Claude to write improved test scripts. New tests are created with source=pdca and approval gated by the original story's automation_mode.",
249
+ scope: "mcp:write",
250
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
251
+ useCase: "Fix my failing QA tests automatically."
252
+ },
253
+ {
254
+ name: "run_qa_story",
255
+ title: "Trigger a manual QA story run",
256
+ description: 'Queue a manual run for an enabled + approved qa_story. Returns the run id immediately; poll qa_story_runs or use get_report_detail for progress. Equivalent to "Run now" in the console.',
257
+ scope: "mcp:write",
258
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
259
+ useCase: "Run the login flow test right now."
260
+ },
261
+ {
262
+ name: "list_byok_keys",
263
+ title: "List BYOK API key pool",
264
+ description: "List all BYOK API keys for the project, grouped by provider. Shows label, priority, status, and cooldown. Never returns the raw key value \u2014 only metadata. Use this to see which keys are active or exhausted.",
265
+ scope: "mcp:read",
266
+ hints: { readOnly: true, idempotent: true, openWorld: true },
267
+ useCase: "Which API keys are active and which are rate-limited?"
268
+ },
269
+ {
270
+ name: "add_byok_key",
271
+ title: "Add a BYOK API key",
272
+ description: "Add a new API key to the project's BYOK pool for a given provider (anthropic, openai, firecrawl, browserbase, cursor). Specify label and priority for ordering. The key is stored encrypted in Supabase Vault.",
273
+ scope: "mcp:write",
274
+ hints: { readOnly: false, destructive: false, idempotent: false, openWorld: true },
275
+ useCase: "Add a backup Anthropic key to the pool."
276
+ },
277
+ {
278
+ name: "list_pending_review_stories",
279
+ title: "List QA stories pending review",
280
+ description: "Get the queue of TDD tests that were auto-generated and are waiting for human approval before they run in the QA schedule.",
281
+ scope: "mcp:read",
282
+ hints: { readOnly: true, idempotent: true, openWorld: true },
283
+ useCase: "What TDD tests need my approval today?"
284
+ },
285
+ {
286
+ name: "approve_qa_story",
287
+ title: "Approve or reject a pending QA story",
288
+ description: "Approve or reject a qa_story that is in pending_review. Approved stories are enabled in the QA schedule immediately.",
289
+ scope: "mcp:write",
290
+ hints: { readOnly: false, destructive: false, idempotent: true, openWorld: true },
291
+ useCase: "Approve this auto-generated test."
201
292
  }
202
293
  ];
203
294
 
@@ -217,7 +308,9 @@ var MushiApiError = class extends Error {
217
308
  function createMushiServer(config) {
218
309
  const { version, apiEndpoint, apiKey, projectId } = config;
219
310
  const doFetch = config.fetch ?? globalThis.fetch;
220
- const grantedScopes = new Set(config.scopes ?? ALL_SCOPES);
311
+ if (config.scopes !== void 0 && config.scopes.length === 0) {
312
+ return new McpServer({ name: "mushi-mushi", version });
313
+ }
221
314
  async function apiCall(path, options) {
222
315
  const res = await doFetch(`${apiEndpoint}${path}`, {
223
316
  ...options,
@@ -263,12 +356,19 @@ function createMushiServer(config) {
263
356
  content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
264
357
  };
265
358
  }
359
+ function jsonResult(value) {
360
+ return {
361
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
362
+ structuredContent: value
363
+ };
364
+ }
266
365
  const server = new McpServer({
267
366
  name: "mushi-mushi",
268
367
  version
269
368
  });
270
- function annotationsFor(name) {
271
- const spec = TOOL_CATALOG.find((t) => t.name === name);
369
+ const ALL_TOOL_CATALOG = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
370
+ function annotationsFor(name, catalog = ALL_TOOL_CATALOG) {
371
+ const spec = catalog.find((t) => t.name === name);
272
372
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
273
373
  const a = {
274
374
  title: spec.title,
@@ -279,33 +379,17 @@ function createMushiServer(config) {
279
379
  if (spec.hints.openWorld !== void 0) a.openWorldHint = spec.hints.openWorld;
280
380
  return a;
281
381
  }
282
- function descOf(name) {
283
- const spec = TOOL_CATALOG.find((t) => t.name === name);
382
+ function descOf(name, catalog = ALL_TOOL_CATALOG) {
383
+ const spec = catalog.find((t) => t.name === name);
284
384
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
285
385
  return spec.description;
286
386
  }
287
- function titleOf(name) {
288
- const spec = TOOL_CATALOG.find((t) => t.name === name);
387
+ function titleOf(name, catalog = ALL_TOOL_CATALOG) {
388
+ const spec = catalog.find((t) => t.name === name);
289
389
  if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
290
390
  return spec.title;
291
391
  }
292
- function shouldRegister(name) {
293
- const spec = TOOL_CATALOG.find((t) => t.name === name);
294
- if (!spec) throw new Error(`[mushi-mcp] tool "${name}" is missing from TOOL_CATALOG`);
295
- return grantedScopes.has(spec.scope);
296
- }
297
- function jsonResult(value) {
298
- return {
299
- content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
300
- structuredContent: value
301
- };
302
- }
303
- const _serverRegisterTool = server.registerTool.bind(server);
304
- const registerScopedTool = ((name, ...rest) => {
305
- if (!shouldRegister(name)) return void 0;
306
- return _serverRegisterTool(name, ...rest);
307
- });
308
- registerScopedTool(
392
+ server.registerTool(
309
393
  "get_recent_reports",
310
394
  {
311
395
  title: titleOf("get_recent_reports"),
@@ -317,12 +401,9 @@ function createMushiServer(config) {
317
401
  severity: z.string().optional().describe("Filter by severity: critical, high, medium, low"),
318
402
  limit: z.number().optional().describe("Max reports to return (default 20, max 100)")
319
403
  },
320
- // Output schema (MCP 2025-06-18): when set, the SDK validates the
321
- // tool's `structuredContent` and lets typed clients deserialize
322
- // without re-parsing the text payload.
323
404
  outputSchema: {
324
- reports: z.array(z.record(z.string(), z.unknown())).describe("Array of report rows"),
325
- total: z.number().describe("Total matching rows (before limit)")
405
+ reports: z.array(z.unknown()),
406
+ total: z.number()
326
407
  }
327
408
  },
328
409
  async (args) => {
@@ -332,13 +413,10 @@ function createMushiServer(config) {
332
413
  if (args.severity) params.set("severity", args.severity);
333
414
  params.set("limit", String(Math.min(args.limit ?? 20, 100)));
334
415
  const data = await apiCall(`/v1/admin/reports?${params}`);
335
- return jsonResult({
336
- reports: data.reports ?? [],
337
- total: data.total ?? 0
338
- });
416
+ return jsonResult(data);
339
417
  }
340
418
  );
341
- registerScopedTool(
419
+ server.registerTool(
342
420
  "get_report_detail",
343
421
  {
344
422
  title: titleOf("get_report_detail"),
@@ -348,7 +426,7 @@ function createMushiServer(config) {
348
426
  },
349
427
  async (args) => jsonText(await apiCall(`/v1/admin/reports/${args.reportId}`))
350
428
  );
351
- registerScopedTool(
429
+ server.registerTool(
352
430
  "search_reports",
353
431
  {
354
432
  title: titleOf("search_reports"),
@@ -360,7 +438,7 @@ function createMushiServer(config) {
360
438
  threshold: z.number().optional().describe("Similarity threshold 0..1, default 0.2")
361
439
  },
362
440
  outputSchema: {
363
- results: z.array(z.record(z.string(), z.unknown())).describe("Ranked report rows with similarity scores")
441
+ results: z.array(z.unknown())
364
442
  }
365
443
  },
366
444
  async (args) => {
@@ -373,10 +451,10 @@ function createMushiServer(config) {
373
451
  ...projectId ? { projectId } : {}
374
452
  })
375
453
  });
376
- return jsonResult({ results: data.results ?? [] });
454
+ return jsonResult(data);
377
455
  }
378
456
  );
379
- registerScopedTool(
457
+ server.registerTool(
380
458
  "get_similar_bugs",
381
459
  {
382
460
  title: titleOf("get_similar_bugs"),
@@ -385,9 +463,6 @@ function createMushiServer(config) {
385
463
  inputSchema: {
386
464
  query: z.string().describe("Component name, page path, or bug description"),
387
465
  limit: z.number().optional().describe("Max results (default 5, max 20)")
388
- },
389
- outputSchema: {
390
- results: z.array(z.record(z.string(), z.unknown())).describe("Ranked report rows with similarity scores")
391
466
  }
392
467
  },
393
468
  async (args) => {
@@ -400,10 +475,10 @@ function createMushiServer(config) {
400
475
  ...projectId ? { projectId } : {}
401
476
  })
402
477
  });
403
- return jsonResult({ results: data.results ?? [] });
478
+ return jsonText(data);
404
479
  }
405
480
  );
406
- registerScopedTool(
481
+ server.registerTool(
407
482
  "get_fix_context",
408
483
  {
409
484
  title: titleOf("get_fix_context"),
@@ -422,7 +497,7 @@ function createMushiServer(config) {
422
497
  });
423
498
  }
424
499
  );
425
- registerScopedTool(
500
+ server.registerTool(
426
501
  "get_fix_timeline",
427
502
  {
428
503
  title: titleOf("get_fix_timeline"),
@@ -432,7 +507,7 @@ function createMushiServer(config) {
432
507
  },
433
508
  async (args) => jsonText(await apiCall(`/v1/admin/fixes/${args.fixId}/timeline`))
434
509
  );
435
- registerScopedTool(
510
+ server.registerTool(
436
511
  "get_blast_radius",
437
512
  {
438
513
  title: titleOf("get_blast_radius"),
@@ -442,7 +517,7 @@ function createMushiServer(config) {
442
517
  },
443
518
  async (args) => jsonText(await apiCall(`/v1/admin/graph/blast-radius/${args.nodeId}`))
444
519
  );
445
- registerScopedTool(
520
+ server.registerTool(
446
521
  "get_knowledge_graph",
447
522
  {
448
523
  title: titleOf("get_knowledge_graph"),
@@ -461,7 +536,7 @@ function createMushiServer(config) {
461
536
  return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
462
537
  }
463
538
  );
464
- registerScopedTool(
539
+ server.registerTool(
465
540
  "graph_neighborhood",
466
541
  {
467
542
  title: titleOf("graph_neighborhood"),
@@ -480,7 +555,7 @@ function createMushiServer(config) {
480
555
  return jsonText(await apiCall(`/v1/admin/graph/traverse?${params}`));
481
556
  }
482
557
  );
483
- registerScopedTool(
558
+ server.registerTool(
484
559
  "graph_node_status",
485
560
  {
486
561
  title: titleOf("graph_node_status"),
@@ -490,7 +565,7 @@ function createMushiServer(config) {
490
565
  },
491
566
  async (args) => jsonText(await apiCall(`/v1/admin/graph/node/${args.nodeId}`))
492
567
  );
493
- registerScopedTool(
568
+ server.registerTool(
494
569
  "inventory_get",
495
570
  {
496
571
  title: titleOf("inventory_get"),
@@ -506,7 +581,7 @@ function createMushiServer(config) {
506
581
  return jsonText(await apiCall(`/v1/admin/inventory/${pid}`));
507
582
  }
508
583
  );
509
- registerScopedTool(
584
+ server.registerTool(
510
585
  "inventory_diff",
511
586
  {
512
587
  title: titleOf("inventory_diff"),
@@ -525,7 +600,7 @@ function createMushiServer(config) {
525
600
  return jsonText(await apiCall(`/v1/admin/inventory/${pid}/diff?${q}`));
526
601
  }
527
602
  );
528
- registerScopedTool(
603
+ server.registerTool(
529
604
  "inventory_findings",
530
605
  {
531
606
  title: titleOf("inventory_findings"),
@@ -547,7 +622,7 @@ function createMushiServer(config) {
547
622
  return jsonText(await apiCall(`/v1/admin/inventory/${pid}/findings${suffix}`));
548
623
  }
549
624
  );
550
- registerScopedTool(
625
+ server.registerTool(
551
626
  "fix_suggest",
552
627
  {
553
628
  title: titleOf("fix_suggest"),
@@ -568,7 +643,7 @@ function createMushiServer(config) {
568
643
  });
569
644
  }
570
645
  );
571
- registerScopedTool(
646
+ server.registerTool(
572
647
  "run_nl_query",
573
648
  {
574
649
  title: titleOf("run_nl_query"),
@@ -584,7 +659,44 @@ function createMushiServer(config) {
584
659
  return jsonText(data);
585
660
  }
586
661
  );
587
- registerScopedTool(
662
+ server.registerTool(
663
+ "setup_check",
664
+ {
665
+ title: titleOf("setup_check"),
666
+ description: descOf("setup_check"),
667
+ annotations: annotationsFor("setup_check"),
668
+ inputSchema: {
669
+ projectId: z.string().optional().describe(
670
+ "Project UUID to check. Falls back to the projectId the server was initialised with."
671
+ )
672
+ }
673
+ },
674
+ async (args) => {
675
+ const resolvedId = args.projectId ?? projectId;
676
+ if (!resolvedId) {
677
+ return jsonText({
678
+ ok: false,
679
+ error: "No projectId provided and none configured on the MCP server. Pass projectId explicitly."
680
+ });
681
+ }
682
+ const data = await apiCall(`/v1/admin/projects/${resolvedId}/preflight`);
683
+ const summary = data.checks.map((c) => ({
684
+ check: c.key,
685
+ label: c.label,
686
+ passed: c.ready,
687
+ hint: c.hint,
688
+ fixPath: c.fixHref
689
+ }));
690
+ return jsonText({
691
+ ready: data.ready,
692
+ repoUrl: data.repoUrl ?? null,
693
+ checks: summary,
694
+ // Human-readable summary for agents that paste the result into a prompt
695
+ summary: data.ready ? `Project ${resolvedId} is ready to dispatch auto-fixes${data.repoUrl ? ` (target: ${data.repoUrl})` : ""}.` : `Project ${resolvedId} cannot dispatch yet \u2014 ${summary.filter((c) => !c.passed).map((c) => c.label).join(", ")}.`
696
+ });
697
+ }
698
+ );
699
+ server.registerTool(
588
700
  "submit_fix_result",
589
701
  {
590
702
  title: titleOf("submit_fix_result"),
@@ -619,7 +731,7 @@ function createMushiServer(config) {
619
731
  return jsonText({ ok: true, fixId: created.fixId });
620
732
  }
621
733
  );
622
- registerScopedTool(
734
+ server.registerTool(
623
735
  "dispatch_fix",
624
736
  {
625
737
  title: titleOf("dispatch_fix"),
@@ -627,20 +739,13 @@ function createMushiServer(config) {
627
739
  annotations: annotationsFor("dispatch_fix"),
628
740
  inputSchema: {
629
741
  reportId: z.string().describe("Report UUID to fix"),
630
- agent: z.enum(["claude_code", "codex", "rest_worker", "mcp", "cursor_cloud"]).optional().describe('Override the agent adapter. Use "cursor_cloud" to dispatch a Cursor Cloud Agent that opens a signed draft PR.'),
631
- backend: z.enum(["default", "claude_code", "cursor_cloud", "mcp"]).optional().describe("Alias for agent \u2014 prefer agent. When both are set, agent wins."),
632
- cursorModel: z.string().optional().describe('Optional model override when agent=cursor_cloud (e.g. "composer-latest").'),
742
+ agent: z.enum(["claude_code", "codex", "rest_worker", "mcp"]).optional().describe("Override the agent adapter"),
633
743
  idempotencyKey: z.string().uuid().optional().describe("Optional RFC 4122 UUID. Resend the same key to safely retry without dispatching a duplicate fix job (Idempotency-Key IETF draft)."),
634
744
  inventoryActionNodeId: z.string().uuid().optional().describe("Optional inventory Action node UUID for spec-traceability (\xA72.10). When provided, the fix-worker embeds the expected_outcome contract in the LLM prompt and runs validateAgainstSpec before opening the PR.")
635
745
  },
636
- // Typed write-tool result. fixId is the cursor for get_fix_timeline so
637
- // downstream tools can chain without re-parsing the text payload.
638
746
  outputSchema: {
639
- fixId: z.string().describe("Newly created fix_attempt UUID"),
640
- status: z.string().optional().describe("Initial status (queued, running, delegated, \u2026)"),
641
- agentId: z.string().optional().describe("Cursor agent ID (bc-\u2026) when agent=cursor_cloud"),
642
- runId: z.string().optional().describe("Cursor run ID when agent=cursor_cloud"),
643
- prUrl: z.string().optional().describe("Draft PR URL when agent=cursor_cloud and auto_create_pr=true")
747
+ fixId: z.string(),
748
+ status: z.string()
644
749
  }
645
750
  },
646
751
  async (args, extra) => {
@@ -658,31 +763,20 @@ function createMushiServer(config) {
658
763
  } catch {
659
764
  }
660
765
  }
661
- const resolvedAgent = args.agent ?? (args.backend !== "default" ? args.backend : void 0);
662
- const data = await apiCall(
663
- "/v1/admin/fixes/dispatch",
664
- {
665
- method: "POST",
666
- headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
667
- body: JSON.stringify({
668
- reportId: args.reportId,
669
- agent: resolvedAgent,
670
- inventoryActionNodeId: args.inventoryActionNodeId,
671
- ...args.cursorModel ? { cursorModel: args.cursorModel } : {},
672
- ...projectId ? { projectId } : {}
673
- })
674
- }
675
- );
676
- return jsonResult({
677
- fixId: data.fixId ?? "",
678
- ...data.status ? { status: data.status } : {},
679
- ...data.agentId ? { agentId: data.agentId } : {},
680
- ...data.runId ? { runId: data.runId } : {},
681
- ...data.prUrl ? { prUrl: data.prUrl } : {}
766
+ const data = await apiCall("/v1/admin/fixes/dispatch", {
767
+ method: "POST",
768
+ headers: args.idempotencyKey ? { "Idempotency-Key": args.idempotencyKey } : void 0,
769
+ body: JSON.stringify({
770
+ reportId: args.reportId,
771
+ agent: args.agent,
772
+ inventoryActionNodeId: args.inventoryActionNodeId,
773
+ ...projectId ? { projectId } : {}
774
+ })
682
775
  });
776
+ return jsonResult(data);
683
777
  }
684
778
  );
685
- registerScopedTool(
779
+ server.registerTool(
686
780
  "trigger_judge",
687
781
  {
688
782
  title: titleOf("trigger_judge"),
@@ -704,7 +798,7 @@ function createMushiServer(config) {
704
798
  return jsonText(data);
705
799
  }
706
800
  );
707
- registerScopedTool(
801
+ server.registerTool(
708
802
  "test_gen_from_report",
709
803
  {
710
804
  title: titleOf("test_gen_from_report"),
@@ -725,7 +819,7 @@ function createMushiServer(config) {
725
819
  return jsonText(data);
726
820
  }
727
821
  );
728
- registerScopedTool(
822
+ server.registerTool(
729
823
  "transition_status",
730
824
  {
731
825
  title: titleOf("transition_status"),
@@ -745,6 +839,26 @@ function createMushiServer(config) {
745
839
  return jsonText(data);
746
840
  }
747
841
  );
842
+ server.registerTool(
843
+ "setup_repo_for_mushi",
844
+ {
845
+ title: titleOf("setup_repo_for_mushi"),
846
+ description: descOf("setup_repo_for_mushi"),
847
+ annotations: annotationsFor("setup_repo_for_mushi"),
848
+ inputSchema: {
849
+ projectId: z.string().optional().describe("Project UUID \u2014 defaults to configured project")
850
+ }
851
+ },
852
+ async (args) => {
853
+ const pid = args.projectId ?? projectId;
854
+ if (!pid) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required for setup_repo_for_mushi");
855
+ const data = await apiCall(`/v1/admin/projects/${pid}/repo/bootstrap`, {
856
+ method: "POST",
857
+ body: JSON.stringify({})
858
+ });
859
+ return jsonText(data);
860
+ }
861
+ );
748
862
  server.resource(
749
863
  "project_stats",
750
864
  "project://stats",
@@ -769,17 +883,15 @@ function createMushiServer(config) {
769
883
  contents: [{ uri: "project://dashboard", mimeType: "application/json", text: JSON.stringify(await apiCall("/v1/admin/dashboard"), null, 2) }]
770
884
  })
771
885
  );
772
- registerScopedTool(
886
+ const rewardsMeta = (name) => annotationsFor(name);
887
+ server.tool(
773
888
  "list_top_contributors",
889
+ rewardsMeta("list_top_contributors").title,
774
890
  {
775
- title: titleOf("list_top_contributors"),
776
- description: descOf("list_top_contributors"),
777
- inputSchema: {
778
- limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
779
- range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
780
- },
781
- annotations: annotationsFor("list_top_contributors")
891
+ limit: z.number().int().min(1).max(100).optional().default(10).describe("Max rows to return (default 10, max 100)"),
892
+ range: z.enum(["30d", "90d", "all"]).optional().default("30d").describe("Time window for points calculation")
782
893
  },
894
+ rewardsMeta("list_top_contributors"),
783
895
  async ({ limit, range }) => ({
784
896
  content: [{
785
897
  type: "text",
@@ -791,18 +903,15 @@ function createMushiServer(config) {
791
903
  }]
792
904
  })
793
905
  );
794
- registerScopedTool(
906
+ server.tool(
795
907
  "award_bonus_points",
908
+ rewardsMeta("award_bonus_points").title,
796
909
  {
797
- title: titleOf("award_bonus_points"),
798
- description: descOf("award_bonus_points"),
799
- inputSchema: {
800
- external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
801
- points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
802
- reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
803
- },
804
- annotations: annotationsFor("award_bonus_points")
910
+ external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
911
+ points: z.number().int().min(1).max(5e4).describe("Bonus points to award (max 50,000 per call)"),
912
+ reason: z.string().max(200).describe("Human-readable reason, logged to end_user_activity")
805
913
  },
914
+ rewardsMeta("award_bonus_points"),
806
915
  async ({ external_user_id, points, reason }) => ({
807
916
  content: [{
808
917
  type: "text",
@@ -818,18 +927,15 @@ function createMushiServer(config) {
818
927
  }]
819
928
  })
820
929
  );
821
- registerScopedTool(
930
+ server.tool(
822
931
  "set_tier",
932
+ rewardsMeta("set_tier").title,
823
933
  {
824
- title: titleOf("set_tier"),
825
- description: descOf("set_tier"),
826
- inputSchema: {
827
- external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
828
- tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
829
- reason: z.string().max(200).optional().describe("Optional reason for manual override")
830
- },
831
- annotations: annotationsFor("set_tier")
934
+ external_user_id: z.string().describe("The host-app user id as passed to Mushi.identify()"),
935
+ tier_slug: z.string().describe('Tier slug to assign, e.g. "champion", "contributor", "explorer"'),
936
+ reason: z.string().max(200).optional().describe("Optional reason for manual override")
832
937
  },
938
+ rewardsMeta("set_tier"),
833
939
  async ({ external_user_id, tier_slug, reason }) => ({
834
940
  content: [{
835
941
  type: "text",
@@ -845,6 +951,34 @@ function createMushiServer(config) {
845
951
  }]
846
952
  })
847
953
  );
954
+ server.resource(
955
+ "privacy_status",
956
+ "privacy://status",
957
+ {
958
+ description: "Returns the privacy posture for this project: storage region, LLM provider, whether BYOK is configured, data retention window, and last audit timestamp."
959
+ },
960
+ async () => ({
961
+ contents: [{
962
+ uri: "privacy://status",
963
+ mimeType: "application/json",
964
+ text: JSON.stringify(await apiCall("/v1/admin/privacy/status"), null, 2)
965
+ }]
966
+ })
967
+ );
968
+ server.resource(
969
+ "evolution_history",
970
+ "evolution://history",
971
+ {
972
+ description: "Returns the project's last 30 days of judge scores, prompt promotions, fixed-bug count, and lesson inductions. Agents can read this to see whether the loop is converging (rising judge scores, falling recurrence) or stalling."
973
+ },
974
+ async () => ({
975
+ contents: [{
976
+ uri: "evolution://history",
977
+ mimeType: "application/json",
978
+ text: JSON.stringify(await apiCall("/v1/admin/evolution/history"), null, 2)
979
+ }]
980
+ })
981
+ );
848
982
  server.resource(
849
983
  "project_integration_health",
850
984
  "project://integration-health",
@@ -945,6 +1079,186 @@ Prefer items that are bottlenecks or critical severity. Skip filler.`
945
1079
  }]
946
1080
  })
947
1081
  );
1082
+ server.registerTool(
1083
+ "map_user_stories",
1084
+ {
1085
+ title: titleOf("map_user_stories", TDD_TOOL_CATALOG),
1086
+ description: descOf("map_user_stories", TDD_TOOL_CATALOG),
1087
+ annotations: annotationsFor("map_user_stories", TDD_TOOL_CATALOG),
1088
+ inputSchema: {
1089
+ projectId: z.string().describe("Project id to map stories for"),
1090
+ baseUrl: z.string().url().describe("Live app URL to crawl"),
1091
+ maxPages: z.number().int().min(1).max(50).optional().describe("Max pages to crawl (default 20)"),
1092
+ provider: z.enum(["firecrawl", "browserbase"]).optional().describe("Crawl provider (default: firecrawl)"),
1093
+ cursorCloudRefine: z.boolean().optional().describe("Dispatch Cursor Cloud agent to refine and open a PR")
1094
+ }
1095
+ },
1096
+ async ({ projectId: projectId2, baseUrl, maxPages, provider, cursorCloudRefine }) => {
1097
+ if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
1098
+ const data = await apiCall(
1099
+ `/v1/admin/inventory/${projectId2}/map-from-live`,
1100
+ { method: "POST", body: JSON.stringify({ base_url: baseUrl, max_pages: maxPages, provider, cursor_cloud_refine: cursorCloudRefine }) }
1101
+ );
1102
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1103
+ }
1104
+ );
1105
+ server.registerTool(
1106
+ "get_map_run_status",
1107
+ {
1108
+ title: titleOf("get_map_run_status", TDD_TOOL_CATALOG),
1109
+ description: descOf("get_map_run_status", TDD_TOOL_CATALOG),
1110
+ annotations: annotationsFor("get_map_run_status", TDD_TOOL_CATALOG),
1111
+ inputSchema: {
1112
+ projectId: z.string().describe("Project id")
1113
+ }
1114
+ },
1115
+ async ({ projectId: projectId2 }) => {
1116
+ const data = await apiCall(`/v1/admin/inventory/${projectId2}/map-runs`);
1117
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1118
+ }
1119
+ );
1120
+ server.registerTool(
1121
+ "generate_tdd_from_story",
1122
+ {
1123
+ title: titleOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
1124
+ description: descOf("generate_tdd_from_story", TDD_TOOL_CATALOG),
1125
+ annotations: annotationsFor("generate_tdd_from_story", TDD_TOOL_CATALOG),
1126
+ inputSchema: {
1127
+ projectId: z.string().describe("Project id"),
1128
+ storyNodeId: z.string().describe("User story id slug from the accepted inventory"),
1129
+ automationMode: z.enum(["auto", "review", "approve"]).optional().describe("Gate mode for the generated test (default: review)"),
1130
+ baseUrl: z.string().url().optional().describe("Override the app base URL"),
1131
+ openPr: z.boolean().optional().describe("Open a draft GitHub PR (default: true)")
1132
+ }
1133
+ },
1134
+ async ({ projectId: projectId2, storyNodeId, automationMode, baseUrl, openPr }) => {
1135
+ if (!projectId2) throw new MushiApiError(400, "MISSING_PROJECT", "projectId is required");
1136
+ const data = await apiCall(
1137
+ `/v1/admin/inventory/${projectId2}/stories/${storyNodeId}/generate-test`,
1138
+ { method: "POST", body: JSON.stringify({ automation_mode: automationMode, base_url: baseUrl, open_pr: openPr }) }
1139
+ );
1140
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1141
+ }
1142
+ );
1143
+ server.registerTool(
1144
+ "improve_qa_story",
1145
+ {
1146
+ title: titleOf("improve_qa_story", TDD_TOOL_CATALOG),
1147
+ description: descOf("improve_qa_story", TDD_TOOL_CATALOG),
1148
+ annotations: annotationsFor("improve_qa_story", TDD_TOOL_CATALOG),
1149
+ inputSchema: {
1150
+ projectId: z.string().optional().describe("Project id (omit to run across all projects)")
1151
+ }
1152
+ },
1153
+ async ({ projectId: projectId2 }) => {
1154
+ const data = await apiCall(
1155
+ "/v1/admin/pdca/improve-qa-stories",
1156
+ { method: "POST", body: JSON.stringify({ project_id: projectId2 }) }
1157
+ );
1158
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1159
+ }
1160
+ );
1161
+ server.registerTool(
1162
+ "run_qa_story",
1163
+ {
1164
+ title: titleOf("run_qa_story", TDD_TOOL_CATALOG),
1165
+ description: descOf("run_qa_story", TDD_TOOL_CATALOG),
1166
+ annotations: annotationsFor("run_qa_story", TDD_TOOL_CATALOG),
1167
+ inputSchema: {
1168
+ projectId: z.string().describe("Project id"),
1169
+ qaStoryId: z.string().describe("qa_story id to run")
1170
+ }
1171
+ },
1172
+ async ({ projectId: projectId2, qaStoryId }) => {
1173
+ const data = await apiCall(
1174
+ `/v1/admin/projects/${projectId2}/qa-stories/${qaStoryId}/run`,
1175
+ { method: "POST" }
1176
+ );
1177
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1178
+ }
1179
+ );
1180
+ server.registerTool(
1181
+ "list_byok_keys",
1182
+ {
1183
+ title: titleOf("list_byok_keys", TDD_TOOL_CATALOG),
1184
+ description: descOf("list_byok_keys", TDD_TOOL_CATALOG),
1185
+ annotations: annotationsFor("list_byok_keys", TDD_TOOL_CATALOG),
1186
+ inputSchema: {
1187
+ projectId: z.string().describe("Project id")
1188
+ }
1189
+ },
1190
+ async ({ projectId: projectId2 }) => {
1191
+ const data = await apiCall(`/v1/admin/byok/keys?project_id=${encodeURIComponent(projectId2)}`);
1192
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1193
+ }
1194
+ );
1195
+ server.registerTool(
1196
+ "add_byok_key",
1197
+ {
1198
+ title: titleOf("add_byok_key", TDD_TOOL_CATALOG),
1199
+ description: descOf("add_byok_key", TDD_TOOL_CATALOG),
1200
+ annotations: annotationsFor("add_byok_key", TDD_TOOL_CATALOG),
1201
+ inputSchema: {
1202
+ projectId: z.string().describe("Project id"),
1203
+ provider: z.enum(["anthropic", "openai", "firecrawl", "browserbase", "cursor"]).describe("Provider slug"),
1204
+ key: z.string().min(10).describe("The API key value to add"),
1205
+ label: z.string().optional().describe("Human-readable label for this key"),
1206
+ priority: z.number().int().min(1).max(999).optional().describe("Priority for ordering (lower = higher priority)")
1207
+ }
1208
+ },
1209
+ async ({ projectId: projectId2, provider, key, label, priority }) => {
1210
+ const data = await apiCall(
1211
+ "/v1/admin/byok/keys",
1212
+ { method: "POST", body: JSON.stringify({ project_id: projectId2, provider_slug: provider, key, label, priority }) }
1213
+ );
1214
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1215
+ }
1216
+ );
1217
+ server.registerTool(
1218
+ "list_pending_review_stories",
1219
+ {
1220
+ title: titleOf("list_pending_review_stories", TDD_TOOL_CATALOG),
1221
+ description: descOf("list_pending_review_stories", TDD_TOOL_CATALOG),
1222
+ annotations: annotationsFor("list_pending_review_stories", TDD_TOOL_CATALOG),
1223
+ inputSchema: {
1224
+ projectId: z.string().describe("Project id")
1225
+ }
1226
+ },
1227
+ async ({ projectId: projectId2 }) => {
1228
+ const data = await apiCall(`/v1/admin/inventory/${projectId2}/stories/pending-review`);
1229
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1230
+ }
1231
+ );
1232
+ server.registerTool(
1233
+ "approve_qa_story",
1234
+ {
1235
+ title: titleOf("approve_qa_story", TDD_TOOL_CATALOG),
1236
+ description: descOf("approve_qa_story", TDD_TOOL_CATALOG),
1237
+ annotations: annotationsFor("approve_qa_story", TDD_TOOL_CATALOG),
1238
+ inputSchema: {
1239
+ projectId: z.string().describe("Project id"),
1240
+ qaStoryId: z.string().describe("QA story id to approve or reject"),
1241
+ status: z.enum(["approved", "rejected"]).describe("New approval status")
1242
+ }
1243
+ },
1244
+ async ({ projectId: projectId2, qaStoryId, status }) => {
1245
+ const data = await apiCall(
1246
+ `/v1/admin/inventory/${projectId2}/stories/${qaStoryId}/approval`,
1247
+ { method: "PATCH", body: JSON.stringify({ status }) }
1248
+ );
1249
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
1250
+ }
1251
+ );
1252
+ const grantedScopes = config.scopes;
1253
+ if (grantedScopes !== void 0) {
1254
+ const toolRegistry = server._registeredTools;
1255
+ const allSpecs = [...TOOL_CATALOG, ...TDD_TOOL_CATALOG];
1256
+ for (const spec of allSpecs) {
1257
+ if (!grantedScopes.includes(spec.scope)) {
1258
+ toolRegistry[spec.name]?.remove();
1259
+ }
1260
+ }
1261
+ }
948
1262
  return server;
949
1263
  }
950
1264
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mushi-mushi/mcp",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "description": "MCP server exposing Mushi Mushi reports to coding agents",
6
6
  "type": "module",
@@ -23,18 +23,30 @@
23
23
  "CODE_OF_CONDUCT.md",
24
24
  "SECURITY.md"
25
25
  ],
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "clean:types": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
29
+ "dev": "tsup --watch",
30
+ "lint": "eslint src/",
31
+ "test": "vitest run",
32
+ "test:smoke": "node scripts/smoke-stdio.mjs",
33
+ "test:localhost": "node scripts/localhost-e2e.mjs",
34
+ "demo": "node scripts/demo-terminal-config.mjs",
35
+ "inspector": "npx --yes @modelcontextprotocol/inspector@latest node ./dist/index.js",
36
+ "typecheck": "tsc --noEmit"
37
+ },
26
38
  "dependencies": {
27
39
  "@modelcontextprotocol/sdk": "^1.29.0",
28
- "zod": "^4.4.2",
29
- "@mushi-mushi/core": "^1.5.0"
40
+ "@mushi-mushi/core": "workspace:^",
41
+ "zod": "^4.4.2"
30
42
  },
31
43
  "devDependencies": {
44
+ "@mushi-mushi/eslint-config": "workspace:*",
32
45
  "@types/node": "^22.19.17",
33
46
  "eslint": "^10.3.0",
34
47
  "tsup": "^8.5.1",
35
48
  "typescript": "^6.0.3",
36
- "vitest": "^4.1.5",
37
- "@mushi-mushi/eslint-config": "0.0.0"
49
+ "vitest": "^4.1.5"
38
50
  },
39
51
  "repository": {
40
52
  "type": "git",
@@ -73,17 +85,5 @@
73
85
  ],
74
86
  "engines": {
75
87
  "node": ">=20"
76
- },
77
- "scripts": {
78
- "build": "tsup",
79
- "clean:types": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
80
- "dev": "tsup --watch",
81
- "lint": "eslint src/",
82
- "test": "vitest run",
83
- "test:smoke": "node scripts/smoke-stdio.mjs",
84
- "test:localhost": "node scripts/localhost-e2e.mjs",
85
- "demo": "node scripts/demo-terminal-config.mjs",
86
- "inspector": "npx --yes @modelcontextprotocol/inspector@latest node ./dist/index.js",
87
- "typecheck": "tsc --noEmit"
88
88
  }
89
- }
89
+ }