@mandujs/mcp 0.36.2 → 0.37.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.
Files changed (153) hide show
  1. package/package.json +4 -4
  2. package/src/activity-adapter.ts +23 -23
  3. package/src/adapters/index.ts +20 -20
  4. package/src/adapters/monitor-adapter.ts +100 -100
  5. package/src/adapters/tool-adapter.ts +90 -90
  6. package/src/executor/index.ts +22 -22
  7. package/src/executor/tool-executor.ts +148 -148
  8. package/src/hooks/config-watcher.ts +173 -173
  9. package/src/hooks/index.ts +23 -23
  10. package/src/hooks/mcp-hooks.ts +227 -227
  11. package/src/index.ts +0 -0
  12. package/src/logging/index.ts +15 -15
  13. package/src/logging/mcp-transport.ts +134 -134
  14. package/src/profiles.ts +34 -34
  15. package/src/registry/index.ts +13 -13
  16. package/src/registry/mcp-tool-registry.ts +298 -298
  17. package/src/resources/skills/guides.ts +1136 -1136
  18. package/src/resources/skills/index.ts +12 -12
  19. package/src/resources/skills/mandu-composition/SKILL.md +91 -91
  20. package/src/resources/skills/mandu-composition/metadata.json +13 -13
  21. package/src/resources/skills/mandu-composition/rules/_sections.md +26 -26
  22. package/src/resources/skills/mandu-composition/rules/_template.md +77 -77
  23. package/src/resources/skills/mandu-composition/rules/comp-arch-avoid-boolean-props.md +146 -146
  24. package/src/resources/skills/mandu-composition/rules/comp-arch-compound-components.md +164 -164
  25. package/src/resources/skills/mandu-composition/rules/comp-island-event.md +161 -161
  26. package/src/resources/skills/mandu-composition/rules/comp-island-slot-split.md +167 -167
  27. package/src/resources/skills/mandu-composition/rules/comp-pattern-children.md +149 -149
  28. package/src/resources/skills/mandu-composition/rules/comp-state-context-interface.md +148 -148
  29. package/src/resources/skills/mandu-composition/rules/comp-state-lift-state.md +150 -150
  30. package/src/resources/skills/mandu-deployment/SKILL.md +92 -92
  31. package/src/resources/skills/mandu-deployment/_sections.md +41 -41
  32. package/src/resources/skills/mandu-deployment/_template.md +38 -38
  33. package/src/resources/skills/mandu-deployment/metadata.json +13 -13
  34. package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +300 -300
  35. package/src/resources/skills/mandu-deployment/rules/deploy-build-bun.md +109 -109
  36. package/src/resources/skills/mandu-deployment/rules/deploy-build-output.md +115 -115
  37. package/src/resources/skills/mandu-deployment/rules/deploy-cicd-github.md +219 -219
  38. package/src/resources/skills/mandu-deployment/rules/deploy-docker-bun.md +150 -150
  39. package/src/resources/skills/mandu-deployment/rules/deploy-docker-compose.md +223 -223
  40. package/src/resources/skills/mandu-deployment/rules/deploy-platform-fly.md +152 -152
  41. package/src/resources/skills/mandu-deployment/rules/deploy-platform-render.md +179 -179
  42. package/src/resources/skills/mandu-deployment/rules/deploy-platform-vercel.md +140 -140
  43. package/src/resources/skills/mandu-fs-routes/SKILL.md +82 -82
  44. package/src/resources/skills/mandu-fs-routes/metadata.json +12 -12
  45. package/src/resources/skills/mandu-fs-routes/rules/_sections.md +36 -36
  46. package/src/resources/skills/mandu-fs-routes/rules/_template.md +69 -69
  47. package/src/resources/skills/mandu-fs-routes/rules/routes-api-methods.md +65 -65
  48. package/src/resources/skills/mandu-fs-routes/rules/routes-dynamic-param.md +93 -93
  49. package/src/resources/skills/mandu-fs-routes/rules/routes-naming-page.md +55 -55
  50. package/src/resources/skills/mandu-guard/SKILL.md +129 -129
  51. package/src/resources/skills/mandu-guard/metadata.json +12 -12
  52. package/src/resources/skills/mandu-guard/rules/_sections.md +36 -36
  53. package/src/resources/skills/mandu-guard/rules/_template.md +82 -82
  54. package/src/resources/skills/mandu-guard/rules/guard-config-rules.md +100 -100
  55. package/src/resources/skills/mandu-guard/rules/guard-layer-direction.md +76 -76
  56. package/src/resources/skills/mandu-guard/rules/guard-preset-mandu.md +81 -81
  57. package/src/resources/skills/mandu-guard/rules/guard-validate-import.md +80 -80
  58. package/src/resources/skills/mandu-hydration/SKILL.md +91 -91
  59. package/src/resources/skills/mandu-hydration/metadata.json +12 -12
  60. package/src/resources/skills/mandu-hydration/rules/_sections.md +31 -31
  61. package/src/resources/skills/mandu-hydration/rules/_template.md +72 -72
  62. package/src/resources/skills/mandu-hydration/rules/hydration-data-event.md +109 -109
  63. package/src/resources/skills/mandu-hydration/rules/hydration-directive-use-client.md +55 -55
  64. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +113 -113
  65. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +68 -68
  66. package/src/resources/skills/mandu-performance/SKILL.md +85 -85
  67. package/src/resources/skills/mandu-performance/metadata.json +14 -14
  68. package/src/resources/skills/mandu-performance/rules/_sections.md +31 -31
  69. package/src/resources/skills/mandu-performance/rules/_template.md +64 -64
  70. package/src/resources/skills/mandu-performance/rules/perf-async-defer-await.md +103 -103
  71. package/src/resources/skills/mandu-performance/rules/perf-async-parallel.md +95 -95
  72. package/src/resources/skills/mandu-performance/rules/perf-bun-file.md +124 -124
  73. package/src/resources/skills/mandu-performance/rules/perf-bun-serve.md +125 -125
  74. package/src/resources/skills/mandu-performance/rules/perf-bundle-imports.md +80 -80
  75. package/src/resources/skills/mandu-performance/rules/perf-bundle-island-lazy.md +145 -145
  76. package/src/resources/skills/mandu-performance/rules/perf-cache-react.md +98 -98
  77. package/src/resources/skills/mandu-performance/rules/perf-render-transitions.md +154 -154
  78. package/src/resources/skills/mandu-security/SKILL.md +87 -87
  79. package/src/resources/skills/mandu-security/metadata.json +13 -13
  80. package/src/resources/skills/mandu-security/rules/_sections.md +31 -31
  81. package/src/resources/skills/mandu-security/rules/_template.md +74 -74
  82. package/src/resources/skills/mandu-security/rules/sec-auth-guard.md +127 -127
  83. package/src/resources/skills/mandu-security/rules/sec-env-management.md +133 -133
  84. package/src/resources/skills/mandu-security/rules/sec-input-validate.md +148 -148
  85. package/src/resources/skills/mandu-security/rules/sec-protect-csrf.md +146 -146
  86. package/src/resources/skills/mandu-security/rules/sec-protect-headers.md +138 -138
  87. package/src/resources/skills/mandu-slot/SKILL.md +85 -85
  88. package/src/resources/skills/mandu-slot/metadata.json +12 -12
  89. package/src/resources/skills/mandu-slot/rules/_sections.md +36 -36
  90. package/src/resources/skills/mandu-slot/rules/_template.md +63 -63
  91. package/src/resources/skills/mandu-slot/rules/slot-basic-structure.md +38 -38
  92. package/src/resources/skills/mandu-slot/rules/slot-ctx-response.md +56 -56
  93. package/src/resources/skills/mandu-slot/rules/slot-guard-auth.md +59 -59
  94. package/src/resources/skills/mandu-slot/rules/slot-http-methods.md +64 -64
  95. package/src/resources/skills/mandu-styling/SKILL.md +154 -154
  96. package/src/resources/skills/mandu-styling/_sections.md +43 -43
  97. package/src/resources/skills/mandu-styling/_template.md +32 -32
  98. package/src/resources/skills/mandu-styling/metadata.json +15 -15
  99. package/src/resources/skills/mandu-styling/rules/style-component-compound.md +235 -235
  100. package/src/resources/skills/mandu-styling/rules/style-component-slots.md +255 -255
  101. package/src/resources/skills/mandu-styling/rules/style-component-tokens.md +205 -205
  102. package/src/resources/skills/mandu-styling/rules/style-island-animations.md +272 -272
  103. package/src/resources/skills/mandu-styling/rules/style-island-scoping.md +167 -167
  104. package/src/resources/skills/mandu-styling/rules/style-island-variants.md +221 -221
  105. package/src/resources/skills/mandu-styling/rules/style-perf-critical.md +209 -209
  106. package/src/resources/skills/mandu-styling/rules/style-perf-purge.md +192 -192
  107. package/src/resources/skills/mandu-styling/rules/style-setup-modules.md +162 -162
  108. package/src/resources/skills/mandu-styling/rules/style-setup-panda.md +164 -164
  109. package/src/resources/skills/mandu-styling/rules/style-setup-tailwind.md +170 -170
  110. package/src/resources/skills/mandu-styling/rules/style-tailwind-v4-gotchas.md +179 -179
  111. package/src/resources/skills/mandu-styling/rules/style-theme-darkmode.md +229 -229
  112. package/src/resources/skills/mandu-testing/SKILL.md +99 -99
  113. package/src/resources/skills/mandu-testing/metadata.json +13 -13
  114. package/src/resources/skills/mandu-testing/rules/_sections.md +26 -26
  115. package/src/resources/skills/mandu-testing/rules/_template.md +65 -65
  116. package/src/resources/skills/mandu-testing/rules/test-component-island.md +195 -195
  117. package/src/resources/skills/mandu-testing/rules/test-e2e-playwright.md +196 -196
  118. package/src/resources/skills/mandu-testing/rules/test-mock-fetch.md +219 -219
  119. package/src/resources/skills/mandu-testing/rules/test-slot-unit.md +192 -192
  120. package/src/resources/skills/mandu-ui/SKILL.md +117 -117
  121. package/src/resources/skills/mandu-ui/_sections.md +23 -23
  122. package/src/resources/skills/mandu-ui/_template.md +32 -32
  123. package/src/resources/skills/mandu-ui/metadata.json +13 -13
  124. package/src/resources/skills/mandu-ui/rules/ui-accessibility-aria.md +232 -232
  125. package/src/resources/skills/mandu-ui/rules/ui-accessibility-focus.md +238 -238
  126. package/src/resources/skills/mandu-ui/rules/ui-composition-patterns.md +259 -259
  127. package/src/resources/skills/mandu-ui/rules/ui-island-integration.md +258 -258
  128. package/src/resources/skills/mandu-ui/rules/ui-radix-patterns.md +213 -213
  129. package/src/resources/skills/mandu-ui/rules/ui-shadcn-setup.md +209 -209
  130. package/src/resources/skills/recipes.ts +932 -932
  131. package/src/server.ts +3 -3
  132. package/src/tools/ate-exemplar.ts +92 -92
  133. package/src/tools/ate-flakes.ts +90 -90
  134. package/src/tools/ate-mutate.ts +103 -103
  135. package/src/tools/ate-mutation-report.ts +64 -64
  136. package/src/tools/ate-oracle-pending.ts +49 -49
  137. package/src/tools/ate-oracle-replay.ts +44 -44
  138. package/src/tools/ate-oracle-verdict.ts +70 -70
  139. package/src/tools/ate-prompt.ts +146 -146
  140. package/src/tools/brain.ts +3 -3
  141. package/src/tools/deploy-plan.ts +378 -378
  142. package/src/tools/deploy-preview.ts +316 -316
  143. package/src/tools/design.ts +825 -825
  144. package/src/tools/docs.ts +350 -350
  145. package/src/tools/kitchen.ts +109 -23
  146. package/src/tools/lint.ts +226 -226
  147. package/src/tools/loop-close.ts +175 -175
  148. package/src/tools/negotiate.ts +263 -263
  149. package/src/tools/run-tests.ts +424 -424
  150. package/src/tools/runtime.ts +1 -1
  151. package/src/tools/slot-validation.ts +199 -199
  152. package/src/tx-lock.ts +73 -73
  153. package/src/utils/runtime-control.ts +52 -52
@@ -27,34 +27,69 @@ export const kitchenToolDefinitions: Tool[] = [
27
27
  required: [],
28
28
  },
29
29
  },
30
+ {
31
+ name: "mandu.devtools.context",
32
+ description:
33
+ "Read the Kitchen Agent Supervisor context pack — one call returns the current situation, recommended skill/MCP tools, knowledge cards, a copyable prompt, and the next safe action. Combines routes, recent errors, HTTP traffic, MCP usage, bundle manifest, diagnose report, and changed files. Use this at session start or whenever you are unsure what to work on next. Requires `mandu dev` to be running.",
34
+ annotations: {
35
+ readOnlyHint: true,
36
+ },
37
+ inputSchema: {
38
+ type: "object",
39
+ properties: {
40
+ includeBundle: {
41
+ type: "boolean",
42
+ description:
43
+ "Include bundle manifest summary (.mandu/manifest.json). Default true. Set false to skip on very large projects.",
44
+ },
45
+ includeDiagnose: {
46
+ type: "boolean",
47
+ description:
48
+ "Include the extended diagnose report. Default true. Set false to lower latency when a11y_hints / package_export_gaps are noisy.",
49
+ },
50
+ includeDiff: {
51
+ type: "boolean",
52
+ description:
53
+ "Include git diff against MANDU_DIFF_BASE (default HEAD). Default true. Set false to skip when git is unavailable.",
54
+ },
55
+ },
56
+ required: [],
57
+ },
58
+ },
30
59
  ];
31
60
 
32
- export function kitchenTools(projectRoot: string) {
33
- const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
34
- "mandu.kitchen.errors": async (args: Record<string, unknown>) => {
35
- const { clear = false } = args as { clear?: boolean };
61
+ /**
62
+ * Resolve the dev server base URL. Prefers the port parsed from the
63
+ * currently running `mandu dev` stdout; falls back to mandu config; final
64
+ * default is 3333. Shared by every Kitchen-backed MCP tool so they all
65
+ * agree on where to fetch from.
66
+ */
67
+ async function resolveDevServerBaseUrl(projectRoot: string): Promise<string> {
68
+ let port: number | undefined;
36
69
 
37
- // Detect port from the running dev server output first, then fall back to config
38
- let port: number | undefined;
39
-
40
- const serverState = getDevServerState();
41
- if (serverState) {
42
- // Parse the actual port from dev server stdout (e.g. "http://localhost:3333")
43
- for (const line of serverState.output) {
44
- const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
45
- if (portMatch) {
46
- port = parseInt(portMatch[1], 10);
47
- }
48
- }
70
+ const serverState = getDevServerState();
71
+ if (serverState) {
72
+ for (const line of serverState.output) {
73
+ const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
74
+ if (portMatch) {
75
+ port = parseInt(portMatch[1], 10);
49
76
  }
77
+ }
78
+ }
50
79
 
51
- // Fall back to config if we couldn't detect from running server
52
- if (!port) {
53
- const config = await loadManduConfig(projectRoot);
54
- port = config.server?.port ?? 3333;
55
- }
80
+ if (!port) {
81
+ const config = await loadManduConfig(projectRoot);
82
+ port = config.server?.port ?? 3333;
83
+ }
56
84
 
57
- const baseUrl = `http://localhost:${port}`;
85
+ return `http://localhost:${port}`;
86
+ }
87
+
88
+ export function kitchenTools(projectRoot: string) {
89
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
90
+ "mandu.kitchen.errors": async (args: Record<string, unknown>) => {
91
+ const { clear = false } = args as { clear?: boolean };
92
+ const baseUrl = await resolveDevServerBaseUrl(projectRoot);
58
93
 
59
94
  try {
60
95
  // Fetch errors from Kitchen API
@@ -98,10 +133,61 @@ export function kitchenTools(projectRoot: string) {
98
133
  };
99
134
  }
100
135
  },
136
+
137
+ /**
138
+ * Plan 18 P0-1 — read-only Agent Supervisor context pack.
139
+ *
140
+ * Fetches `/__kitchen/api/agent-context` and returns the full pack
141
+ * so an agent can self-orient with a single call: current situation,
142
+ * top tool recommendation, knowledge cards, copyable prompt, and the
143
+ * next safe action. Three optional toggles let the caller skip
144
+ * expensive signals when latency matters.
145
+ */
146
+ "mandu.devtools.context": async (args: Record<string, unknown>) => {
147
+ const {
148
+ includeBundle = true,
149
+ includeDiagnose = true,
150
+ includeDiff = true,
151
+ } = args as { includeBundle?: boolean; includeDiagnose?: boolean; includeDiff?: boolean };
152
+
153
+ const baseUrl = await resolveDevServerBaseUrl(projectRoot);
154
+ const params = new URLSearchParams();
155
+ if (!includeBundle) params.set("bundle", "0");
156
+ if (!includeDiagnose) params.set("diagnose", "0");
157
+ if (!includeDiff) params.set("diff", "0");
158
+ const query = params.toString();
159
+ const url = `${baseUrl}/__kitchen/api/agent-context${query ? `?${query}` : ""}`;
160
+
161
+ try {
162
+ const res = await fetch(url);
163
+ if (!res.ok) {
164
+ return {
165
+ success: false,
166
+ message: `Dev server not reachable at ${baseUrl}. Is 'mandu dev' running?`,
167
+ status: res.status,
168
+ };
169
+ }
170
+
171
+ const pack = await res.json() as Record<string, unknown>;
172
+ return {
173
+ success: true,
174
+ message: "Agent Supervisor context pack retrieved.",
175
+ pack,
176
+ relatedSkills: ["mandu-agent-workflow", "mandu-debug"],
177
+ };
178
+ } catch {
179
+ return {
180
+ success: false,
181
+ message: `Cannot connect to dev server at ${baseUrl}. Make sure 'mandu dev' is running.`,
182
+ };
183
+ }
184
+ },
101
185
  };
102
186
 
103
- // Backward-compatible alias
187
+ // Backward-compatible aliases (underscore form for legacy MCP clients
188
+ // that don't accept dots in tool names).
104
189
  handlers["mandu_kitchen_errors"] = handlers["mandu.kitchen.errors"];
190
+ handlers["mandu_devtools_context"] = handlers["mandu.devtools.context"];
105
191
 
106
192
  return handlers;
107
193
  }
package/src/tools/lint.ts CHANGED
@@ -1,226 +1,226 @@
1
- /**
2
- * MCP tools — `mandu.lint` + `mandu.lint.setup`
3
- *
4
- * Gives agents a first-class way to:
5
- * 1. Run oxlint on a project and get structured error/warning counts
6
- * (read-only, no side effects).
7
- * 2. One-shot install oxlint + scaffold `.oxlintrc.json` + wire
8
- * scripts on an existing project (destructive — writes files).
9
- *
10
- * The setup tool mirrors `mandu lint --setup`. We spawn the CLI as a
11
- * subprocess rather than linking to the CLI source so MCP builds
12
- * don't pull `packages/cli` into their import graph.
13
- */
14
-
15
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
16
- import path from "node:path";
17
- import fs from "node:fs/promises";
18
-
19
- // ─────────────────────────────────────────────────────────────────────────
20
- // Shared helpers
21
- // ─────────────────────────────────────────────────────────────────────────
22
-
23
- async function oxlintAvailable(rootDir: string): Promise<boolean> {
24
- const binName = process.platform === "win32" ? "oxlint.exe" : "oxlint";
25
- const localBin = path.resolve(rootDir, "node_modules", ".bin", binName);
26
- try {
27
- await fs.access(localBin);
28
- return true;
29
- } catch {
30
- // Not in local node_modules — probe PATH.
31
- }
32
- try {
33
- const proc = Bun.spawn({
34
- cmd: ["bun", "x", "oxlint", "--version"],
35
- cwd: rootDir,
36
- stdout: "ignore",
37
- stderr: "ignore",
38
- });
39
- return (await proc.exited) === 0;
40
- } catch {
41
- return false;
42
- }
43
- }
44
-
45
- interface LintParsed {
46
- errors: number;
47
- warnings: number;
48
- raw: string;
49
- parseFailed: boolean;
50
- }
51
-
52
- async function runOxlintOnce(rootDir: string, typeAware: boolean): Promise<LintParsed> {
53
- const cmd = typeAware
54
- ? ["bun", "x", "oxlint", "--type-aware", "."]
55
- : ["bun", "x", "oxlint", "."];
56
- const proc = Bun.spawn({
57
- cmd,
58
- cwd: rootDir,
59
- stdout: "pipe",
60
- stderr: "pipe",
61
- });
62
- const [stdout, stderr] = await Promise.all([
63
- new Response(proc.stdout).text(),
64
- new Response(proc.stderr).text(),
65
- ]);
66
- await proc.exited;
67
- const raw = stderr + stdout;
68
- const match = raw.match(/Found (\d+) warnings? and (\d+) errors?/);
69
- if (!match) {
70
- return { errors: 0, warnings: 0, raw, parseFailed: true };
71
- }
72
- return {
73
- errors: Number(match[2]),
74
- warnings: Number(match[1]),
75
- raw,
76
- parseFailed: false,
77
- };
78
- }
79
-
80
- // ─────────────────────────────────────────────────────────────────────────
81
- // mandu.lint — run oxlint, return counts
82
- // ─────────────────────────────────────────────────────────────────────────
83
-
84
- interface LintInput {
85
- typeAware?: unknown;
86
- }
87
-
88
- interface LintResult {
89
- installed: boolean;
90
- ran: boolean;
91
- errors: number;
92
- warnings: number;
93
- passed: boolean;
94
- hint?: string;
95
- rawTail?: string;
96
- }
97
-
98
- async function handleLint(projectRoot: string, input: LintInput): Promise<LintResult> {
99
- const typeAware = input.typeAware === true;
100
- const installed = await oxlintAvailable(projectRoot);
101
- if (!installed) {
102
- return {
103
- installed: false,
104
- ran: false,
105
- errors: 0,
106
- warnings: 0,
107
- passed: false,
108
- hint:
109
- "oxlint is not installed in this project. Call `mandu.lint.setup` or run `mandu lint --setup` in the shell to install + configure.",
110
- };
111
- }
112
- const result = await runOxlintOnce(projectRoot, typeAware);
113
- if (result.parseFailed) {
114
- return {
115
- installed: true,
116
- ran: true,
117
- errors: 0,
118
- warnings: 0,
119
- passed: false,
120
- hint: "oxlint ran but output could not be parsed. See `rawTail` for the last 2 KB.",
121
- rawTail: result.raw.slice(-2048),
122
- };
123
- }
124
- return {
125
- installed: true,
126
- ran: true,
127
- errors: result.errors,
128
- warnings: result.warnings,
129
- passed: result.errors === 0,
130
- };
131
- }
132
-
133
- // ─────────────────────────────────────────────────────────────────────────
134
- // mandu.lint.setup — install oxlint + wire scripts + baseline
135
- // ─────────────────────────────────────────────────────────────────────────
136
-
137
- interface LintSetupInput {
138
- dryRun?: unknown;
139
- }
140
-
141
- interface LintSetupResult {
142
- ok: boolean;
143
- stdout: string;
144
- stderr: string;
145
- exitCode: number;
146
- dryRun: boolean;
147
- hint?: string;
148
- }
149
-
150
- async function handleLintSetup(projectRoot: string, input: LintSetupInput): Promise<LintSetupResult> {
151
- const dryRun = input.dryRun === true;
152
- const args = ["run", "--cwd", projectRoot, "mandu", "lint", "--setup", "--yes"];
153
- if (dryRun) args.push("--dry-run");
154
- const proc = Bun.spawn({
155
- cmd: ["bun", ...args],
156
- cwd: projectRoot,
157
- stdout: "pipe",
158
- stderr: "pipe",
159
- });
160
- const [stdout, stderr] = await Promise.all([
161
- new Response(proc.stdout).text(),
162
- new Response(proc.stderr).text(),
163
- ]);
164
- const exitCode = await proc.exited;
165
- const ok = exitCode === 0;
166
- return {
167
- ok,
168
- stdout,
169
- stderr,
170
- exitCode,
171
- dryRun,
172
- hint: ok
173
- ? dryRun
174
- ? "Dry-run completed. Re-run without `dryRun: true` to apply the changes."
175
- : "Setup completed. Run `mandu.lint` to see the current baseline."
176
- : "Setup command failed. See `stderr` for details. A frequent cause is `mandu` CLI not being installed — `bun add -D @mandujs/cli` in the project first.",
177
- };
178
- }
179
-
180
- // ─────────────────────────────────────────────────────────────────────────
181
- // MCP tool definitions + handler map
182
- // ─────────────────────────────────────────────────────────────────────────
183
-
184
- export const lintToolDefinitions: Tool[] = [
185
- {
186
- name: "mandu.lint",
187
- description:
188
- "Run oxlint on the project and return structured error/warning counts. Read-only — never modifies files. Pass `typeAware: true` to also run `oxlint --type-aware` (requires `oxlint-tsgolint`). Agents should call this after edits as part of the guardrail chain alongside `mandu.guard.check` and `mandu.doctor`.",
189
- annotations: { readOnlyHint: true },
190
- inputSchema: {
191
- type: "object",
192
- properties: {
193
- typeAware: {
194
- type: "boolean",
195
- description:
196
- "Run `oxlint --type-aware` in addition to the core pass (requires `oxlint-tsgolint`). Default false.",
197
- },
198
- },
199
- required: [],
200
- },
201
- },
202
- {
203
- name: "mandu.lint.setup",
204
- description:
205
- "Install oxlint into an existing project: copies `.oxlintrc.json`, wires `lint`/`lint:fix` scripts, adds `oxlint` devDep, runs `bun install`. Idempotent — re-running produces no additional changes. Destructive — writes files. Set `dryRun: true` to print the plan only.",
206
- annotations: { readOnlyHint: false, destructiveHint: true },
207
- inputSchema: {
208
- type: "object",
209
- properties: {
210
- dryRun: {
211
- type: "boolean",
212
- description: "Print the plan without writing files. Default false.",
213
- },
214
- },
215
- required: [],
216
- },
217
- },
218
- ];
219
-
220
- export function lintTools(projectRoot: string) {
221
- const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
222
- "mandu.lint": async (args) => handleLint(projectRoot, args as LintInput),
223
- "mandu.lint.setup": async (args) => handleLintSetup(projectRoot, args as LintSetupInput),
224
- };
225
- return handlers;
226
- }
1
+ /**
2
+ * MCP tools — `mandu.lint` + `mandu.lint.setup`
3
+ *
4
+ * Gives agents a first-class way to:
5
+ * 1. Run oxlint on a project and get structured error/warning counts
6
+ * (read-only, no side effects).
7
+ * 2. One-shot install oxlint + scaffold `.oxlintrc.json` + wire
8
+ * scripts on an existing project (destructive — writes files).
9
+ *
10
+ * The setup tool mirrors `mandu lint --setup`. We spawn the CLI as a
11
+ * subprocess rather than linking to the CLI source so MCP builds
12
+ * don't pull `packages/cli` into their import graph.
13
+ */
14
+
15
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
16
+ import path from "node:path";
17
+ import fs from "node:fs/promises";
18
+
19
+ // ─────────────────────────────────────────────────────────────────────────
20
+ // Shared helpers
21
+ // ─────────────────────────────────────────────────────────────────────────
22
+
23
+ async function oxlintAvailable(rootDir: string): Promise<boolean> {
24
+ const binName = process.platform === "win32" ? "oxlint.exe" : "oxlint";
25
+ const localBin = path.resolve(rootDir, "node_modules", ".bin", binName);
26
+ try {
27
+ await fs.access(localBin);
28
+ return true;
29
+ } catch {
30
+ // Not in local node_modules — probe PATH.
31
+ }
32
+ try {
33
+ const proc = Bun.spawn({
34
+ cmd: ["bun", "x", "oxlint", "--version"],
35
+ cwd: rootDir,
36
+ stdout: "ignore",
37
+ stderr: "ignore",
38
+ });
39
+ return (await proc.exited) === 0;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+
45
+ interface LintParsed {
46
+ errors: number;
47
+ warnings: number;
48
+ raw: string;
49
+ parseFailed: boolean;
50
+ }
51
+
52
+ async function runOxlintOnce(rootDir: string, typeAware: boolean): Promise<LintParsed> {
53
+ const cmd = typeAware
54
+ ? ["bun", "x", "oxlint", "--type-aware", "."]
55
+ : ["bun", "x", "oxlint", "."];
56
+ const proc = Bun.spawn({
57
+ cmd,
58
+ cwd: rootDir,
59
+ stdout: "pipe",
60
+ stderr: "pipe",
61
+ });
62
+ const [stdout, stderr] = await Promise.all([
63
+ new Response(proc.stdout).text(),
64
+ new Response(proc.stderr).text(),
65
+ ]);
66
+ await proc.exited;
67
+ const raw = stderr + stdout;
68
+ const match = raw.match(/Found (\d+) warnings? and (\d+) errors?/);
69
+ if (!match) {
70
+ return { errors: 0, warnings: 0, raw, parseFailed: true };
71
+ }
72
+ return {
73
+ errors: Number(match[2]),
74
+ warnings: Number(match[1]),
75
+ raw,
76
+ parseFailed: false,
77
+ };
78
+ }
79
+
80
+ // ─────────────────────────────────────────────────────────────────────────
81
+ // mandu.lint — run oxlint, return counts
82
+ // ─────────────────────────────────────────────────────────────────────────
83
+
84
+ interface LintInput {
85
+ typeAware?: unknown;
86
+ }
87
+
88
+ interface LintResult {
89
+ installed: boolean;
90
+ ran: boolean;
91
+ errors: number;
92
+ warnings: number;
93
+ passed: boolean;
94
+ hint?: string;
95
+ rawTail?: string;
96
+ }
97
+
98
+ async function handleLint(projectRoot: string, input: LintInput): Promise<LintResult> {
99
+ const typeAware = input.typeAware === true;
100
+ const installed = await oxlintAvailable(projectRoot);
101
+ if (!installed) {
102
+ return {
103
+ installed: false,
104
+ ran: false,
105
+ errors: 0,
106
+ warnings: 0,
107
+ passed: false,
108
+ hint:
109
+ "oxlint is not installed in this project. Call `mandu.lint.setup` or run `mandu lint --setup` in the shell to install + configure.",
110
+ };
111
+ }
112
+ const result = await runOxlintOnce(projectRoot, typeAware);
113
+ if (result.parseFailed) {
114
+ return {
115
+ installed: true,
116
+ ran: true,
117
+ errors: 0,
118
+ warnings: 0,
119
+ passed: false,
120
+ hint: "oxlint ran but output could not be parsed. See `rawTail` for the last 2 KB.",
121
+ rawTail: result.raw.slice(-2048),
122
+ };
123
+ }
124
+ return {
125
+ installed: true,
126
+ ran: true,
127
+ errors: result.errors,
128
+ warnings: result.warnings,
129
+ passed: result.errors === 0,
130
+ };
131
+ }
132
+
133
+ // ─────────────────────────────────────────────────────────────────────────
134
+ // mandu.lint.setup — install oxlint + wire scripts + baseline
135
+ // ─────────────────────────────────────────────────────────────────────────
136
+
137
+ interface LintSetupInput {
138
+ dryRun?: unknown;
139
+ }
140
+
141
+ interface LintSetupResult {
142
+ ok: boolean;
143
+ stdout: string;
144
+ stderr: string;
145
+ exitCode: number;
146
+ dryRun: boolean;
147
+ hint?: string;
148
+ }
149
+
150
+ async function handleLintSetup(projectRoot: string, input: LintSetupInput): Promise<LintSetupResult> {
151
+ const dryRun = input.dryRun === true;
152
+ const args = ["run", "--cwd", projectRoot, "mandu", "lint", "--setup", "--yes"];
153
+ if (dryRun) args.push("--dry-run");
154
+ const proc = Bun.spawn({
155
+ cmd: ["bun", ...args],
156
+ cwd: projectRoot,
157
+ stdout: "pipe",
158
+ stderr: "pipe",
159
+ });
160
+ const [stdout, stderr] = await Promise.all([
161
+ new Response(proc.stdout).text(),
162
+ new Response(proc.stderr).text(),
163
+ ]);
164
+ const exitCode = await proc.exited;
165
+ const ok = exitCode === 0;
166
+ return {
167
+ ok,
168
+ stdout,
169
+ stderr,
170
+ exitCode,
171
+ dryRun,
172
+ hint: ok
173
+ ? dryRun
174
+ ? "Dry-run completed. Re-run without `dryRun: true` to apply the changes."
175
+ : "Setup completed. Run `mandu.lint` to see the current baseline."
176
+ : "Setup command failed. See `stderr` for details. A frequent cause is `mandu` CLI not being installed — `bun add -D @mandujs/cli` in the project first.",
177
+ };
178
+ }
179
+
180
+ // ─────────────────────────────────────────────────────────────────────────
181
+ // MCP tool definitions + handler map
182
+ // ─────────────────────────────────────────────────────────────────────────
183
+
184
+ export const lintToolDefinitions: Tool[] = [
185
+ {
186
+ name: "mandu.lint",
187
+ description:
188
+ "Run oxlint on the project and return structured error/warning counts. Read-only — never modifies files. Pass `typeAware: true` to also run `oxlint --type-aware` (requires `oxlint-tsgolint`). Agents should call this after edits as part of the guardrail chain alongside `mandu.guard.check` and `mandu.doctor`.",
189
+ annotations: { readOnlyHint: true },
190
+ inputSchema: {
191
+ type: "object",
192
+ properties: {
193
+ typeAware: {
194
+ type: "boolean",
195
+ description:
196
+ "Run `oxlint --type-aware` in addition to the core pass (requires `oxlint-tsgolint`). Default false.",
197
+ },
198
+ },
199
+ required: [],
200
+ },
201
+ },
202
+ {
203
+ name: "mandu.lint.setup",
204
+ description:
205
+ "Install oxlint into an existing project: copies `.oxlintrc.json`, wires `lint`/`lint:fix` scripts, adds `oxlint` devDep, runs `bun install`. Idempotent — re-running produces no additional changes. Destructive — writes files. Set `dryRun: true` to print the plan only.",
206
+ annotations: { readOnlyHint: false, destructiveHint: true },
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ dryRun: {
211
+ type: "boolean",
212
+ description: "Print the plan without writing files. Default false.",
213
+ },
214
+ },
215
+ required: [],
216
+ },
217
+ },
218
+ ];
219
+
220
+ export function lintTools(projectRoot: string) {
221
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
222
+ "mandu.lint": async (args) => handleLint(projectRoot, args as LintInput),
223
+ "mandu.lint.setup": async (args) => handleLintSetup(projectRoot, args as LintSetupInput),
224
+ };
225
+ return handlers;
226
+ }