@mandujs/mcp 0.18.4 → 0.18.5

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.18.4",
3
+ "version": "0.18.5",
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.18.2",
35
+ "@mandujs/core": "^0.18.8",
36
36
  "@mandujs/ate": "^0.17.0",
37
37
  "@modelcontextprotocol/sdk": "^1.25.3"
38
38
  },
@@ -0,0 +1,185 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+
5
+ export const componentToolDefinitions: Tool[] = [
6
+ {
7
+ name: "mandu_add_component",
8
+ description:
9
+ "Scaffold a new client-side component in the correct FSD (Feature-Sliced Design) layer. " +
10
+ "Mandu projects organize client components under src/client/ following FSD layers: " +
11
+ "shared (reusable primitives), entities (domain objects), features (user interactions), " +
12
+ "widgets (composite blocks), pages (page-level controllers). " +
13
+ "Creates the component file and updates the layer's public API index.ts. " +
14
+ "Use this instead of manually creating files in app/ to maintain FSD architecture.",
15
+ inputSchema: {
16
+ type: "object",
17
+ properties: {
18
+ name: {
19
+ type: "string",
20
+ description: "Component name in PascalCase (e.g., 'ReactionBar', 'UserAvatar')",
21
+ },
22
+ layer: {
23
+ type: "string",
24
+ enum: ["shared", "entities", "features", "widgets", "pages"],
25
+ description:
26
+ "FSD layer: " +
27
+ "'shared' (reusable UI primitives, utils — no business logic), " +
28
+ "'entities' (domain models and their UI — User, Message, Post), " +
29
+ "'features' (user interactions that change state — like, comment, follow), " +
30
+ "'widgets' (composite sections combining entities+features), " +
31
+ "'pages' (page-level client components — use sparingly, prefer features/entities)",
32
+ },
33
+ slice: {
34
+ type: "string",
35
+ description:
36
+ "Feature slice name in kebab-case (required for features/entities/widgets). " +
37
+ "Examples: 'chat-reaction', 'user-profile', 'post-feed'. " +
38
+ "For 'shared' layer, use segment name like 'ui', 'lib', 'api'.",
39
+ },
40
+ segment: {
41
+ type: "string",
42
+ enum: ["ui", "model", "api", "lib", "config"],
43
+ description: "Segment within the slice (default: 'ui'). 'ui' for React components, 'model' for hooks/store, 'api' for data fetching.",
44
+ },
45
+ description: {
46
+ type: "string",
47
+ description: "Brief description of what this component does (added as a comment)",
48
+ },
49
+ },
50
+ required: ["name", "layer"],
51
+ },
52
+ },
53
+ ];
54
+
55
+ function toKebabCase(name: string): string {
56
+ return name
57
+ .replace(/([A-Z])/g, "-$1")
58
+ .toLowerCase()
59
+ .replace(/^-/, "");
60
+ }
61
+
62
+ export function componentTools(projectRoot: string) {
63
+ return {
64
+ mandu_add_component: async (args: Record<string, unknown>) => {
65
+ const {
66
+ name,
67
+ layer,
68
+ slice,
69
+ segment = "ui",
70
+ description = "",
71
+ } = args as {
72
+ name: string;
73
+ layer: "shared" | "entities" | "features" | "widgets" | "pages";
74
+ slice?: string;
75
+ segment?: string;
76
+ description?: string;
77
+ };
78
+
79
+ // Validate: features/entities/widgets require a slice
80
+ if (["features", "entities", "widgets"].includes(layer) && !slice) {
81
+ return {
82
+ success: false,
83
+ error: `The '${layer}' layer requires a 'slice' name (e.g., 'chat-reaction', 'user-profile').`,
84
+ };
85
+ }
86
+
87
+ // Build the file path
88
+ const clientBase = path.join(projectRoot, "src", "client");
89
+ let componentDir: string;
90
+ let indexPath: string;
91
+
92
+ if (layer === "shared") {
93
+ const sliceName = slice || "ui";
94
+ componentDir = path.join(clientBase, "shared", sliceName);
95
+ indexPath = path.join(clientBase, "shared", sliceName, "index.ts");
96
+ } else if (layer === "pages") {
97
+ componentDir = path.join(clientBase, "pages");
98
+ indexPath = path.join(clientBase, "pages", "index.ts");
99
+ } else {
100
+ const sliceName = slice!;
101
+ componentDir = path.join(clientBase, layer, sliceName, segment);
102
+ indexPath = path.join(clientBase, layer, sliceName, "index.ts");
103
+ }
104
+
105
+ const kebabName = toKebabCase(name);
106
+ const componentFile = path.join(componentDir, `${kebabName}.tsx`);
107
+ const relativePath = path.relative(projectRoot, componentFile).replace(/\\/g, "/");
108
+
109
+ // Check if file already exists
110
+ try {
111
+ await fs.access(componentFile);
112
+ return {
113
+ success: false,
114
+ error: `Component file already exists: ${relativePath}`,
115
+ };
116
+ } catch {
117
+ // Good - file doesn't exist
118
+ }
119
+
120
+ // Create directory
121
+ await fs.mkdir(componentDir, { recursive: true });
122
+
123
+ // Generate component template
124
+ const descComment = description ? `\n * ${description}` : "";
125
+ const template = `/**
126
+ * ${name} Component${descComment}
127
+ * Layer: ${layer}${slice ? ` / ${slice}` : ""}
128
+ */
129
+
130
+ import { useState } from "react";
131
+
132
+ interface ${name}Props {
133
+ // TODO: Define props
134
+ className?: string;
135
+ }
136
+
137
+ export function ${name}({ className }: ${name}Props) {
138
+ return (
139
+ <div className={className}>
140
+ {/* TODO: Implement ${name} */}
141
+ </div>
142
+ );
143
+ }
144
+ `;
145
+
146
+ await fs.writeFile(componentFile, template, "utf-8");
147
+
148
+ // Update index.ts (create or append export)
149
+ const exportLine = `export { ${name} } from "./${segment}/${kebabName}.js";\n`;
150
+ const simpleExportLine = `export { ${name} } from "./${kebabName}.js";\n`;
151
+
152
+ try {
153
+ let indexContent = "";
154
+ try {
155
+ indexContent = await fs.readFile(indexPath, "utf-8");
156
+ } catch {
157
+ // index.ts doesn't exist yet
158
+ }
159
+
160
+ const exportToAdd = layer === "shared" || layer === "pages" ? simpleExportLine : exportLine;
161
+
162
+ if (!indexContent.includes(`{ ${name} }`)) {
163
+ await fs.writeFile(indexPath, indexContent + exportToAdd, "utf-8");
164
+ }
165
+ } catch {
166
+ // index update failed - not critical
167
+ }
168
+
169
+ return {
170
+ success: true,
171
+ component: name,
172
+ layer,
173
+ slice: slice || null,
174
+ segment,
175
+ createdFiles: [relativePath],
176
+ updatedFiles: [path.relative(projectRoot, indexPath).replace(/\\/g, "/")],
177
+ message: `Created ${name} in ${layer}${slice ? `/${slice}` : ""}/${segment}`,
178
+ nextSteps: [
179
+ `Edit ${relativePath} to implement the component`,
180
+ `Import with: import { ${name} } from "@/client/${layer}${slice ? `/${slice}` : ""}"`,
181
+ ],
182
+ };
183
+ },
184
+ };
185
+ }
@@ -24,6 +24,7 @@ export { seoTools, seoToolDefinitions } from "./seo.js";
24
24
  export { projectTools, projectToolDefinitions } from "./project.js";
25
25
  export { ateTools, ateToolDefinitions } from "./ate.js";
26
26
  export { resourceTools, resourceToolDefinitions } from "./resource.js";
27
+ export { componentTools, componentToolDefinitions } from "./component.js";
27
28
 
28
29
  // 도구 모듈 import (등록용)
29
30
  import { specTools, specToolDefinitions } from "./spec.js";
@@ -40,6 +41,7 @@ import { seoTools, seoToolDefinitions } from "./seo.js";
40
41
  import { projectTools, projectToolDefinitions } from "./project.js";
41
42
  import { ateTools, ateToolDefinitions } from "./ate.js";
42
43
  import { resourceTools, resourceToolDefinitions } from "./resource.js";
44
+ import { componentTools, componentToolDefinitions } from "./component.js";
43
45
 
44
46
  /**
45
47
  * 도구 모듈 정보
@@ -73,6 +75,7 @@ const TOOL_MODULES: ToolModule[] = [
73
75
  { category: "project", definitions: projectToolDefinitions, handlers: projectTools as ToolModule["handlers"], requiresServer: true },
74
76
  { category: "ate", definitions: ateToolDefinitions as any, handlers: ateTools as any },
75
77
  { category: "resource", definitions: resourceToolDefinitions, handlers: resourceTools },
78
+ { category: "component", definitions: componentToolDefinitions, handlers: componentTools },
76
79
  ];
77
80
 
78
81
  /**