@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,211 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ loadManifest,
4
+ runGuardCheck,
5
+ runAutoCorrect,
6
+ ErrorClassifier,
7
+ type ManduError,
8
+ type GeneratedMap,
9
+ } from "@mandujs/core";
10
+ import { getProjectPaths, readJsonFile } from "../utils/project.js";
11
+
12
+ export const guardToolDefinitions: Tool[] = [
13
+ {
14
+ name: "mandu_guard_check",
15
+ description:
16
+ "Run guard checks to validate spec integrity, generated files, and slot files",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: {
20
+ autoCorrect: {
21
+ type: "boolean",
22
+ description: "If true, attempt to automatically fix violations",
23
+ },
24
+ },
25
+ required: [],
26
+ },
27
+ },
28
+ {
29
+ name: "mandu_analyze_error",
30
+ description:
31
+ "Analyze a ManduError JSON to provide actionable fix guidance",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ errorJson: {
36
+ type: "string",
37
+ description: "The ManduError JSON string to analyze",
38
+ },
39
+ },
40
+ required: ["errorJson"],
41
+ },
42
+ },
43
+ ];
44
+
45
+ export function guardTools(projectRoot: string) {
46
+ const paths = getProjectPaths(projectRoot);
47
+
48
+ return {
49
+ mandu_guard_check: async (args: Record<string, unknown>) => {
50
+ const { autoCorrect = false } = args as { autoCorrect?: boolean };
51
+
52
+ // Load manifest
53
+ const manifestResult = await loadManifest(paths.manifestPath);
54
+ if (!manifestResult.success || !manifestResult.data) {
55
+ return {
56
+ error: "Failed to load manifest",
57
+ details: manifestResult.errors,
58
+ };
59
+ }
60
+
61
+ // Run guard check
62
+ const checkResult = await runGuardCheck(manifestResult.data, projectRoot);
63
+
64
+ if (checkResult.passed) {
65
+ return {
66
+ passed: true,
67
+ violations: [],
68
+ message: "All guard checks passed",
69
+ };
70
+ }
71
+
72
+ // If auto-correct requested and there are violations
73
+ if (autoCorrect && checkResult.violations.length > 0) {
74
+ const autoCorrectResult = await runAutoCorrect(
75
+ checkResult.violations,
76
+ manifestResult.data,
77
+ projectRoot
78
+ );
79
+
80
+ return {
81
+ passed: autoCorrectResult.fixed,
82
+ violations: autoCorrectResult.remainingViolations,
83
+ autoCorrect: {
84
+ attempted: true,
85
+ fixed: autoCorrectResult.fixed,
86
+ steps: autoCorrectResult.steps,
87
+ retriedCount: autoCorrectResult.retriedCount,
88
+ rolledBack: autoCorrectResult.rolledBack,
89
+ changeId: autoCorrectResult.changeId,
90
+ },
91
+ };
92
+ }
93
+
94
+ return {
95
+ passed: false,
96
+ violations: checkResult.violations.map((v) => ({
97
+ ruleId: v.ruleId,
98
+ file: v.file,
99
+ message: v.message,
100
+ suggestion: v.suggestion,
101
+ })),
102
+ message: `Found ${checkResult.violations.length} violation(s)`,
103
+ tip: "Use autoCorrect: true to attempt automatic fixes",
104
+ };
105
+ },
106
+
107
+ mandu_analyze_error: async (args: Record<string, unknown>) => {
108
+ const { errorJson } = args as { errorJson: string };
109
+
110
+ let error: ManduError;
111
+ try {
112
+ error = JSON.parse(errorJson) as ManduError;
113
+ } catch {
114
+ return {
115
+ error: "Invalid JSON format",
116
+ tip: "Provide a valid ManduError JSON string",
117
+ };
118
+ }
119
+
120
+ // Load generated map for better analysis
121
+ const generatedMap = await readJsonFile<GeneratedMap>(paths.generatedMapPath);
122
+
123
+ // Provide analysis based on error type
124
+ const analysis: Record<string, unknown> = {
125
+ errorType: error.errorType,
126
+ code: error.code,
127
+ summary: error.summary,
128
+ };
129
+
130
+ switch (error.errorType) {
131
+ case "SPEC_ERROR":
132
+ analysis.category = "Specification Error";
133
+ analysis.fixLocation = error.fix?.file || "spec/routes.manifest.json";
134
+ analysis.actions = [
135
+ "Check the spec file for JSON syntax errors",
136
+ "Validate route IDs are unique",
137
+ "Ensure patterns start with /",
138
+ "For page routes, verify componentModule is specified",
139
+ ];
140
+ break;
141
+
142
+ case "LOGIC_ERROR":
143
+ analysis.category = "Business Logic Error";
144
+ analysis.fixLocation = error.fix?.file || "spec/slots/";
145
+ analysis.actions = [
146
+ "Review the slot file at the specified location",
147
+ error.fix?.suggestion || "Check the handler logic",
148
+ "Verify ctx.body() and ctx.params are used correctly",
149
+ "Add proper error handling in the slot",
150
+ ];
151
+ if (error.fix?.line) {
152
+ analysis.lineNumber = error.fix.line;
153
+ }
154
+ break;
155
+
156
+ case "FRAMEWORK_BUG":
157
+ analysis.category = "Framework Internal Error";
158
+ analysis.fixLocation = error.fix?.file || "packages/core/";
159
+ analysis.actions = [
160
+ "This appears to be a framework bug",
161
+ "Check GitHub issues for similar problems",
162
+ "Consider filing a bug report with the error details",
163
+ ];
164
+ analysis.reportUrl = "https://github.com/konamgil/mandu/issues";
165
+ break;
166
+
167
+ default:
168
+ analysis.category = "Unknown Error";
169
+ analysis.actions = [
170
+ "Review the error message for details",
171
+ error.fix?.suggestion || "Check related files",
172
+ ];
173
+ }
174
+
175
+ // Add route context if available
176
+ if (error.route) {
177
+ analysis.routeContext = {
178
+ routeId: error.route.id,
179
+ pattern: error.route.pattern,
180
+ kind: error.route.kind,
181
+ };
182
+
183
+ // Try to find slot mapping
184
+ if (generatedMap && error.route.id) {
185
+ for (const [, entry] of Object.entries(generatedMap.files)) {
186
+ if (entry.routeId === error.route.id && entry.slotMapping) {
187
+ analysis.slotFile = entry.slotMapping.slotPath;
188
+ break;
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ // Add debug info if available
195
+ if (error.debug) {
196
+ analysis.debug = {
197
+ hasStack: !!error.debug.stack,
198
+ generatedFile: error.debug.generatedFile,
199
+ };
200
+ }
201
+
202
+ return {
203
+ analysis,
204
+ originalError: {
205
+ message: error.message,
206
+ timestamp: error.timestamp,
207
+ },
208
+ };
209
+ },
210
+ };
211
+ }
@@ -0,0 +1,138 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ listChanges,
4
+ pruneHistory,
5
+ getChangeStats,
6
+ readSnapshotById,
7
+ } from "@mandujs/core";
8
+
9
+ export const historyToolDefinitions: Tool[] = [
10
+ {
11
+ name: "mandu_list_history",
12
+ description: "List the change history with snapshots",
13
+ inputSchema: {
14
+ type: "object",
15
+ properties: {
16
+ limit: {
17
+ type: "number",
18
+ description: "Maximum number of entries to return (default: 10)",
19
+ },
20
+ },
21
+ required: [],
22
+ },
23
+ },
24
+ {
25
+ name: "mandu_get_snapshot",
26
+ description: "Get details of a specific snapshot",
27
+ inputSchema: {
28
+ type: "object",
29
+ properties: {
30
+ snapshotId: {
31
+ type: "string",
32
+ description: "The snapshot ID to retrieve",
33
+ },
34
+ },
35
+ required: ["snapshotId"],
36
+ },
37
+ },
38
+ {
39
+ name: "mandu_prune_history",
40
+ description: "Remove old snapshots to free up space",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ keepCount: {
45
+ type: "number",
46
+ description: "Number of snapshots to keep (default: 5)",
47
+ },
48
+ },
49
+ required: [],
50
+ },
51
+ },
52
+ ];
53
+
54
+ export function historyTools(projectRoot: string) {
55
+ return {
56
+ mandu_list_history: async (args: Record<string, unknown>) => {
57
+ const { limit = 10 } = args as { limit?: number };
58
+
59
+ const changes = await listChanges(projectRoot);
60
+ const stats = await getChangeStats(projectRoot);
61
+
62
+ // Sort by createdAt descending and limit
63
+ const sortedChanges = changes
64
+ .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
65
+ .slice(0, limit);
66
+
67
+ return {
68
+ changes: sortedChanges.map((c) => ({
69
+ id: c.id,
70
+ message: c.message,
71
+ status: c.status,
72
+ createdAt: c.createdAt,
73
+ snapshotId: c.snapshotId,
74
+ autoGenerated: c.autoGenerated,
75
+ })),
76
+ stats: {
77
+ total: stats.total,
78
+ active: stats.active,
79
+ committed: stats.committed,
80
+ rolledBack: stats.rolledBack,
81
+ snapshotCount: stats.snapshotCount,
82
+ },
83
+ };
84
+ },
85
+
86
+ mandu_get_snapshot: async (args: Record<string, unknown>) => {
87
+ const { snapshotId } = args as { snapshotId: string };
88
+
89
+ const snapshot = await readSnapshotById(projectRoot, snapshotId);
90
+ if (!snapshot) {
91
+ return { error: `Snapshot not found: ${snapshotId}` };
92
+ }
93
+
94
+ const slotFiles = Object.keys(snapshot.slotContents);
95
+
96
+ return {
97
+ id: snapshot.id,
98
+ timestamp: snapshot.timestamp,
99
+ manifest: {
100
+ version: snapshot.manifest.version,
101
+ routeCount: snapshot.manifest.routes.length,
102
+ routes: snapshot.manifest.routes.map((r) => ({
103
+ id: r.id,
104
+ pattern: r.pattern,
105
+ kind: r.kind,
106
+ })),
107
+ },
108
+ hasLock: !!snapshot.lock,
109
+ lock: snapshot.lock
110
+ ? {
111
+ routesHash: snapshot.lock.routesHash,
112
+ updatedAt: snapshot.lock.updatedAt,
113
+ }
114
+ : null,
115
+ slotFiles,
116
+ slotCount: slotFiles.length,
117
+ };
118
+ },
119
+
120
+ mandu_prune_history: async (args: Record<string, unknown>) => {
121
+ const { keepCount = 5 } = args as { keepCount?: number };
122
+
123
+ const deletedIds = await pruneHistory(projectRoot, keepCount);
124
+
125
+ const stats = await getChangeStats(projectRoot);
126
+
127
+ return {
128
+ success: true,
129
+ deletedCount: deletedIds.length,
130
+ deletedIds,
131
+ remaining: {
132
+ snapshotCount: stats.snapshotCount,
133
+ changeCount: stats.total,
134
+ },
135
+ };
136
+ },
137
+ };
138
+ }
@@ -0,0 +1,6 @@
1
+ export { specTools, specToolDefinitions } from "./spec.js";
2
+ export { generateTools, generateToolDefinitions } from "./generate.js";
3
+ export { transactionTools, transactionToolDefinitions } from "./transaction.js";
4
+ export { historyTools, historyToolDefinitions } from "./history.js";
5
+ export { guardTools, guardToolDefinitions } from "./guard.js";
6
+ export { slotTools, slotToolDefinitions } from "./slot.js";
@@ -0,0 +1,166 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import { loadManifest } from "@mandujs/core";
3
+ import { getProjectPaths, isInsideProject } from "../utils/project.js";
4
+ import path from "path";
5
+ import fs from "fs/promises";
6
+
7
+ export const slotToolDefinitions: Tool[] = [
8
+ {
9
+ name: "mandu_read_slot",
10
+ description: "Read the contents of a slot file for a specific route",
11
+ inputSchema: {
12
+ type: "object",
13
+ properties: {
14
+ routeId: {
15
+ type: "string",
16
+ description: "The route ID whose slot file to read",
17
+ },
18
+ },
19
+ required: ["routeId"],
20
+ },
21
+ },
22
+ {
23
+ name: "mandu_write_slot",
24
+ description: "Write or update the contents of a slot file",
25
+ inputSchema: {
26
+ type: "object",
27
+ properties: {
28
+ routeId: {
29
+ type: "string",
30
+ description: "The route ID whose slot file to write",
31
+ },
32
+ content: {
33
+ type: "string",
34
+ description: "The TypeScript content to write to the slot file",
35
+ },
36
+ },
37
+ required: ["routeId", "content"],
38
+ },
39
+ },
40
+ ];
41
+
42
+ export function slotTools(projectRoot: string) {
43
+ const paths = getProjectPaths(projectRoot);
44
+
45
+ return {
46
+ mandu_read_slot: async (args: Record<string, unknown>) => {
47
+ const { routeId } = args as { routeId: string };
48
+
49
+ // Load manifest to find the route
50
+ const manifestResult = await loadManifest(paths.manifestPath);
51
+ if (!manifestResult.success || !manifestResult.data) {
52
+ return { error: manifestResult.errors };
53
+ }
54
+
55
+ const route = manifestResult.data.routes.find((r) => r.id === routeId);
56
+ if (!route) {
57
+ return { error: `Route not found: ${routeId}` };
58
+ }
59
+
60
+ if (!route.slotModule) {
61
+ return {
62
+ error: `Route '${routeId}' does not have a slotModule defined`,
63
+ tip: "Add slotModule to the route spec or use mandu_update_route",
64
+ };
65
+ }
66
+
67
+ const slotPath = path.join(projectRoot, route.slotModule);
68
+
69
+ // Security check
70
+ if (!isInsideProject(slotPath, projectRoot)) {
71
+ return { error: "Slot path is outside project directory" };
72
+ }
73
+
74
+ try {
75
+ const file = Bun.file(slotPath);
76
+ if (!(await file.exists())) {
77
+ return {
78
+ exists: false,
79
+ slotPath: route.slotModule,
80
+ message: "Slot file does not exist. Run mandu_generate to create it.",
81
+ };
82
+ }
83
+
84
+ const content = await file.text();
85
+ return {
86
+ exists: true,
87
+ slotPath: route.slotModule,
88
+ content,
89
+ lineCount: content.split("\n").length,
90
+ };
91
+ } catch (error) {
92
+ return {
93
+ error: `Failed to read slot file: ${error instanceof Error ? error.message : String(error)}`,
94
+ };
95
+ }
96
+ },
97
+
98
+ mandu_write_slot: async (args: Record<string, unknown>) => {
99
+ const { routeId, content } = args as { routeId: string; content: string };
100
+
101
+ // Load manifest to find the route
102
+ const manifestResult = await loadManifest(paths.manifestPath);
103
+ if (!manifestResult.success || !manifestResult.data) {
104
+ return { error: manifestResult.errors };
105
+ }
106
+
107
+ const route = manifestResult.data.routes.find((r) => r.id === routeId);
108
+ if (!route) {
109
+ return { error: `Route not found: ${routeId}` };
110
+ }
111
+
112
+ if (!route.slotModule) {
113
+ return {
114
+ error: `Route '${routeId}' does not have a slotModule defined`,
115
+ tip: "Add slotModule to the route spec first",
116
+ };
117
+ }
118
+
119
+ const slotPath = path.join(projectRoot, route.slotModule);
120
+
121
+ // Security check
122
+ if (!isInsideProject(slotPath, projectRoot)) {
123
+ return { error: "Slot path is outside project directory" };
124
+ }
125
+
126
+ // Basic validation - check for Mandu.filling() pattern
127
+ if (!content.includes("Mandu") && !content.includes("filling")) {
128
+ return {
129
+ warning: "Slot content doesn't appear to use Mandu.filling() pattern",
130
+ tip: "Slot files should export a default using Mandu.filling()",
131
+ proceeding: true,
132
+ };
133
+ }
134
+
135
+ try {
136
+ // Ensure directory exists
137
+ const slotDir = path.dirname(slotPath);
138
+ await fs.mkdir(slotDir, { recursive: true });
139
+
140
+ // Check if file exists (for backup/warning)
141
+ const file = Bun.file(slotPath);
142
+ const existed = await file.exists();
143
+ let previousContent: string | null = null;
144
+
145
+ if (existed) {
146
+ previousContent = await file.text();
147
+ }
148
+
149
+ // Write the new content
150
+ await Bun.write(slotPath, content);
151
+
152
+ return {
153
+ success: true,
154
+ slotPath: route.slotModule,
155
+ action: existed ? "updated" : "created",
156
+ lineCount: content.split("\n").length,
157
+ previousLineCount: previousContent ? previousContent.split("\n").length : null,
158
+ };
159
+ } catch (error) {
160
+ return {
161
+ error: `Failed to write slot file: ${error instanceof Error ? error.message : String(error)}`,
162
+ };
163
+ }
164
+ },
165
+ };
166
+ }