@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 +2 -2
- package/src/tools/component.ts +185 -0
- package/src/tools/index.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.18.
|
|
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.
|
|
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
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -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
|
/**
|