@vizejs/musea-mcp-server 0.381.0 → 0.384.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/dist/cli.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{src-C9whMuv-.mjs → src-CJlr_RgD.mjs} +1048 -1046
- package/package.json +2 -2
|
@@ -19,1096 +19,1098 @@ function loadNative() {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
//#endregion
|
|
22
|
-
//#region src/
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
22
|
+
//#region src/markdown-template.ts
|
|
23
|
+
const SELF_TAG_NAME = "Self";
|
|
24
|
+
const TEMPLATE_FENCE_LANGUAGES = new Set([
|
|
25
|
+
"",
|
|
26
|
+
"html",
|
|
27
|
+
"template",
|
|
28
|
+
"vue"
|
|
29
|
+
]);
|
|
30
|
+
function formatGeneratedMarkdown(markdown, componentName) {
|
|
31
|
+
return markdown.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
32
|
+
const normalizedLang = lang.toLowerCase();
|
|
33
|
+
return formatCodeFence(lang, TEMPLATE_FENCE_LANGUAGES.has(normalizedLang) ? rewriteSelfComponentTags(code, componentName) : code);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function formatCodeFence(lang, code) {
|
|
37
|
+
const lines = code.split("\n");
|
|
38
|
+
let minIndent = Infinity;
|
|
39
|
+
for (const line of lines) if (line.trim()) {
|
|
40
|
+
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
41
|
+
minIndent = Math.min(minIndent, indent);
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
return
|
|
43
|
+
if (minIndent === Infinity) minIndent = 0;
|
|
44
|
+
return `\`\`\`${lang}\n${(minIndent > 0 ? lines.map((line) => line.slice(minIndent)) : lines).join("\n")}\`\`\``;
|
|
47
45
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
46
|
+
function rewriteSelfComponentTags(code, componentName) {
|
|
47
|
+
const replacements = [];
|
|
48
|
+
try {
|
|
49
|
+
collectSelfTagReplacements(baseParse(code, { comments: true }).children, replacements, componentName);
|
|
50
|
+
} catch {
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
if (replacements.length === 0) return code;
|
|
54
|
+
let rewritten = code;
|
|
55
|
+
for (const replacement of replacements.sort((left, right) => right.start - left.start)) rewritten = rewritten.slice(0, replacement.start) + replacement.text + rewritten.slice(replacement.end);
|
|
56
|
+
return rewritten;
|
|
50
57
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
58
|
+
function collectSelfTagReplacements(nodes, replacements, componentName) {
|
|
59
|
+
for (const node of nodes) {
|
|
60
|
+
if (node.type !== NodeTypes.ELEMENT) continue;
|
|
61
|
+
if (node.tag === SELF_TAG_NAME) addSelfElementReplacements(node, replacements, componentName);
|
|
62
|
+
collectSelfTagReplacements(node.children, replacements, componentName);
|
|
63
|
+
}
|
|
53
64
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
65
|
+
function addSelfElementReplacements(node, replacements, componentName) {
|
|
66
|
+
const openStart = node.loc.start.offset + 1;
|
|
67
|
+
const openEnd = openStart + 4;
|
|
68
|
+
replacements.push({
|
|
69
|
+
start: openStart,
|
|
70
|
+
end: openEnd,
|
|
71
|
+
text: componentName
|
|
72
|
+
});
|
|
73
|
+
if (node.isSelfClosing) return;
|
|
74
|
+
const closeTagIndex = node.loc.source.lastIndexOf(`</${SELF_TAG_NAME}>`);
|
|
75
|
+
if (closeTagIndex < 0) return;
|
|
76
|
+
const closeStart = node.loc.start.offset + closeTagIndex + 2;
|
|
77
|
+
replacements.push({
|
|
78
|
+
start: closeStart,
|
|
79
|
+
end: closeStart + 4,
|
|
80
|
+
text: componentName
|
|
81
|
+
});
|
|
58
82
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region src/palette-typescript.ts
|
|
85
|
+
function buildPaletteTypescript(title, controls) {
|
|
86
|
+
const members = controls.map((control) => ts.factory.createPropertySignature(void 0, propertyNameNode(control.name), control.required ? void 0 : ts.factory.createToken(ts.SyntaxKind.QuestionToken), controlTypeNode(control)));
|
|
87
|
+
const declaration = ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], interfaceNameFromTitle(title), void 0, void 0, members);
|
|
88
|
+
const sourceFile = ts.createSourceFile("palette.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
89
|
+
return `${ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printNode(ts.EmitHint.Unspecified, declaration, sourceFile)}\n`;
|
|
90
|
+
}
|
|
91
|
+
function interfaceNameFromTitle(title) {
|
|
92
|
+
const candidate = `${title.replace(/\s+/g, "") || "Component"}Props`.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
93
|
+
return /^[A-Za-z_$]/.test(candidate) ? candidate : `_${candidate}`;
|
|
94
|
+
}
|
|
95
|
+
function propertyNameNode(name) {
|
|
96
|
+
return /^[$A-Z_a-z][$\w]*$/.test(name) ? ts.factory.createIdentifier(name) : ts.factory.createStringLiteral(name);
|
|
97
|
+
}
|
|
98
|
+
function controlTypeNode(control) {
|
|
99
|
+
if (control.control === "boolean") return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
100
|
+
if (control.control === "number") return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
101
|
+
if (control.control === "select" && control.options.length > 0) return ts.factory.createUnionTypeNode(control.options.map((option) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(String(option.value)))));
|
|
102
|
+
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
103
|
+
}
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/musea.ts
|
|
106
|
+
function normalize(value) {
|
|
107
|
+
return value?.trim().toLowerCase() ?? "";
|
|
108
|
+
}
|
|
109
|
+
function normalizePathLike(value) {
|
|
110
|
+
return value.replace(/\\/g, "/").toLowerCase();
|
|
111
|
+
}
|
|
112
|
+
function tokenize(query) {
|
|
113
|
+
return Array.from(new Set(query.toLowerCase().split(/[\s/,_-]+/).map((term) => term.trim()).filter(Boolean)));
|
|
114
|
+
}
|
|
115
|
+
function toProjectPath(projectRoot, absolutePath) {
|
|
116
|
+
const root = realpathNearest(projectRoot);
|
|
117
|
+
const resolved = realpathNearest(absolutePath);
|
|
118
|
+
const relativePath = path.relative(root, resolved);
|
|
119
|
+
return isProjectPath(root, resolved) ? relativePath || "." : resolved;
|
|
120
|
+
}
|
|
121
|
+
function realpathNearest(targetPath) {
|
|
122
|
+
let current = path.resolve(targetPath);
|
|
123
|
+
const missingParts = [];
|
|
124
|
+
while (true) try {
|
|
125
|
+
const real = fs.realpathSync.native(current);
|
|
126
|
+
return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
|
|
127
|
+
} catch {
|
|
128
|
+
const parent = path.dirname(current);
|
|
129
|
+
if (parent === current) return path.resolve(targetPath);
|
|
130
|
+
missingParts.push(path.basename(current));
|
|
131
|
+
current = parent;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isProjectPath(projectRoot, candidatePath) {
|
|
135
|
+
const root = realpathNearest(projectRoot);
|
|
136
|
+
const candidate = realpathNearest(candidatePath);
|
|
137
|
+
const relativePath = path.relative(root, candidate);
|
|
138
|
+
return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
139
|
+
}
|
|
140
|
+
function resolveProjectPath(projectRoot, inputPath, label = "path") {
|
|
141
|
+
if (inputPath.includes("\0")) throw new McpError(ErrorCode.InvalidParams, `${label} contains an invalid character`);
|
|
142
|
+
const root = path.resolve(projectRoot);
|
|
143
|
+
const resolvedPath = path.isAbsolute(inputPath) ? path.resolve(inputPath) : path.resolve(root, inputPath);
|
|
144
|
+
if (!isProjectPath(root, resolvedPath)) throw new McpError(ErrorCode.InvalidParams, `${label} must stay inside the project root`);
|
|
145
|
+
return resolvedPath;
|
|
146
|
+
}
|
|
147
|
+
function buildResourceUris$1(relativePath, variantNames, hasComponentSource) {
|
|
148
|
+
const encodedPath = encodeURIComponent(relativePath);
|
|
149
|
+
return {
|
|
150
|
+
component: `musea://component/${encodedPath}`,
|
|
151
|
+
docs: `musea://docs/${encodedPath}`,
|
|
152
|
+
source: `musea://source/${encodedPath}`,
|
|
153
|
+
componentSource: hasComponentSource ? `musea://component-source/${encodedPath}` : void 0,
|
|
154
|
+
variants: variantNames.map((variantName) => ({
|
|
155
|
+
name: variantName,
|
|
156
|
+
uri: `musea://variant/${encodedPath}/${encodeURIComponent(variantName)}`
|
|
157
|
+
}))
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function addScore(reasons, reason, amount, scoreRef) {
|
|
161
|
+
reasons.add(reason);
|
|
162
|
+
scoreRef.value += amount;
|
|
163
|
+
}
|
|
164
|
+
function getComponentCandidates(info) {
|
|
165
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
166
|
+
if (info.component) {
|
|
167
|
+
candidates.add(info.component);
|
|
168
|
+
candidates.add(path.basename(info.component));
|
|
169
|
+
candidates.add(path.basename(info.component, path.extname(info.component)));
|
|
170
|
+
}
|
|
171
|
+
return Array.from(candidates);
|
|
172
|
+
}
|
|
173
|
+
function scoreArtInfo(projectRoot, info, query) {
|
|
174
|
+
const normalizedQuery = normalize(query);
|
|
175
|
+
if (!normalizedQuery) return null;
|
|
176
|
+
const relativePath = toProjectPath(projectRoot, info.path);
|
|
177
|
+
const relativePathNorm = normalizePathLike(relativePath);
|
|
178
|
+
const titleNorm = normalize(info.title);
|
|
179
|
+
const descriptionNorm = normalize(info.description);
|
|
180
|
+
const categoryNorm = normalize(info.category);
|
|
181
|
+
const tagNorms = info.tags.map((tag) => normalize(tag));
|
|
182
|
+
const variantNorms = info.variantNames.map((variantName) => normalize(variantName));
|
|
183
|
+
const componentNorms = getComponentCandidates(info).map((component) => normalizePathLike(component));
|
|
184
|
+
const reasons = /* @__PURE__ */ new Set();
|
|
185
|
+
const scoreRef = { value: 0 };
|
|
186
|
+
if (relativePathNorm === normalizePathLike(query)) addScore(reasons, "exact path match", 220, scoreRef);
|
|
187
|
+
else if (relativePathNorm.includes(normalizePathLike(query))) addScore(reasons, "path match", 70, scoreRef);
|
|
188
|
+
if (titleNorm === normalizedQuery) addScore(reasons, "exact title match", 200, scoreRef);
|
|
189
|
+
else if (titleNorm.startsWith(normalizedQuery)) addScore(reasons, "title prefix match", 140, scoreRef);
|
|
190
|
+
else if (titleNorm.includes(normalizedQuery)) addScore(reasons, "title match", 110, scoreRef);
|
|
191
|
+
if (categoryNorm === normalizedQuery) addScore(reasons, "exact category match", 120, scoreRef);
|
|
192
|
+
else if (categoryNorm.includes(normalizedQuery)) addScore(reasons, "category match", 55, scoreRef);
|
|
193
|
+
if (descriptionNorm.includes(normalizedQuery)) addScore(reasons, "description match", 60, scoreRef);
|
|
194
|
+
for (const tag of tagNorms) {
|
|
195
|
+
if (tag === normalizedQuery) {
|
|
196
|
+
addScore(reasons, "exact tag match", 130, scoreRef);
|
|
197
|
+
break;
|
|
69
198
|
}
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
continue;
|
|
199
|
+
if (tag.includes(normalizedQuery)) {
|
|
200
|
+
addScore(reasons, "tag match", 80, scoreRef);
|
|
201
|
+
break;
|
|
74
202
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
203
|
+
}
|
|
204
|
+
for (const variant of variantNorms) {
|
|
205
|
+
if (variant === normalizedQuery) {
|
|
206
|
+
addScore(reasons, "exact variant match", 130, scoreRef);
|
|
207
|
+
break;
|
|
79
208
|
}
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
continue;
|
|
209
|
+
if (variant.includes(normalizedQuery)) {
|
|
210
|
+
addScore(reasons, "variant match", 90, scoreRef);
|
|
211
|
+
break;
|
|
84
212
|
}
|
|
85
|
-
source += escapeRegExp(char);
|
|
86
|
-
index += 1;
|
|
87
213
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region src/tools/definitions.ts
|
|
95
|
-
/**
|
|
96
|
-
* MCP tool definitions for Musea.
|
|
97
|
-
*
|
|
98
|
-
* Declares the schema (name, description, input parameters) for each tool
|
|
99
|
-
* exposed by the MCP server: component analysis, registry, code generation,
|
|
100
|
-
* documentation, and design tokens.
|
|
101
|
-
*/
|
|
102
|
-
const toolDefinitions = [
|
|
103
|
-
{
|
|
104
|
-
name: "analyze_component",
|
|
105
|
-
description: "Statically analyze a Vue SFC to extract its props and emits. Accepts a Vue component path directly, or an art-file reference that resolves to the linked component source.",
|
|
106
|
-
inputSchema: {
|
|
107
|
-
type: "object",
|
|
108
|
-
properties: {
|
|
109
|
-
path: {
|
|
110
|
-
type: "string",
|
|
111
|
-
description: "Path to the .vue component file or .art.vue file (relative to project root)"
|
|
112
|
-
},
|
|
113
|
-
title: {
|
|
114
|
-
type: "string",
|
|
115
|
-
description: "Resolve an art file by its display title, then analyze its component source"
|
|
116
|
-
},
|
|
117
|
-
component: {
|
|
118
|
-
type: "string",
|
|
119
|
-
description: "Resolve an art file by its component reference or component basename, then analyze it"
|
|
120
|
-
},
|
|
121
|
-
query: {
|
|
122
|
-
type: "string",
|
|
123
|
-
description: "Fuzzy-search an art file, then analyze the linked component source"
|
|
124
|
-
},
|
|
125
|
-
ref: {
|
|
126
|
-
type: "string",
|
|
127
|
-
description: "Generic art-file reference: path, title, component name, or search text"
|
|
128
|
-
}
|
|
129
|
-
},
|
|
130
|
-
required: []
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: "get_palette",
|
|
135
|
-
description: "Derive an interactive props palette (control types, defaults, ranges, options) for a component described by an Art file. Falls back to SFC analysis when native palette inference is sparse.",
|
|
136
|
-
inputSchema: {
|
|
137
|
-
type: "object",
|
|
138
|
-
properties: {
|
|
139
|
-
path: {
|
|
140
|
-
type: "string",
|
|
141
|
-
description: "Path to the .art.vue file (relative to project root)"
|
|
142
|
-
},
|
|
143
|
-
title: {
|
|
144
|
-
type: "string",
|
|
145
|
-
description: "Resolve an art file by title instead of path"
|
|
146
|
-
},
|
|
147
|
-
component: {
|
|
148
|
-
type: "string",
|
|
149
|
-
description: "Resolve an art file by component reference or component basename"
|
|
150
|
-
},
|
|
151
|
-
query: {
|
|
152
|
-
type: "string",
|
|
153
|
-
description: "Fuzzy-search an art file before generating the palette"
|
|
154
|
-
},
|
|
155
|
-
ref: {
|
|
156
|
-
type: "string",
|
|
157
|
-
description: "Generic art-file reference: path, title, component name, or search text"
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
required: []
|
|
214
|
+
for (const component of componentNorms) {
|
|
215
|
+
if (component === normalizePathLike(query)) {
|
|
216
|
+
addScore(reasons, "exact component match", 180, scoreRef);
|
|
217
|
+
break;
|
|
161
218
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
description: "List components registered in the design system. Returns titles, categories, tags, status, variant names, and related resource URIs.",
|
|
166
|
-
inputSchema: {
|
|
167
|
-
type: "object",
|
|
168
|
-
properties: {
|
|
169
|
-
category: {
|
|
170
|
-
type: "string",
|
|
171
|
-
description: "Filter by category"
|
|
172
|
-
},
|
|
173
|
-
tag: {
|
|
174
|
-
type: "string",
|
|
175
|
-
description: "Filter by tag"
|
|
176
|
-
},
|
|
177
|
-
status: {
|
|
178
|
-
type: "string",
|
|
179
|
-
enum: [
|
|
180
|
-
"draft",
|
|
181
|
-
"ready",
|
|
182
|
-
"deprecated"
|
|
183
|
-
],
|
|
184
|
-
description: "Filter by status badge"
|
|
185
|
-
},
|
|
186
|
-
component: {
|
|
187
|
-
type: "string",
|
|
188
|
-
description: "Filter by component reference or component basename"
|
|
189
|
-
},
|
|
190
|
-
limit: {
|
|
191
|
-
type: "number",
|
|
192
|
-
description: "Maximum number of components to return (default: all)"
|
|
193
|
-
},
|
|
194
|
-
includeVariants: {
|
|
195
|
-
type: "boolean",
|
|
196
|
-
description: "Include per-variant metadata in the result (default: false)"
|
|
197
|
-
},
|
|
198
|
-
sortBy: {
|
|
199
|
-
type: "string",
|
|
200
|
-
enum: [
|
|
201
|
-
"title",
|
|
202
|
-
"category",
|
|
203
|
-
"status",
|
|
204
|
-
"variants"
|
|
205
|
-
],
|
|
206
|
-
description: "Sort order for the result list (default: title)"
|
|
207
|
-
}
|
|
208
|
-
}
|
|
219
|
+
if (component.includes(normalizePathLike(query))) {
|
|
220
|
+
addScore(reasons, "component match", 100, scoreRef);
|
|
221
|
+
break;
|
|
209
222
|
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
223
|
+
}
|
|
224
|
+
const terms = tokenize(query);
|
|
225
|
+
for (const term of terms) {
|
|
226
|
+
if (term === normalizedQuery) continue;
|
|
227
|
+
if (titleNorm.includes(term)) addScore(reasons, `title contains "${term}"`, 18, scoreRef);
|
|
228
|
+
if (descriptionNorm.includes(term)) addScore(reasons, `description contains "${term}"`, 10, scoreRef);
|
|
229
|
+
if (categoryNorm.includes(term)) addScore(reasons, `category contains "${term}"`, 10, scoreRef);
|
|
230
|
+
if (tagNorms.some((tag) => tag.includes(term))) addScore(reasons, `tag contains "${term}"`, 16, scoreRef);
|
|
231
|
+
if (variantNorms.some((variant) => variant.includes(term))) addScore(reasons, `variant contains "${term}"`, 14, scoreRef);
|
|
232
|
+
if (componentNorms.some((component) => component.includes(term))) addScore(reasons, `component contains "${term}"`, 14, scoreRef);
|
|
233
|
+
}
|
|
234
|
+
if (scoreRef.value <= 0) return null;
|
|
235
|
+
return {
|
|
236
|
+
info,
|
|
237
|
+
relativePath,
|
|
238
|
+
score: scoreRef.value,
|
|
239
|
+
reasons: Array.from(reasons).slice(0, 5)
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function compareArtResults(left, right) {
|
|
243
|
+
return right.score - left.score || (left.info.order ?? Number.MAX_SAFE_INTEGER) - (right.info.order ?? Number.MAX_SAFE_INTEGER) || left.info.title.localeCompare(right.info.title);
|
|
244
|
+
}
|
|
245
|
+
async function searchArtInfos(ctx, query, filters) {
|
|
246
|
+
const arts = Array.from((await ctx.scanArtFiles()).values());
|
|
247
|
+
const category = normalize(filters?.category);
|
|
248
|
+
const tag = normalize(filters?.tag);
|
|
249
|
+
const status = normalize(filters?.status);
|
|
250
|
+
const componentFilter = normalize(filters?.component);
|
|
251
|
+
return arts.filter((info) => {
|
|
252
|
+
if (category && normalize(info.category) !== category) return false;
|
|
253
|
+
if (tag && !info.tags.some((item) => normalize(item) === tag)) return false;
|
|
254
|
+
if (status && normalize(info.status) !== status) return false;
|
|
255
|
+
if (componentFilter && !getComponentCandidates(info).some((candidate) => normalizePathLike(candidate).includes(componentFilter))) return false;
|
|
256
|
+
return true;
|
|
257
|
+
}).map((info) => scoreArtInfo(ctx.projectRoot, info, query)).filter((result) => result != null).sort(compareArtResults).slice(0, filters?.limit ?? 10);
|
|
258
|
+
}
|
|
259
|
+
function buildAlternatives(results) {
|
|
260
|
+
return results.slice(1, 4).map((result) => ({
|
|
261
|
+
path: result.relativePath,
|
|
262
|
+
title: result.info.title,
|
|
263
|
+
component: result.info.component,
|
|
264
|
+
score: result.score,
|
|
265
|
+
reasons: result.reasons
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
async function resolveArtReference(ctx, args) {
|
|
269
|
+
const arts = Array.from((await ctx.scanArtFiles()).values());
|
|
270
|
+
const pathArg = typeof args?.path === "string" ? args.path : void 0;
|
|
271
|
+
const titleArg = typeof args?.title === "string" ? args.title : void 0;
|
|
272
|
+
const componentArg = typeof args?.component === "string" ? args.component : void 0;
|
|
273
|
+
const queryArg = typeof args?.query === "string" ? args.query : void 0;
|
|
274
|
+
const refArg = typeof args?.ref === "string" ? args.ref : void 0;
|
|
275
|
+
if (pathArg) {
|
|
276
|
+
const resolvedPath = resolveProjectPath(ctx.projectRoot, pathArg, "path");
|
|
277
|
+
const normalizedResolvedPath = normalizePathLike(resolvedPath);
|
|
278
|
+
const normalizedRelativePath = normalizePathLike(path.relative(ctx.projectRoot, resolvedPath));
|
|
279
|
+
const directMatch = arts.find((info) => {
|
|
280
|
+
const infoRelativePath = normalizePathLike(path.relative(ctx.projectRoot, info.path));
|
|
281
|
+
return normalizePathLike(info.path) === normalizedResolvedPath || infoRelativePath === normalizedRelativePath;
|
|
282
|
+
});
|
|
283
|
+
if (directMatch) return {
|
|
284
|
+
info: directMatch,
|
|
285
|
+
absolutePath: directMatch.path,
|
|
286
|
+
relativePath: toProjectPath(ctx.projectRoot, directMatch.path),
|
|
287
|
+
matchedBy: "path",
|
|
288
|
+
matchValue: pathArg,
|
|
289
|
+
score: 999,
|
|
290
|
+
reasons: ["exact path match"],
|
|
291
|
+
alternatives: []
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (titleArg) {
|
|
295
|
+
const matches = arts.filter((info) => normalize(info.title) === normalize(titleArg));
|
|
296
|
+
if (matches.length > 0) {
|
|
297
|
+
const primary = matches[0];
|
|
298
|
+
return {
|
|
299
|
+
info: primary,
|
|
300
|
+
absolutePath: primary.path,
|
|
301
|
+
relativePath: toProjectPath(ctx.projectRoot, primary.path),
|
|
302
|
+
matchedBy: "title",
|
|
303
|
+
matchValue: titleArg,
|
|
304
|
+
score: 950,
|
|
305
|
+
reasons: ["exact title match"],
|
|
306
|
+
alternatives: matches.slice(1, 4).map((info) => ({
|
|
307
|
+
path: toProjectPath(ctx.projectRoot, info.path),
|
|
308
|
+
title: info.title,
|
|
309
|
+
component: info.component,
|
|
310
|
+
score: 900,
|
|
311
|
+
reasons: ["exact title match"]
|
|
312
|
+
}))
|
|
313
|
+
};
|
|
251
314
|
}
|
|
252
|
-
}
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
},
|
|
275
|
-
ref: {
|
|
276
|
-
type: "string",
|
|
277
|
-
description: "Generic art-file reference: path, title, component name, or search text"
|
|
278
|
-
},
|
|
279
|
-
variant: {
|
|
280
|
-
type: "string",
|
|
281
|
-
description: "Variant name"
|
|
282
|
-
},
|
|
283
|
-
includeAnalysis: {
|
|
284
|
-
type: "boolean",
|
|
285
|
-
description: "Include resolved component props/emits analysis (default: false)"
|
|
286
|
-
}
|
|
287
|
-
},
|
|
288
|
-
required: ["variant"]
|
|
315
|
+
}
|
|
316
|
+
if (componentArg) {
|
|
317
|
+
const normalizedComponent = normalizePathLike(componentArg);
|
|
318
|
+
const matches = arts.filter((info) => getComponentCandidates(info).some((candidate) => normalizePathLike(candidate) === normalizedComponent));
|
|
319
|
+
if (matches.length > 0) {
|
|
320
|
+
const primary = matches[0];
|
|
321
|
+
return {
|
|
322
|
+
info: primary,
|
|
323
|
+
absolutePath: primary.path,
|
|
324
|
+
relativePath: toProjectPath(ctx.projectRoot, primary.path),
|
|
325
|
+
matchedBy: "component",
|
|
326
|
+
matchValue: componentArg,
|
|
327
|
+
score: 930,
|
|
328
|
+
reasons: ["exact component match"],
|
|
329
|
+
alternatives: matches.slice(1, 4).map((info) => ({
|
|
330
|
+
path: toProjectPath(ctx.projectRoot, info.path),
|
|
331
|
+
title: info.title,
|
|
332
|
+
component: info.component,
|
|
333
|
+
score: 880,
|
|
334
|
+
reasons: ["exact component match"]
|
|
335
|
+
}))
|
|
336
|
+
};
|
|
289
337
|
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
338
|
+
}
|
|
339
|
+
const queryValue = queryArg ?? refArg ?? pathArg ?? titleArg ?? componentArg;
|
|
340
|
+
if (!queryValue) throw new McpError(ErrorCode.InvalidParams, "Provide one of: path, title, component, query, or ref");
|
|
341
|
+
const results = await searchArtInfos(ctx, queryValue, { limit: 4 });
|
|
342
|
+
if (results.length === 0) throw new McpError(ErrorCode.InvalidParams, `No component matched "${queryValue}". Try list_components or search_components first.`);
|
|
343
|
+
const primary = results[0];
|
|
344
|
+
return {
|
|
345
|
+
info: primary.info,
|
|
346
|
+
absolutePath: primary.info.path,
|
|
347
|
+
relativePath: primary.relativePath,
|
|
348
|
+
matchedBy: queryArg ? "query" : "ref",
|
|
349
|
+
matchValue: queryValue,
|
|
350
|
+
score: primary.score,
|
|
351
|
+
reasons: primary.reasons,
|
|
352
|
+
alternatives: buildAlternatives(results)
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
function resolveComponentSourcePath(artAbsolutePath, componentReference) {
|
|
356
|
+
if (!componentReference) return null;
|
|
357
|
+
if (path.isAbsolute(componentReference)) return componentReference;
|
|
358
|
+
return path.resolve(path.dirname(artAbsolutePath), componentReference);
|
|
359
|
+
}
|
|
360
|
+
async function getComponentSourceDescriptor(ctx, resolved) {
|
|
361
|
+
const componentPath = resolveComponentSourcePath(resolved.absolutePath, resolved.info.component);
|
|
362
|
+
if (!componentPath) return {
|
|
363
|
+
reference: resolved.info.component,
|
|
364
|
+
exists: false,
|
|
365
|
+
error: "This art file does not declare a component source."
|
|
366
|
+
};
|
|
367
|
+
if (!isProjectPath(ctx.projectRoot, componentPath)) return {
|
|
368
|
+
reference: resolved.info.component,
|
|
369
|
+
absolutePath: componentPath,
|
|
370
|
+
path: componentPath,
|
|
371
|
+
exists: false,
|
|
372
|
+
error: "Component source is outside the project root."
|
|
373
|
+
};
|
|
374
|
+
try {
|
|
375
|
+
await fs.promises.access(componentPath, fs.constants.R_OK);
|
|
376
|
+
return {
|
|
377
|
+
reference: resolved.info.component,
|
|
378
|
+
absolutePath: componentPath,
|
|
379
|
+
path: toProjectPath(ctx.projectRoot, componentPath),
|
|
380
|
+
exists: true
|
|
381
|
+
};
|
|
382
|
+
} catch {
|
|
383
|
+
return {
|
|
384
|
+
reference: resolved.info.component,
|
|
385
|
+
absolutePath: componentPath,
|
|
386
|
+
path: toProjectPath(ctx.projectRoot, componentPath),
|
|
387
|
+
exists: false,
|
|
388
|
+
error: `Component source not found: ${toProjectPath(ctx.projectRoot, componentPath)}`
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function normalizeDefaultValue(value) {
|
|
393
|
+
if (value === "true") return true;
|
|
394
|
+
if (value === "false") return false;
|
|
395
|
+
if (typeof value === "string" && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
|
|
396
|
+
return value;
|
|
397
|
+
}
|
|
398
|
+
function inferControlType(type) {
|
|
399
|
+
const normalizedType = type.toLowerCase();
|
|
400
|
+
if (normalizedType === "boolean") return "boolean";
|
|
401
|
+
if (normalizedType === "number") return "number";
|
|
402
|
+
if (normalizedType.includes("|") && !normalizedType.includes("=>")) return "select";
|
|
403
|
+
return "text";
|
|
404
|
+
}
|
|
405
|
+
function extractOptionsFromType(type) {
|
|
406
|
+
const options = [];
|
|
407
|
+
for (const match of type.matchAll(/["']([^"']+)["']/g)) options.push({
|
|
408
|
+
label: match[1],
|
|
409
|
+
value: match[1]
|
|
410
|
+
});
|
|
411
|
+
return options;
|
|
412
|
+
}
|
|
413
|
+
function buildPaletteFromAnalysis(title, analysis) {
|
|
414
|
+
const controls = analysis.props.map((prop) => {
|
|
415
|
+
const control = inferControlType(prop.type);
|
|
416
|
+
return {
|
|
417
|
+
name: prop.name,
|
|
418
|
+
control,
|
|
419
|
+
defaultValue: normalizeDefaultValue(prop.defaultValue),
|
|
420
|
+
description: void 0,
|
|
421
|
+
required: prop.required,
|
|
422
|
+
options: control === "select" ? extractOptionsFromType(prop.type) : [],
|
|
423
|
+
range: void 0,
|
|
424
|
+
group: void 0
|
|
425
|
+
};
|
|
426
|
+
});
|
|
427
|
+
return {
|
|
428
|
+
title,
|
|
429
|
+
controls,
|
|
430
|
+
groups: [],
|
|
431
|
+
json: JSON.stringify({
|
|
432
|
+
title,
|
|
433
|
+
controls
|
|
434
|
+
}, null, 2),
|
|
435
|
+
typescript: buildPaletteTypescript(title, controls)
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
async function analyzeResolvedComponent(ctx, binding, resolved) {
|
|
439
|
+
const sourceDescriptor = await getComponentSourceDescriptor(ctx, resolved);
|
|
440
|
+
if (!sourceDescriptor.exists || !sourceDescriptor.absolutePath) return {
|
|
441
|
+
source: sourceDescriptor,
|
|
442
|
+
analysis: null
|
|
443
|
+
};
|
|
444
|
+
if (!binding.analyzeSfc) return {
|
|
445
|
+
source: {
|
|
446
|
+
...sourceDescriptor,
|
|
447
|
+
error: "analyzeSfc is not available in the native binding."
|
|
448
|
+
},
|
|
449
|
+
analysis: null
|
|
450
|
+
};
|
|
451
|
+
const source = await fs.promises.readFile(sourceDescriptor.absolutePath, "utf-8");
|
|
452
|
+
const analysis = binding.analyzeSfc(source, { filename: sourceDescriptor.absolutePath });
|
|
453
|
+
return {
|
|
454
|
+
source: sourceDescriptor,
|
|
455
|
+
analysis: {
|
|
456
|
+
path: sourceDescriptor.path ?? sourceDescriptor.absolutePath,
|
|
457
|
+
props: analysis.props.map((prop) => ({
|
|
458
|
+
name: prop.name,
|
|
459
|
+
type: prop.type,
|
|
460
|
+
required: prop.required,
|
|
461
|
+
defaultValue: prop.default_value
|
|
462
|
+
})),
|
|
463
|
+
emits: analysis.emits
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
async function buildPalette(ctx, binding, resolved, source) {
|
|
468
|
+
let palette = null;
|
|
469
|
+
if (binding.generateArtPalette) {
|
|
470
|
+
const generated = binding.generateArtPalette(source, { filename: resolved.absolutePath });
|
|
471
|
+
palette = {
|
|
472
|
+
title: generated.title,
|
|
473
|
+
controls: generated.controls.map((control) => ({
|
|
474
|
+
name: control.name,
|
|
475
|
+
control: control.control,
|
|
476
|
+
defaultValue: control.default_value,
|
|
477
|
+
description: control.description,
|
|
478
|
+
required: control.required,
|
|
479
|
+
options: control.options,
|
|
480
|
+
range: control.range,
|
|
481
|
+
group: control.group
|
|
482
|
+
})),
|
|
483
|
+
groups: generated.groups,
|
|
484
|
+
json: generated.json,
|
|
485
|
+
typescript: generated.typescript
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (palette && palette.controls.length > 0) return palette;
|
|
489
|
+
const { analysis } = await analyzeResolvedComponent(ctx, binding, resolved);
|
|
490
|
+
if (!analysis || analysis.props.length === 0) return palette;
|
|
491
|
+
return buildPaletteFromAnalysis(resolved.info.title, analysis);
|
|
492
|
+
}
|
|
493
|
+
async function buildDocumentation(binding, resolved, source, options) {
|
|
494
|
+
if (!binding.generateArtDoc) return null;
|
|
495
|
+
const doc = binding.generateArtDoc(source, { filename: resolved.absolutePath }, {
|
|
496
|
+
include_source: options?.includeSource,
|
|
497
|
+
include_templates: options?.includeTemplates,
|
|
498
|
+
include_metadata: true
|
|
499
|
+
});
|
|
500
|
+
return {
|
|
501
|
+
markdown: formatGeneratedMarkdown(doc.markdown, resolved.info.title || "Component"),
|
|
502
|
+
title: doc.title,
|
|
503
|
+
category: doc.category,
|
|
504
|
+
variantCount: doc.variant_count
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async function buildComponentDetails(ctx, binding, resolved, options) {
|
|
508
|
+
const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
|
|
509
|
+
const parsed = binding.parseArt(source, { filename: resolved.absolutePath });
|
|
510
|
+
const componentState = await analyzeResolvedComponent(ctx, binding, resolved);
|
|
511
|
+
const palette = options?.includePalette === false ? null : await buildPalette(ctx, binding, resolved, source);
|
|
512
|
+
const documentation = options?.includeDocumentation === true ? await buildDocumentation(binding, resolved, source) : null;
|
|
513
|
+
const resourceUris = buildResourceUris$1(resolved.relativePath, parsed.variants.map((variant) => variant.name), Boolean(componentState.source.reference));
|
|
514
|
+
return {
|
|
515
|
+
path: resolved.relativePath,
|
|
516
|
+
match: {
|
|
517
|
+
matchedBy: resolved.matchedBy,
|
|
518
|
+
matchValue: resolved.matchValue,
|
|
519
|
+
score: resolved.score,
|
|
520
|
+
reasons: resolved.reasons,
|
|
521
|
+
alternatives: resolved.alternatives
|
|
522
|
+
},
|
|
523
|
+
metadata: parsed.metadata,
|
|
524
|
+
variants: parsed.variants.map((variant) => ({
|
|
525
|
+
name: variant.name,
|
|
526
|
+
template: variant.template,
|
|
527
|
+
isDefault: variant.is_default,
|
|
528
|
+
skipVrt: variant.skip_vrt
|
|
529
|
+
})),
|
|
530
|
+
defaultVariant: parsed.variants.find((variant) => variant.is_default)?.name,
|
|
531
|
+
variantNames: parsed.variants.map((variant) => variant.name),
|
|
532
|
+
hasScriptSetup: parsed.has_script_setup,
|
|
533
|
+
hasScript: parsed.has_script,
|
|
534
|
+
styleCount: parsed.style_count,
|
|
535
|
+
componentSource: componentState.source,
|
|
536
|
+
componentAnalysis: options?.includeAnalysis === false ? void 0 : componentState.analysis ?? {
|
|
537
|
+
path: componentState.source.path,
|
|
538
|
+
props: [],
|
|
539
|
+
emits: [],
|
|
540
|
+
error: componentState.source.error
|
|
541
|
+
},
|
|
542
|
+
palette,
|
|
543
|
+
documentation,
|
|
544
|
+
resources: resourceUris
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function buildCatalogMarkdown(arts, projectRoot) {
|
|
548
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
549
|
+
for (const art of arts) {
|
|
550
|
+
const category = art.category || "Uncategorized";
|
|
551
|
+
const list = grouped.get(category) ?? [];
|
|
552
|
+
list.push(art);
|
|
553
|
+
grouped.set(category, list);
|
|
554
|
+
}
|
|
555
|
+
let markdown = "# Musea Component Catalog\n\n";
|
|
556
|
+
for (const [category, items] of Array.from(grouped.entries()).sort(([left], [right]) => left.localeCompare(right))) {
|
|
557
|
+
markdown += `## ${category}\n\n`;
|
|
558
|
+
for (const item of items.sort((left, right) => left.title.localeCompare(right.title))) {
|
|
559
|
+
const relativePath = toProjectPath(projectRoot, item.path);
|
|
560
|
+
markdown += `- **${item.title}** \`${relativePath}\``;
|
|
561
|
+
if (item.description) markdown += ` — ${item.description}`;
|
|
562
|
+
markdown += "\n";
|
|
563
|
+
if (item.variantNames.length > 0) markdown += ` Variants: ${item.variantNames.join(", ")}\n`;
|
|
564
|
+
if (item.tags.length > 0) markdown += ` Tags: ${item.tags.join(", ")}\n`;
|
|
328
565
|
}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
566
|
+
markdown += "\n";
|
|
567
|
+
}
|
|
568
|
+
return markdown;
|
|
569
|
+
}
|
|
570
|
+
function buildIndexSummary(ctx, arts) {
|
|
571
|
+
const categories = /* @__PURE__ */ new Map();
|
|
572
|
+
const tags = /* @__PURE__ */ new Map();
|
|
573
|
+
for (const art of arts) {
|
|
574
|
+
categories.set(art.category || "Uncategorized", (categories.get(art.category || "Uncategorized") ?? 0) + 1);
|
|
575
|
+
for (const tag of art.tags) tags.set(tag, (tags.get(tag) ?? 0) + 1);
|
|
576
|
+
}
|
|
577
|
+
return {
|
|
578
|
+
componentCount: arts.length,
|
|
579
|
+
categories: Array.from(categories.entries()).map(([name, count]) => ({
|
|
580
|
+
name,
|
|
581
|
+
count
|
|
582
|
+
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
583
|
+
tags: Array.from(tags.entries()).map(([name, count]) => ({
|
|
584
|
+
name,
|
|
585
|
+
count
|
|
586
|
+
})).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
|
|
587
|
+
components: arts.slice().sort((left, right) => left.title.localeCompare(right.title)).map((art) => ({
|
|
588
|
+
path: toProjectPath(ctx.projectRoot, art.path),
|
|
589
|
+
title: art.title,
|
|
590
|
+
description: art.description,
|
|
591
|
+
component: art.component,
|
|
592
|
+
category: art.category,
|
|
593
|
+
status: art.status,
|
|
594
|
+
tags: art.tags,
|
|
595
|
+
variantCount: art.variantCount,
|
|
596
|
+
variantNames: art.variantNames,
|
|
597
|
+
defaultVariant: art.defaultVariant
|
|
598
|
+
}))
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
function getProjectPath(projectRoot, absolutePath) {
|
|
602
|
+
return toProjectPath(projectRoot, absolutePath);
|
|
603
|
+
}
|
|
604
|
+
//#endregion
|
|
605
|
+
//#region src/scanner.ts
|
|
606
|
+
async function findArtFiles(root, include, exclude) {
|
|
607
|
+
const files = [];
|
|
608
|
+
async function scan(dir) {
|
|
609
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
610
|
+
for (const entry of entries) {
|
|
611
|
+
const fullPath = path.join(dir, entry.name);
|
|
612
|
+
const relative = normalizePath(path.relative(root, fullPath));
|
|
613
|
+
let excluded = false;
|
|
614
|
+
for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
|
|
615
|
+
excluded = true;
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
618
|
+
if (excluded) continue;
|
|
619
|
+
if (entry.isSymbolicLink()) continue;
|
|
620
|
+
if (entry.isDirectory()) await scan(fullPath);
|
|
621
|
+
else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
|
|
622
|
+
if (!isProjectPath(root, fullPath)) continue;
|
|
623
|
+
for (const pattern of include) if (matchGlob(relative, pattern)) {
|
|
624
|
+
files.push(fullPath);
|
|
625
|
+
break;
|
|
364
626
|
}
|
|
365
|
-
}
|
|
366
|
-
required: ["task"]
|
|
627
|
+
}
|
|
367
628
|
}
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
description: "Generate one variant per enum/union value (default: true)"
|
|
394
|
-
}
|
|
395
|
-
},
|
|
396
|
-
required: ["componentPath"]
|
|
629
|
+
}
|
|
630
|
+
await scan(root);
|
|
631
|
+
return files;
|
|
632
|
+
}
|
|
633
|
+
function normalizePath(value) {
|
|
634
|
+
return value.replaceAll(path.sep, "/");
|
|
635
|
+
}
|
|
636
|
+
function matchGlob(filepath, pattern) {
|
|
637
|
+
return globToRegExp(pattern).test(normalizePath(filepath));
|
|
638
|
+
}
|
|
639
|
+
function globToRegExp(pattern) {
|
|
640
|
+
const normalized = normalizePath(pattern);
|
|
641
|
+
if (normalized.endsWith("/**")) return new RegExp(`^${globSource(normalized.slice(0, -3))}(?:/.*)?$`);
|
|
642
|
+
return new RegExp(`^${globSource(normalized)}$`);
|
|
643
|
+
}
|
|
644
|
+
function globSource(pattern) {
|
|
645
|
+
let source = "";
|
|
646
|
+
for (let index = 0; index < pattern.length;) {
|
|
647
|
+
const char = pattern[index];
|
|
648
|
+
const next = pattern[index + 1];
|
|
649
|
+
const afterNext = pattern[index + 2];
|
|
650
|
+
if (char === "*" && next === "*" && afterNext === "/") {
|
|
651
|
+
source += "(?:.*/)?";
|
|
652
|
+
index += 3;
|
|
653
|
+
continue;
|
|
397
654
|
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
inputSchema: {
|
|
403
|
-
type: "object",
|
|
404
|
-
properties: {
|
|
405
|
-
path: {
|
|
406
|
-
type: "string",
|
|
407
|
-
description: "Path to the .art.vue file"
|
|
408
|
-
},
|
|
409
|
-
title: {
|
|
410
|
-
type: "string",
|
|
411
|
-
description: "Resolve an art file by title"
|
|
412
|
-
},
|
|
413
|
-
component: {
|
|
414
|
-
type: "string",
|
|
415
|
-
description: "Resolve an art file by component reference or component basename"
|
|
416
|
-
},
|
|
417
|
-
query: {
|
|
418
|
-
type: "string",
|
|
419
|
-
description: "Fuzzy-search an art file before converting to CSF"
|
|
420
|
-
},
|
|
421
|
-
ref: {
|
|
422
|
-
type: "string",
|
|
423
|
-
description: "Generic art-file reference: path, title, component name, or search text"
|
|
424
|
-
}
|
|
425
|
-
},
|
|
426
|
-
required: []
|
|
655
|
+
if (char === "*" && next === "*") {
|
|
656
|
+
source += ".*";
|
|
657
|
+
index += 2;
|
|
658
|
+
continue;
|
|
427
659
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
inputSchema: {
|
|
433
|
-
type: "object",
|
|
434
|
-
properties: {
|
|
435
|
-
path: {
|
|
436
|
-
type: "string",
|
|
437
|
-
description: "Path to the .art.vue file (relative to project root)"
|
|
438
|
-
},
|
|
439
|
-
title: {
|
|
440
|
-
type: "string",
|
|
441
|
-
description: "Resolve an art file by title"
|
|
442
|
-
},
|
|
443
|
-
component: {
|
|
444
|
-
type: "string",
|
|
445
|
-
description: "Resolve an art file by component reference or component basename"
|
|
446
|
-
},
|
|
447
|
-
query: {
|
|
448
|
-
type: "string",
|
|
449
|
-
description: "Fuzzy-search an art file before generating docs"
|
|
450
|
-
},
|
|
451
|
-
ref: {
|
|
452
|
-
type: "string",
|
|
453
|
-
description: "Generic art-file reference: path, title, component name, or search text"
|
|
454
|
-
},
|
|
455
|
-
includeSource: {
|
|
456
|
-
type: "boolean",
|
|
457
|
-
description: "Embed source code in the output (default: false)"
|
|
458
|
-
},
|
|
459
|
-
includeTemplates: {
|
|
460
|
-
type: "boolean",
|
|
461
|
-
description: "Embed variant templates in the output (default: false)"
|
|
462
|
-
}
|
|
463
|
-
},
|
|
464
|
-
required: []
|
|
660
|
+
if (char === "*") {
|
|
661
|
+
source += "[^/]*";
|
|
662
|
+
index += 1;
|
|
663
|
+
continue;
|
|
465
664
|
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
inputSchema: {
|
|
471
|
-
type: "object",
|
|
472
|
-
properties: {
|
|
473
|
-
includeSource: {
|
|
474
|
-
type: "boolean",
|
|
475
|
-
description: "Embed source code in the catalog (default: false)"
|
|
476
|
-
},
|
|
477
|
-
includeTemplates: {
|
|
478
|
-
type: "boolean",
|
|
479
|
-
description: "Embed variant templates in the catalog (default: false)"
|
|
480
|
-
}
|
|
481
|
-
}
|
|
665
|
+
if (char === "?") {
|
|
666
|
+
source += "[^/]";
|
|
667
|
+
index += 1;
|
|
668
|
+
continue;
|
|
482
669
|
}
|
|
483
|
-
|
|
670
|
+
source += escapeRegExp(char);
|
|
671
|
+
index += 1;
|
|
672
|
+
}
|
|
673
|
+
return source;
|
|
674
|
+
}
|
|
675
|
+
function escapeRegExp(value) {
|
|
676
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
677
|
+
}
|
|
678
|
+
//#endregion
|
|
679
|
+
//#region src/tools/definitions.ts
|
|
680
|
+
/**
|
|
681
|
+
* MCP tool definitions for Musea.
|
|
682
|
+
*
|
|
683
|
+
* Declares the schema (name, description, input parameters) for each tool
|
|
684
|
+
* exposed by the MCP server: component analysis, registry, code generation,
|
|
685
|
+
* documentation, and design tokens.
|
|
686
|
+
*/
|
|
687
|
+
const toolDefinitions = [
|
|
484
688
|
{
|
|
485
|
-
name: "
|
|
486
|
-
description: "
|
|
689
|
+
name: "analyze_component",
|
|
690
|
+
description: "Statically analyze a Vue SFC to extract its props and emits. Accepts a Vue component path directly, or an art-file reference that resolves to the linked component source.",
|
|
487
691
|
inputSchema: {
|
|
488
692
|
type: "object",
|
|
489
693
|
properties: {
|
|
490
|
-
|
|
694
|
+
path: {
|
|
491
695
|
type: "string",
|
|
492
|
-
description: "Path to
|
|
696
|
+
description: "Path to the .vue component file or .art.vue file (relative to project root)"
|
|
493
697
|
},
|
|
494
|
-
|
|
495
|
-
type: "string",
|
|
496
|
-
enum: ["json", "markdown"],
|
|
497
|
-
description: "Output format (default: json)"
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
{
|
|
503
|
-
name: "search_tokens",
|
|
504
|
-
description: "Search flattened design tokens by token name, category path, value, or description. Much more practical than loading the full token tree for large systems.",
|
|
505
|
-
inputSchema: {
|
|
506
|
-
type: "object",
|
|
507
|
-
properties: {
|
|
508
|
-
query: {
|
|
698
|
+
title: {
|
|
509
699
|
type: "string",
|
|
510
|
-
description: "
|
|
700
|
+
description: "Resolve an art file by its display title, then analyze its component source"
|
|
511
701
|
},
|
|
512
|
-
|
|
702
|
+
component: {
|
|
513
703
|
type: "string",
|
|
514
|
-
description: "
|
|
704
|
+
description: "Resolve an art file by its component reference or component basename, then analyze it"
|
|
515
705
|
},
|
|
516
|
-
|
|
706
|
+
query: {
|
|
517
707
|
type: "string",
|
|
518
|
-
description: "
|
|
708
|
+
description: "Fuzzy-search an art file, then analyze the linked component source"
|
|
519
709
|
},
|
|
520
|
-
|
|
521
|
-
type: "
|
|
522
|
-
description: "
|
|
710
|
+
ref: {
|
|
711
|
+
type: "string",
|
|
712
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
523
713
|
}
|
|
524
714
|
},
|
|
525
|
-
required: [
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
];
|
|
529
|
-
//#endregion
|
|
530
|
-
//#region src/markdown-template.ts
|
|
531
|
-
const SELF_TAG_NAME = "Self";
|
|
532
|
-
const TEMPLATE_FENCE_LANGUAGES = new Set([
|
|
533
|
-
"",
|
|
534
|
-
"html",
|
|
535
|
-
"template",
|
|
536
|
-
"vue"
|
|
537
|
-
]);
|
|
538
|
-
function formatGeneratedMarkdown(markdown, componentName) {
|
|
539
|
-
return markdown.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
540
|
-
const normalizedLang = lang.toLowerCase();
|
|
541
|
-
return formatCodeFence(lang, TEMPLATE_FENCE_LANGUAGES.has(normalizedLang) ? rewriteSelfComponentTags(code, componentName) : code);
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
function formatCodeFence(lang, code) {
|
|
545
|
-
const lines = code.split("\n");
|
|
546
|
-
let minIndent = Infinity;
|
|
547
|
-
for (const line of lines) if (line.trim()) {
|
|
548
|
-
const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
|
|
549
|
-
minIndent = Math.min(minIndent, indent);
|
|
550
|
-
}
|
|
551
|
-
if (minIndent === Infinity) minIndent = 0;
|
|
552
|
-
return `\`\`\`${lang}\n${(minIndent > 0 ? lines.map((line) => line.slice(minIndent)) : lines).join("\n")}\`\`\``;
|
|
553
|
-
}
|
|
554
|
-
function rewriteSelfComponentTags(code, componentName) {
|
|
555
|
-
const replacements = [];
|
|
556
|
-
try {
|
|
557
|
-
collectSelfTagReplacements(baseParse(code, { comments: true }).children, replacements, componentName);
|
|
558
|
-
} catch {
|
|
559
|
-
return code;
|
|
560
|
-
}
|
|
561
|
-
if (replacements.length === 0) return code;
|
|
562
|
-
let rewritten = code;
|
|
563
|
-
for (const replacement of replacements.sort((left, right) => right.start - left.start)) rewritten = rewritten.slice(0, replacement.start) + replacement.text + rewritten.slice(replacement.end);
|
|
564
|
-
return rewritten;
|
|
565
|
-
}
|
|
566
|
-
function collectSelfTagReplacements(nodes, replacements, componentName) {
|
|
567
|
-
for (const node of nodes) {
|
|
568
|
-
if (node.type !== NodeTypes.ELEMENT) continue;
|
|
569
|
-
if (node.tag === SELF_TAG_NAME) addSelfElementReplacements(node, replacements, componentName);
|
|
570
|
-
collectSelfTagReplacements(node.children, replacements, componentName);
|
|
571
|
-
}
|
|
572
|
-
}
|
|
573
|
-
function addSelfElementReplacements(node, replacements, componentName) {
|
|
574
|
-
const openStart = node.loc.start.offset + 1;
|
|
575
|
-
const openEnd = openStart + 4;
|
|
576
|
-
replacements.push({
|
|
577
|
-
start: openStart,
|
|
578
|
-
end: openEnd,
|
|
579
|
-
text: componentName
|
|
580
|
-
});
|
|
581
|
-
if (node.isSelfClosing) return;
|
|
582
|
-
const closeTagIndex = node.loc.source.lastIndexOf(`</${SELF_TAG_NAME}>`);
|
|
583
|
-
if (closeTagIndex < 0) return;
|
|
584
|
-
const closeStart = node.loc.start.offset + closeTagIndex + 2;
|
|
585
|
-
replacements.push({
|
|
586
|
-
start: closeStart,
|
|
587
|
-
end: closeStart + 4,
|
|
588
|
-
text: componentName
|
|
589
|
-
});
|
|
590
|
-
}
|
|
591
|
-
//#endregion
|
|
592
|
-
//#region src/palette-typescript.ts
|
|
593
|
-
function buildPaletteTypescript(title, controls) {
|
|
594
|
-
const members = controls.map((control) => ts.factory.createPropertySignature(void 0, propertyNameNode(control.name), control.required ? void 0 : ts.factory.createToken(ts.SyntaxKind.QuestionToken), controlTypeNode(control)));
|
|
595
|
-
const declaration = ts.factory.createInterfaceDeclaration([ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)], interfaceNameFromTitle(title), void 0, void 0, members);
|
|
596
|
-
const sourceFile = ts.createSourceFile("palette.ts", "", ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
|
|
597
|
-
return `${ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }).printNode(ts.EmitHint.Unspecified, declaration, sourceFile)}\n`;
|
|
598
|
-
}
|
|
599
|
-
function interfaceNameFromTitle(title) {
|
|
600
|
-
const candidate = `${title.replace(/\s+/g, "") || "Component"}Props`.replace(/[^A-Za-z0-9_$]/g, "_");
|
|
601
|
-
return /^[A-Za-z_$]/.test(candidate) ? candidate : `_${candidate}`;
|
|
602
|
-
}
|
|
603
|
-
function propertyNameNode(name) {
|
|
604
|
-
return /^[$A-Z_a-z][$\w]*$/.test(name) ? ts.factory.createIdentifier(name) : ts.factory.createStringLiteral(name);
|
|
605
|
-
}
|
|
606
|
-
function controlTypeNode(control) {
|
|
607
|
-
if (control.control === "boolean") return ts.factory.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
608
|
-
if (control.control === "number") return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
609
|
-
if (control.control === "select" && control.options.length > 0) return ts.factory.createUnionTypeNode(control.options.map((option) => ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(String(option.value)))));
|
|
610
|
-
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
611
|
-
}
|
|
612
|
-
//#endregion
|
|
613
|
-
//#region src/musea.ts
|
|
614
|
-
function normalize(value) {
|
|
615
|
-
return value?.trim().toLowerCase() ?? "";
|
|
616
|
-
}
|
|
617
|
-
function normalizePathLike(value) {
|
|
618
|
-
return value.replace(/\\/g, "/").toLowerCase();
|
|
619
|
-
}
|
|
620
|
-
function tokenize(query) {
|
|
621
|
-
return Array.from(new Set(query.toLowerCase().split(/[\s/,_-]+/).map((term) => term.trim()).filter(Boolean)));
|
|
622
|
-
}
|
|
623
|
-
function toProjectPath(projectRoot, absolutePath) {
|
|
624
|
-
const root = realpathNearest(projectRoot);
|
|
625
|
-
const resolved = realpathNearest(absolutePath);
|
|
626
|
-
const relativePath = path.relative(root, resolved);
|
|
627
|
-
return isProjectPath(root, resolved) ? relativePath || "." : resolved;
|
|
628
|
-
}
|
|
629
|
-
function realpathNearest(targetPath) {
|
|
630
|
-
let current = path.resolve(targetPath);
|
|
631
|
-
const missingParts = [];
|
|
632
|
-
while (true) try {
|
|
633
|
-
const real = fs.realpathSync.native(current);
|
|
634
|
-
return missingParts.length > 0 ? path.join(real, ...missingParts.reverse()) : real;
|
|
635
|
-
} catch {
|
|
636
|
-
const parent = path.dirname(current);
|
|
637
|
-
if (parent === current) return path.resolve(targetPath);
|
|
638
|
-
missingParts.push(path.basename(current));
|
|
639
|
-
current = parent;
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
function isProjectPath(projectRoot, candidatePath) {
|
|
643
|
-
const root = realpathNearest(projectRoot);
|
|
644
|
-
const candidate = realpathNearest(candidatePath);
|
|
645
|
-
const relativePath = path.relative(root, candidate);
|
|
646
|
-
return relativePath === "" || !relativePath.startsWith("..") && !path.isAbsolute(relativePath);
|
|
647
|
-
}
|
|
648
|
-
function resolveProjectPath(projectRoot, inputPath, label = "path") {
|
|
649
|
-
if (inputPath.includes("\0")) throw new McpError(ErrorCode.InvalidParams, `${label} contains an invalid character`);
|
|
650
|
-
const root = path.resolve(projectRoot);
|
|
651
|
-
const resolvedPath = path.isAbsolute(inputPath) ? path.resolve(inputPath) : path.resolve(root, inputPath);
|
|
652
|
-
if (!isProjectPath(root, resolvedPath)) throw new McpError(ErrorCode.InvalidParams, `${label} must stay inside the project root`);
|
|
653
|
-
return resolvedPath;
|
|
654
|
-
}
|
|
655
|
-
function buildResourceUris$1(relativePath, variantNames, hasComponentSource) {
|
|
656
|
-
const encodedPath = encodeURIComponent(relativePath);
|
|
657
|
-
return {
|
|
658
|
-
component: `musea://component/${encodedPath}`,
|
|
659
|
-
docs: `musea://docs/${encodedPath}`,
|
|
660
|
-
source: `musea://source/${encodedPath}`,
|
|
661
|
-
componentSource: hasComponentSource ? `musea://component-source/${encodedPath}` : void 0,
|
|
662
|
-
variants: variantNames.map((variantName) => ({
|
|
663
|
-
name: variantName,
|
|
664
|
-
uri: `musea://variant/${encodedPath}/${encodeURIComponent(variantName)}`
|
|
665
|
-
}))
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
function addScore(reasons, reason, amount, scoreRef) {
|
|
669
|
-
reasons.add(reason);
|
|
670
|
-
scoreRef.value += amount;
|
|
671
|
-
}
|
|
672
|
-
function getComponentCandidates(info) {
|
|
673
|
-
const candidates = /* @__PURE__ */ new Set();
|
|
674
|
-
if (info.component) {
|
|
675
|
-
candidates.add(info.component);
|
|
676
|
-
candidates.add(path.basename(info.component));
|
|
677
|
-
candidates.add(path.basename(info.component, path.extname(info.component)));
|
|
678
|
-
}
|
|
679
|
-
return Array.from(candidates);
|
|
680
|
-
}
|
|
681
|
-
function scoreArtInfo(projectRoot, info, query) {
|
|
682
|
-
const normalizedQuery = normalize(query);
|
|
683
|
-
if (!normalizedQuery) return null;
|
|
684
|
-
const relativePath = toProjectPath(projectRoot, info.path);
|
|
685
|
-
const relativePathNorm = normalizePathLike(relativePath);
|
|
686
|
-
const titleNorm = normalize(info.title);
|
|
687
|
-
const descriptionNorm = normalize(info.description);
|
|
688
|
-
const categoryNorm = normalize(info.category);
|
|
689
|
-
const tagNorms = info.tags.map((tag) => normalize(tag));
|
|
690
|
-
const variantNorms = info.variantNames.map((variantName) => normalize(variantName));
|
|
691
|
-
const componentNorms = getComponentCandidates(info).map((component) => normalizePathLike(component));
|
|
692
|
-
const reasons = /* @__PURE__ */ new Set();
|
|
693
|
-
const scoreRef = { value: 0 };
|
|
694
|
-
if (relativePathNorm === normalizePathLike(query)) addScore(reasons, "exact path match", 220, scoreRef);
|
|
695
|
-
else if (relativePathNorm.includes(normalizePathLike(query))) addScore(reasons, "path match", 70, scoreRef);
|
|
696
|
-
if (titleNorm === normalizedQuery) addScore(reasons, "exact title match", 200, scoreRef);
|
|
697
|
-
else if (titleNorm.startsWith(normalizedQuery)) addScore(reasons, "title prefix match", 140, scoreRef);
|
|
698
|
-
else if (titleNorm.includes(normalizedQuery)) addScore(reasons, "title match", 110, scoreRef);
|
|
699
|
-
if (categoryNorm === normalizedQuery) addScore(reasons, "exact category match", 120, scoreRef);
|
|
700
|
-
else if (categoryNorm.includes(normalizedQuery)) addScore(reasons, "category match", 55, scoreRef);
|
|
701
|
-
if (descriptionNorm.includes(normalizedQuery)) addScore(reasons, "description match", 60, scoreRef);
|
|
702
|
-
for (const tag of tagNorms) {
|
|
703
|
-
if (tag === normalizedQuery) {
|
|
704
|
-
addScore(reasons, "exact tag match", 130, scoreRef);
|
|
705
|
-
break;
|
|
706
|
-
}
|
|
707
|
-
if (tag.includes(normalizedQuery)) {
|
|
708
|
-
addScore(reasons, "tag match", 80, scoreRef);
|
|
709
|
-
break;
|
|
715
|
+
required: []
|
|
710
716
|
}
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "get_palette",
|
|
720
|
+
description: "Derive an interactive props palette (control types, defaults, ranges, options) for a component described by an Art file. Falls back to SFC analysis when native palette inference is sparse.",
|
|
721
|
+
inputSchema: {
|
|
722
|
+
type: "object",
|
|
723
|
+
properties: {
|
|
724
|
+
path: {
|
|
725
|
+
type: "string",
|
|
726
|
+
description: "Path to the .art.vue file (relative to project root)"
|
|
727
|
+
},
|
|
728
|
+
title: {
|
|
729
|
+
type: "string",
|
|
730
|
+
description: "Resolve an art file by title instead of path"
|
|
731
|
+
},
|
|
732
|
+
component: {
|
|
733
|
+
type: "string",
|
|
734
|
+
description: "Resolve an art file by component reference or component basename"
|
|
735
|
+
},
|
|
736
|
+
query: {
|
|
737
|
+
type: "string",
|
|
738
|
+
description: "Fuzzy-search an art file before generating the palette"
|
|
739
|
+
},
|
|
740
|
+
ref: {
|
|
741
|
+
type: "string",
|
|
742
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
743
|
+
}
|
|
744
|
+
},
|
|
745
|
+
required: []
|
|
716
746
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: "list_components",
|
|
750
|
+
description: "List components registered in the design system. Returns titles, categories, tags, status, variant names, and related resource URIs.",
|
|
751
|
+
inputSchema: {
|
|
752
|
+
type: "object",
|
|
753
|
+
properties: {
|
|
754
|
+
category: {
|
|
755
|
+
type: "string",
|
|
756
|
+
description: "Filter by category"
|
|
757
|
+
},
|
|
758
|
+
tag: {
|
|
759
|
+
type: "string",
|
|
760
|
+
description: "Filter by tag"
|
|
761
|
+
},
|
|
762
|
+
status: {
|
|
763
|
+
type: "string",
|
|
764
|
+
enum: [
|
|
765
|
+
"draft",
|
|
766
|
+
"ready",
|
|
767
|
+
"deprecated"
|
|
768
|
+
],
|
|
769
|
+
description: "Filter by status badge"
|
|
770
|
+
},
|
|
771
|
+
component: {
|
|
772
|
+
type: "string",
|
|
773
|
+
description: "Filter by component reference or component basename"
|
|
774
|
+
},
|
|
775
|
+
limit: {
|
|
776
|
+
type: "number",
|
|
777
|
+
description: "Maximum number of components to return (default: all)"
|
|
778
|
+
},
|
|
779
|
+
includeVariants: {
|
|
780
|
+
type: "boolean",
|
|
781
|
+
description: "Include per-variant metadata in the result (default: false)"
|
|
782
|
+
},
|
|
783
|
+
sortBy: {
|
|
784
|
+
type: "string",
|
|
785
|
+
enum: [
|
|
786
|
+
"title",
|
|
787
|
+
"category",
|
|
788
|
+
"status",
|
|
789
|
+
"variants"
|
|
790
|
+
],
|
|
791
|
+
description: "Sort order for the result list (default: title)"
|
|
792
|
+
}
|
|
793
|
+
}
|
|
720
794
|
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "get_component",
|
|
798
|
+
description: "Get full details of a design-system component: metadata, variants, source-component analysis, palette data, documentation, and related resource URIs.",
|
|
799
|
+
inputSchema: {
|
|
800
|
+
type: "object",
|
|
801
|
+
properties: {
|
|
802
|
+
path: {
|
|
803
|
+
type: "string",
|
|
804
|
+
description: "Path to the .art.vue file (relative to project root)"
|
|
805
|
+
},
|
|
806
|
+
title: {
|
|
807
|
+
type: "string",
|
|
808
|
+
description: "Resolve an art file by display title instead of path"
|
|
809
|
+
},
|
|
810
|
+
component: {
|
|
811
|
+
type: "string",
|
|
812
|
+
description: "Resolve an art file by component reference or component basename"
|
|
813
|
+
},
|
|
814
|
+
query: {
|
|
815
|
+
type: "string",
|
|
816
|
+
description: "Fuzzy-search an art file before loading details"
|
|
817
|
+
},
|
|
818
|
+
ref: {
|
|
819
|
+
type: "string",
|
|
820
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
821
|
+
},
|
|
822
|
+
includeAnalysis: {
|
|
823
|
+
type: "boolean",
|
|
824
|
+
description: "Include resolved component props/emits analysis (default: true)"
|
|
825
|
+
},
|
|
826
|
+
includePalette: {
|
|
827
|
+
type: "boolean",
|
|
828
|
+
description: "Include inferred palette data (default: true)"
|
|
829
|
+
},
|
|
830
|
+
includeDocumentation: {
|
|
831
|
+
type: "boolean",
|
|
832
|
+
description: "Include generated Markdown docs inline (default: false)"
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
required: []
|
|
726
836
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
name: "get_variant",
|
|
840
|
+
description: "Retrieve a single variant (template and metadata) from a component, resolving the component by path, title, component name, or fuzzy query.",
|
|
841
|
+
inputSchema: {
|
|
842
|
+
type: "object",
|
|
843
|
+
properties: {
|
|
844
|
+
path: {
|
|
845
|
+
type: "string",
|
|
846
|
+
description: "Path to the .art.vue file"
|
|
847
|
+
},
|
|
848
|
+
title: {
|
|
849
|
+
type: "string",
|
|
850
|
+
description: "Resolve an art file by title"
|
|
851
|
+
},
|
|
852
|
+
component: {
|
|
853
|
+
type: "string",
|
|
854
|
+
description: "Resolve an art file by component reference or component basename"
|
|
855
|
+
},
|
|
856
|
+
query: {
|
|
857
|
+
type: "string",
|
|
858
|
+
description: "Fuzzy-search an art file before looking up the variant"
|
|
859
|
+
},
|
|
860
|
+
ref: {
|
|
861
|
+
type: "string",
|
|
862
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
863
|
+
},
|
|
864
|
+
variant: {
|
|
865
|
+
type: "string",
|
|
866
|
+
description: "Variant name"
|
|
867
|
+
},
|
|
868
|
+
includeAnalysis: {
|
|
869
|
+
type: "boolean",
|
|
870
|
+
description: "Include resolved component props/emits analysis (default: false)"
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
required: ["variant"]
|
|
730
874
|
}
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
path: result.relativePath,
|
|
770
|
-
title: result.info.title,
|
|
771
|
-
component: result.info.component,
|
|
772
|
-
score: result.score,
|
|
773
|
-
reasons: result.reasons
|
|
774
|
-
}));
|
|
775
|
-
}
|
|
776
|
-
async function resolveArtReference(ctx, args) {
|
|
777
|
-
const arts = Array.from((await ctx.scanArtFiles()).values());
|
|
778
|
-
const pathArg = typeof args?.path === "string" ? args.path : void 0;
|
|
779
|
-
const titleArg = typeof args?.title === "string" ? args.title : void 0;
|
|
780
|
-
const componentArg = typeof args?.component === "string" ? args.component : void 0;
|
|
781
|
-
const queryArg = typeof args?.query === "string" ? args.query : void 0;
|
|
782
|
-
const refArg = typeof args?.ref === "string" ? args.ref : void 0;
|
|
783
|
-
if (pathArg) {
|
|
784
|
-
const resolvedPath = resolveProjectPath(ctx.projectRoot, pathArg, "path");
|
|
785
|
-
const normalizedResolvedPath = normalizePathLike(resolvedPath);
|
|
786
|
-
const normalizedRelativePath = normalizePathLike(path.relative(ctx.projectRoot, resolvedPath));
|
|
787
|
-
const directMatch = arts.find((info) => {
|
|
788
|
-
const infoRelativePath = normalizePathLike(path.relative(ctx.projectRoot, info.path));
|
|
789
|
-
return normalizePathLike(info.path) === normalizedResolvedPath || infoRelativePath === normalizedRelativePath;
|
|
790
|
-
});
|
|
791
|
-
if (directMatch) return {
|
|
792
|
-
info: directMatch,
|
|
793
|
-
absolutePath: directMatch.path,
|
|
794
|
-
relativePath: toProjectPath(ctx.projectRoot, directMatch.path),
|
|
795
|
-
matchedBy: "path",
|
|
796
|
-
matchValue: pathArg,
|
|
797
|
-
score: 999,
|
|
798
|
-
reasons: ["exact path match"],
|
|
799
|
-
alternatives: []
|
|
800
|
-
};
|
|
801
|
-
}
|
|
802
|
-
if (titleArg) {
|
|
803
|
-
const matches = arts.filter((info) => normalize(info.title) === normalize(titleArg));
|
|
804
|
-
if (matches.length > 0) {
|
|
805
|
-
const primary = matches[0];
|
|
806
|
-
return {
|
|
807
|
-
info: primary,
|
|
808
|
-
absolutePath: primary.path,
|
|
809
|
-
relativePath: toProjectPath(ctx.projectRoot, primary.path),
|
|
810
|
-
matchedBy: "title",
|
|
811
|
-
matchValue: titleArg,
|
|
812
|
-
score: 950,
|
|
813
|
-
reasons: ["exact title match"],
|
|
814
|
-
alternatives: matches.slice(1, 4).map((info) => ({
|
|
815
|
-
path: toProjectPath(ctx.projectRoot, info.path),
|
|
816
|
-
title: info.title,
|
|
817
|
-
component: info.component,
|
|
818
|
-
score: 900,
|
|
819
|
-
reasons: ["exact title match"]
|
|
820
|
-
}))
|
|
821
|
-
};
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "search_components",
|
|
878
|
+
description: "Ranked full-text search over component titles, descriptions, categories, tags, component names, and variant names.",
|
|
879
|
+
inputSchema: {
|
|
880
|
+
type: "object",
|
|
881
|
+
properties: {
|
|
882
|
+
query: {
|
|
883
|
+
type: "string",
|
|
884
|
+
description: "Search query"
|
|
885
|
+
},
|
|
886
|
+
category: {
|
|
887
|
+
type: "string",
|
|
888
|
+
description: "Restrict matches to one category"
|
|
889
|
+
},
|
|
890
|
+
tag: {
|
|
891
|
+
type: "string",
|
|
892
|
+
description: "Restrict matches to one tag"
|
|
893
|
+
},
|
|
894
|
+
status: {
|
|
895
|
+
type: "string",
|
|
896
|
+
enum: [
|
|
897
|
+
"draft",
|
|
898
|
+
"ready",
|
|
899
|
+
"deprecated"
|
|
900
|
+
],
|
|
901
|
+
description: "Restrict matches to one status"
|
|
902
|
+
},
|
|
903
|
+
component: {
|
|
904
|
+
type: "string",
|
|
905
|
+
description: "Restrict matches to a component reference/basename before searching"
|
|
906
|
+
},
|
|
907
|
+
limit: {
|
|
908
|
+
type: "number",
|
|
909
|
+
description: "Maximum number of results to return (default: 10)"
|
|
910
|
+
}
|
|
911
|
+
},
|
|
912
|
+
required: ["query"]
|
|
822
913
|
}
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
name: "recommend_components",
|
|
917
|
+
description: "Intent-oriented component recommendation. Useful when the user describes a task or UX goal rather than knowing exact component names.",
|
|
918
|
+
inputSchema: {
|
|
919
|
+
type: "object",
|
|
920
|
+
properties: {
|
|
921
|
+
task: {
|
|
922
|
+
type: "string",
|
|
923
|
+
description: "Intent or UI task to solve"
|
|
924
|
+
},
|
|
925
|
+
category: {
|
|
926
|
+
type: "string",
|
|
927
|
+
description: "Optional category filter"
|
|
928
|
+
},
|
|
929
|
+
tag: {
|
|
930
|
+
type: "string",
|
|
931
|
+
description: "Optional tag filter"
|
|
932
|
+
},
|
|
933
|
+
status: {
|
|
934
|
+
type: "string",
|
|
935
|
+
enum: [
|
|
936
|
+
"draft",
|
|
937
|
+
"ready",
|
|
938
|
+
"deprecated"
|
|
939
|
+
],
|
|
940
|
+
description: "Optional status filter"
|
|
941
|
+
},
|
|
942
|
+
component: {
|
|
943
|
+
type: "string",
|
|
944
|
+
description: "Optional component reference/basename filter"
|
|
945
|
+
},
|
|
946
|
+
limit: {
|
|
947
|
+
type: "number",
|
|
948
|
+
description: "Maximum number of recommendations to return (default: 5)"
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
required: ["task"]
|
|
845
952
|
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
906
|
-
function inferControlType(type) {
|
|
907
|
-
const normalizedType = type.toLowerCase();
|
|
908
|
-
if (normalizedType === "boolean") return "boolean";
|
|
909
|
-
if (normalizedType === "number") return "number";
|
|
910
|
-
if (normalizedType.includes("|") && !normalizedType.includes("=>")) return "select";
|
|
911
|
-
return "text";
|
|
912
|
-
}
|
|
913
|
-
function extractOptionsFromType(type) {
|
|
914
|
-
const options = [];
|
|
915
|
-
for (const match of type.matchAll(/["']([^"']+)["']/g)) options.push({
|
|
916
|
-
label: match[1],
|
|
917
|
-
value: match[1]
|
|
918
|
-
});
|
|
919
|
-
return options;
|
|
920
|
-
}
|
|
921
|
-
function buildPaletteFromAnalysis(title, analysis) {
|
|
922
|
-
const controls = analysis.props.map((prop) => {
|
|
923
|
-
const control = inferControlType(prop.type);
|
|
924
|
-
return {
|
|
925
|
-
name: prop.name,
|
|
926
|
-
control,
|
|
927
|
-
defaultValue: normalizeDefaultValue(prop.defaultValue),
|
|
928
|
-
description: void 0,
|
|
929
|
-
required: prop.required,
|
|
930
|
-
options: control === "select" ? extractOptionsFromType(prop.type) : [],
|
|
931
|
-
range: void 0,
|
|
932
|
-
group: void 0
|
|
933
|
-
};
|
|
934
|
-
});
|
|
935
|
-
return {
|
|
936
|
-
title,
|
|
937
|
-
controls,
|
|
938
|
-
groups: [],
|
|
939
|
-
json: JSON.stringify({
|
|
940
|
-
title,
|
|
941
|
-
controls
|
|
942
|
-
}, null, 2),
|
|
943
|
-
typescript: buildPaletteTypescript(title, controls)
|
|
944
|
-
};
|
|
945
|
-
}
|
|
946
|
-
async function analyzeResolvedComponent(ctx, binding, resolved) {
|
|
947
|
-
const sourceDescriptor = await getComponentSourceDescriptor(ctx, resolved);
|
|
948
|
-
if (!sourceDescriptor.exists || !sourceDescriptor.absolutePath) return {
|
|
949
|
-
source: sourceDescriptor,
|
|
950
|
-
analysis: null
|
|
951
|
-
};
|
|
952
|
-
if (!binding.analyzeSfc) return {
|
|
953
|
-
source: {
|
|
954
|
-
...sourceDescriptor,
|
|
955
|
-
error: "analyzeSfc is not available in the native binding."
|
|
956
|
-
},
|
|
957
|
-
analysis: null
|
|
958
|
-
};
|
|
959
|
-
const source = await fs.promises.readFile(sourceDescriptor.absolutePath, "utf-8");
|
|
960
|
-
const analysis = binding.analyzeSfc(source, { filename: sourceDescriptor.absolutePath });
|
|
961
|
-
return {
|
|
962
|
-
source: sourceDescriptor,
|
|
963
|
-
analysis: {
|
|
964
|
-
path: sourceDescriptor.path ?? sourceDescriptor.absolutePath,
|
|
965
|
-
props: analysis.props.map((prop) => ({
|
|
966
|
-
name: prop.name,
|
|
967
|
-
type: prop.type,
|
|
968
|
-
required: prop.required,
|
|
969
|
-
defaultValue: prop.default_value
|
|
970
|
-
})),
|
|
971
|
-
emits: analysis.emits
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
name: "generate_variants",
|
|
956
|
+
description: "Analyze a Vue component's props and auto-generate an .art.vue file containing appropriate variant combinations (default, boolean toggles, enum values, etc.).",
|
|
957
|
+
inputSchema: {
|
|
958
|
+
type: "object",
|
|
959
|
+
properties: {
|
|
960
|
+
componentPath: {
|
|
961
|
+
type: "string",
|
|
962
|
+
description: "Path to the .vue component file (relative to project root)"
|
|
963
|
+
},
|
|
964
|
+
maxVariants: {
|
|
965
|
+
type: "number",
|
|
966
|
+
description: "Maximum number of variants to generate (default: 20)"
|
|
967
|
+
},
|
|
968
|
+
includeDefault: {
|
|
969
|
+
type: "boolean",
|
|
970
|
+
description: "Include a default variant (default: true)"
|
|
971
|
+
},
|
|
972
|
+
includeBooleanToggles: {
|
|
973
|
+
type: "boolean",
|
|
974
|
+
description: "Generate variants that toggle each boolean prop (default: true)"
|
|
975
|
+
},
|
|
976
|
+
includeEnumVariants: {
|
|
977
|
+
type: "boolean",
|
|
978
|
+
description: "Generate one variant per enum/union value (default: true)"
|
|
979
|
+
}
|
|
980
|
+
},
|
|
981
|
+
required: ["componentPath"]
|
|
982
|
+
}
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
name: "generate_csf",
|
|
986
|
+
description: "Convert an .art.vue file into Storybook CSF 3.0 code for integration with existing Storybook setups.",
|
|
987
|
+
inputSchema: {
|
|
988
|
+
type: "object",
|
|
989
|
+
properties: {
|
|
990
|
+
path: {
|
|
991
|
+
type: "string",
|
|
992
|
+
description: "Path to the .art.vue file"
|
|
993
|
+
},
|
|
994
|
+
title: {
|
|
995
|
+
type: "string",
|
|
996
|
+
description: "Resolve an art file by title"
|
|
997
|
+
},
|
|
998
|
+
component: {
|
|
999
|
+
type: "string",
|
|
1000
|
+
description: "Resolve an art file by component reference or component basename"
|
|
1001
|
+
},
|
|
1002
|
+
query: {
|
|
1003
|
+
type: "string",
|
|
1004
|
+
description: "Fuzzy-search an art file before converting to CSF"
|
|
1005
|
+
},
|
|
1006
|
+
ref: {
|
|
1007
|
+
type: "string",
|
|
1008
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
1009
|
+
}
|
|
1010
|
+
},
|
|
1011
|
+
required: []
|
|
972
1012
|
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
if (item.variantNames.length > 0) markdown += ` Variants: ${item.variantNames.join(", ")}\n`;
|
|
1072
|
-
if (item.tags.length > 0) markdown += ` Tags: ${item.tags.join(", ")}\n`;
|
|
1013
|
+
},
|
|
1014
|
+
{
|
|
1015
|
+
name: "generate_docs",
|
|
1016
|
+
description: "Generate Markdown documentation for a design-system component from its .art.vue definition.",
|
|
1017
|
+
inputSchema: {
|
|
1018
|
+
type: "object",
|
|
1019
|
+
properties: {
|
|
1020
|
+
path: {
|
|
1021
|
+
type: "string",
|
|
1022
|
+
description: "Path to the .art.vue file (relative to project root)"
|
|
1023
|
+
},
|
|
1024
|
+
title: {
|
|
1025
|
+
type: "string",
|
|
1026
|
+
description: "Resolve an art file by title"
|
|
1027
|
+
},
|
|
1028
|
+
component: {
|
|
1029
|
+
type: "string",
|
|
1030
|
+
description: "Resolve an art file by component reference or component basename"
|
|
1031
|
+
},
|
|
1032
|
+
query: {
|
|
1033
|
+
type: "string",
|
|
1034
|
+
description: "Fuzzy-search an art file before generating docs"
|
|
1035
|
+
},
|
|
1036
|
+
ref: {
|
|
1037
|
+
type: "string",
|
|
1038
|
+
description: "Generic art-file reference: path, title, component name, or search text"
|
|
1039
|
+
},
|
|
1040
|
+
includeSource: {
|
|
1041
|
+
type: "boolean",
|
|
1042
|
+
description: "Embed source code in the output (default: false)"
|
|
1043
|
+
},
|
|
1044
|
+
includeTemplates: {
|
|
1045
|
+
type: "boolean",
|
|
1046
|
+
description: "Embed variant templates in the output (default: false)"
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
required: []
|
|
1050
|
+
}
|
|
1051
|
+
},
|
|
1052
|
+
{
|
|
1053
|
+
name: "generate_catalog",
|
|
1054
|
+
description: "Produce a single Markdown catalog covering every component in the design system, grouped by category.",
|
|
1055
|
+
inputSchema: {
|
|
1056
|
+
type: "object",
|
|
1057
|
+
properties: {
|
|
1058
|
+
includeSource: {
|
|
1059
|
+
type: "boolean",
|
|
1060
|
+
description: "Embed source code in the catalog (default: false)"
|
|
1061
|
+
},
|
|
1062
|
+
includeTemplates: {
|
|
1063
|
+
type: "boolean",
|
|
1064
|
+
description: "Embed variant templates in the catalog (default: false)"
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
name: "get_tokens",
|
|
1071
|
+
description: "Read design tokens (colors, spacing, typography, etc.) from a Style Dictionary-compatible JSON file or directory. Auto-detects common paths if not specified.",
|
|
1072
|
+
inputSchema: {
|
|
1073
|
+
type: "object",
|
|
1074
|
+
properties: {
|
|
1075
|
+
tokensPath: {
|
|
1076
|
+
type: "string",
|
|
1077
|
+
description: "Path to tokens JSON file or directory (relative to project root). Auto-detects tokens/, design-tokens/, or style-dictionary/ if omitted."
|
|
1078
|
+
},
|
|
1079
|
+
format: {
|
|
1080
|
+
type: "string",
|
|
1081
|
+
enum: ["json", "markdown"],
|
|
1082
|
+
description: "Output format (default: json)"
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
name: "search_tokens",
|
|
1089
|
+
description: "Search flattened design tokens by token name, category path, value, or description. Much more practical than loading the full token tree for large systems.",
|
|
1090
|
+
inputSchema: {
|
|
1091
|
+
type: "object",
|
|
1092
|
+
properties: {
|
|
1093
|
+
query: {
|
|
1094
|
+
type: "string",
|
|
1095
|
+
description: "Search query"
|
|
1096
|
+
},
|
|
1097
|
+
tokensPath: {
|
|
1098
|
+
type: "string",
|
|
1099
|
+
description: "Path to tokens JSON file or directory (relative to project root). Auto-detects common locations if omitted."
|
|
1100
|
+
},
|
|
1101
|
+
type: {
|
|
1102
|
+
type: "string",
|
|
1103
|
+
description: "Optional token type filter, e.g. color, dimension, typography"
|
|
1104
|
+
},
|
|
1105
|
+
limit: {
|
|
1106
|
+
type: "number",
|
|
1107
|
+
description: "Maximum number of matches to return (default: 20)"
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
required: ["query"]
|
|
1073
1111
|
}
|
|
1074
|
-
markdown += "\n";
|
|
1075
|
-
}
|
|
1076
|
-
return markdown;
|
|
1077
|
-
}
|
|
1078
|
-
function buildIndexSummary(ctx, arts) {
|
|
1079
|
-
const categories = /* @__PURE__ */ new Map();
|
|
1080
|
-
const tags = /* @__PURE__ */ new Map();
|
|
1081
|
-
for (const art of arts) {
|
|
1082
|
-
categories.set(art.category || "Uncategorized", (categories.get(art.category || "Uncategorized") ?? 0) + 1);
|
|
1083
|
-
for (const tag of art.tags) tags.set(tag, (tags.get(tag) ?? 0) + 1);
|
|
1084
1112
|
}
|
|
1085
|
-
|
|
1086
|
-
componentCount: arts.length,
|
|
1087
|
-
categories: Array.from(categories.entries()).map(([name, count]) => ({
|
|
1088
|
-
name,
|
|
1089
|
-
count
|
|
1090
|
-
})).sort((left, right) => left.name.localeCompare(right.name)),
|
|
1091
|
-
tags: Array.from(tags.entries()).map(([name, count]) => ({
|
|
1092
|
-
name,
|
|
1093
|
-
count
|
|
1094
|
-
})).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
|
|
1095
|
-
components: arts.slice().sort((left, right) => left.title.localeCompare(right.title)).map((art) => ({
|
|
1096
|
-
path: toProjectPath(ctx.projectRoot, art.path),
|
|
1097
|
-
title: art.title,
|
|
1098
|
-
description: art.description,
|
|
1099
|
-
component: art.component,
|
|
1100
|
-
category: art.category,
|
|
1101
|
-
status: art.status,
|
|
1102
|
-
tags: art.tags,
|
|
1103
|
-
variantCount: art.variantCount,
|
|
1104
|
-
variantNames: art.variantNames,
|
|
1105
|
-
defaultVariant: art.defaultVariant
|
|
1106
|
-
}))
|
|
1107
|
-
};
|
|
1108
|
-
}
|
|
1109
|
-
function getProjectPath(projectRoot, absolutePath) {
|
|
1110
|
-
return toProjectPath(projectRoot, absolutePath);
|
|
1111
|
-
}
|
|
1113
|
+
];
|
|
1112
1114
|
//#endregion
|
|
1113
1115
|
//#region src/tools/handler/analysis.ts
|
|
1114
1116
|
/**
|
|
@@ -1835,7 +1837,7 @@ function createMuseaServer(config) {
|
|
|
1835
1837
|
const now = Date.now();
|
|
1836
1838
|
if (now - lastScanTime < 5e3 && artCache.size > 0) return artCache;
|
|
1837
1839
|
const binding = loadNative();
|
|
1838
|
-
const files = await findArtFiles(projectRoot, include, exclude);
|
|
1840
|
+
const files = (await findArtFiles(projectRoot, include, exclude)).filter((file) => isProjectPath(projectRoot, file));
|
|
1839
1841
|
artCache = /* @__PURE__ */ new Map();
|
|
1840
1842
|
for (const file of files) try {
|
|
1841
1843
|
const source = await fs.promises.readFile(file, "utf-8");
|