@mandujs/mcp 0.3.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.
@@ -0,0 +1,322 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ loadManifest,
4
+ validateManifest,
5
+ writeLock,
6
+ type RouteSpec,
7
+ type RoutesManifest,
8
+ } from "@mandujs/core";
9
+ import { getProjectPaths, readJsonFile, writeJsonFile } from "../utils/project.js";
10
+ import path from "path";
11
+
12
+ export const specToolDefinitions: Tool[] = [
13
+ {
14
+ name: "mandu_list_routes",
15
+ description: "List all routes in the current Mandu project",
16
+ inputSchema: {
17
+ type: "object",
18
+ properties: {},
19
+ required: [],
20
+ },
21
+ },
22
+ {
23
+ name: "mandu_get_route",
24
+ description: "Get details of a specific route by ID",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ routeId: {
29
+ type: "string",
30
+ description: "The route ID to retrieve",
31
+ },
32
+ },
33
+ required: ["routeId"],
34
+ },
35
+ },
36
+ {
37
+ name: "mandu_add_route",
38
+ description: "Add a new route to the manifest",
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ id: {
43
+ type: "string",
44
+ description: "Unique route identifier",
45
+ },
46
+ pattern: {
47
+ type: "string",
48
+ description: "URL pattern (e.g., /api/users/:id)",
49
+ },
50
+ kind: {
51
+ type: "string",
52
+ enum: ["api", "page"],
53
+ description: "Route type: api or page",
54
+ },
55
+ slotModule: {
56
+ type: "string",
57
+ description: "Path to slot file (optional)",
58
+ },
59
+ componentModule: {
60
+ type: "string",
61
+ description: "Path to component module (required for page kind)",
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",
76
+ },
77
+ updates: {
78
+ type: "object",
79
+ description: "Partial route updates",
80
+ properties: {
81
+ pattern: { type: "string" },
82
+ kind: { type: "string", enum: ["api", "page"] },
83
+ slotModule: { type: "string" },
84
+ componentModule: { type: "string" },
85
+ },
86
+ },
87
+ },
88
+ required: ["routeId", "updates"],
89
+ },
90
+ },
91
+ {
92
+ name: "mandu_delete_route",
93
+ description: "Delete a route from the manifest",
94
+ inputSchema: {
95
+ type: "object",
96
+ properties: {
97
+ routeId: {
98
+ type: "string",
99
+ description: "The route ID to delete",
100
+ },
101
+ },
102
+ required: ["routeId"],
103
+ },
104
+ },
105
+ {
106
+ name: "mandu_validate_spec",
107
+ description: "Validate the current spec manifest",
108
+ inputSchema: {
109
+ type: "object",
110
+ properties: {},
111
+ required: [],
112
+ },
113
+ },
114
+ ];
115
+
116
+ export function specTools(projectRoot: string) {
117
+ const paths = getProjectPaths(projectRoot);
118
+
119
+ return {
120
+ mandu_list_routes: async () => {
121
+ const result = await loadManifest(paths.manifestPath);
122
+ if (!result.success || !result.data) {
123
+ return { error: result.errors };
124
+ }
125
+
126
+ return {
127
+ version: result.data.version,
128
+ routes: result.data.routes.map((r) => ({
129
+ id: r.id,
130
+ pattern: r.pattern,
131
+ kind: r.kind,
132
+ slotModule: r.slotModule,
133
+ componentModule: r.componentModule,
134
+ })),
135
+ count: result.data.routes.length,
136
+ };
137
+ },
138
+
139
+ mandu_get_route: async (args: Record<string, unknown>) => {
140
+ const { routeId } = args as { routeId: string };
141
+
142
+ const result = await loadManifest(paths.manifestPath);
143
+ if (!result.success || !result.data) {
144
+ return { error: result.errors };
145
+ }
146
+
147
+ const route = result.data.routes.find((r) => r.id === routeId);
148
+ if (!route) {
149
+ return { error: `Route not found: ${routeId}` };
150
+ }
151
+
152
+ return { route };
153
+ },
154
+
155
+ mandu_add_route: async (args: Record<string, unknown>) => {
156
+ const { id, pattern, kind, slotModule, componentModule } = args as {
157
+ id: string;
158
+ pattern: string;
159
+ kind: "api" | "page";
160
+ slotModule?: string;
161
+ componentModule?: string;
162
+ };
163
+
164
+ // Load current manifest
165
+ const result = await loadManifest(paths.manifestPath);
166
+ if (!result.success || !result.data) {
167
+ return { error: result.errors };
168
+ }
169
+
170
+ // Check for duplicate
171
+ if (result.data.routes.some((r) => r.id === id)) {
172
+ return { error: `Route with id '${id}' already exists` };
173
+ }
174
+
175
+ if (result.data.routes.some((r) => r.pattern === pattern)) {
176
+ return { error: `Route with pattern '${pattern}' already exists` };
177
+ }
178
+
179
+ // Build new route
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
+ };
187
+
188
+ if (kind === "page") {
189
+ newRoute.componentModule = componentModule || `apps/web/generated/routes/${id}.route.tsx`;
190
+ }
191
+
192
+ // Validate new route
193
+ const newManifest: RoutesManifest = {
194
+ version: result.data.version,
195
+ routes: [...result.data.routes, newRoute],
196
+ };
197
+
198
+ const validation = validateManifest(newManifest);
199
+ if (!validation.success) {
200
+ return { error: validation.errors };
201
+ }
202
+
203
+ // Write updated manifest
204
+ await writeJsonFile(paths.manifestPath, newManifest);
205
+
206
+ // Update lock file
207
+ await writeLock(paths.lockPath, newManifest);
208
+
209
+ return {
210
+ success: true,
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 };
226
+ }
227
+
228
+ // Find route
229
+ const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
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);
260
+
261
+ return {
262
+ success: true,
263
+ route: updatedRoute,
264
+ message: `Route '${routeId}' updated successfully`,
265
+ };
266
+ },
267
+
268
+ mandu_delete_route: async (args: Record<string, unknown>) => {
269
+ const { routeId } = args as { routeId: string };
270
+
271
+ // Load current manifest
272
+ const result = await loadManifest(paths.manifestPath);
273
+ if (!result.success || !result.data) {
274
+ return { error: result.errors };
275
+ }
276
+
277
+ // Find route
278
+ const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
279
+ if (routeIndex === -1) {
280
+ return { error: `Route not found: ${routeId}` };
281
+ }
282
+
283
+ const deletedRoute = result.data.routes[routeIndex];
284
+
285
+ // Remove route
286
+ const newRoutes = result.data.routes.filter((r) => r.id !== routeId);
287
+
288
+ const newManifest: RoutesManifest = {
289
+ version: result.data.version,
290
+ routes: newRoutes,
291
+ };
292
+
293
+ // Write updated manifest
294
+ await writeJsonFile(paths.manifestPath, newManifest);
295
+
296
+ // Update lock file
297
+ await writeLock(paths.lockPath, newManifest);
298
+
299
+ return {
300
+ success: true,
301
+ deletedRoute,
302
+ message: `Route '${routeId}' deleted successfully`,
303
+ };
304
+ },
305
+
306
+ mandu_validate_spec: async () => {
307
+ const result = await loadManifest(paths.manifestPath);
308
+ if (!result.success) {
309
+ return {
310
+ valid: false,
311
+ errors: result.errors,
312
+ };
313
+ }
314
+
315
+ return {
316
+ valid: true,
317
+ routeCount: result.data?.routes.length || 0,
318
+ version: result.data?.version,
319
+ };
320
+ },
321
+ };
322
+ }
@@ -0,0 +1,154 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ beginChange,
4
+ commitChange,
5
+ rollbackChange,
6
+ getTransactionStatus,
7
+ hasActiveTransaction,
8
+ } from "@mandujs/core";
9
+
10
+ export const transactionToolDefinitions: Tool[] = [
11
+ {
12
+ name: "mandu_begin",
13
+ description:
14
+ "Begin a new transaction. Creates a snapshot of the current spec state for safe rollback.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ message: {
19
+ type: "string",
20
+ description: "Description of the changes being made",
21
+ },
22
+ },
23
+ required: [],
24
+ },
25
+ },
26
+ {
27
+ name: "mandu_commit",
28
+ description: "Commit the current transaction, finalizing all changes",
29
+ inputSchema: {
30
+ type: "object",
31
+ properties: {},
32
+ required: [],
33
+ },
34
+ },
35
+ {
36
+ name: "mandu_rollback",
37
+ description: "Rollback the current transaction, restoring the previous state",
38
+ inputSchema: {
39
+ type: "object",
40
+ properties: {
41
+ changeId: {
42
+ type: "string",
43
+ description: "Specific change ID to rollback (optional, defaults to active transaction)",
44
+ },
45
+ },
46
+ required: [],
47
+ },
48
+ },
49
+ {
50
+ name: "mandu_tx_status",
51
+ description: "Get the current transaction status",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: {},
55
+ required: [],
56
+ },
57
+ },
58
+ ];
59
+
60
+ export function transactionTools(projectRoot: string) {
61
+ return {
62
+ mandu_begin: async (args: Record<string, unknown>) => {
63
+ const { message } = args as { message?: string };
64
+
65
+ // Check if there's already an active transaction
66
+ const isActive = await hasActiveTransaction(projectRoot);
67
+ if (isActive) {
68
+ const status = await getTransactionStatus(projectRoot);
69
+ return {
70
+ error: "Active transaction already exists",
71
+ activeTransaction: status.change,
72
+ };
73
+ }
74
+
75
+ const change = await beginChange(projectRoot, {
76
+ message: message || "MCP transaction",
77
+ });
78
+
79
+ return {
80
+ success: true,
81
+ changeId: change.id,
82
+ snapshotId: change.snapshotId,
83
+ message: change.message,
84
+ createdAt: change.createdAt,
85
+ tip: "Use mandu_commit to finalize or mandu_rollback to revert changes",
86
+ };
87
+ },
88
+
89
+ mandu_commit: async () => {
90
+ const isActive = await hasActiveTransaction(projectRoot);
91
+ if (!isActive) {
92
+ return {
93
+ error: "No active transaction to commit",
94
+ };
95
+ }
96
+
97
+ const result = await commitChange(projectRoot);
98
+
99
+ return {
100
+ success: result.success,
101
+ changeId: result.changeId,
102
+ message: result.message,
103
+ };
104
+ },
105
+
106
+ mandu_rollback: async (args: Record<string, unknown>) => {
107
+ const { changeId } = args as { changeId?: string };
108
+
109
+ const isActive = await hasActiveTransaction(projectRoot);
110
+ if (!isActive && !changeId) {
111
+ return {
112
+ error: "No active transaction to rollback. Provide a changeId to rollback a specific change.",
113
+ };
114
+ }
115
+
116
+ const result = await rollbackChange(projectRoot, changeId);
117
+
118
+ return {
119
+ success: result.success,
120
+ changeId: result.changeId,
121
+ restored: {
122
+ filesRestored: result.restoreResult.restoredFiles.length,
123
+ filesFailed: result.restoreResult.failedFiles.length,
124
+ errors: result.restoreResult.errors,
125
+ },
126
+ };
127
+ },
128
+
129
+ mandu_tx_status: async () => {
130
+ const { state, change } = await getTransactionStatus(projectRoot);
131
+
132
+ if (!state.active) {
133
+ return {
134
+ hasActiveTransaction: false,
135
+ message: "No active transaction",
136
+ };
137
+ }
138
+
139
+ return {
140
+ hasActiveTransaction: true,
141
+ changeId: state.changeId,
142
+ snapshotId: state.snapshotId,
143
+ change: change
144
+ ? {
145
+ id: change.id,
146
+ message: change.message,
147
+ status: change.status,
148
+ createdAt: change.createdAt,
149
+ }
150
+ : null,
151
+ };
152
+ },
153
+ };
154
+ }
@@ -0,0 +1,71 @@
1
+ import path from "path";
2
+ import fs from "fs/promises";
3
+
4
+ /**
5
+ * Find the Mandu project root by looking for routes.manifest.json
6
+ */
7
+ export async function findProjectRoot(startDir: string = process.cwd()): Promise<string | null> {
8
+ let currentDir = path.resolve(startDir);
9
+
10
+ while (currentDir !== path.dirname(currentDir)) {
11
+ const manifestPath = path.join(currentDir, "spec", "routes.manifest.json");
12
+ try {
13
+ await fs.access(manifestPath);
14
+ return currentDir;
15
+ } catch {
16
+ currentDir = path.dirname(currentDir);
17
+ }
18
+ }
19
+
20
+ return null;
21
+ }
22
+
23
+ /**
24
+ * Get standard paths for a Mandu project
25
+ */
26
+ export function getProjectPaths(rootDir: string) {
27
+ return {
28
+ root: rootDir,
29
+ specDir: path.join(rootDir, "spec"),
30
+ manifestPath: path.join(rootDir, "spec", "routes.manifest.json"),
31
+ lockPath: path.join(rootDir, "spec", "spec.lock.json"),
32
+ slotsDir: path.join(rootDir, "spec", "slots"),
33
+ historyDir: path.join(rootDir, "spec", "history"),
34
+ generatedMapPath: path.join(rootDir, "packages", "core", "map", "generated.map.json"),
35
+ serverRoutesDir: path.join(rootDir, "apps", "server", "generated", "routes"),
36
+ webRoutesDir: path.join(rootDir, "apps", "web", "generated", "routes"),
37
+ };
38
+ }
39
+
40
+ /**
41
+ * Check if a path is inside the project
42
+ */
43
+ export function isInsideProject(filePath: string, rootDir: string): boolean {
44
+ const resolved = path.resolve(filePath);
45
+ const root = path.resolve(rootDir);
46
+ return resolved.startsWith(root);
47
+ }
48
+
49
+ /**
50
+ * Read JSON file safely
51
+ */
52
+ export async function readJsonFile<T>(filePath: string): Promise<T | null> {
53
+ try {
54
+ const file = Bun.file(filePath);
55
+ if (!(await file.exists())) {
56
+ return null;
57
+ }
58
+ return await file.json();
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Write JSON file safely
66
+ */
67
+ export async function writeJsonFile(filePath: string, data: unknown): Promise<void> {
68
+ const dir = path.dirname(filePath);
69
+ await fs.mkdir(dir, { recursive: true });
70
+ await Bun.write(filePath, JSON.stringify(data, null, 2));
71
+ }