@mandujs/mcp 0.17.1 → 0.18.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/mcp",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -32,8 +32,8 @@
32
32
  "access": "public"
33
33
  },
34
34
  "dependencies": {
35
- "@mandujs/core": "^0.16.0",
36
- "@mandujs/ate": "0.17.0",
35
+ "@mandujs/core": "^0.18.2",
36
+ "@mandujs/ate": "^0.17.0",
37
37
  "@modelcontextprotocol/sdk": "^1.25.3"
38
38
  },
39
39
  "engines": {
@@ -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 and components from the spec manifest. Creates server handlers, page components, and slot files.",
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
- created: result.created,
88
- deleted: result.deleted,
89
- skipped: result.skipped,
90
- errors: result.errors,
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
- createdCount: result.created.length,
93
- deletedCount: result.deleted.length,
94
- skippedCount: result.skipped.length,
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
  },
@@ -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
+ }