@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.
@@ -19,1096 +19,1098 @@ function loadNative() {
19
19
  }
20
20
  }
21
21
  //#endregion
22
- //#region src/scanner.ts
23
- async function findArtFiles(root, include, exclude) {
24
- const files = [];
25
- async function scan(dir) {
26
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
27
- for (const entry of entries) {
28
- const fullPath = path.join(dir, entry.name);
29
- const relative = normalizePath(path.relative(root, fullPath));
30
- let excluded = false;
31
- for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
32
- excluded = true;
33
- break;
34
- }
35
- if (excluded) continue;
36
- if (entry.isDirectory()) await scan(fullPath);
37
- else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
38
- for (const pattern of include) if (matchGlob(relative, pattern)) {
39
- files.push(fullPath);
40
- break;
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
- await scan(root);
46
- return files;
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 normalizePath(value) {
49
- return value.replaceAll(path.sep, "/");
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 matchGlob(filepath, pattern) {
52
- return globToRegExp(pattern).test(normalizePath(filepath));
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 globToRegExp(pattern) {
55
- const normalized = normalizePath(pattern);
56
- if (normalized.endsWith("/**")) return new RegExp(`^${globSource(normalized.slice(0, -3))}(?:/.*)?$`);
57
- return new RegExp(`^${globSource(normalized)}$`);
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
- function globSource(pattern) {
60
- let source = "";
61
- for (let index = 0; index < pattern.length;) {
62
- const char = pattern[index];
63
- const next = pattern[index + 1];
64
- const afterNext = pattern[index + 2];
65
- if (char === "*" && next === "*" && afterNext === "/") {
66
- source += "(?:.*/)?";
67
- index += 3;
68
- continue;
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 (char === "*" && next === "*") {
71
- source += ".*";
72
- index += 2;
73
- continue;
199
+ if (tag.includes(normalizedQuery)) {
200
+ addScore(reasons, "tag match", 80, scoreRef);
201
+ break;
74
202
  }
75
- if (char === "*") {
76
- source += "[^/]*";
77
- index += 1;
78
- continue;
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 (char === "?") {
81
- source += "[^/]";
82
- index += 1;
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
- return source;
89
- }
90
- function escapeRegExp(value) {
91
- return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
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
- name: "list_components",
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
- name: "get_component",
213
- description: "Get full details of a design-system component: metadata, variants, source-component analysis, palette data, documentation, and related resource URIs.",
214
- inputSchema: {
215
- type: "object",
216
- properties: {
217
- path: {
218
- type: "string",
219
- description: "Path to the .art.vue file (relative to project root)"
220
- },
221
- title: {
222
- type: "string",
223
- description: "Resolve an art file by display title instead of path"
224
- },
225
- component: {
226
- type: "string",
227
- description: "Resolve an art file by component reference or component basename"
228
- },
229
- query: {
230
- type: "string",
231
- description: "Fuzzy-search an art file before loading details"
232
- },
233
- ref: {
234
- type: "string",
235
- description: "Generic art-file reference: path, title, component name, or search text"
236
- },
237
- includeAnalysis: {
238
- type: "boolean",
239
- description: "Include resolved component props/emits analysis (default: true)"
240
- },
241
- includePalette: {
242
- type: "boolean",
243
- description: "Include inferred palette data (default: true)"
244
- },
245
- includeDocumentation: {
246
- type: "boolean",
247
- description: "Include generated Markdown docs inline (default: false)"
248
- }
249
- },
250
- required: []
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
- name: "get_variant",
255
- description: "Retrieve a single variant (template and metadata) from a component, resolving the component by path, title, component name, or fuzzy query.",
256
- inputSchema: {
257
- type: "object",
258
- properties: {
259
- path: {
260
- type: "string",
261
- description: "Path to the .art.vue file"
262
- },
263
- title: {
264
- type: "string",
265
- description: "Resolve an art file by title"
266
- },
267
- component: {
268
- type: "string",
269
- description: "Resolve an art file by component reference or component basename"
270
- },
271
- query: {
272
- type: "string",
273
- description: "Fuzzy-search an art file before looking up the variant"
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
- name: "search_components",
293
- description: "Ranked full-text search over component titles, descriptions, categories, tags, component names, and variant names.",
294
- inputSchema: {
295
- type: "object",
296
- properties: {
297
- query: {
298
- type: "string",
299
- description: "Search query"
300
- },
301
- category: {
302
- type: "string",
303
- description: "Restrict matches to one category"
304
- },
305
- tag: {
306
- type: "string",
307
- description: "Restrict matches to one tag"
308
- },
309
- status: {
310
- type: "string",
311
- enum: [
312
- "draft",
313
- "ready",
314
- "deprecated"
315
- ],
316
- description: "Restrict matches to one status"
317
- },
318
- component: {
319
- type: "string",
320
- description: "Restrict matches to a component reference/basename before searching"
321
- },
322
- limit: {
323
- type: "number",
324
- description: "Maximum number of results to return (default: 10)"
325
- }
326
- },
327
- required: ["query"]
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
- name: "recommend_components",
332
- description: "Intent-oriented component recommendation. Useful when the user describes a task or UX goal rather than knowing exact component names.",
333
- inputSchema: {
334
- type: "object",
335
- properties: {
336
- task: {
337
- type: "string",
338
- description: "Intent or UI task to solve"
339
- },
340
- category: {
341
- type: "string",
342
- description: "Optional category filter"
343
- },
344
- tag: {
345
- type: "string",
346
- description: "Optional tag filter"
347
- },
348
- status: {
349
- type: "string",
350
- enum: [
351
- "draft",
352
- "ready",
353
- "deprecated"
354
- ],
355
- description: "Optional status filter"
356
- },
357
- component: {
358
- type: "string",
359
- description: "Optional component reference/basename filter"
360
- },
361
- limit: {
362
- type: "number",
363
- description: "Maximum number of recommendations to return (default: 5)"
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
- name: "generate_variants",
371
- description: "Analyze a Vue component's props and auto-generate an .art.vue file containing appropriate variant combinations (default, boolean toggles, enum values, etc.).",
372
- inputSchema: {
373
- type: "object",
374
- properties: {
375
- componentPath: {
376
- type: "string",
377
- description: "Path to the .vue component file (relative to project root)"
378
- },
379
- maxVariants: {
380
- type: "number",
381
- description: "Maximum number of variants to generate (default: 20)"
382
- },
383
- includeDefault: {
384
- type: "boolean",
385
- description: "Include a default variant (default: true)"
386
- },
387
- includeBooleanToggles: {
388
- type: "boolean",
389
- description: "Generate variants that toggle each boolean prop (default: true)"
390
- },
391
- includeEnumVariants: {
392
- type: "boolean",
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
- name: "generate_csf",
401
- description: "Convert an .art.vue file into Storybook CSF 3.0 code for integration with existing Storybook setups.",
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
- name: "generate_docs",
431
- description: "Generate Markdown documentation for a design-system component from its .art.vue definition.",
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
- name: "generate_catalog",
469
- description: "Produce a single Markdown catalog covering every component in the design system, grouped by category.",
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: "get_tokens",
486
- description: "Read design tokens (colors, spacing, typography, etc.) from a Style Dictionary-compatible JSON file or directory. Auto-detects common paths if not specified.",
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
- tokensPath: {
694
+ path: {
491
695
  type: "string",
492
- description: "Path to tokens JSON file or directory (relative to project root). Auto-detects tokens/, design-tokens/, or style-dictionary/ if omitted."
696
+ description: "Path to the .vue component file or .art.vue file (relative to project root)"
493
697
  },
494
- format: {
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: "Search query"
700
+ description: "Resolve an art file by its display title, then analyze its component source"
511
701
  },
512
- tokensPath: {
702
+ component: {
513
703
  type: "string",
514
- description: "Path to tokens JSON file or directory (relative to project root). Auto-detects common locations if omitted."
704
+ description: "Resolve an art file by its component reference or component basename, then analyze it"
515
705
  },
516
- type: {
706
+ query: {
517
707
  type: "string",
518
- description: "Optional token type filter, e.g. color, dimension, typography"
708
+ description: "Fuzzy-search an art file, then analyze the linked component source"
519
709
  },
520
- limit: {
521
- type: "number",
522
- description: "Maximum number of matches to return (default: 20)"
710
+ ref: {
711
+ type: "string",
712
+ description: "Generic art-file reference: path, title, component name, or search text"
523
713
  }
524
714
  },
525
- required: ["query"]
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
- for (const variant of variantNorms) {
713
- if (variant === normalizedQuery) {
714
- addScore(reasons, "exact variant match", 130, scoreRef);
715
- break;
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
- if (variant.includes(normalizedQuery)) {
718
- addScore(reasons, "variant match", 90, scoreRef);
719
- break;
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
- for (const component of componentNorms) {
723
- if (component === normalizePathLike(query)) {
724
- addScore(reasons, "exact component match", 180, scoreRef);
725
- break;
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
- if (component.includes(normalizePathLike(query))) {
728
- addScore(reasons, "component match", 100, scoreRef);
729
- break;
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
- const terms = tokenize(query);
733
- for (const term of terms) {
734
- if (term === normalizedQuery) continue;
735
- if (titleNorm.includes(term)) addScore(reasons, `title contains "${term}"`, 18, scoreRef);
736
- if (descriptionNorm.includes(term)) addScore(reasons, `description contains "${term}"`, 10, scoreRef);
737
- if (categoryNorm.includes(term)) addScore(reasons, `category contains "${term}"`, 10, scoreRef);
738
- if (tagNorms.some((tag) => tag.includes(term))) addScore(reasons, `tag contains "${term}"`, 16, scoreRef);
739
- if (variantNorms.some((variant) => variant.includes(term))) addScore(reasons, `variant contains "${term}"`, 14, scoreRef);
740
- if (componentNorms.some((component) => component.includes(term))) addScore(reasons, `component contains "${term}"`, 14, scoreRef);
741
- }
742
- if (scoreRef.value <= 0) return null;
743
- return {
744
- info,
745
- relativePath,
746
- score: scoreRef.value,
747
- reasons: Array.from(reasons).slice(0, 5)
748
- };
749
- }
750
- function compareArtResults(left, right) {
751
- 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);
752
- }
753
- async function searchArtInfos(ctx, query, filters) {
754
- const arts = Array.from((await ctx.scanArtFiles()).values());
755
- const category = normalize(filters?.category);
756
- const tag = normalize(filters?.tag);
757
- const status = normalize(filters?.status);
758
- const componentFilter = normalize(filters?.component);
759
- return arts.filter((info) => {
760
- if (category && normalize(info.category) !== category) return false;
761
- if (tag && !info.tags.some((item) => normalize(item) === tag)) return false;
762
- if (status && normalize(info.status) !== status) return false;
763
- if (componentFilter && !getComponentCandidates(info).some((candidate) => normalizePathLike(candidate).includes(componentFilter))) return false;
764
- return true;
765
- }).map((info) => scoreArtInfo(ctx.projectRoot, info, query)).filter((result) => result != null).sort(compareArtResults).slice(0, filters?.limit ?? 10);
766
- }
767
- function buildAlternatives(results) {
768
- return results.slice(1, 4).map((result) => ({
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
- if (componentArg) {
825
- const normalizedComponent = normalizePathLike(componentArg);
826
- const matches = arts.filter((info) => getComponentCandidates(info).some((candidate) => normalizePathLike(candidate) === normalizedComponent));
827
- if (matches.length > 0) {
828
- const primary = matches[0];
829
- return {
830
- info: primary,
831
- absolutePath: primary.path,
832
- relativePath: toProjectPath(ctx.projectRoot, primary.path),
833
- matchedBy: "component",
834
- matchValue: componentArg,
835
- score: 930,
836
- reasons: ["exact component match"],
837
- alternatives: matches.slice(1, 4).map((info) => ({
838
- path: toProjectPath(ctx.projectRoot, info.path),
839
- title: info.title,
840
- component: info.component,
841
- score: 880,
842
- reasons: ["exact component match"]
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
- const queryValue = queryArg ?? refArg ?? pathArg ?? titleArg ?? componentArg;
848
- if (!queryValue) throw new McpError(ErrorCode.InvalidParams, "Provide one of: path, title, component, query, or ref");
849
- const results = await searchArtInfos(ctx, queryValue, { limit: 4 });
850
- if (results.length === 0) throw new McpError(ErrorCode.InvalidParams, `No component matched "${queryValue}". Try list_components or search_components first.`);
851
- const primary = results[0];
852
- return {
853
- info: primary.info,
854
- absolutePath: primary.info.path,
855
- relativePath: primary.relativePath,
856
- matchedBy: queryArg ? "query" : "ref",
857
- matchValue: queryValue,
858
- score: primary.score,
859
- reasons: primary.reasons,
860
- alternatives: buildAlternatives(results)
861
- };
862
- }
863
- function resolveComponentSourcePath(artAbsolutePath, componentReference) {
864
- if (!componentReference) return null;
865
- if (path.isAbsolute(componentReference)) return componentReference;
866
- return path.resolve(path.dirname(artAbsolutePath), componentReference);
867
- }
868
- async function getComponentSourceDescriptor(ctx, resolved) {
869
- const componentPath = resolveComponentSourcePath(resolved.absolutePath, resolved.info.component);
870
- if (!componentPath) return {
871
- reference: resolved.info.component,
872
- exists: false,
873
- error: "This art file does not declare a component source."
874
- };
875
- if (!isProjectPath(ctx.projectRoot, componentPath)) return {
876
- reference: resolved.info.component,
877
- absolutePath: componentPath,
878
- path: componentPath,
879
- exists: false,
880
- error: "Component source is outside the project root."
881
- };
882
- try {
883
- await fs.promises.access(componentPath, fs.constants.R_OK);
884
- return {
885
- reference: resolved.info.component,
886
- absolutePath: componentPath,
887
- path: toProjectPath(ctx.projectRoot, componentPath),
888
- exists: true
889
- };
890
- } catch {
891
- return {
892
- reference: resolved.info.component,
893
- absolutePath: componentPath,
894
- path: toProjectPath(ctx.projectRoot, componentPath),
895
- exists: false,
896
- error: `Component source not found: ${toProjectPath(ctx.projectRoot, componentPath)}`
897
- };
898
- }
899
- }
900
- function normalizeDefaultValue(value) {
901
- if (value === "true") return true;
902
- if (value === "false") return false;
903
- if (typeof value === "string" && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
904
- return value;
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
- async function buildPalette(ctx, binding, resolved, source) {
976
- let palette = null;
977
- if (binding.generateArtPalette) {
978
- const generated = binding.generateArtPalette(source, { filename: resolved.absolutePath });
979
- palette = {
980
- title: generated.title,
981
- controls: generated.controls.map((control) => ({
982
- name: control.name,
983
- control: control.control,
984
- defaultValue: control.default_value,
985
- description: control.description,
986
- required: control.required,
987
- options: control.options,
988
- range: control.range,
989
- group: control.group
990
- })),
991
- groups: generated.groups,
992
- json: generated.json,
993
- typescript: generated.typescript
994
- };
995
- }
996
- if (palette && palette.controls.length > 0) return palette;
997
- const { analysis } = await analyzeResolvedComponent(ctx, binding, resolved);
998
- if (!analysis || analysis.props.length === 0) return palette;
999
- return buildPaletteFromAnalysis(resolved.info.title, analysis);
1000
- }
1001
- async function buildDocumentation(binding, resolved, source, options) {
1002
- if (!binding.generateArtDoc) return null;
1003
- const doc = binding.generateArtDoc(source, { filename: resolved.absolutePath }, {
1004
- include_source: options?.includeSource,
1005
- include_templates: options?.includeTemplates,
1006
- include_metadata: true
1007
- });
1008
- return {
1009
- markdown: formatGeneratedMarkdown(doc.markdown, resolved.info.title || "Component"),
1010
- title: doc.title,
1011
- category: doc.category,
1012
- variantCount: doc.variant_count
1013
- };
1014
- }
1015
- async function buildComponentDetails(ctx, binding, resolved, options) {
1016
- const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
1017
- const parsed = binding.parseArt(source, { filename: resolved.absolutePath });
1018
- const componentState = await analyzeResolvedComponent(ctx, binding, resolved);
1019
- const palette = options?.includePalette === false ? null : await buildPalette(ctx, binding, resolved, source);
1020
- const documentation = options?.includeDocumentation === true ? await buildDocumentation(binding, resolved, source) : null;
1021
- const resourceUris = buildResourceUris$1(resolved.relativePath, parsed.variants.map((variant) => variant.name), Boolean(componentState.source.reference));
1022
- return {
1023
- path: resolved.relativePath,
1024
- match: {
1025
- matchedBy: resolved.matchedBy,
1026
- matchValue: resolved.matchValue,
1027
- score: resolved.score,
1028
- reasons: resolved.reasons,
1029
- alternatives: resolved.alternatives
1030
- },
1031
- metadata: parsed.metadata,
1032
- variants: parsed.variants.map((variant) => ({
1033
- name: variant.name,
1034
- template: variant.template,
1035
- isDefault: variant.is_default,
1036
- skipVrt: variant.skip_vrt
1037
- })),
1038
- defaultVariant: parsed.variants.find((variant) => variant.is_default)?.name,
1039
- variantNames: parsed.variants.map((variant) => variant.name),
1040
- hasScriptSetup: parsed.has_script_setup,
1041
- hasScript: parsed.has_script,
1042
- styleCount: parsed.style_count,
1043
- componentSource: componentState.source,
1044
- componentAnalysis: options?.includeAnalysis === false ? void 0 : componentState.analysis ?? {
1045
- path: componentState.source.path,
1046
- props: [],
1047
- emits: [],
1048
- error: componentState.source.error
1049
- },
1050
- palette,
1051
- documentation,
1052
- resources: resourceUris
1053
- };
1054
- }
1055
- function buildCatalogMarkdown(arts, projectRoot) {
1056
- const grouped = /* @__PURE__ */ new Map();
1057
- for (const art of arts) {
1058
- const category = art.category || "Uncategorized";
1059
- const list = grouped.get(category) ?? [];
1060
- list.push(art);
1061
- grouped.set(category, list);
1062
- }
1063
- let markdown = "# Musea Component Catalog\n\n";
1064
- for (const [category, items] of Array.from(grouped.entries()).sort(([left], [right]) => left.localeCompare(right))) {
1065
- markdown += `## ${category}\n\n`;
1066
- for (const item of items.sort((left, right) => left.title.localeCompare(right.title))) {
1067
- const relativePath = toProjectPath(projectRoot, item.path);
1068
- markdown += `- **${item.title}** \`${relativePath}\``;
1069
- if (item.description) markdown += ` — ${item.description}`;
1070
- markdown += "\n";
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
- return {
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");