@mandujs/mcp 0.30.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 +2 -2
- package/src/resources/skills/loader.ts +218 -218
- package/src/resources/skills/mandu-deployment/rules/db-provider-supabase.md +300 -0
- package/src/server.ts +2 -1
- package/src/tools/ai-brief.ts +443 -443
- package/src/tools/decisions.ts +270 -270
- package/src/tools/docs.ts +349 -0
- package/src/tools/extract-contract.ts +406 -406
- package/src/tools/guard.ts +56 -3
- package/src/tools/index.ts +4 -0
- package/src/tools/migrate-route-conventions.ts +345 -345
- package/src/tools/resource.ts +2 -1
- package/src/tools/rewrite-generated-barrel.ts +403 -403
- package/src/resources/skills/mandu-deployment/rules/deploy-platform-supabase.md +0 -323
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "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,7 +34,7 @@
|
|
|
34
34
|
"access": "public"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@mandujs/core": "^0.
|
|
37
|
+
"@mandujs/core": "^0.43.0",
|
|
38
38
|
"@mandujs/ate": "^0.25.1",
|
|
39
39
|
"@mandujs/skills": "^0.18.0",
|
|
40
40
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
@@ -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
|
+
}
|