@mandujs/mcp 0.18.4 → 0.18.6

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.6",
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.14",
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
+ }
@@ -280,23 +280,31 @@ export const guardToolDefinitions: Tool[] = [
280
280
  "Negotiate with the framework before implementing a feature. " +
281
281
  "Describes your intent and gets back the recommended project structure, " +
282
282
  "file templates, and related architecture decisions. " +
283
- "Use this BEFORE writing code to ensure architectural consistency.",
283
+ "Use this BEFORE writing code to ensure architectural consistency. " +
284
+ "IMPORTANT: Always provide 'featureName' as a short English slug (e.g., 'chat', 'user-auth', 'payment'). " +
285
+ "Even if the user speaks Korean, YOU must translate the feature name to English.",
284
286
  inputSchema: {
285
287
  type: "object",
286
288
  properties: {
287
289
  intent: {
288
290
  type: "string",
289
- description: "What you want to implement (e.g., '사용자 인증 기능 추가', 'Add payment integration')",
291
+ description: "What you want to implement, in any language (e.g., '사용자 인증 기능 추가', 'Add payment integration')",
292
+ },
293
+ featureName: {
294
+ type: "string",
295
+ description: "REQUIRED: Short English slug for the feature name (e.g., 'chat', 'user-auth', 'payment', 'file-upload'). " +
296
+ "You MUST translate the user's intent to a concise English identifier. " +
297
+ "Use lowercase kebab-case. This becomes the directory/module name.",
290
298
  },
291
299
  requirements: {
292
300
  type: "array",
293
301
  items: { type: "string" },
294
- description: "Specific requirements (e.g., ['JWT 기반', 'OAuth 지원'])",
302
+ description: "Specific requirements (e.g., ['JWT-based', 'OAuth support'])",
295
303
  },
296
304
  constraints: {
297
305
  type: "array",
298
306
  items: { type: "string" },
299
- description: "Constraints to respect (e.g., ['기존 User 모델 활용', 'Redis 세션'])",
307
+ description: "Constraints to respect (e.g., ['use existing User model', 'Redis sessions'])",
300
308
  },
301
309
  category: {
302
310
  type: "string",
@@ -976,8 +984,9 @@ Mandu.filling()
976
984
  // ═══════════════════════════════════════════════════════════════════════════
977
985
 
978
986
  mandu_negotiate: async (args: Record<string, unknown>) => {
979
- const { intent, requirements, constraints, category, preset } = args as {
987
+ const { intent, featureName, requirements, constraints, category, preset } = args as {
980
988
  intent: string;
989
+ featureName?: string;
981
990
  requirements?: string[];
982
991
  constraints?: string[];
983
992
  category?: FeatureCategory;
@@ -993,6 +1002,7 @@ Mandu.filling()
993
1002
 
994
1003
  const request: NegotiationRequest = {
995
1004
  intent,
1005
+ featureName,
996
1006
  requirements,
997
1007
  constraints,
998
1008
  category,
@@ -1041,8 +1051,9 @@ Mandu.filling()
1041
1051
  },
1042
1052
 
1043
1053
  mandu_generate_scaffold: async (args: Record<string, unknown>) => {
1044
- const { intent, category, dryRun = false, overwrite = false, preset } = args as {
1054
+ const { intent, featureName, category, dryRun = false, overwrite = false, preset } = args as {
1045
1055
  intent: string;
1056
+ featureName?: string;
1046
1057
  category?: FeatureCategory;
1047
1058
  dryRun?: boolean;
1048
1059
  overwrite?: boolean;
@@ -1057,7 +1068,7 @@ Mandu.filling()
1057
1068
  }
1058
1069
 
1059
1070
  // 먼저 협상하여 구조 계획 얻기
1060
- const plan = await negotiate({ intent, category, preset }, projectRoot);
1071
+ const plan = await negotiate({ intent, featureName, category, preset }, projectRoot);
1061
1072
 
1062
1073
  if (!plan.approved) {
1063
1074
  return {
@@ -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
  /**