@mandujs/mcp 0.38.4 → 0.38.6

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 (35) hide show
  1. package/README.md +3 -3
  2. package/package.json +2 -2
  3. package/src/executor/error-handler.ts +2 -76
  4. package/src/index.ts +5 -5
  5. package/src/prompts.ts +4 -4
  6. package/src/resources/skills/guides.ts +49 -49
  7. package/src/resources/skills/loader.ts +8 -8
  8. package/src/resources/skills/mandu-agent-workflow/SKILL.md +124 -124
  9. package/src/resources/skills/mandu-agent-workflow/metadata.json +7 -7
  10. package/src/resources/skills/mandu-composition/SKILL.md +47 -47
  11. package/src/resources/skills/mandu-deployment/SKILL.md +53 -53
  12. package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +3 -3
  13. package/src/resources/skills/mandu-fs-routes/SKILL.md +47 -47
  14. package/src/resources/skills/mandu-guard/SKILL.md +58 -58
  15. package/src/resources/skills/mandu-hydration/SKILL.md +67 -67
  16. package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +54 -54
  17. package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +60 -60
  18. package/src/resources/skills/mandu-performance/SKILL.md +47 -47
  19. package/src/resources/skills/mandu-security/SKILL.md +47 -47
  20. package/src/resources/skills/mandu-slot/SKILL.md +48 -48
  21. package/src/resources/skills/mandu-styling/SKILL.md +52 -52
  22. package/src/resources/skills/mandu-testing/SKILL.md +55 -55
  23. package/src/resources/skills/mandu-ui/SKILL.md +52 -52
  24. package/src/resources/skills/recipes.ts +28 -28
  25. package/src/server.ts +3 -3
  26. package/src/tools/agent.ts +443 -443
  27. package/src/tools/ate.ts +37 -37
  28. package/src/tools/composite.ts +13 -13
  29. package/src/tools/guard.ts +0 -35
  30. package/src/tools/hydration.ts +197 -197
  31. package/src/tools/index.ts +4 -18
  32. package/src/tools/kitchen.ts +72 -72
  33. package/src/tools/runtime.ts +579 -579
  34. package/src/tools/slot-validation.ts +19 -19
  35. package/src/tools/transaction.ts +4 -18
@@ -18,17 +18,17 @@ export const hydrationToolDefinitions: Tool[] = [
18
18
  name: "mandu.build",
19
19
  description:
20
20
  "Build client bundles for hydration. Compiles client slots (.client.ts) into browser-ready JavaScript bundles.",
21
- annotations: {
22
- destructiveHint: true,
23
- readOnlyHint: false,
24
- idempotentHint: true,
25
- },
26
- inputSchema: {
27
- type: "object",
28
- properties: {
29
- minify: {
30
- type: "boolean",
31
- description: "Minify the output bundles (default: true in production)",
21
+ annotations: {
22
+ destructiveHint: true,
23
+ readOnlyHint: false,
24
+ idempotentHint: true,
25
+ },
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ minify: {
30
+ type: "boolean",
31
+ description: "Minify the output bundles (default: true in production)",
32
32
  },
33
33
  sourcemap: {
34
34
  type: "boolean",
@@ -39,67 +39,67 @@ export const hydrationToolDefinitions: Tool[] = [
39
39
  items: { type: "string" },
40
40
  description:
41
41
  "Only rebuild specific islands by routeId. Skips Runtime/Router/Vendor rebuild for faster incremental updates. Omit to rebuild everything.",
42
- },
43
- },
44
- required: [],
45
- additionalProperties: false,
46
- },
47
- },
42
+ },
43
+ },
44
+ required: [],
45
+ additionalProperties: false,
46
+ },
47
+ },
48
48
  {
49
49
  name: "mandu.build.status",
50
50
  description:
51
51
  "Get the current build status, bundle manifest, and statistics for client bundles.",
52
- annotations: {
53
- readOnlyHint: true,
54
- },
55
- inputSchema: {
56
- type: "object",
57
- properties: {},
58
- required: [],
59
- additionalProperties: false,
60
- },
61
- },
62
- {
63
- name: "mandu.island.list",
64
- description:
65
- "Legacy alias for page client mount diagnostics. Prefer mandu.pageClientMount.list for terminology that separates page-level hydration from nested islands.",
66
- annotations: {
67
- readOnlyHint: true,
68
- },
69
- inputSchema: {
70
- type: "object",
71
- properties: {},
72
- required: [],
73
- additionalProperties: false,
74
- },
75
- },
76
- {
77
- name: "mandu.pageClientMount.list",
78
- description:
79
- "List page-level client mounts: page routes that need a route-level clientModule and bundle. This is distinct from nested islands/partials.",
80
- annotations: {
81
- readOnlyHint: true,
82
- },
83
- inputSchema: {
84
- type: "object",
85
- properties: {},
86
- required: [],
87
- additionalProperties: false,
88
- },
89
- },
90
- {
91
- name: "mandu.hydration.set",
52
+ annotations: {
53
+ readOnlyHint: true,
54
+ },
55
+ inputSchema: {
56
+ type: "object",
57
+ properties: {},
58
+ required: [],
59
+ additionalProperties: false,
60
+ },
61
+ },
62
+ {
63
+ name: "mandu.island.list",
64
+ description:
65
+ "Legacy alias for page client mount diagnostics. Prefer mandu.pageClientMount.list for terminology that separates page-level hydration from nested islands.",
66
+ annotations: {
67
+ readOnlyHint: true,
68
+ },
69
+ inputSchema: {
70
+ type: "object",
71
+ properties: {},
72
+ required: [],
73
+ additionalProperties: false,
74
+ },
75
+ },
76
+ {
77
+ name: "mandu.pageClientMount.list",
78
+ description:
79
+ "List page-level client mounts: page routes that need a route-level clientModule and bundle. This is distinct from nested islands/partials.",
80
+ annotations: {
81
+ readOnlyHint: true,
82
+ },
83
+ inputSchema: {
84
+ type: "object",
85
+ properties: {},
86
+ required: [],
87
+ additionalProperties: false,
88
+ },
89
+ },
90
+ {
91
+ name: "mandu.hydration.set",
92
92
  description:
93
93
  "Set hydration configuration for a specific route. Updates the route's hydration strategy and priority.",
94
- annotations: {
95
- readOnlyHint: false,
96
- },
97
- inputSchema: {
98
- type: "object",
99
- properties: {
100
- routeId: {
101
- type: "string",
102
- description: "The route ID to configure",
94
+ annotations: {
95
+ readOnlyHint: false,
96
+ },
97
+ inputSchema: {
98
+ type: "object",
99
+ properties: {
100
+ routeId: {
101
+ type: "string",
102
+ description: "The route ID to configure",
103
103
  },
104
104
  strategy: {
105
105
  type: "string",
@@ -117,25 +117,25 @@ export const hydrationToolDefinitions: Tool[] = [
117
117
  type: "boolean",
118
118
  description: "Whether to preload the bundle with modulepreload",
119
119
  },
120
- },
121
- required: ["routeId"],
122
- additionalProperties: false,
123
- },
124
- },
125
- {
120
+ },
121
+ required: ["routeId"],
122
+ additionalProperties: false,
123
+ },
124
+ },
125
+ {
126
126
  name: "mandu.hydration.addClientSlot",
127
127
  description:
128
128
  "Add a client slot file for a route to enable hydration. Creates the .client.ts file and updates the manifest.",
129
- annotations: {
130
- destructiveHint: false,
131
- readOnlyHint: false,
132
- },
133
- inputSchema: {
134
- type: "object",
135
- properties: {
136
- routeId: {
137
- type: "string",
138
- description: "The route ID to add client slot for",
129
+ annotations: {
130
+ destructiveHint: false,
131
+ readOnlyHint: false,
132
+ },
133
+ inputSchema: {
134
+ type: "object",
135
+ properties: {
136
+ routeId: {
137
+ type: "string",
138
+ description: "The route ID to add client slot for",
139
139
  },
140
140
  strategy: {
141
141
  type: "string",
@@ -147,14 +147,14 @@ export const hydrationToolDefinitions: Tool[] = [
147
147
  enum: ["immediate", "visible", "idle", "interaction"],
148
148
  description: "Hydration priority (default: visible)",
149
149
  },
150
- },
151
- required: ["routeId"],
152
- additionalProperties: false,
153
- },
154
- },
155
- ];
156
-
157
- export function hydrationTools(projectRoot: string) {
150
+ },
151
+ required: ["routeId"],
152
+ additionalProperties: false,
153
+ },
154
+ },
155
+ ];
156
+
157
+ export function hydrationTools(projectRoot: string) {
158
158
  const paths = getProjectPaths(projectRoot);
159
159
 
160
160
  const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
@@ -234,76 +234,76 @@ export function hydrationTools(projectRoot: string) {
234
234
  };
235
235
  },
236
236
 
237
- "mandu.pageClientMount.list": async () => {
238
- // Load manifest
239
- const manifestResult = await loadManifest(paths.manifestPath);
240
- if (!manifestResult.success || !manifestResult.data) {
241
- return { error: manifestResult.errors };
242
- }
243
-
244
- const pageClientMounts = manifestResult.data.routes
245
- .filter((route) => route.kind === "page")
246
- .map((route) => {
247
- const hydration = getRouteHydration(route);
248
- const needsClientMount = needsHydration(route);
249
- const warning = needsClientMount && !route.clientModule
250
- ? `Route has hydration strategy '${hydration.strategy}' but no clientModule; build would emit no route bundle.`
251
- : null;
252
-
253
- return {
254
- routeId: route.id,
255
- pattern: route.pattern,
256
- hasClientModule: !!route.clientModule,
257
- clientModule: route.clientModule || null,
258
- needsClientMount,
259
- // Backward compatibility for older agents that read `isIsland`.
260
- isIsland: needsClientMount,
261
- status: needsClientMount ? (route.clientModule ? "ready" : "broken") : "static",
262
- warning,
263
- hydration: {
264
- strategy: hydration.strategy,
265
- priority: hydration.priority,
237
+ "mandu.pageClientMount.list": async () => {
238
+ // Load manifest
239
+ const manifestResult = await loadManifest(paths.manifestPath);
240
+ if (!manifestResult.success || !manifestResult.data) {
241
+ return { error: manifestResult.errors };
242
+ }
243
+
244
+ const pageClientMounts = manifestResult.data.routes
245
+ .filter((route) => route.kind === "page")
246
+ .map((route) => {
247
+ const hydration = getRouteHydration(route);
248
+ const needsClientMount = needsHydration(route);
249
+ const warning = needsClientMount && !route.clientModule
250
+ ? `Route has hydration strategy '${hydration.strategy}' but no clientModule; build would emit no route bundle.`
251
+ : null;
252
+
253
+ return {
254
+ routeId: route.id,
255
+ pattern: route.pattern,
256
+ hasClientModule: !!route.clientModule,
257
+ clientModule: route.clientModule || null,
258
+ needsClientMount,
259
+ // Backward compatibility for older agents that read `isIsland`.
260
+ isIsland: needsClientMount,
261
+ status: needsClientMount ? (route.clientModule ? "ready" : "broken") : "static",
262
+ warning,
263
+ hydration: {
264
+ strategy: hydration.strategy,
265
+ priority: hydration.priority,
266
266
  preload: hydration.preload,
267
- },
268
- };
269
- });
270
-
271
- const pageClientMountCount = pageClientMounts.filter((i) => i.needsClientMount).length;
272
- const staticCount = pageClientMounts.filter((i) => !i.needsClientMount).length;
273
- const activeMounts = pageClientMounts.filter((i) => i.needsClientMount);
274
- const staticPages = pageClientMounts.filter((i) => !i.needsClientMount);
275
-
276
- return {
277
- terminology: {
278
- pageClientMount:
279
- "A page route whose whole page is hydrated from a route-level clientModule and route bundle.",
280
- island:
281
- "A nested or route-local island bundle, distinct from page client mounts. Use mandu.runtime.status for both collections.",
282
- },
283
- totalPages: pageClientMounts.length,
284
- pageClientMountCount,
285
- staticCount,
286
- pageClientMounts: activeMounts,
287
- staticPages,
288
- // Backward compatibility for older clients.
289
- islandCount: pageClientMountCount,
290
- islands: activeMounts,
291
- };
292
- },
293
-
294
- "mandu.island.list": async () => handlers["mandu.pageClientMount.list"]({}),
295
-
296
- "mandu.hydration.set": async (args: Record<string, unknown>) => {
297
- const validationError = validateRouteIdArgs(
298
- args,
299
- "mandu.hydration.set",
300
- ["routeId", "strategy", "priority", "preload"],
301
- );
302
- if (validationError) return { error: validationError };
303
-
304
- const { routeId, strategy, priority, preload } = args as {
305
- routeId: string;
306
- strategy?: SpecHydrationStrategy;
267
+ },
268
+ };
269
+ });
270
+
271
+ const pageClientMountCount = pageClientMounts.filter((i) => i.needsClientMount).length;
272
+ const staticCount = pageClientMounts.filter((i) => !i.needsClientMount).length;
273
+ const activeMounts = pageClientMounts.filter((i) => i.needsClientMount);
274
+ const staticPages = pageClientMounts.filter((i) => !i.needsClientMount);
275
+
276
+ return {
277
+ terminology: {
278
+ pageClientMount:
279
+ "A page route whose whole page is hydrated from a route-level clientModule and route bundle.",
280
+ island:
281
+ "A nested or route-local island bundle, distinct from page client mounts. Use mandu.runtime.status for both collections.",
282
+ },
283
+ totalPages: pageClientMounts.length,
284
+ pageClientMountCount,
285
+ staticCount,
286
+ pageClientMounts: activeMounts,
287
+ staticPages,
288
+ // Backward compatibility for older clients.
289
+ islandCount: pageClientMountCount,
290
+ islands: activeMounts,
291
+ };
292
+ },
293
+
294
+ "mandu.island.list": async () => handlers["mandu.pageClientMount.list"]({}),
295
+
296
+ "mandu.hydration.set": async (args: Record<string, unknown>) => {
297
+ const validationError = validateRouteIdArgs(
298
+ args,
299
+ "mandu.hydration.set",
300
+ ["routeId", "strategy", "priority", "preload"],
301
+ );
302
+ if (validationError) return { error: validationError };
303
+
304
+ const { routeId, strategy, priority, preload } = args as {
305
+ routeId: string;
306
+ strategy?: SpecHydrationStrategy;
307
307
  priority?: HydrationPriority;
308
308
  preload?: boolean;
309
309
  };
@@ -357,19 +357,19 @@ export function hydrationTools(projectRoot: string) {
357
357
  newHydration,
358
358
  message: `Updated hydration config for ${routeId}`,
359
359
  };
360
- },
361
-
362
- "mandu.hydration.addClientSlot": async (args: Record<string, unknown>) => {
363
- const validationError = validateRouteIdArgs(
364
- args,
365
- "mandu.hydration.addClientSlot",
366
- ["routeId", "strategy", "priority"],
367
- );
368
- if (validationError) return { error: validationError };
369
-
370
- const { routeId, strategy = "island", priority = "visible" } = args as {
371
- routeId: string;
372
- strategy?: SpecHydrationStrategy;
360
+ },
361
+
362
+ "mandu.hydration.addClientSlot": async (args: Record<string, unknown>) => {
363
+ const validationError = validateRouteIdArgs(
364
+ args,
365
+ "mandu.hydration.addClientSlot",
366
+ ["routeId", "strategy", "priority"],
367
+ );
368
+ if (validationError) return { error: validationError };
369
+
370
+ const { routeId, strategy = "island", priority = "visible" } = args as {
371
+ routeId: string;
372
+ strategy?: SpecHydrationStrategy;
373
373
  priority?: HydrationPriority;
374
374
  };
375
375
 
@@ -449,31 +449,31 @@ export function hydrationTools(projectRoot: string) {
449
449
  };
450
450
 
451
451
  // Backward-compatible aliases (deprecated)
452
- handlers["mandu_build"] = handlers["mandu.build"];
453
- handlers["mandu_build_status"] = handlers["mandu.build.status"];
454
- handlers["mandu_list_islands"] = handlers["mandu.island.list"];
455
- handlers["mandu_page_client_mount_list"] = handlers["mandu.pageClientMount.list"];
456
- handlers["mandu_set_hydration"] = handlers["mandu.hydration.set"];
457
- handlers["mandu_hydration_set"] = handlers["mandu.hydration.set"];
458
- handlers["mandu_add_client_slot"] = handlers["mandu.hydration.addClientSlot"];
459
- handlers["mandu_hydration_add_client_slot"] = handlers["mandu.hydration.addClientSlot"];
460
-
461
- return handlers;
462
- }
463
-
464
- function validateRouteIdArgs(
465
- args: Record<string, unknown>,
466
- toolName: string,
467
- allowedKeys: readonly string[],
468
- ): string | null {
469
- if (typeof args.routeId !== "string" || args.routeId.trim().length === 0) {
470
- const allowed = new Set(allowedKeys);
471
- const unknownKeys = Object.keys(args).filter((key) => !allowed.has(key));
472
- const got = unknownKeys.length > 0 ? ` (got unknown key${unknownKeys.length === 1 ? "" : "s"} '${unknownKeys.join("', '")}')` : "";
473
- return `${toolName}: missing required parameter 'routeId'${got}`;
474
- }
475
- return null;
476
- }
452
+ handlers["mandu_build"] = handlers["mandu.build"];
453
+ handlers["mandu_build_status"] = handlers["mandu.build.status"];
454
+ handlers["mandu_list_islands"] = handlers["mandu.island.list"];
455
+ handlers["mandu_page_client_mount_list"] = handlers["mandu.pageClientMount.list"];
456
+ handlers["mandu_set_hydration"] = handlers["mandu.hydration.set"];
457
+ handlers["mandu_hydration_set"] = handlers["mandu.hydration.set"];
458
+ handlers["mandu_add_client_slot"] = handlers["mandu.hydration.addClientSlot"];
459
+ handlers["mandu_hydration_add_client_slot"] = handlers["mandu.hydration.addClientSlot"];
460
+
461
+ return handlers;
462
+ }
463
+
464
+ function validateRouteIdArgs(
465
+ args: Record<string, unknown>,
466
+ toolName: string,
467
+ allowedKeys: readonly string[],
468
+ ): string | null {
469
+ if (typeof args.routeId !== "string" || args.routeId.trim().length === 0) {
470
+ const allowed = new Set(allowedKeys);
471
+ const unknownKeys = Object.keys(args).filter((key) => !allowed.has(key));
472
+ const got = unknownKeys.length > 0 ? ` (got unknown key${unknownKeys.length === 1 ? "" : "s"} '${unknownKeys.join("', '")}')` : "";
473
+ return `${toolName}: missing required parameter 'routeId'${got}`;
474
+ }
475
+ return null;
476
+ }
477
477
 
478
478
  /**
479
479
  * Generate a client slot template
@@ -290,10 +290,9 @@ export const TOOL_MODULES: ToolModule[] = [
290
290
  export function validateBuiltinToolModules(
291
291
  modules: readonly ToolModule[] = TOOL_MODULES
292
292
  ): string[] {
293
- const issues: string[] = [];
294
- const categories = new Set<string>();
295
- const toolNames = new Map<string, string>();
296
- const descriptions = new Map<string, string>();
293
+ const issues: string[] = [];
294
+ const categories = new Set<string>();
295
+ const toolNames = new Map<string, string>();
297
296
 
298
297
  for (const module of modules) {
299
298
  if (categories.has(module.category)) {
@@ -306,20 +305,7 @@ export function validateBuiltinToolModules(
306
305
  }
307
306
 
308
307
  for (const definition of module.definitions) {
309
- if (!definition.description?.trim()) {
310
- issues.push(`tool definition is missing description: ${definition.name} in ${module.category}`);
311
- }
312
- const normalizedDescription = definition.description?.trim().replace(/\s+/g, " ").toLowerCase();
313
- if (normalizedDescription) {
314
- const previousTool = descriptions.get(normalizedDescription);
315
- if (previousTool) {
316
- issues.push(
317
- `duplicate tool description: ${definition.name} and ${previousTool} both describe the same action`
318
- );
319
- }
320
- descriptions.set(normalizedDescription, definition.name);
321
- }
322
- const previousCategory = toolNames.get(definition.name);
308
+ const previousCategory = toolNames.get(definition.name);
323
309
  if (previousCategory) {
324
310
  issues.push(
325
311
  `duplicate tool definition: ${definition.name} in ${previousCategory} and ${module.category}`
@@ -4,10 +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";
10
- import { readRuntimeControl } from "../utils/runtime-control.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";
11
11
 
12
12
  export const kitchenToolDefinitions: Tool[] = [
13
13
  {
@@ -20,18 +20,18 @@ export const kitchenToolDefinitions: Tool[] = [
20
20
  inputSchema: {
21
21
  type: "object",
22
22
  properties: {
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
- },
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
+ },
35
35
  },
36
36
  required: [],
37
37
  },
@@ -56,19 +56,19 @@ export const kitchenToolDefinitions: Tool[] = [
56
56
  description:
57
57
  "Include the extended diagnose report. Default true. Set false to lower latency when a11y_hints / package_export_gaps are noisy.",
58
58
  },
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
- },
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
+ },
72
72
  },
73
73
  required: [],
74
74
  },
@@ -76,32 +76,32 @@ export const kitchenToolDefinitions: Tool[] = [
76
76
  ];
77
77
 
78
78
  /**
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.
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.
83
83
  */
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();
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();
105
105
  if (serverState) {
106
106
  for (const line of serverState.output) {
107
107
  const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
@@ -115,21 +115,21 @@ async function resolveDevServerBaseUrl(
115
115
  const config = await loadManduConfig(projectRoot);
116
116
  port = config.server?.port ?? 3333;
117
117
  }
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
- }
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
+ }
127
127
 
128
128
  export function kitchenTools(projectRoot: string) {
129
129
  const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
130
- "mandu.kitchen.errors": async (args: Record<string, unknown>) => {
131
- const { clear = false } = args as { clear?: boolean };
132
- const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
130
+ "mandu.kitchen.errors": async (args: Record<string, unknown>) => {
131
+ const { clear = false } = args as { clear?: boolean };
132
+ const baseUrl = await resolveDevServerBaseUrl(projectRoot, args);
133
133
 
134
134
  try {
135
135
  // Fetch errors from Kitchen API
@@ -185,12 +185,12 @@ export function kitchenTools(projectRoot: string) {
185
185
  */
186
186
  "mandu.devtools.context": async (args: Record<string, unknown>) => {
187
187
  const {
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);
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);
194
194
  const params = new URLSearchParams();
195
195
  if (!includeBundle) params.set("bundle", "0");
196
196
  if (!includeDiagnose) params.set("diagnose", "0");