@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.
- package/README.md +3 -3
- package/package.json +2 -2
- package/src/executor/error-handler.ts +2 -76
- package/src/index.ts +5 -5
- package/src/prompts.ts +4 -4
- package/src/resources/skills/guides.ts +49 -49
- package/src/resources/skills/loader.ts +8 -8
- package/src/resources/skills/mandu-agent-workflow/SKILL.md +124 -124
- package/src/resources/skills/mandu-agent-workflow/metadata.json +7 -7
- package/src/resources/skills/mandu-composition/SKILL.md +47 -47
- package/src/resources/skills/mandu-deployment/SKILL.md +53 -53
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +3 -3
- package/src/resources/skills/mandu-fs-routes/SKILL.md +47 -47
- package/src/resources/skills/mandu-guard/SKILL.md +58 -58
- package/src/resources/skills/mandu-hydration/SKILL.md +67 -67
- package/src/resources/skills/mandu-hydration/rules/hydration-island-setup.md +54 -54
- package/src/resources/skills/mandu-hydration/rules/hydration-priority-visible.md +60 -60
- package/src/resources/skills/mandu-performance/SKILL.md +47 -47
- package/src/resources/skills/mandu-security/SKILL.md +47 -47
- package/src/resources/skills/mandu-slot/SKILL.md +48 -48
- package/src/resources/skills/mandu-styling/SKILL.md +52 -52
- package/src/resources/skills/mandu-testing/SKILL.md +55 -55
- package/src/resources/skills/mandu-ui/SKILL.md +52 -52
- package/src/resources/skills/recipes.ts +28 -28
- package/src/server.ts +3 -3
- package/src/tools/agent.ts +443 -443
- package/src/tools/ate.ts +37 -37
- package/src/tools/composite.ts +13 -13
- package/src/tools/guard.ts +0 -35
- package/src/tools/hydration.ts +197 -197
- package/src/tools/index.ts +4 -18
- package/src/tools/kitchen.ts +72 -72
- package/src/tools/runtime.ts +579 -579
- package/src/tools/slot-validation.ts +19 -19
- package/src/tools/transaction.ts +4 -18
package/src/tools/hydration.ts
CHANGED
|
@@ -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
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
-
|
|
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}`
|
package/src/tools/kitchen.ts
CHANGED
|
@@ -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");
|