@mandujs/mcp 0.17.1 → 0.18.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/package.json +2 -2
- package/src/tools/generate.ts +68 -12
- package/src/tools/index.ts +3 -0
- package/src/tools/resource.ts +654 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "^0.
|
|
35
|
+
"@mandujs/core": "^0.18.0",
|
|
36
36
|
"@mandujs/ate": "0.17.0",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
38
38
|
},
|
package/src/tools/generate.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { loadManifest, generateRoutes, generateManifest, GENERATED_RELATIVE_PATHS, type GeneratedMap } from "@mandujs/core";
|
|
2
|
+
import { loadManifest, generateRoutes, generateManifest, GENERATED_RELATIVE_PATHS, type GeneratedMap, parseResourceSchema, generateResourceArtifacts } from "@mandujs/core";
|
|
3
3
|
import { getProjectPaths, readJsonFile } from "../utils/project.js";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fs from "fs/promises";
|
|
4
6
|
|
|
5
7
|
export const generateToolDefinitions: Tool[] = [
|
|
6
8
|
{
|
|
7
9
|
name: "mandu_generate",
|
|
8
10
|
description:
|
|
9
|
-
"Generate route handlers
|
|
11
|
+
"Generate route handlers, components, and resource artifacts. Creates server handlers, page components, slot files, and resource CRUD operations.",
|
|
10
12
|
inputSchema: {
|
|
11
13
|
type: "object",
|
|
12
14
|
properties: {
|
|
@@ -14,6 +16,10 @@ export const generateToolDefinitions: Tool[] = [
|
|
|
14
16
|
type: "boolean",
|
|
15
17
|
description: "If true, show what would be generated without writing files",
|
|
16
18
|
},
|
|
19
|
+
resources: {
|
|
20
|
+
type: "boolean",
|
|
21
|
+
description: "Include resource artifact generation (default: true)",
|
|
22
|
+
},
|
|
17
23
|
},
|
|
18
24
|
required: [],
|
|
19
25
|
},
|
|
@@ -34,7 +40,7 @@ export function generateTools(projectRoot: string) {
|
|
|
34
40
|
|
|
35
41
|
return {
|
|
36
42
|
mandu_generate: async (args: Record<string, unknown>) => {
|
|
37
|
-
const { dryRun } = args as { dryRun?: boolean };
|
|
43
|
+
const { dryRun, resources = true } = args as { dryRun?: boolean; resources?: boolean };
|
|
38
44
|
|
|
39
45
|
// Regenerate manifest from FS Routes first
|
|
40
46
|
const fsResult = await generateManifest(projectRoot);
|
|
@@ -79,19 +85,69 @@ export function generateTools(projectRoot: string) {
|
|
|
79
85
|
};
|
|
80
86
|
}
|
|
81
87
|
|
|
82
|
-
// Actually generate
|
|
88
|
+
// Actually generate routes
|
|
83
89
|
const result = await generateRoutes(manifestResult.data, projectRoot);
|
|
84
90
|
|
|
91
|
+
// Generate resources if enabled
|
|
92
|
+
let resourceResults: {
|
|
93
|
+
created: string[];
|
|
94
|
+
skipped: string[];
|
|
95
|
+
errors: string[];
|
|
96
|
+
} = {
|
|
97
|
+
created: [],
|
|
98
|
+
skipped: [],
|
|
99
|
+
errors: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (resources) {
|
|
103
|
+
try {
|
|
104
|
+
const resourcesDir = path.join(projectRoot, "spec", "resources");
|
|
105
|
+
const entries = await fs.readdir(resourcesDir, { withFileTypes: true });
|
|
106
|
+
const resourceDirs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
107
|
+
|
|
108
|
+
for (const resourceName of resourceDirs) {
|
|
109
|
+
const schemaPath = path.join(resourcesDir, resourceName, "schema.ts");
|
|
110
|
+
try {
|
|
111
|
+
const parsed = await parseResourceSchema(schemaPath);
|
|
112
|
+
const resourceResult = await generateResourceArtifacts(parsed, {
|
|
113
|
+
rootDir: projectRoot,
|
|
114
|
+
force: false,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
resourceResults.created.push(...resourceResult.created);
|
|
118
|
+
resourceResults.skipped.push(...resourceResult.skipped);
|
|
119
|
+
resourceResults.errors.push(...resourceResult.errors);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
resourceResults.errors.push(`Failed to generate resource '${resourceName}': ${err instanceof Error ? err.message : String(err)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// spec/resources/ doesn't exist or is empty - not an error
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
85
129
|
return {
|
|
86
|
-
success: result.success,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
success: result.success && resourceResults.errors.length === 0,
|
|
131
|
+
routes: {
|
|
132
|
+
created: result.created,
|
|
133
|
+
deleted: result.deleted,
|
|
134
|
+
skipped: result.skipped,
|
|
135
|
+
errors: result.errors,
|
|
136
|
+
},
|
|
137
|
+
resources: resources
|
|
138
|
+
? {
|
|
139
|
+
created: resourceResults.created,
|
|
140
|
+
skipped: resourceResults.skipped,
|
|
141
|
+
errors: resourceResults.errors,
|
|
142
|
+
}
|
|
143
|
+
: undefined,
|
|
91
144
|
summary: {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
145
|
+
routesCreated: result.created.length,
|
|
146
|
+
routesDeleted: result.deleted.length,
|
|
147
|
+
routesSkipped: result.skipped.length,
|
|
148
|
+
resourcesCreated: resourceResults.created.length,
|
|
149
|
+
resourcesSkipped: resourceResults.skipped.length,
|
|
150
|
+
totalErrors: result.errors.length + resourceResults.errors.length,
|
|
95
151
|
},
|
|
96
152
|
};
|
|
97
153
|
},
|
package/src/tools/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { runtimeTools, runtimeToolDefinitions } from "./runtime.js";
|
|
|
23
23
|
export { seoTools, seoToolDefinitions } from "./seo.js";
|
|
24
24
|
export { projectTools, projectToolDefinitions } from "./project.js";
|
|
25
25
|
export { ateTools, ateToolDefinitions } from "./ate.js";
|
|
26
|
+
export { resourceTools, resourceToolDefinitions } from "./resource.js";
|
|
26
27
|
|
|
27
28
|
// 도구 모듈 import (등록용)
|
|
28
29
|
import { specTools, specToolDefinitions } from "./spec.js";
|
|
@@ -38,6 +39,7 @@ import { runtimeTools, runtimeToolDefinitions } from "./runtime.js";
|
|
|
38
39
|
import { seoTools, seoToolDefinitions } from "./seo.js";
|
|
39
40
|
import { projectTools, projectToolDefinitions } from "./project.js";
|
|
40
41
|
import { ateTools, ateToolDefinitions } from "./ate.js";
|
|
42
|
+
import { resourceTools, resourceToolDefinitions } from "./resource.js";
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* 도구 모듈 정보
|
|
@@ -70,6 +72,7 @@ const TOOL_MODULES: ToolModule[] = [
|
|
|
70
72
|
{ category: "seo", definitions: seoToolDefinitions, handlers: seoTools },
|
|
71
73
|
{ category: "project", definitions: projectToolDefinitions, handlers: projectTools as ToolModule["handlers"], requiresServer: true },
|
|
72
74
|
{ category: "ate", definitions: ateToolDefinitions as any, handlers: ateTools as any },
|
|
75
|
+
{ category: "resource", definitions: resourceToolDefinitions, handlers: resourceTools },
|
|
73
76
|
];
|
|
74
77
|
|
|
75
78
|
/**
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource Management Tools
|
|
3
|
+
* MCP tools for managing Mandu resources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
+
import {
|
|
8
|
+
defineResource,
|
|
9
|
+
parseResourceSchema,
|
|
10
|
+
generateResourceArtifacts,
|
|
11
|
+
type ResourceDefinition,
|
|
12
|
+
type ResourceField,
|
|
13
|
+
type FieldType,
|
|
14
|
+
type GeneratorOptions,
|
|
15
|
+
type GeneratorResult,
|
|
16
|
+
FieldTypes,
|
|
17
|
+
} from "@mandujs/core";
|
|
18
|
+
import path from "path";
|
|
19
|
+
import fs from "fs/promises";
|
|
20
|
+
|
|
21
|
+
// ============================================
|
|
22
|
+
// Tool Definitions
|
|
23
|
+
// ============================================
|
|
24
|
+
|
|
25
|
+
export const resourceToolDefinitions: Tool[] = [
|
|
26
|
+
{
|
|
27
|
+
name: "mandu.resource.create",
|
|
28
|
+
description:
|
|
29
|
+
"Create a new resource with schema definition. " +
|
|
30
|
+
"Generates schema file in spec/resources/{name}/schema.ts and creates " +
|
|
31
|
+
"CRUD handlers, types, contracts, and API clients based on the schema.",
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: "object",
|
|
34
|
+
properties: {
|
|
35
|
+
name: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description: "Resource name in singular form (e.g., 'user', 'post', 'product')",
|
|
38
|
+
},
|
|
39
|
+
fields: {
|
|
40
|
+
type: "object",
|
|
41
|
+
description: "Field definitions as key-value pairs",
|
|
42
|
+
additionalProperties: {
|
|
43
|
+
type: "object",
|
|
44
|
+
properties: {
|
|
45
|
+
type: {
|
|
46
|
+
type: "string",
|
|
47
|
+
enum: FieldTypes,
|
|
48
|
+
description: "Field data type",
|
|
49
|
+
},
|
|
50
|
+
required: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
description: "Whether this field is required (default: false)",
|
|
53
|
+
},
|
|
54
|
+
default: {
|
|
55
|
+
description: "Default value for this field",
|
|
56
|
+
},
|
|
57
|
+
description: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Field description",
|
|
60
|
+
},
|
|
61
|
+
items: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: FieldTypes,
|
|
64
|
+
description: "Array element type (required if type is 'array')",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
required: ["type"],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
description: {
|
|
71
|
+
type: "string",
|
|
72
|
+
description: "Resource description",
|
|
73
|
+
},
|
|
74
|
+
tags: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: { type: "string" },
|
|
77
|
+
description: "API tags for categorization",
|
|
78
|
+
},
|
|
79
|
+
endpoints: {
|
|
80
|
+
type: "object",
|
|
81
|
+
description: "Which endpoints to enable (default: all true)",
|
|
82
|
+
properties: {
|
|
83
|
+
list: { type: "boolean" },
|
|
84
|
+
get: { type: "boolean" },
|
|
85
|
+
create: { type: "boolean" },
|
|
86
|
+
update: { type: "boolean" },
|
|
87
|
+
delete: { type: "boolean" },
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
required: ["name", "fields"],
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "mandu.resource.list",
|
|
96
|
+
description:
|
|
97
|
+
"List all resources in the project. " +
|
|
98
|
+
"Scans spec/resources/ directory and returns resource names with field summaries.",
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: "object",
|
|
101
|
+
properties: {},
|
|
102
|
+
required: [],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: "mandu.resource.get",
|
|
107
|
+
description:
|
|
108
|
+
"Get detailed information about a specific resource. " +
|
|
109
|
+
"Returns full schema definition including fields, types, and options.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: {
|
|
113
|
+
resourceName: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "The resource name to retrieve (e.g., 'user', 'post')",
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
required: ["resourceName"],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "mandu.resource.addField",
|
|
123
|
+
description:
|
|
124
|
+
"Add a new field to an existing resource schema. " +
|
|
125
|
+
"⚠️ IMPORTANT: Preserves custom slot logic by using force: false during regeneration. " +
|
|
126
|
+
"Updates schema file and regenerates artifacts without overwriting slot implementations.",
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: "object",
|
|
129
|
+
properties: {
|
|
130
|
+
resourceName: {
|
|
131
|
+
type: "string",
|
|
132
|
+
description: "The resource to modify (e.g., 'user')",
|
|
133
|
+
},
|
|
134
|
+
fieldName: {
|
|
135
|
+
type: "string",
|
|
136
|
+
description: "Name of the new field (e.g., 'phoneNumber', 'email')",
|
|
137
|
+
},
|
|
138
|
+
fieldType: {
|
|
139
|
+
type: "string",
|
|
140
|
+
enum: FieldTypes,
|
|
141
|
+
description: "Data type of the new field",
|
|
142
|
+
},
|
|
143
|
+
required: {
|
|
144
|
+
type: "boolean",
|
|
145
|
+
description: "Whether the field is required (default: false)",
|
|
146
|
+
},
|
|
147
|
+
default: {
|
|
148
|
+
description: "Default value for the field",
|
|
149
|
+
},
|
|
150
|
+
description: {
|
|
151
|
+
type: "string",
|
|
152
|
+
description: "Field description",
|
|
153
|
+
},
|
|
154
|
+
items: {
|
|
155
|
+
type: "string",
|
|
156
|
+
enum: FieldTypes,
|
|
157
|
+
description: "Array element type (required if fieldType is 'array')",
|
|
158
|
+
},
|
|
159
|
+
force: {
|
|
160
|
+
type: "boolean",
|
|
161
|
+
description:
|
|
162
|
+
"⚠️ WARNING: Overwrites custom slot logic if true. " +
|
|
163
|
+
"Only use when you want to reset slot to default template. (default: false)",
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
required: ["resourceName", "fieldName", "fieldType"],
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "mandu.resource.removeField",
|
|
171
|
+
description:
|
|
172
|
+
"Remove a field from an existing resource schema. " +
|
|
173
|
+
"Updates schema file and regenerates artifacts with force: false to preserve slots.",
|
|
174
|
+
inputSchema: {
|
|
175
|
+
type: "object",
|
|
176
|
+
properties: {
|
|
177
|
+
resourceName: {
|
|
178
|
+
type: "string",
|
|
179
|
+
description: "The resource to modify",
|
|
180
|
+
},
|
|
181
|
+
fieldName: {
|
|
182
|
+
type: "string",
|
|
183
|
+
description: "Name of the field to remove",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
required: ["resourceName", "fieldName"],
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
// ============================================
|
|
192
|
+
// Helper Functions
|
|
193
|
+
// ============================================
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get spec/resources directory path
|
|
197
|
+
*/
|
|
198
|
+
function getResourcesDir(projectRoot: string): string {
|
|
199
|
+
return path.join(projectRoot, "spec", "resources");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get resource schema file path
|
|
204
|
+
*/
|
|
205
|
+
function getResourceSchemaPath(projectRoot: string, resourceName: string): string {
|
|
206
|
+
return path.join(getResourcesDir(projectRoot), resourceName, "schema.ts");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Check if file exists
|
|
211
|
+
*/
|
|
212
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
213
|
+
try {
|
|
214
|
+
await fs.access(filePath);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Ensure directory exists
|
|
223
|
+
*/
|
|
224
|
+
async function ensureDir(dirPath: string): Promise<void> {
|
|
225
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* List all resource directories
|
|
230
|
+
*/
|
|
231
|
+
async function listResourceDirs(projectRoot: string): Promise<string[]> {
|
|
232
|
+
const resourcesDir = getResourcesDir(projectRoot);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const entries = await fs.readdir(resourcesDir, { withFileTypes: true });
|
|
236
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
237
|
+
} catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Generate resource schema file content
|
|
244
|
+
*/
|
|
245
|
+
function generateSchemaFileContent(definition: ResourceDefinition): string {
|
|
246
|
+
const { name, fields, options } = definition;
|
|
247
|
+
|
|
248
|
+
const fieldsCode = Object.entries(fields)
|
|
249
|
+
.map(([fieldName, field]) => {
|
|
250
|
+
const props: string[] = [`type: "${field.type}"`];
|
|
251
|
+
if (field.required) props.push("required: true");
|
|
252
|
+
if (field.default !== undefined) props.push(`default: ${JSON.stringify(field.default)}`);
|
|
253
|
+
if (field.description) props.push(`description: "${field.description}"`);
|
|
254
|
+
if (field.items) props.push(`items: "${field.items}"`);
|
|
255
|
+
|
|
256
|
+
return ` ${fieldName}: { ${props.join(", ")} },`;
|
|
257
|
+
})
|
|
258
|
+
.join("\n");
|
|
259
|
+
|
|
260
|
+
const optionsCode = options
|
|
261
|
+
? ` options: {
|
|
262
|
+
${options.description ? ` description: "${options.description}",\n` : ""}${
|
|
263
|
+
options.tags ? ` tags: ${JSON.stringify(options.tags)},\n` : ""
|
|
264
|
+
}${
|
|
265
|
+
options.endpoints
|
|
266
|
+
? ` endpoints: ${JSON.stringify(options.endpoints, null, 6).replace(/\n/g, "\n ")},\n`
|
|
267
|
+
: ""
|
|
268
|
+
} },`
|
|
269
|
+
: "";
|
|
270
|
+
|
|
271
|
+
return `import { defineResource } from "@mandujs/core";
|
|
272
|
+
|
|
273
|
+
export const ${name}Resource = defineResource({
|
|
274
|
+
name: "${name}",
|
|
275
|
+
fields: {
|
|
276
|
+
${fieldsCode}
|
|
277
|
+
},
|
|
278
|
+
${optionsCode}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
export default ${name}Resource;
|
|
282
|
+
`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Parse schema file to extract ResourceDefinition
|
|
287
|
+
*/
|
|
288
|
+
async function readResourceDefinition(
|
|
289
|
+
projectRoot: string,
|
|
290
|
+
resourceName: string
|
|
291
|
+
): Promise<ResourceDefinition | null> {
|
|
292
|
+
const schemaPath = getResourceSchemaPath(projectRoot, resourceName);
|
|
293
|
+
|
|
294
|
+
if (!(await fileExists(schemaPath))) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
// Use parseResourceSchema from core
|
|
300
|
+
const parsed = await parseResourceSchema(schemaPath);
|
|
301
|
+
return parsed.definition;
|
|
302
|
+
} catch (error) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Failed to parse resource schema: ${error instanceof Error ? error.message : String(error)}`
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// ============================================
|
|
310
|
+
// Tool Handlers
|
|
311
|
+
// ============================================
|
|
312
|
+
|
|
313
|
+
export function resourceTools(projectRoot: string) {
|
|
314
|
+
return {
|
|
315
|
+
/**
|
|
316
|
+
* Create a new resource
|
|
317
|
+
*/
|
|
318
|
+
"mandu.resource.create": async (args: Record<string, unknown>) => {
|
|
319
|
+
const { name, fields, description, tags, endpoints } = args as {
|
|
320
|
+
name: string;
|
|
321
|
+
fields: Record<string, ResourceField>;
|
|
322
|
+
description?: string;
|
|
323
|
+
tags?: string[];
|
|
324
|
+
endpoints?: {
|
|
325
|
+
list?: boolean;
|
|
326
|
+
get?: boolean;
|
|
327
|
+
create?: boolean;
|
|
328
|
+
update?: boolean;
|
|
329
|
+
delete?: boolean;
|
|
330
|
+
};
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// Validation
|
|
334
|
+
if (!name || !fields) {
|
|
335
|
+
return {
|
|
336
|
+
error: "Missing required parameters: name and fields",
|
|
337
|
+
tip: "Provide resource name (singular) and field definitions",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Check if resource already exists
|
|
342
|
+
const schemaPath = getResourceSchemaPath(projectRoot, name);
|
|
343
|
+
if (await fileExists(schemaPath)) {
|
|
344
|
+
return {
|
|
345
|
+
error: `Resource '${name}' already exists`,
|
|
346
|
+
tip: "Use mandu.resource.get to view existing resource or mandu.resource.addField to add fields",
|
|
347
|
+
existingPath: schemaPath,
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
try {
|
|
352
|
+
// Define resource
|
|
353
|
+
const definition = defineResource({
|
|
354
|
+
name,
|
|
355
|
+
fields,
|
|
356
|
+
options: {
|
|
357
|
+
description,
|
|
358
|
+
tags,
|
|
359
|
+
endpoints,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Create schema file
|
|
364
|
+
const resourceDir = path.dirname(schemaPath);
|
|
365
|
+
await ensureDir(resourceDir);
|
|
366
|
+
|
|
367
|
+
const schemaContent = generateSchemaFileContent(definition);
|
|
368
|
+
await fs.writeFile(schemaPath, schemaContent, "utf-8");
|
|
369
|
+
|
|
370
|
+
// Parse and generate artifacts
|
|
371
|
+
const parsed = await parseResourceSchema(schemaPath);
|
|
372
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
373
|
+
rootDir: projectRoot,
|
|
374
|
+
force: false,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
success: true,
|
|
379
|
+
resourceName: name,
|
|
380
|
+
schemaFile: schemaPath,
|
|
381
|
+
generated: {
|
|
382
|
+
created: result.created,
|
|
383
|
+
skipped: result.skipped,
|
|
384
|
+
},
|
|
385
|
+
fieldCount: Object.keys(fields).length,
|
|
386
|
+
message: `Resource '${name}' created successfully with ${Object.keys(fields).length} fields`,
|
|
387
|
+
tip: "Run mandu.resource.get to view full resource details or start implementing slot logic",
|
|
388
|
+
};
|
|
389
|
+
} catch (error) {
|
|
390
|
+
return {
|
|
391
|
+
error: `Failed to create resource: ${error instanceof Error ? error.message : String(error)}`,
|
|
392
|
+
tip: "Check field definitions and resource name format",
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* List all resources
|
|
399
|
+
*/
|
|
400
|
+
"mandu.resource.list": async () => {
|
|
401
|
+
try {
|
|
402
|
+
const resourceDirs = await listResourceDirs(projectRoot);
|
|
403
|
+
|
|
404
|
+
if (resourceDirs.length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
resources: [],
|
|
407
|
+
total: 0,
|
|
408
|
+
message: "No resources found in spec/resources/",
|
|
409
|
+
tip: "Use mandu.resource.create to create your first resource",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Read each resource
|
|
414
|
+
const resources = await Promise.all(
|
|
415
|
+
resourceDirs.map(async (resourceName) => {
|
|
416
|
+
try {
|
|
417
|
+
const definition = await readResourceDefinition(projectRoot, resourceName);
|
|
418
|
+
if (!definition) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return {
|
|
423
|
+
name: resourceName,
|
|
424
|
+
fieldCount: Object.keys(definition.fields).length,
|
|
425
|
+
fields: Object.keys(definition.fields),
|
|
426
|
+
description: definition.options?.description,
|
|
427
|
+
tags: definition.options?.tags,
|
|
428
|
+
};
|
|
429
|
+
} catch {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
})
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
const validResources = resources.filter((r) => r !== null);
|
|
436
|
+
|
|
437
|
+
return {
|
|
438
|
+
resources: validResources,
|
|
439
|
+
total: validResources.length,
|
|
440
|
+
tip: "Use mandu.resource.get with resourceName to see full details",
|
|
441
|
+
};
|
|
442
|
+
} catch (error) {
|
|
443
|
+
return {
|
|
444
|
+
error: `Failed to list resources: ${error instanceof Error ? error.message : String(error)}`,
|
|
445
|
+
tip: "Ensure spec/resources/ directory exists",
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Get resource details
|
|
452
|
+
*/
|
|
453
|
+
"mandu.resource.get": async (args: Record<string, unknown>) => {
|
|
454
|
+
const { resourceName } = args as { resourceName: string };
|
|
455
|
+
|
|
456
|
+
if (!resourceName) {
|
|
457
|
+
return {
|
|
458
|
+
error: "Missing required parameter: resourceName",
|
|
459
|
+
tip: "Use mandu.resource.list to see available resources",
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const definition = await readResourceDefinition(projectRoot, resourceName);
|
|
465
|
+
|
|
466
|
+
if (!definition) {
|
|
467
|
+
return {
|
|
468
|
+
error: `Resource '${resourceName}' not found`,
|
|
469
|
+
tip: "Use mandu.resource.list to see available resources or mandu.resource.create to create it",
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
name: definition.name,
|
|
475
|
+
fields: definition.fields,
|
|
476
|
+
options: definition.options,
|
|
477
|
+
fieldCount: Object.keys(definition.fields).length,
|
|
478
|
+
schemaFile: getResourceSchemaPath(projectRoot, resourceName),
|
|
479
|
+
tip: "Use mandu.resource.addField to add new fields or mandu.resource.removeField to remove fields",
|
|
480
|
+
};
|
|
481
|
+
} catch (error) {
|
|
482
|
+
return {
|
|
483
|
+
error: `Failed to read resource: ${error instanceof Error ? error.message : String(error)}`,
|
|
484
|
+
tip: "Check if schema file is valid TypeScript",
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Add field to resource
|
|
491
|
+
*/
|
|
492
|
+
"mandu.resource.addField": async (args: Record<string, unknown>) => {
|
|
493
|
+
const { resourceName, fieldName, fieldType, required, default: defaultValue, description, items, force = false } =
|
|
494
|
+
args as {
|
|
495
|
+
resourceName: string;
|
|
496
|
+
fieldName: string;
|
|
497
|
+
fieldType: FieldType;
|
|
498
|
+
required?: boolean;
|
|
499
|
+
default?: unknown;
|
|
500
|
+
description?: string;
|
|
501
|
+
items?: FieldType;
|
|
502
|
+
force?: boolean;
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
// Validation
|
|
506
|
+
if (!resourceName || !fieldName || !fieldType) {
|
|
507
|
+
return {
|
|
508
|
+
error: "Missing required parameters: resourceName, fieldName, fieldType",
|
|
509
|
+
tip: "Provide all required parameters",
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
try {
|
|
514
|
+
// Read existing resource
|
|
515
|
+
const definition = await readResourceDefinition(projectRoot, resourceName);
|
|
516
|
+
|
|
517
|
+
if (!definition) {
|
|
518
|
+
return {
|
|
519
|
+
error: `Resource '${resourceName}' not found`,
|
|
520
|
+
tip: "Use mandu.resource.list to see available resources",
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// Check if field already exists
|
|
525
|
+
if (definition.fields[fieldName]) {
|
|
526
|
+
return {
|
|
527
|
+
error: `Field '${fieldName}' already exists in resource '${resourceName}'`,
|
|
528
|
+
tip: "Use mandu.resource.get to view existing fields or choose a different field name",
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Add field
|
|
533
|
+
const newField: ResourceField = {
|
|
534
|
+
type: fieldType,
|
|
535
|
+
required,
|
|
536
|
+
default: defaultValue,
|
|
537
|
+
description,
|
|
538
|
+
items,
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
definition.fields[fieldName] = newField;
|
|
542
|
+
|
|
543
|
+
// Update schema file
|
|
544
|
+
const schemaPath = getResourceSchemaPath(projectRoot, resourceName);
|
|
545
|
+
const schemaContent = generateSchemaFileContent(definition);
|
|
546
|
+
await fs.writeFile(schemaPath, schemaContent, "utf-8");
|
|
547
|
+
|
|
548
|
+
// Regenerate artifacts (force: false to preserve slots!)
|
|
549
|
+
const parsed = await parseResourceSchema(schemaPath);
|
|
550
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
551
|
+
rootDir: projectRoot,
|
|
552
|
+
force: force, // Use force parameter from args
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
success: true,
|
|
557
|
+
resourceName,
|
|
558
|
+
fieldAdded: fieldName,
|
|
559
|
+
fieldType,
|
|
560
|
+
filesUpdated: [schemaPath, ...result.created],
|
|
561
|
+
slotsPreserved: result.skipped,
|
|
562
|
+
forceUsed: force,
|
|
563
|
+
message: `Field '${fieldName}' added to resource '${resourceName}'. ${force ? "⚠️ Slots overwritten!" : "Slots preserved."}`,
|
|
564
|
+
tip: force
|
|
565
|
+
? "⚠️ Custom slot logic was overwritten because force: true was used"
|
|
566
|
+
: "Custom slot logic preserved. Run mandu_generate to apply changes to all resources.",
|
|
567
|
+
};
|
|
568
|
+
} catch (error) {
|
|
569
|
+
return {
|
|
570
|
+
error: `Failed to add field: ${error instanceof Error ? error.message : String(error)}`,
|
|
571
|
+
tip: "Check field type and resource name",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Remove field from resource
|
|
578
|
+
*/
|
|
579
|
+
"mandu.resource.removeField": async (args: Record<string, unknown>) => {
|
|
580
|
+
const { resourceName, fieldName } = args as {
|
|
581
|
+
resourceName: string;
|
|
582
|
+
fieldName: string;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
// Validation
|
|
586
|
+
if (!resourceName || !fieldName) {
|
|
587
|
+
return {
|
|
588
|
+
error: "Missing required parameters: resourceName, fieldName",
|
|
589
|
+
tip: "Provide both resourceName and fieldName",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
// Read existing resource
|
|
595
|
+
const definition = await readResourceDefinition(projectRoot, resourceName);
|
|
596
|
+
|
|
597
|
+
if (!definition) {
|
|
598
|
+
return {
|
|
599
|
+
error: `Resource '${resourceName}' not found`,
|
|
600
|
+
tip: "Use mandu.resource.list to see available resources",
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Check if field exists
|
|
605
|
+
if (!definition.fields[fieldName]) {
|
|
606
|
+
return {
|
|
607
|
+
error: `Field '${fieldName}' not found in resource '${resourceName}'`,
|
|
608
|
+
tip: "Use mandu.resource.get to view existing fields",
|
|
609
|
+
availableFields: Object.keys(definition.fields),
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Remove field
|
|
614
|
+
delete definition.fields[fieldName];
|
|
615
|
+
|
|
616
|
+
// Validate at least one field remains
|
|
617
|
+
if (Object.keys(definition.fields).length === 0) {
|
|
618
|
+
return {
|
|
619
|
+
error: `Cannot remove field '${fieldName}': resource must have at least one field`,
|
|
620
|
+
tip: "Add other fields before removing this one, or delete the entire resource",
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Update schema file
|
|
625
|
+
const schemaPath = getResourceSchemaPath(projectRoot, resourceName);
|
|
626
|
+
const schemaContent = generateSchemaFileContent(definition);
|
|
627
|
+
await fs.writeFile(schemaPath, schemaContent, "utf-8");
|
|
628
|
+
|
|
629
|
+
// Regenerate artifacts (force: false to preserve slots!)
|
|
630
|
+
const parsed = await parseResourceSchema(schemaPath);
|
|
631
|
+
const result = await generateResourceArtifacts(parsed, {
|
|
632
|
+
rootDir: projectRoot,
|
|
633
|
+
force: false,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
success: true,
|
|
638
|
+
resourceName,
|
|
639
|
+
fieldRemoved: fieldName,
|
|
640
|
+
filesUpdated: [schemaPath, ...result.created],
|
|
641
|
+
slotsPreserved: result.skipped,
|
|
642
|
+
remainingFields: Object.keys(definition.fields),
|
|
643
|
+
message: `Field '${fieldName}' removed from resource '${resourceName}'. Slots preserved.`,
|
|
644
|
+
tip: "Run mandu_generate to apply changes to all resources",
|
|
645
|
+
};
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return {
|
|
648
|
+
error: `Failed to remove field: ${error instanceof Error ? error.message : String(error)}`,
|
|
649
|
+
tip: "Check field name and resource name",
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
},
|
|
653
|
+
};
|
|
654
|
+
}
|