@mandujs/mcp 0.37.1 → 0.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -4
- package/package.json +4 -4
- package/src/prompts.ts +4 -4
- package/src/resources/skills/guides.ts +49 -40
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +3 -3
- package/src/resources/skills/mandu-hydration/SKILL.md +19 -11
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +54 -7
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +60 -37
- package/src/resources/skills/recipes.ts +28 -19
- package/src/tools/ate.ts +37 -24
- package/src/tools/composite.ts +13 -11
- package/src/tools/guard.ts +162 -68
- package/src/tools/index.ts +61 -30
- package/src/tools/kitchen.ts +72 -32
- package/src/tools/slot-validation.ts +19 -125
package/src/tools/guard.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
2
|
import { type ManduError } from "@mandujs/core/error";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
import {
|
|
4
|
+
checkDirectory,
|
|
5
|
+
getDefaultFsRoutesGuardPolicy,
|
|
6
|
+
validateAndReport,
|
|
7
|
+
loadManifest,
|
|
8
|
+
runGuardCheck,
|
|
9
|
+
runAutoCorrect,
|
|
10
|
+
type GeneratedMap,
|
|
8
11
|
// Self-Healing Guard imports
|
|
9
12
|
checkWithHealing,
|
|
10
13
|
healAll,
|
|
@@ -12,16 +15,19 @@ import {
|
|
|
12
15
|
// Follow-up E — type-aware lint bridge
|
|
13
16
|
runTsgolint,
|
|
14
17
|
type GuardConfig,
|
|
15
|
-
type ViolationType,
|
|
16
|
-
type GuardPreset,
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
type ViolationType,
|
|
19
|
+
type GuardPreset,
|
|
20
|
+
type Violation,
|
|
21
|
+
} from "@mandujs/core";
|
|
22
|
+
import { getProjectPaths, readJsonFile, readConfig } from "../utils/project.js";
|
|
23
|
+
import fs from "fs/promises";
|
|
24
|
+
import path from "path";
|
|
19
25
|
|
|
20
26
|
export const guardToolDefinitions: Tool[] = [
|
|
21
27
|
{
|
|
22
|
-
name: "mandu.guard.check",
|
|
23
|
-
description:
|
|
24
|
-
"Run
|
|
28
|
+
name: "mandu.guard.check",
|
|
29
|
+
description:
|
|
30
|
+
"Run the same architecture guard used by `mandu guard`, plus legacy spec/generated/slot checks. Set typeAware=true to additionally run `oxlint --type-aware` (tsgolint) and merge its results.",
|
|
25
31
|
annotations: {
|
|
26
32
|
readOnlyHint: true,
|
|
27
33
|
},
|
|
@@ -120,8 +126,37 @@ export const guardToolDefinitions: Tool[] = [
|
|
|
120
126
|
},
|
|
121
127
|
];
|
|
122
128
|
|
|
123
|
-
export function guardTools(projectRoot: string) {
|
|
124
|
-
const paths = getProjectPaths(projectRoot);
|
|
129
|
+
export function guardTools(projectRoot: string) {
|
|
130
|
+
const paths = getProjectPaths(projectRoot);
|
|
131
|
+
|
|
132
|
+
const pathExists = async (candidate: string): Promise<boolean> => {
|
|
133
|
+
try {
|
|
134
|
+
await fs.access(candidate);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const summarizeArchitectureViolation = (violation: Violation) => ({
|
|
142
|
+
ruleId: violation.ruleName,
|
|
143
|
+
type: violation.type,
|
|
144
|
+
file: path.relative(projectRoot, violation.filePath).replace(/\\/g, "/") || violation.filePath,
|
|
145
|
+
line: violation.line,
|
|
146
|
+
column: violation.column,
|
|
147
|
+
message: violation.ruleDescription,
|
|
148
|
+
suggestion: violation.suggestions[0],
|
|
149
|
+
fromLayer: violation.fromLayer,
|
|
150
|
+
toLayer: violation.toLayer,
|
|
151
|
+
importStatement: violation.importStatement,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const summarizeLegacyViolation = (v: Awaited<ReturnType<typeof runGuardCheck>>["violations"][number]) => ({
|
|
155
|
+
ruleId: v.ruleId,
|
|
156
|
+
file: v.file,
|
|
157
|
+
message: v.message,
|
|
158
|
+
suggestion: v.suggestion,
|
|
159
|
+
});
|
|
125
160
|
|
|
126
161
|
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
127
162
|
"mandu.guard.check": async (args: Record<string, unknown>) => {
|
|
@@ -137,25 +172,44 @@ export function guardTools(projectRoot: string) {
|
|
|
137
172
|
error: "Failed to load manifest",
|
|
138
173
|
details: manifestResult.errors,
|
|
139
174
|
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const projectConfig = await validateAndReport(projectRoot);
|
|
178
|
+
const guardConfigFromFile = (projectConfig?.guard ?? {}) as GuardConfig;
|
|
179
|
+
const preset = guardConfigFromFile.preset ?? "mandu";
|
|
180
|
+
const enableFsRoutes = await pathExists(paths.appDir);
|
|
181
|
+
|
|
182
|
+
const architectureReport = await checkDirectory(
|
|
183
|
+
{
|
|
184
|
+
preset,
|
|
185
|
+
srcDir: guardConfigFromFile.srcDir ?? "src",
|
|
186
|
+
exclude: guardConfigFromFile.exclude,
|
|
187
|
+
fsRoutes: getDefaultFsRoutesGuardPolicy(enableFsRoutes),
|
|
188
|
+
},
|
|
189
|
+
projectRoot
|
|
190
|
+
);
|
|
191
|
+
const architecturePassed = architectureReport.bySeverity.error === 0;
|
|
192
|
+
const architectureViolations = architectureReport.violations.map(
|
|
193
|
+
summarizeArchitectureViolation
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// Run guard check
|
|
197
|
+
const checkResult = await runGuardCheck(manifestResult.data, projectRoot);
|
|
144
198
|
|
|
145
199
|
// Follow-up E — resolve type-aware defaulting from config, then
|
|
146
200
|
// run the bridge when enabled. Result envelope is always included
|
|
147
201
|
// in the tool response so MCP clients have a single stable shape.
|
|
148
|
-
let
|
|
149
|
-
try {
|
|
150
|
-
|
|
151
|
-
} catch {
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
const typeAwareCfg = (
|
|
155
|
-
|
|
156
|
-
| { typeAware?: Record<string, unknown> }
|
|
157
|
-
| undefined
|
|
158
|
-
)?.typeAware;
|
|
202
|
+
let rawProjectConfig: Awaited<ReturnType<typeof readConfig>> | undefined;
|
|
203
|
+
try {
|
|
204
|
+
rawProjectConfig = projectConfig ?? await readConfig(projectRoot);
|
|
205
|
+
} catch {
|
|
206
|
+
rawProjectConfig = projectConfig ?? undefined;
|
|
207
|
+
}
|
|
208
|
+
const typeAwareCfg = (
|
|
209
|
+
rawProjectConfig?.guard as
|
|
210
|
+
| { typeAware?: Record<string, unknown> }
|
|
211
|
+
| undefined
|
|
212
|
+
)?.typeAware;
|
|
159
213
|
const typeAwareEnabled =
|
|
160
214
|
typeAwareArg !== undefined ? typeAwareArg : typeAwareCfg !== undefined;
|
|
161
215
|
|
|
@@ -175,21 +229,40 @@ export function guardTools(projectRoot: string) {
|
|
|
175
229
|
skipped: bridge.skipped,
|
|
176
230
|
summary: bridge.summary,
|
|
177
231
|
violations: bridge.violations,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const typeAwareViolations = typeAwareResponse
|
|
236
|
+
? typeAwareResponse.violations as Array<{ severity: string }>
|
|
237
|
+
: [];
|
|
238
|
+
const typeAwareErrorCount = typeAwareViolations.filter(
|
|
239
|
+
(v) => v.severity === "error",
|
|
240
|
+
).length;
|
|
241
|
+
const legacyViolations = checkResult.violations.map(summarizeLegacyViolation);
|
|
242
|
+
const combinedViolations = [
|
|
243
|
+
...architectureViolations,
|
|
244
|
+
...legacyViolations,
|
|
245
|
+
];
|
|
246
|
+
const allPassed = checkResult.passed && architecturePassed && typeAwareErrorCount === 0;
|
|
247
|
+
const blockingViolationCount = combinedViolations.length + typeAwareErrorCount;
|
|
248
|
+
|
|
249
|
+
if (allPassed) {
|
|
250
|
+
return {
|
|
251
|
+
passed: true,
|
|
252
|
+
violations: [],
|
|
253
|
+
message: "All guard checks passed",
|
|
254
|
+
architecture: {
|
|
255
|
+
passed: true,
|
|
256
|
+
totalViolations: architectureReport.totalViolations,
|
|
257
|
+
bySeverity: architectureReport.bySeverity,
|
|
258
|
+
},
|
|
259
|
+
legacy: {
|
|
260
|
+
passed: true,
|
|
261
|
+
violations: 0,
|
|
262
|
+
},
|
|
263
|
+
relatedSkills: ["mandu-guard-guide", "mandu-debug"],
|
|
264
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
265
|
+
};
|
|
193
266
|
}
|
|
194
267
|
|
|
195
268
|
// If auto-correct requested and there are violations
|
|
@@ -200,33 +273,54 @@ export function guardTools(projectRoot: string) {
|
|
|
200
273
|
projectRoot
|
|
201
274
|
);
|
|
202
275
|
|
|
203
|
-
return {
|
|
204
|
-
passed:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
276
|
+
return {
|
|
277
|
+
passed:
|
|
278
|
+
autoCorrectResult.fixed &&
|
|
279
|
+
architecturePassed &&
|
|
280
|
+
typeAwareErrorCount === 0,
|
|
281
|
+
violations: [
|
|
282
|
+
...architectureViolations,
|
|
283
|
+
...autoCorrectResult.remainingViolations.map(summarizeLegacyViolation),
|
|
284
|
+
],
|
|
285
|
+
autoCorrect: {
|
|
286
|
+
attempted: true,
|
|
287
|
+
fixed: autoCorrectResult.fixed,
|
|
209
288
|
steps: autoCorrectResult.steps,
|
|
210
289
|
retriedCount: autoCorrectResult.retriedCount,
|
|
211
290
|
rolledBack: autoCorrectResult.rolledBack,
|
|
212
|
-
changeId: autoCorrectResult.changeId,
|
|
213
|
-
},
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
291
|
+
changeId: autoCorrectResult.changeId,
|
|
292
|
+
},
|
|
293
|
+
architecture: {
|
|
294
|
+
passed: architecturePassed,
|
|
295
|
+
totalViolations: architectureReport.totalViolations,
|
|
296
|
+
bySeverity: architectureReport.bySeverity,
|
|
297
|
+
violations: architectureViolations,
|
|
298
|
+
},
|
|
299
|
+
legacy: {
|
|
300
|
+
passed: autoCorrectResult.fixed,
|
|
301
|
+
violations: autoCorrectResult.remainingViolations.length,
|
|
302
|
+
},
|
|
303
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
passed: false,
|
|
309
|
+
violations: combinedViolations,
|
|
310
|
+
message: `Found ${blockingViolationCount} violation(s)`,
|
|
311
|
+
architecture: {
|
|
312
|
+
passed: architecturePassed,
|
|
313
|
+
totalViolations: architectureReport.totalViolations,
|
|
314
|
+
bySeverity: architectureReport.bySeverity,
|
|
315
|
+
violations: architectureViolations,
|
|
316
|
+
},
|
|
317
|
+
legacy: {
|
|
318
|
+
passed: checkResult.passed,
|
|
319
|
+
violations: legacyViolations.length,
|
|
320
|
+
},
|
|
321
|
+
tip: "Use autoCorrect: true to attempt automatic fixes",
|
|
322
|
+
relatedSkills: ["mandu-guard-guide", "mandu-debug"],
|
|
323
|
+
...(typeAwareResponse ? { typeAware: typeAwareResponse } : {}),
|
|
230
324
|
};
|
|
231
325
|
},
|
|
232
326
|
|
package/src/tools/index.ts
CHANGED
|
@@ -162,10 +162,10 @@ import {
|
|
|
162
162
|
/**
|
|
163
163
|
* 도구 모듈 정보
|
|
164
164
|
*/
|
|
165
|
-
interface ToolModule {
|
|
166
|
-
category: string;
|
|
167
|
-
definitions: Tool[];
|
|
168
|
-
handlers: (
|
|
165
|
+
export interface ToolModule {
|
|
166
|
+
category: string;
|
|
167
|
+
definitions: Tool[];
|
|
168
|
+
handlers: (
|
|
169
169
|
projectRoot: string,
|
|
170
170
|
server?: Server,
|
|
171
171
|
monitor?: ActivityMonitor
|
|
@@ -188,7 +188,7 @@ interface ToolModule {
|
|
|
188
188
|
/**
|
|
189
189
|
* 빌트인 도구 모듈 목록
|
|
190
190
|
*/
|
|
191
|
-
const TOOL_MODULES: ToolModule[] = [
|
|
191
|
+
export const TOOL_MODULES: ToolModule[] = [
|
|
192
192
|
{ category: "spec", definitions: specToolDefinitions, handlers: specTools },
|
|
193
193
|
{ category: "generate", definitions: generateToolDefinitions, handlers: generateTools },
|
|
194
194
|
{ category: "transaction", definitions: transactionToolDefinitions, handlers: transactionTools },
|
|
@@ -200,18 +200,18 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
200
200
|
{ category: "slot", definitions: slotToolDefinitions, handlers: slotTools },
|
|
201
201
|
{ category: "hydration", definitions: hydrationToolDefinitions, handlers: hydrationTools },
|
|
202
202
|
{ category: "contract", definitions: contractToolDefinitions, handlers: contractTools },
|
|
203
|
-
{ category: "brain", definitions: brainToolDefinitions, handlers: brainTools
|
|
203
|
+
{ category: "brain", definitions: brainToolDefinitions, handlers: brainTools, requiresServer: true },
|
|
204
204
|
{ category: "runtime", definitions: runtimeToolDefinitions, handlers: runtimeTools },
|
|
205
205
|
{ category: "seo", definitions: seoToolDefinitions, handlers: seoTools },
|
|
206
|
-
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools
|
|
206
|
+
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools, requiresServer: true },
|
|
207
207
|
// ate + ate-run accept an optional Server so notifications/progress
|
|
208
208
|
// can flow (issue #238). `acceptsServer: true` forwards the server
|
|
209
209
|
// when available but still registers when it isn't — callers that
|
|
210
210
|
// boot without an MCP transport get progress no-oped silently.
|
|
211
|
-
{ category: "ate", definitions: ateToolDefinitions, handlers: ateTools
|
|
212
|
-
{ category: "ate-phase5", definitions: atePhase5ToolDefinitions, handlers: createAtePhase5Handlers
|
|
211
|
+
{ category: "ate", definitions: ateToolDefinitions, handlers: ateTools, acceptsServer: true },
|
|
212
|
+
{ category: "ate-phase5", definitions: atePhase5ToolDefinitions, handlers: createAtePhase5Handlers },
|
|
213
213
|
{ category: "ate-context", definitions: ateContextToolDefinitions, handlers: ateContextTools },
|
|
214
|
-
{ category: "ate-run", definitions: ateRunToolDefinitions, handlers: ateRunTools
|
|
214
|
+
{ category: "ate-run", definitions: ateRunToolDefinitions, handlers: ateRunTools, acceptsServer: true },
|
|
215
215
|
{ category: "ate-flakes", definitions: ateFlakesToolDefinitions, handlers: ateFlakesTools },
|
|
216
216
|
{ category: "ate-prompt", definitions: atePromptToolDefinitions, handlers: atePromptTools },
|
|
217
217
|
{ category: "ate-exemplar", definitions: ateExemplarToolDefinitions, handlers: ateExemplarTools },
|
|
@@ -282,10 +282,41 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
282
282
|
definitions: extractContractToolDefinitions,
|
|
283
283
|
handlers: extractContractTools,
|
|
284
284
|
},
|
|
285
|
-
];
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
export function validateBuiltinToolModules(
|
|
288
|
+
modules: readonly ToolModule[] = TOOL_MODULES
|
|
289
|
+
): string[] {
|
|
290
|
+
const issues: string[] = [];
|
|
291
|
+
const categories = new Set<string>();
|
|
292
|
+
const toolNames = new Map<string, string>();
|
|
293
|
+
|
|
294
|
+
for (const module of modules) {
|
|
295
|
+
if (categories.has(module.category)) {
|
|
296
|
+
issues.push(`duplicate tool category: ${module.category}`);
|
|
297
|
+
}
|
|
298
|
+
categories.add(module.category);
|
|
299
|
+
|
|
300
|
+
if (module.definitions.length === 0) {
|
|
301
|
+
issues.push(`tool category has no definitions: ${module.category}`);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
for (const definition of module.definitions) {
|
|
305
|
+
const previousCategory = toolNames.get(definition.name);
|
|
306
|
+
if (previousCategory) {
|
|
307
|
+
issues.push(
|
|
308
|
+
`duplicate tool definition: ${definition.name} in ${previousCategory} and ${module.category}`
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
toolNames.set(definition.name, module.category);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return issues;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* 빌트인 도구들을 레지스트리에 등록
|
|
289
320
|
*
|
|
290
321
|
* @param projectRoot - 프로젝트 루트 경로
|
|
291
322
|
* @param server - MCP Server 인스턴스 (선택, brain/project 도구에 필요)
|
|
@@ -306,11 +337,15 @@ export function registerBuiltinTools(
|
|
|
306
337
|
monitor?: ActivityMonitor,
|
|
307
338
|
options?: { profile?: McpProfile }
|
|
308
339
|
): void {
|
|
309
|
-
const allowedCategories = options?.profile
|
|
310
|
-
? getProfileCategories(options.profile)
|
|
311
|
-
: null;
|
|
312
|
-
|
|
313
|
-
|
|
340
|
+
const allowedCategories = options?.profile
|
|
341
|
+
? getProfileCategories(options.profile)
|
|
342
|
+
: null;
|
|
343
|
+
const definitionIssues = validateBuiltinToolModules();
|
|
344
|
+
if (definitionIssues.length > 0) {
|
|
345
|
+
throw new Error(`Invalid MCP tool module registry:\n${definitionIssues.join("\n")}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
for (const module of TOOL_MODULES) {
|
|
314
349
|
// Profile filtering: skip categories not in the allowed list
|
|
315
350
|
if (allowedCategories && !allowedCategories.includes(module.category)) {
|
|
316
351
|
continue;
|
|
@@ -321,17 +356,13 @@ export function registerBuiltinTools(
|
|
|
321
356
|
continue;
|
|
322
357
|
}
|
|
323
358
|
|
|
324
|
-
try {
|
|
325
|
-
let handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>>;
|
|
326
|
-
if (module.requiresServer) {
|
|
327
|
-
handlers =
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
);
|
|
332
|
-
} else if (module.acceptsServer) {
|
|
333
|
-
// Forward the Server when available; fall back to just projectRoot.
|
|
334
|
-
handlers = server
|
|
359
|
+
try {
|
|
360
|
+
let handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>>;
|
|
361
|
+
if (module.requiresServer) {
|
|
362
|
+
handlers = module.handlers(projectRoot, server, monitor);
|
|
363
|
+
} else if (module.acceptsServer) {
|
|
364
|
+
// Forward the Server when available; fall back to just projectRoot.
|
|
365
|
+
handlers = server
|
|
335
366
|
? module.handlers(projectRoot, server)
|
|
336
367
|
: module.handlers(projectRoot);
|
|
337
368
|
} else {
|
package/src/tools/kitchen.ts
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Enables any MCP-compatible agent to read client-side errors in real-time.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
-
import { loadManduConfig } from "@mandujs/core";
|
|
9
|
-
import { getDevServerState } from "./project.js";
|
|
7
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { loadManduConfig } from "@mandujs/core";
|
|
9
|
+
import { getDevServerState } from "./project.js";
|
|
10
|
+
import { readRuntimeControl } from "../utils/runtime-control.js";
|
|
10
11
|
|
|
11
12
|
export const kitchenToolDefinitions: Tool[] = [
|
|
12
13
|
{
|
|
@@ -19,10 +20,18 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
19
20
|
inputSchema: {
|
|
20
21
|
type: "object",
|
|
21
22
|
properties: {
|
|
22
|
-
clear: {
|
|
23
|
-
type: "boolean",
|
|
24
|
-
description: "Clear errors after reading (default: false)",
|
|
25
|
-
},
|
|
23
|
+
clear: {
|
|
24
|
+
type: "boolean",
|
|
25
|
+
description: "Clear errors after reading (default: false)",
|
|
26
|
+
},
|
|
27
|
+
baseURL: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Explicit dev server URL. Overrides runtime-control/config discovery.",
|
|
30
|
+
},
|
|
31
|
+
port: {
|
|
32
|
+
type: "number",
|
|
33
|
+
description: "Explicit dev server port. Overrides runtime-control/config discovery.",
|
|
34
|
+
},
|
|
26
35
|
},
|
|
27
36
|
required: [],
|
|
28
37
|
},
|
|
@@ -47,11 +56,19 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
47
56
|
description:
|
|
48
57
|
"Include the extended diagnose report. Default true. Set false to lower latency when a11y_hints / package_export_gaps are noisy.",
|
|
49
58
|
},
|
|
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
|
-
},
|
|
59
|
+
includeDiff: {
|
|
60
|
+
type: "boolean",
|
|
61
|
+
description:
|
|
62
|
+
"Include git diff against MANDU_DIFF_BASE (default HEAD). Default true. Set false to skip when git is unavailable.",
|
|
63
|
+
},
|
|
64
|
+
baseURL: {
|
|
65
|
+
type: "string",
|
|
66
|
+
description: "Explicit dev server URL. Overrides runtime-control/config discovery.",
|
|
67
|
+
},
|
|
68
|
+
port: {
|
|
69
|
+
type: "number",
|
|
70
|
+
description: "Explicit dev server port. Overrides runtime-control/config discovery.",
|
|
71
|
+
},
|
|
55
72
|
},
|
|
56
73
|
required: [],
|
|
57
74
|
},
|
|
@@ -59,15 +76,32 @@ export const kitchenToolDefinitions: Tool[] = [
|
|
|
59
76
|
];
|
|
60
77
|
|
|
61
78
|
/**
|
|
62
|
-
* Resolve the dev server base URL. Prefers
|
|
63
|
-
*
|
|
64
|
-
* default
|
|
65
|
-
* agree on where to fetch from.
|
|
79
|
+
* Resolve the dev server base URL. Prefers explicit args, then
|
|
80
|
+
* `.mandu/runtime-control.json` from the running dev/start process, then
|
|
81
|
+
* captured stdout, then config/default 3333. Shared by every Kitchen-backed
|
|
82
|
+
* MCP tool so they all agree on where to fetch from.
|
|
66
83
|
*/
|
|
67
|
-
async function resolveDevServerBaseUrl(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
async function resolveDevServerBaseUrl(
|
|
85
|
+
projectRoot: string,
|
|
86
|
+
args: { baseURL?: unknown; port?: unknown } = {},
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
if (typeof args.baseURL === "string" && args.baseURL.trim()) {
|
|
89
|
+
return args.baseURL.trim().replace(/\/+$/, "");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const explicitPort = normalizePort(args.port);
|
|
93
|
+
if (explicitPort) {
|
|
94
|
+
return `http://localhost:${explicitPort}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const control = await readRuntimeControl(projectRoot);
|
|
98
|
+
if (control?.baseUrl) {
|
|
99
|
+
return control.baseUrl.replace(/\/+$/, "");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let port: number | undefined;
|
|
103
|
+
|
|
104
|
+
const serverState = getDevServerState();
|
|
71
105
|
if (serverState) {
|
|
72
106
|
for (const line of serverState.output) {
|
|
73
107
|
const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
|
|
@@ -81,15 +115,21 @@ async function resolveDevServerBaseUrl(projectRoot: string): Promise<string> {
|
|
|
81
115
|
const config = await loadManduConfig(projectRoot);
|
|
82
116
|
port = config.server?.port ?? 3333;
|
|
83
117
|
}
|
|
84
|
-
|
|
85
|
-
return `http://localhost:${port}`;
|
|
86
|
-
}
|
|
118
|
+
|
|
119
|
+
return `http://localhost:${port}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function normalizePort(value: unknown): number | undefined {
|
|
123
|
+
const raw = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
|
|
124
|
+
if (!Number.isInteger(raw) || raw < 1 || raw > 65535) return undefined;
|
|
125
|
+
return raw;
|
|
126
|
+
}
|
|
87
127
|
|
|
88
128
|
export function kitchenTools(projectRoot: string) {
|
|
89
129
|
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);
|
|
130
|
+
"mandu.kitchen.errors": async (args: Record<string, unknown>) => {
|
|
131
|
+
const { clear = false } = args as { clear?: boolean };
|
|
132
|
+
const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
|
|
93
133
|
|
|
94
134
|
try {
|
|
95
135
|
// Fetch errors from Kitchen API
|
|
@@ -145,12 +185,12 @@ export function kitchenTools(projectRoot: string) {
|
|
|
145
185
|
*/
|
|
146
186
|
"mandu.devtools.context": async (args: Record<string, unknown>) => {
|
|
147
187
|
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);
|
|
188
|
+
includeBundle = true,
|
|
189
|
+
includeDiagnose = true,
|
|
190
|
+
includeDiff = true,
|
|
191
|
+
} = args as { includeBundle?: boolean; includeDiagnose?: boolean; includeDiff?: boolean };
|
|
192
|
+
|
|
193
|
+
const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
|
|
154
194
|
const params = new URLSearchParams();
|
|
155
195
|
if (!includeBundle) params.set("bundle", "0");
|
|
156
196
|
if (!includeDiagnose) params.set("diagnose", "0");
|