@mandujs/mcp 0.29.0 → 0.31.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/mcp",
3
- "version": "0.29.0",
3
+ "version": "0.31.0",
4
4
  "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -34,8 +34,8 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@mandujs/core": "^0.42.0",
38
- "@mandujs/ate": "^0.25.0",
37
+ "@mandujs/core": "^0.43.0",
38
+ "@mandujs/ate": "^0.25.1",
39
39
  "@mandujs/skills": "^0.18.0",
40
40
  "@modelcontextprotocol/sdk": "^1.25.3"
41
41
  },
@@ -1,218 +1,218 @@
1
- /**
2
- * Mandu MCP Skills - File-based Skill Loader
3
- * Agent Skills 패턴으로 구성된 스킬을 파일 시스템에서 로드
4
- */
5
-
6
- import { readdir, readFile } from "fs/promises";
7
- import { join, dirname } from "path";
8
- import { fileURLToPath } from "url";
9
-
10
- const __filename = fileURLToPath(import.meta.url);
11
- const __dirname = dirname(__filename);
12
-
13
- export interface SkillMeta {
14
- id: string;
15
- name: string;
16
- description: string;
17
- version: string;
18
- author: string;
19
- }
20
-
21
- export interface RuleMeta {
22
- id: string;
23
- title: string;
24
- impact: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
25
- impactDescription: string;
26
- tags: string[];
27
- }
28
-
29
- // Available skills
30
- const SKILL_IDS = [
31
- "mandu-slot",
32
- "mandu-fs-routes",
33
- "mandu-hydration",
34
- "mandu-guard",
35
- "mandu-performance",
36
- "mandu-composition",
37
- "mandu-security",
38
- "mandu-testing",
39
- "mandu-deployment",
40
- "mandu-styling",
41
- "mandu-ui",
42
- ];
43
-
44
- /**
45
- * Parse YAML frontmatter from markdown content
46
- */
47
- function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
48
- const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
49
- if (!match) {
50
- return { frontmatter: {}, body: content };
51
- }
52
-
53
- const [, yamlStr, body] = match;
54
- const frontmatter: Record<string, unknown> = {};
55
-
56
- // Simple YAML parsing (key: value pairs)
57
- for (const line of yamlStr.split("\n")) {
58
- const colonIndex = line.indexOf(":");
59
- if (colonIndex > 0) {
60
- const key = line.slice(0, colonIndex).trim();
61
- let value: unknown = line.slice(colonIndex + 1).trim();
62
-
63
- // Handle multiline values (description with |)
64
- if (value === "|") {
65
- continue; // Will be captured in subsequent lines
66
- }
67
-
68
- // Parse arrays (tags)
69
- if (typeof value === "string" && value.includes(",")) {
70
- value = value.split(",").map((s) => s.trim());
71
- }
72
-
73
- frontmatter[key] = value;
74
- }
75
- }
76
-
77
- return { frontmatter, body };
78
- }
79
-
80
- /**
81
- * List all available skills
82
- */
83
- export function listSkills(): SkillMeta[] {
84
- return SKILL_IDS.map((id) => {
85
- const name = id.replace("mandu-", "");
86
- return {
87
- id,
88
- name,
89
- description: getSkillDescription(id),
90
- version: "1.0.0",
91
- author: "mandu",
92
- };
93
- });
94
- }
95
-
96
- function getSkillDescription(id: string): string {
97
- const descriptions: Record<string, string> = {
98
- "mandu-slot": "Business logic with Mandu.filling() API",
99
- "mandu-fs-routes": "File-system based routing patterns",
100
- "mandu-hydration": "Island hydration and client components",
101
- "mandu-guard": "Architecture enforcement and layer dependencies",
102
- "mandu-performance": "Performance optimization patterns for Mandu apps",
103
- "mandu-composition": "React composition patterns for Islands and state",
104
- "mandu-security": "Security best practices for authentication and protection",
105
- "mandu-testing": "Testing patterns with Bun test and Playwright",
106
- "mandu-deployment": "Production deployment with Render, Supabase, Docker, and CI/CD",
107
- "mandu-styling": "CSS framework integration with Tailwind, Panda CSS, and theming",
108
- "mandu-ui": "UI component library integration with shadcn/ui and accessibility",
109
- };
110
- return descriptions[id] || "";
111
- }
112
-
113
- /**
114
- * Get a skill's SKILL.md content
115
- */
116
- export async function getSkill(skillId: string): Promise<{ meta: SkillMeta; content: string } | null> {
117
- if (!SKILL_IDS.includes(skillId)) {
118
- return null;
119
- }
120
-
121
- const skillPath = join(__dirname, skillId, "SKILL.md");
122
-
123
- try {
124
- const content = await readFile(skillPath, "utf-8");
125
- const { frontmatter, body } = parseFrontmatter(content);
126
-
127
- return {
128
- meta: {
129
- id: skillId,
130
- name: (frontmatter.name as string) || skillId,
131
- description: (frontmatter.description as string) || "",
132
- version: ((frontmatter.metadata as Record<string, string>)?.version as string) || "1.0.0",
133
- author: ((frontmatter.metadata as Record<string, string>)?.author as string) || "mandu",
134
- },
135
- content: body,
136
- };
137
- } catch {
138
- return null;
139
- }
140
- }
141
-
142
- /**
143
- * List rules for a skill
144
- */
145
- export async function listSkillRules(skillId: string): Promise<RuleMeta[]> {
146
- if (!SKILL_IDS.includes(skillId)) {
147
- return [];
148
- }
149
-
150
- const rulesPath = join(__dirname, skillId, "rules");
151
-
152
- try {
153
- const files = await readdir(rulesPath);
154
- const rules: RuleMeta[] = [];
155
-
156
- for (const file of files) {
157
- if (!file.endsWith(".md")) continue;
158
-
159
- const ruleId = file.replace(".md", "");
160
- const content = await readFile(join(rulesPath, file), "utf-8");
161
- const { frontmatter } = parseFrontmatter(content);
162
-
163
- rules.push({
164
- id: ruleId,
165
- title: (frontmatter.title as string) || ruleId,
166
- impact: (frontmatter.impact as RuleMeta["impact"]) || "MEDIUM",
167
- impactDescription: (frontmatter.impactDescription as string) || "",
168
- tags: Array.isArray(frontmatter.tags)
169
- ? (frontmatter.tags as string[])
170
- : typeof frontmatter.tags === "string"
171
- ? frontmatter.tags.split(",").map((s: string) => s.trim())
172
- : [],
173
- });
174
- }
175
-
176
- // Sort by impact priority
177
- const impactOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
178
- return rules.sort((a, b) => impactOrder[a.impact] - impactOrder[b.impact]);
179
- } catch {
180
- return [];
181
- }
182
- }
183
-
184
- /**
185
- * Get a specific rule's content
186
- */
187
- export async function getSkillRule(
188
- skillId: string,
189
- ruleId: string
190
- ): Promise<{ meta: RuleMeta; content: string } | null> {
191
- if (!SKILL_IDS.includes(skillId)) {
192
- return null;
193
- }
194
-
195
- const rulePath = join(__dirname, skillId, "rules", `${ruleId}.md`);
196
-
197
- try {
198
- const content = await readFile(rulePath, "utf-8");
199
- const { frontmatter, body } = parseFrontmatter(content);
200
-
201
- return {
202
- meta: {
203
- id: ruleId,
204
- title: (frontmatter.title as string) || ruleId,
205
- impact: (frontmatter.impact as RuleMeta["impact"]) || "MEDIUM",
206
- impactDescription: (frontmatter.impactDescription as string) || "",
207
- tags: Array.isArray(frontmatter.tags)
208
- ? (frontmatter.tags as string[])
209
- : typeof frontmatter.tags === "string"
210
- ? frontmatter.tags.split(",").map((s: string) => s.trim())
211
- : [],
212
- },
213
- content: body,
214
- };
215
- } catch {
216
- return null;
217
- }
218
- }
1
+ /**
2
+ * Mandu MCP Skills - File-based Skill Loader
3
+ * Agent Skills 패턴으로 구성된 스킬을 파일 시스템에서 로드
4
+ */
5
+
6
+ import { readdir, readFile } from "fs/promises";
7
+ import { join, dirname } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ export interface SkillMeta {
14
+ id: string;
15
+ name: string;
16
+ description: string;
17
+ version: string;
18
+ author: string;
19
+ }
20
+
21
+ export interface RuleMeta {
22
+ id: string;
23
+ title: string;
24
+ impact: "CRITICAL" | "HIGH" | "MEDIUM" | "LOW";
25
+ impactDescription: string;
26
+ tags: string[];
27
+ }
28
+
29
+ // Available skills
30
+ const SKILL_IDS = [
31
+ "mandu-slot",
32
+ "mandu-fs-routes",
33
+ "mandu-hydration",
34
+ "mandu-guard",
35
+ "mandu-performance",
36
+ "mandu-composition",
37
+ "mandu-security",
38
+ "mandu-testing",
39
+ "mandu-deployment",
40
+ "mandu-styling",
41
+ "mandu-ui",
42
+ ];
43
+
44
+ /**
45
+ * Parse YAML frontmatter from markdown content
46
+ */
47
+ function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
48
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
49
+ if (!match) {
50
+ return { frontmatter: {}, body: content };
51
+ }
52
+
53
+ const [, yamlStr, body] = match;
54
+ const frontmatter: Record<string, unknown> = {};
55
+
56
+ // Simple YAML parsing (key: value pairs)
57
+ for (const line of yamlStr.split("\n")) {
58
+ const colonIndex = line.indexOf(":");
59
+ if (colonIndex > 0) {
60
+ const key = line.slice(0, colonIndex).trim();
61
+ let value: unknown = line.slice(colonIndex + 1).trim();
62
+
63
+ // Handle multiline values (description with |)
64
+ if (value === "|") {
65
+ continue; // Will be captured in subsequent lines
66
+ }
67
+
68
+ // Parse arrays (tags)
69
+ if (typeof value === "string" && value.includes(",")) {
70
+ value = value.split(",").map((s) => s.trim());
71
+ }
72
+
73
+ frontmatter[key] = value;
74
+ }
75
+ }
76
+
77
+ return { frontmatter, body };
78
+ }
79
+
80
+ /**
81
+ * List all available skills
82
+ */
83
+ export function listSkills(): SkillMeta[] {
84
+ return SKILL_IDS.map((id) => {
85
+ const name = id.replace("mandu-", "");
86
+ return {
87
+ id,
88
+ name,
89
+ description: getSkillDescription(id),
90
+ version: "1.0.0",
91
+ author: "mandu",
92
+ };
93
+ });
94
+ }
95
+
96
+ function getSkillDescription(id: string): string {
97
+ const descriptions: Record<string, string> = {
98
+ "mandu-slot": "Business logic with Mandu.filling() API",
99
+ "mandu-fs-routes": "File-system based routing patterns",
100
+ "mandu-hydration": "Island hydration and client components",
101
+ "mandu-guard": "Architecture enforcement and layer dependencies",
102
+ "mandu-performance": "Performance optimization patterns for Mandu apps",
103
+ "mandu-composition": "React composition patterns for Islands and state",
104
+ "mandu-security": "Security best practices for authentication and protection",
105
+ "mandu-testing": "Testing patterns with Bun test and Playwright",
106
+ "mandu-deployment": "Production deployment with Render, Supabase, Docker, and CI/CD",
107
+ "mandu-styling": "CSS framework integration with Tailwind, Panda CSS, and theming",
108
+ "mandu-ui": "UI component library integration with shadcn/ui and accessibility",
109
+ };
110
+ return descriptions[id] || "";
111
+ }
112
+
113
+ /**
114
+ * Get a skill's SKILL.md content
115
+ */
116
+ export async function getSkill(skillId: string): Promise<{ meta: SkillMeta; content: string } | null> {
117
+ if (!SKILL_IDS.includes(skillId)) {
118
+ return null;
119
+ }
120
+
121
+ const skillPath = join(__dirname, skillId, "SKILL.md");
122
+
123
+ try {
124
+ const content = await readFile(skillPath, "utf-8");
125
+ const { frontmatter, body } = parseFrontmatter(content);
126
+
127
+ return {
128
+ meta: {
129
+ id: skillId,
130
+ name: (frontmatter.name as string) || skillId,
131
+ description: (frontmatter.description as string) || "",
132
+ version: ((frontmatter.metadata as Record<string, string>)?.version as string) || "1.0.0",
133
+ author: ((frontmatter.metadata as Record<string, string>)?.author as string) || "mandu",
134
+ },
135
+ content: body,
136
+ };
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * List rules for a skill
144
+ */
145
+ export async function listSkillRules(skillId: string): Promise<RuleMeta[]> {
146
+ if (!SKILL_IDS.includes(skillId)) {
147
+ return [];
148
+ }
149
+
150
+ const rulesPath = join(__dirname, skillId, "rules");
151
+
152
+ try {
153
+ const files = await readdir(rulesPath);
154
+ const rules: RuleMeta[] = [];
155
+
156
+ for (const file of files) {
157
+ if (!file.endsWith(".md")) continue;
158
+
159
+ const ruleId = file.replace(".md", "");
160
+ const content = await readFile(join(rulesPath, file), "utf-8");
161
+ const { frontmatter } = parseFrontmatter(content);
162
+
163
+ rules.push({
164
+ id: ruleId,
165
+ title: (frontmatter.title as string) || ruleId,
166
+ impact: (frontmatter.impact as RuleMeta["impact"]) || "MEDIUM",
167
+ impactDescription: (frontmatter.impactDescription as string) || "",
168
+ tags: Array.isArray(frontmatter.tags)
169
+ ? (frontmatter.tags as string[])
170
+ : typeof frontmatter.tags === "string"
171
+ ? frontmatter.tags.split(",").map((s: string) => s.trim())
172
+ : [],
173
+ });
174
+ }
175
+
176
+ // Sort by impact priority
177
+ const impactOrder = { CRITICAL: 0, HIGH: 1, MEDIUM: 2, LOW: 3 };
178
+ return rules.sort((a, b) => impactOrder[a.impact] - impactOrder[b.impact]);
179
+ } catch {
180
+ return [];
181
+ }
182
+ }
183
+
184
+ /**
185
+ * Get a specific rule's content
186
+ */
187
+ export async function getSkillRule(
188
+ skillId: string,
189
+ ruleId: string
190
+ ): Promise<{ meta: RuleMeta; content: string } | null> {
191
+ if (!SKILL_IDS.includes(skillId)) {
192
+ return null;
193
+ }
194
+
195
+ const rulePath = join(__dirname, skillId, "rules", `${ruleId}.md`);
196
+
197
+ try {
198
+ const content = await readFile(rulePath, "utf-8");
199
+ const { frontmatter, body } = parseFrontmatter(content);
200
+
201
+ return {
202
+ meta: {
203
+ id: ruleId,
204
+ title: (frontmatter.title as string) || ruleId,
205
+ impact: (frontmatter.impact as RuleMeta["impact"]) || "MEDIUM",
206
+ impactDescription: (frontmatter.impactDescription as string) || "",
207
+ tags: Array.isArray(frontmatter.tags)
208
+ ? (frontmatter.tags as string[])
209
+ : typeof frontmatter.tags === "string"
210
+ ? frontmatter.tags.split(",").map((s: string) => s.trim())
211
+ : [],
212
+ },
213
+ content: body,
214
+ };
215
+ } catch {
216
+ return null;
217
+ }
218
+ }