@mandujs/mcp 0.12.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +366 -367
- package/package.json +3 -2
- package/src/activity-monitor.ts +847 -847
- package/src/index.ts +106 -106
- package/src/tools/ate.ts +129 -0
- package/src/tools/generate.ts +7 -4
- package/src/tools/guard.ts +17 -4
- package/src/tools/hydration.ts +10 -10
- package/src/tools/index.ts +4 -1
- package/src/tools/spec.ts +80 -159
- package/src/utils/project.ts +22 -12
package/src/tools/spec.ts
CHANGED
|
@@ -3,16 +3,19 @@ import {
|
|
|
3
3
|
loadManifest,
|
|
4
4
|
validateManifest,
|
|
5
5
|
writeLock,
|
|
6
|
+
generateManifest,
|
|
7
|
+
GENERATED_RELATIVE_PATHS,
|
|
6
8
|
type RouteSpec,
|
|
7
9
|
type RoutesManifest,
|
|
8
10
|
} from "@mandujs/core";
|
|
9
11
|
import { getProjectPaths, readJsonFile, writeJsonFile } from "../utils/project.js";
|
|
10
12
|
import path from "path";
|
|
13
|
+
import fs from "fs/promises";
|
|
11
14
|
|
|
12
15
|
export const specToolDefinitions: Tool[] = [
|
|
13
16
|
{
|
|
14
17
|
name: "mandu_list_routes",
|
|
15
|
-
description: "List all routes in the current Mandu project",
|
|
18
|
+
description: "List all routes in the current Mandu project (reads from .mandu/routes.manifest.json)",
|
|
16
19
|
inputSchema: {
|
|
17
20
|
type: "object",
|
|
18
21
|
properties: {},
|
|
@@ -35,62 +38,34 @@ export const specToolDefinitions: Tool[] = [
|
|
|
35
38
|
},
|
|
36
39
|
{
|
|
37
40
|
name: "mandu_add_route",
|
|
38
|
-
description: "Add a new route
|
|
41
|
+
description: "Add a new route by scaffolding files in app/ and optionally in spec/slots/ and spec/contracts/",
|
|
39
42
|
inputSchema: {
|
|
40
43
|
type: "object",
|
|
41
44
|
properties: {
|
|
42
|
-
|
|
45
|
+
path: {
|
|
43
46
|
type: "string",
|
|
44
|
-
description: "
|
|
45
|
-
},
|
|
46
|
-
pattern: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "URL pattern (e.g., /api/users/:id)",
|
|
47
|
+
description: "Route path relative to app/ (e.g., 'api/users' or 'blog/[slug]')",
|
|
49
48
|
},
|
|
50
49
|
kind: {
|
|
51
50
|
type: "string",
|
|
52
51
|
enum: ["api", "page"],
|
|
53
|
-
description: "Route type: api or page",
|
|
54
|
-
},
|
|
55
|
-
slotModule: {
|
|
56
|
-
type: "string",
|
|
57
|
-
description: "Path to slot file (optional)",
|
|
52
|
+
description: "Route type: api (route.ts) or page (page.tsx)",
|
|
58
53
|
},
|
|
59
|
-
|
|
60
|
-
type: "
|
|
61
|
-
description: "
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
required: ["id", "pattern", "kind"],
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: "mandu_update_route",
|
|
69
|
-
description: "Update an existing route",
|
|
70
|
-
inputSchema: {
|
|
71
|
-
type: "object",
|
|
72
|
-
properties: {
|
|
73
|
-
routeId: {
|
|
74
|
-
type: "string",
|
|
75
|
-
description: "The route ID to update",
|
|
54
|
+
withSlot: {
|
|
55
|
+
type: "boolean",
|
|
56
|
+
description: "Scaffold a slot file in spec/slots/ (default: true)",
|
|
76
57
|
},
|
|
77
|
-
|
|
78
|
-
type: "
|
|
79
|
-
description: "
|
|
80
|
-
properties: {
|
|
81
|
-
pattern: { type: "string" },
|
|
82
|
-
kind: { type: "string", enum: ["api", "page"] },
|
|
83
|
-
slotModule: { type: "string" },
|
|
84
|
-
componentModule: { type: "string" },
|
|
85
|
-
},
|
|
58
|
+
withContract: {
|
|
59
|
+
type: "boolean",
|
|
60
|
+
description: "Scaffold a contract file in spec/contracts/",
|
|
86
61
|
},
|
|
87
62
|
},
|
|
88
|
-
required: ["
|
|
63
|
+
required: ["path", "kind"],
|
|
89
64
|
},
|
|
90
65
|
},
|
|
91
66
|
{
|
|
92
67
|
name: "mandu_delete_route",
|
|
93
|
-
description: "Delete a route
|
|
68
|
+
description: "Delete a route's app/ files and rescan (preserves slot/contract files)",
|
|
94
69
|
inputSchema: {
|
|
95
70
|
type: "object",
|
|
96
71
|
properties: {
|
|
@@ -103,8 +78,8 @@ export const specToolDefinitions: Tool[] = [
|
|
|
103
78
|
},
|
|
104
79
|
},
|
|
105
80
|
{
|
|
106
|
-
name: "
|
|
107
|
-
description: "Validate the current
|
|
81
|
+
name: "mandu_validate_manifest",
|
|
82
|
+
description: "Validate the current routes manifest (.mandu/routes.manifest.json)",
|
|
108
83
|
inputSchema: {
|
|
109
84
|
type: "object",
|
|
110
85
|
properties: {},
|
|
@@ -130,6 +105,7 @@ export function specTools(projectRoot: string) {
|
|
|
130
105
|
pattern: r.pattern,
|
|
131
106
|
kind: r.kind,
|
|
132
107
|
slotModule: r.slotModule,
|
|
108
|
+
contractModule: r.contractModule,
|
|
133
109
|
componentModule: r.componentModule,
|
|
134
110
|
})),
|
|
135
111
|
count: result.data.routes.length,
|
|
@@ -153,157 +129,102 @@ export function specTools(projectRoot: string) {
|
|
|
153
129
|
},
|
|
154
130
|
|
|
155
131
|
mandu_add_route: async (args: Record<string, unknown>) => {
|
|
156
|
-
const {
|
|
157
|
-
|
|
158
|
-
pattern: string;
|
|
132
|
+
const { path: routePath, kind, withSlot = true, withContract = false } = args as {
|
|
133
|
+
path: string;
|
|
159
134
|
kind: "api" | "page";
|
|
160
|
-
|
|
161
|
-
|
|
135
|
+
withSlot?: boolean;
|
|
136
|
+
withContract?: boolean;
|
|
162
137
|
};
|
|
163
138
|
|
|
164
|
-
|
|
165
|
-
const result = await loadManifest(paths.manifestPath);
|
|
166
|
-
if (!result.success || !result.data) {
|
|
167
|
-
return { error: result.errors };
|
|
168
|
-
}
|
|
139
|
+
const createdFiles: string[] = [];
|
|
169
140
|
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
if (result.data.routes.some((r) => r.pattern === pattern)) {
|
|
176
|
-
return { error: `Route with pattern '${pattern}' already exists` };
|
|
177
|
-
}
|
|
141
|
+
// Scaffold app/ file
|
|
142
|
+
const fileName = kind === "api" ? "route.ts" : "page.tsx";
|
|
143
|
+
const appFilePath = path.join(paths.appDir, routePath, fileName);
|
|
144
|
+
const appFileDir = path.dirname(appFilePath);
|
|
178
145
|
|
|
179
|
-
|
|
180
|
-
const newRoute: RouteSpec = {
|
|
181
|
-
id,
|
|
182
|
-
pattern,
|
|
183
|
-
kind,
|
|
184
|
-
module: `apps/server/generated/routes/${id}.route.ts`,
|
|
185
|
-
slotModule: slotModule || `spec/slots/${id}.slot.ts`,
|
|
186
|
-
};
|
|
146
|
+
await fs.mkdir(appFileDir, { recursive: true });
|
|
187
147
|
|
|
188
|
-
if (kind === "
|
|
189
|
-
|
|
148
|
+
if (kind === "api") {
|
|
149
|
+
await Bun.write(appFilePath, `export function GET(req: Request) {\n return Response.json({ message: "Hello" });\n}\n`);
|
|
150
|
+
} else {
|
|
151
|
+
await Bun.write(appFilePath, `export default function Page() {\n return <div>Page</div>;\n}\n`);
|
|
190
152
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
153
|
+
createdFiles.push(`app/${routePath}/${fileName}`);
|
|
154
|
+
|
|
155
|
+
// Derive route ID from path
|
|
156
|
+
const routeId = routePath.replace(/\//g, "-").replace(/[\[\]\.]/g, "");
|
|
157
|
+
|
|
158
|
+
// Scaffold slot if requested
|
|
159
|
+
if (withSlot) {
|
|
160
|
+
const slotPath = path.join(paths.slotsDir, `${routeId}.slot.ts`);
|
|
161
|
+
await fs.mkdir(paths.slotsDir, { recursive: true });
|
|
162
|
+
if (!(await Bun.file(slotPath).exists())) {
|
|
163
|
+
await Bun.write(slotPath, `export default function slot(req: Request) {\n return {};\n}\n`);
|
|
164
|
+
createdFiles.push(`spec/slots/${routeId}.slot.ts`);
|
|
165
|
+
}
|
|
201
166
|
}
|
|
202
167
|
|
|
203
|
-
//
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
route: newRoute,
|
|
212
|
-
message: `Route '${id}' added successfully`,
|
|
213
|
-
};
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
mandu_update_route: async (args: Record<string, unknown>) => {
|
|
217
|
-
const { routeId, updates } = args as {
|
|
218
|
-
routeId: string;
|
|
219
|
-
updates: Partial<RouteSpec>;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
// Load current manifest
|
|
223
|
-
const result = await loadManifest(paths.manifestPath);
|
|
224
|
-
if (!result.success || !result.data) {
|
|
225
|
-
return { error: result.errors };
|
|
168
|
+
// Scaffold contract if requested
|
|
169
|
+
if (withContract) {
|
|
170
|
+
const contractPath = path.join(paths.contractsDir, `${routeId}.contract.ts`);
|
|
171
|
+
await fs.mkdir(paths.contractsDir, { recursive: true });
|
|
172
|
+
if (!(await Bun.file(contractPath).exists())) {
|
|
173
|
+
await Bun.write(contractPath, `import { z } from "zod";\n\nexport const contract = {\n request: z.object({}),\n response: z.object({}),\n};\n`);
|
|
174
|
+
createdFiles.push(`spec/contracts/${routeId}.contract.ts`);
|
|
175
|
+
}
|
|
226
176
|
}
|
|
227
177
|
|
|
228
|
-
//
|
|
229
|
-
const
|
|
230
|
-
if (routeIndex === -1) {
|
|
231
|
-
return { error: `Route not found: ${routeId}` };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Apply updates
|
|
235
|
-
const updatedRoute = {
|
|
236
|
-
...result.data.routes[routeIndex],
|
|
237
|
-
...updates,
|
|
238
|
-
id: routeId, // ID cannot be changed
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
const newRoutes = [...result.data.routes];
|
|
242
|
-
newRoutes[routeIndex] = updatedRoute as RouteSpec;
|
|
243
|
-
|
|
244
|
-
// Validate
|
|
245
|
-
const newManifest: RoutesManifest = {
|
|
246
|
-
version: result.data.version,
|
|
247
|
-
routes: newRoutes,
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const validation = validateManifest(newManifest);
|
|
251
|
-
if (!validation.success) {
|
|
252
|
-
return { error: validation.errors };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Write updated manifest
|
|
256
|
-
await writeJsonFile(paths.manifestPath, newManifest);
|
|
257
|
-
|
|
258
|
-
// Update lock file
|
|
259
|
-
await writeLock(paths.lockPath, newManifest);
|
|
178
|
+
// Rescan to regenerate manifest with auto-linking
|
|
179
|
+
const genResult = await generateManifest(projectRoot);
|
|
260
180
|
|
|
261
181
|
return {
|
|
262
182
|
success: true,
|
|
263
|
-
|
|
264
|
-
|
|
183
|
+
routeId,
|
|
184
|
+
createdFiles,
|
|
185
|
+
totalRoutes: genResult.manifest.routes.length,
|
|
186
|
+
message: `Route '${routeId}' scaffolded successfully`,
|
|
265
187
|
};
|
|
266
188
|
},
|
|
267
189
|
|
|
268
190
|
mandu_delete_route: async (args: Record<string, unknown>) => {
|
|
269
191
|
const { routeId } = args as { routeId: string };
|
|
270
192
|
|
|
271
|
-
// Load current manifest
|
|
193
|
+
// Load current manifest to find the route
|
|
272
194
|
const result = await loadManifest(paths.manifestPath);
|
|
273
195
|
if (!result.success || !result.data) {
|
|
274
196
|
return { error: result.errors };
|
|
275
197
|
}
|
|
276
198
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (routeIndex === -1) {
|
|
199
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
200
|
+
if (!route) {
|
|
280
201
|
return { error: `Route not found: ${routeId}` };
|
|
281
202
|
}
|
|
282
203
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Write updated manifest
|
|
294
|
-
await writeJsonFile(paths.manifestPath, newManifest);
|
|
204
|
+
// Delete app/ source file (module path points to generated; need to find source)
|
|
205
|
+
const deletedFiles: string[] = [];
|
|
206
|
+
if (route.module && route.module.startsWith("app/")) {
|
|
207
|
+
const fullPath = path.join(projectRoot, route.module);
|
|
208
|
+
try {
|
|
209
|
+
await fs.unlink(fullPath);
|
|
210
|
+
deletedFiles.push(route.module);
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
295
213
|
|
|
296
|
-
//
|
|
297
|
-
await
|
|
214
|
+
// Rescan manifest (slot/contract files preserved)
|
|
215
|
+
const genResult = await generateManifest(projectRoot);
|
|
298
216
|
|
|
299
217
|
return {
|
|
300
218
|
success: true,
|
|
301
|
-
deletedRoute,
|
|
302
|
-
|
|
219
|
+
deletedRoute: route,
|
|
220
|
+
deletedFiles,
|
|
221
|
+
preservedFiles: [route.slotModule, route.contractModule].filter(Boolean),
|
|
222
|
+
totalRoutes: genResult.manifest.routes.length,
|
|
223
|
+
message: `Route '${routeId}' deleted from app/. Slot/contract files preserved.`,
|
|
303
224
|
};
|
|
304
225
|
},
|
|
305
226
|
|
|
306
|
-
|
|
227
|
+
mandu_validate_manifest: async () => {
|
|
307
228
|
const result = await loadManifest(paths.manifestPath);
|
|
308
229
|
if (!result.success) {
|
|
309
230
|
return {
|
package/src/utils/project.ts
CHANGED
|
@@ -3,19 +3,27 @@ import fs from "fs/promises";
|
|
|
3
3
|
import { pathToFileURL } from "url";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Find the Mandu project root by looking for
|
|
6
|
+
* Find the Mandu project root by looking for app/ directory or mandu.config.*
|
|
7
7
|
*/
|
|
8
8
|
export async function findProjectRoot(startDir: string = process.cwd()): Promise<string | null> {
|
|
9
9
|
let currentDir = path.resolve(startDir);
|
|
10
10
|
|
|
11
11
|
while (currentDir !== path.dirname(currentDir)) {
|
|
12
|
-
|
|
12
|
+
// Check for app/ directory (FS Routes source)
|
|
13
13
|
try {
|
|
14
|
-
await fs.
|
|
15
|
-
return currentDir;
|
|
16
|
-
} catch {
|
|
17
|
-
|
|
14
|
+
const appStat = await fs.stat(path.join(currentDir, "app"));
|
|
15
|
+
if (appStat.isDirectory()) return currentDir;
|
|
16
|
+
} catch {}
|
|
17
|
+
|
|
18
|
+
// Check for mandu.config.* files
|
|
19
|
+
for (const configFile of ["mandu.config.ts", "mandu.config.js", "mandu.config.json"]) {
|
|
20
|
+
try {
|
|
21
|
+
await fs.access(path.join(currentDir, configFile));
|
|
22
|
+
return currentDir;
|
|
23
|
+
} catch {}
|
|
18
24
|
}
|
|
25
|
+
|
|
26
|
+
currentDir = path.dirname(currentDir);
|
|
19
27
|
}
|
|
20
28
|
|
|
21
29
|
return null;
|
|
@@ -27,14 +35,16 @@ export async function findProjectRoot(startDir: string = process.cwd()): Promise
|
|
|
27
35
|
export function getProjectPaths(rootDir: string) {
|
|
28
36
|
return {
|
|
29
37
|
root: rootDir,
|
|
38
|
+
appDir: path.join(rootDir, "app"),
|
|
30
39
|
specDir: path.join(rootDir, "spec"),
|
|
31
|
-
manifestPath: path.join(rootDir, "
|
|
32
|
-
lockPath: path.join(rootDir, "
|
|
40
|
+
manifestPath: path.join(rootDir, ".mandu", "routes.manifest.json"),
|
|
41
|
+
lockPath: path.join(rootDir, ".mandu", "spec.lock.json"),
|
|
33
42
|
slotsDir: path.join(rootDir, "spec", "slots"),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
43
|
+
contractsDir: path.join(rootDir, "spec", "contracts"),
|
|
44
|
+
historyDir: path.join(rootDir, ".mandu", "history"),
|
|
45
|
+
generatedMapPath: path.join(rootDir, ".mandu", "generated", "generated.map.json"),
|
|
46
|
+
serverRoutesDir: path.join(rootDir, ".mandu", "generated", "server", "routes"),
|
|
47
|
+
webRoutesDir: path.join(rootDir, ".mandu", "generated", "web", "routes"),
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
|