@vizejs/musea-mcp-server 0.61.0 → 0.63.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.
@@ -0,0 +1,1840 @@
1
+ import { createRequire } from "node:module";
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ //#region src/native.ts
8
+ let native = null;
9
+ function loadNative() {
10
+ if (native) return native;
11
+ const require = createRequire(import.meta.url);
12
+ try {
13
+ native = require("@vizejs/native");
14
+ return native;
15
+ } catch (e) {
16
+ throw new Error(`Failed to load @vizejs/native. Make sure it's installed: ${String(e)}`);
17
+ }
18
+ }
19
+ //#endregion
20
+ //#region src/scanner.ts
21
+ async function findArtFiles(root, include, exclude) {
22
+ const files = [];
23
+ async function scan(dir) {
24
+ const entries = await fs.promises.readdir(dir, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ const fullPath = path.join(dir, entry.name);
27
+ const relative = path.relative(root, fullPath);
28
+ let excluded = false;
29
+ for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
30
+ excluded = true;
31
+ break;
32
+ }
33
+ if (excluded) continue;
34
+ if (entry.isDirectory()) await scan(fullPath);
35
+ else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
36
+ for (const pattern of include) if (matchGlob(relative, pattern)) {
37
+ files.push(fullPath);
38
+ break;
39
+ }
40
+ }
41
+ }
42
+ }
43
+ await scan(root);
44
+ return files;
45
+ }
46
+ function matchGlob(filepath, pattern) {
47
+ const regex = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*").replace(/\./g, "\\.");
48
+ return new RegExp(`^${regex}$`).test(filepath);
49
+ }
50
+ //#endregion
51
+ //#region src/tools/definitions.ts
52
+ /**
53
+ * MCP tool definitions for Musea.
54
+ *
55
+ * Declares the schema (name, description, input parameters) for each tool
56
+ * exposed by the MCP server: component analysis, registry, code generation,
57
+ * documentation, and design tokens.
58
+ */
59
+ const toolDefinitions = [
60
+ {
61
+ name: "analyze_component",
62
+ 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.",
63
+ inputSchema: {
64
+ type: "object",
65
+ properties: {
66
+ path: {
67
+ type: "string",
68
+ description: "Path to the .vue component file or .art.vue file (relative to project root)"
69
+ },
70
+ title: {
71
+ type: "string",
72
+ description: "Resolve an art file by its display title, then analyze its component source"
73
+ },
74
+ component: {
75
+ type: "string",
76
+ description: "Resolve an art file by its component reference or component basename, then analyze it"
77
+ },
78
+ query: {
79
+ type: "string",
80
+ description: "Fuzzy-search an art file, then analyze the linked component source"
81
+ },
82
+ ref: {
83
+ type: "string",
84
+ description: "Generic art-file reference: path, title, component name, or search text"
85
+ }
86
+ },
87
+ required: []
88
+ }
89
+ },
90
+ {
91
+ name: "get_palette",
92
+ 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.",
93
+ inputSchema: {
94
+ type: "object",
95
+ properties: {
96
+ path: {
97
+ type: "string",
98
+ description: "Path to the .art.vue file (relative to project root)"
99
+ },
100
+ title: {
101
+ type: "string",
102
+ description: "Resolve an art file by title instead of path"
103
+ },
104
+ component: {
105
+ type: "string",
106
+ description: "Resolve an art file by component reference or component basename"
107
+ },
108
+ query: {
109
+ type: "string",
110
+ description: "Fuzzy-search an art file before generating the palette"
111
+ },
112
+ ref: {
113
+ type: "string",
114
+ description: "Generic art-file reference: path, title, component name, or search text"
115
+ }
116
+ },
117
+ required: []
118
+ }
119
+ },
120
+ {
121
+ name: "list_components",
122
+ description: "List components registered in the design system. Returns titles, categories, tags, status, variant names, and related resource URIs.",
123
+ inputSchema: {
124
+ type: "object",
125
+ properties: {
126
+ category: {
127
+ type: "string",
128
+ description: "Filter by category"
129
+ },
130
+ tag: {
131
+ type: "string",
132
+ description: "Filter by tag"
133
+ },
134
+ status: {
135
+ type: "string",
136
+ enum: [
137
+ "draft",
138
+ "ready",
139
+ "deprecated"
140
+ ],
141
+ description: "Filter by status badge"
142
+ },
143
+ component: {
144
+ type: "string",
145
+ description: "Filter by component reference or component basename"
146
+ },
147
+ limit: {
148
+ type: "number",
149
+ description: "Maximum number of components to return (default: all)"
150
+ },
151
+ includeVariants: {
152
+ type: "boolean",
153
+ description: "Include per-variant metadata in the result (default: false)"
154
+ },
155
+ sortBy: {
156
+ type: "string",
157
+ enum: [
158
+ "title",
159
+ "category",
160
+ "status",
161
+ "variants"
162
+ ],
163
+ description: "Sort order for the result list (default: title)"
164
+ }
165
+ }
166
+ }
167
+ },
168
+ {
169
+ name: "get_component",
170
+ description: "Get full details of a design-system component: metadata, variants, source-component analysis, palette data, documentation, and related resource URIs.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ path: {
175
+ type: "string",
176
+ description: "Path to the .art.vue file (relative to project root)"
177
+ },
178
+ title: {
179
+ type: "string",
180
+ description: "Resolve an art file by display title instead of path"
181
+ },
182
+ component: {
183
+ type: "string",
184
+ description: "Resolve an art file by component reference or component basename"
185
+ },
186
+ query: {
187
+ type: "string",
188
+ description: "Fuzzy-search an art file before loading details"
189
+ },
190
+ ref: {
191
+ type: "string",
192
+ description: "Generic art-file reference: path, title, component name, or search text"
193
+ },
194
+ includeAnalysis: {
195
+ type: "boolean",
196
+ description: "Include resolved component props/emits analysis (default: true)"
197
+ },
198
+ includePalette: {
199
+ type: "boolean",
200
+ description: "Include inferred palette data (default: true)"
201
+ },
202
+ includeDocumentation: {
203
+ type: "boolean",
204
+ description: "Include generated Markdown docs inline (default: false)"
205
+ }
206
+ },
207
+ required: []
208
+ }
209
+ },
210
+ {
211
+ name: "get_variant",
212
+ description: "Retrieve a single variant (template and metadata) from a component, resolving the component by path, title, component name, or fuzzy query.",
213
+ inputSchema: {
214
+ type: "object",
215
+ properties: {
216
+ path: {
217
+ type: "string",
218
+ description: "Path to the .art.vue file"
219
+ },
220
+ title: {
221
+ type: "string",
222
+ description: "Resolve an art file by title"
223
+ },
224
+ component: {
225
+ type: "string",
226
+ description: "Resolve an art file by component reference or component basename"
227
+ },
228
+ query: {
229
+ type: "string",
230
+ description: "Fuzzy-search an art file before looking up the variant"
231
+ },
232
+ ref: {
233
+ type: "string",
234
+ description: "Generic art-file reference: path, title, component name, or search text"
235
+ },
236
+ variant: {
237
+ type: "string",
238
+ description: "Variant name"
239
+ },
240
+ includeAnalysis: {
241
+ type: "boolean",
242
+ description: "Include resolved component props/emits analysis (default: false)"
243
+ }
244
+ },
245
+ required: ["variant"]
246
+ }
247
+ },
248
+ {
249
+ name: "search_components",
250
+ description: "Ranked full-text search over component titles, descriptions, categories, tags, component names, and variant names.",
251
+ inputSchema: {
252
+ type: "object",
253
+ properties: {
254
+ query: {
255
+ type: "string",
256
+ description: "Search query"
257
+ },
258
+ category: {
259
+ type: "string",
260
+ description: "Restrict matches to one category"
261
+ },
262
+ tag: {
263
+ type: "string",
264
+ description: "Restrict matches to one tag"
265
+ },
266
+ status: {
267
+ type: "string",
268
+ enum: [
269
+ "draft",
270
+ "ready",
271
+ "deprecated"
272
+ ],
273
+ description: "Restrict matches to one status"
274
+ },
275
+ component: {
276
+ type: "string",
277
+ description: "Restrict matches to a component reference/basename before searching"
278
+ },
279
+ limit: {
280
+ type: "number",
281
+ description: "Maximum number of results to return (default: 10)"
282
+ }
283
+ },
284
+ required: ["query"]
285
+ }
286
+ },
287
+ {
288
+ name: "recommend_components",
289
+ description: "Intent-oriented component recommendation. Useful when the user describes a task or UX goal rather than knowing exact component names.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ task: {
294
+ type: "string",
295
+ description: "Intent or UI task to solve"
296
+ },
297
+ category: {
298
+ type: "string",
299
+ description: "Optional category filter"
300
+ },
301
+ tag: {
302
+ type: "string",
303
+ description: "Optional tag filter"
304
+ },
305
+ status: {
306
+ type: "string",
307
+ enum: [
308
+ "draft",
309
+ "ready",
310
+ "deprecated"
311
+ ],
312
+ description: "Optional status filter"
313
+ },
314
+ component: {
315
+ type: "string",
316
+ description: "Optional component reference/basename filter"
317
+ },
318
+ limit: {
319
+ type: "number",
320
+ description: "Maximum number of recommendations to return (default: 5)"
321
+ }
322
+ },
323
+ required: ["task"]
324
+ }
325
+ },
326
+ {
327
+ name: "generate_variants",
328
+ description: "Analyze a Vue component's props and auto-generate an .art.vue file containing appropriate variant combinations (default, boolean toggles, enum values, etc.).",
329
+ inputSchema: {
330
+ type: "object",
331
+ properties: {
332
+ componentPath: {
333
+ type: "string",
334
+ description: "Path to the .vue component file (relative to project root)"
335
+ },
336
+ maxVariants: {
337
+ type: "number",
338
+ description: "Maximum number of variants to generate (default: 20)"
339
+ },
340
+ includeDefault: {
341
+ type: "boolean",
342
+ description: "Include a default variant (default: true)"
343
+ },
344
+ includeBooleanToggles: {
345
+ type: "boolean",
346
+ description: "Generate variants that toggle each boolean prop (default: true)"
347
+ },
348
+ includeEnumVariants: {
349
+ type: "boolean",
350
+ description: "Generate one variant per enum/union value (default: true)"
351
+ }
352
+ },
353
+ required: ["componentPath"]
354
+ }
355
+ },
356
+ {
357
+ name: "generate_csf",
358
+ description: "Convert an .art.vue file into Storybook CSF 3.0 code for integration with existing Storybook setups.",
359
+ inputSchema: {
360
+ type: "object",
361
+ properties: {
362
+ path: {
363
+ type: "string",
364
+ description: "Path to the .art.vue file"
365
+ },
366
+ title: {
367
+ type: "string",
368
+ description: "Resolve an art file by title"
369
+ },
370
+ component: {
371
+ type: "string",
372
+ description: "Resolve an art file by component reference or component basename"
373
+ },
374
+ query: {
375
+ type: "string",
376
+ description: "Fuzzy-search an art file before converting to CSF"
377
+ },
378
+ ref: {
379
+ type: "string",
380
+ description: "Generic art-file reference: path, title, component name, or search text"
381
+ }
382
+ },
383
+ required: []
384
+ }
385
+ },
386
+ {
387
+ name: "generate_docs",
388
+ description: "Generate Markdown documentation for a design-system component from its .art.vue definition.",
389
+ inputSchema: {
390
+ type: "object",
391
+ properties: {
392
+ path: {
393
+ type: "string",
394
+ description: "Path to the .art.vue file (relative to project root)"
395
+ },
396
+ title: {
397
+ type: "string",
398
+ description: "Resolve an art file by title"
399
+ },
400
+ component: {
401
+ type: "string",
402
+ description: "Resolve an art file by component reference or component basename"
403
+ },
404
+ query: {
405
+ type: "string",
406
+ description: "Fuzzy-search an art file before generating docs"
407
+ },
408
+ ref: {
409
+ type: "string",
410
+ description: "Generic art-file reference: path, title, component name, or search text"
411
+ },
412
+ includeSource: {
413
+ type: "boolean",
414
+ description: "Embed source code in the output (default: false)"
415
+ },
416
+ includeTemplates: {
417
+ type: "boolean",
418
+ description: "Embed variant templates in the output (default: false)"
419
+ }
420
+ },
421
+ required: []
422
+ }
423
+ },
424
+ {
425
+ name: "generate_catalog",
426
+ description: "Produce a single Markdown catalog covering every component in the design system, grouped by category.",
427
+ inputSchema: {
428
+ type: "object",
429
+ properties: {
430
+ includeSource: {
431
+ type: "boolean",
432
+ description: "Embed source code in the catalog (default: false)"
433
+ },
434
+ includeTemplates: {
435
+ type: "boolean",
436
+ description: "Embed variant templates in the catalog (default: false)"
437
+ }
438
+ }
439
+ }
440
+ },
441
+ {
442
+ name: "get_tokens",
443
+ description: "Read design tokens (colors, spacing, typography, etc.) from a Style Dictionary-compatible JSON file or directory. Auto-detects common paths if not specified.",
444
+ inputSchema: {
445
+ type: "object",
446
+ properties: {
447
+ tokensPath: {
448
+ type: "string",
449
+ description: "Path to tokens JSON file or directory (relative to project root). Auto-detects tokens/, design-tokens/, or style-dictionary/ if omitted."
450
+ },
451
+ format: {
452
+ type: "string",
453
+ enum: ["json", "markdown"],
454
+ description: "Output format (default: json)"
455
+ }
456
+ }
457
+ }
458
+ },
459
+ {
460
+ name: "search_tokens",
461
+ 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.",
462
+ inputSchema: {
463
+ type: "object",
464
+ properties: {
465
+ query: {
466
+ type: "string",
467
+ description: "Search query"
468
+ },
469
+ tokensPath: {
470
+ type: "string",
471
+ description: "Path to tokens JSON file or directory (relative to project root). Auto-detects common locations if omitted."
472
+ },
473
+ type: {
474
+ type: "string",
475
+ description: "Optional token type filter, e.g. color, dimension, typography"
476
+ },
477
+ limit: {
478
+ type: "number",
479
+ description: "Maximum number of matches to return (default: 20)"
480
+ }
481
+ },
482
+ required: ["query"]
483
+ }
484
+ }
485
+ ];
486
+ //#endregion
487
+ //#region src/musea.ts
488
+ function normalize(value) {
489
+ return value?.trim().toLowerCase() ?? "";
490
+ }
491
+ function normalizePathLike(value) {
492
+ return value.replace(/\\/g, "/").toLowerCase();
493
+ }
494
+ function tokenize(query) {
495
+ return Array.from(new Set(query.toLowerCase().split(/[\s/,_-]+/).map((term) => term.trim()).filter(Boolean)));
496
+ }
497
+ function toProjectPath(projectRoot, absolutePath) {
498
+ const relativePath = path.relative(projectRoot, absolutePath);
499
+ if (!relativePath || relativePath.startsWith("..") || path.isAbsolute(relativePath)) return absolutePath;
500
+ return relativePath;
501
+ }
502
+ function buildResourceUris$1(relativePath, variantNames, hasComponentSource) {
503
+ const encodedPath = encodeURIComponent(relativePath);
504
+ return {
505
+ component: `musea://component/${encodedPath}`,
506
+ docs: `musea://docs/${encodedPath}`,
507
+ source: `musea://source/${encodedPath}`,
508
+ componentSource: hasComponentSource ? `musea://component-source/${encodedPath}` : void 0,
509
+ variants: variantNames.map((variantName) => ({
510
+ name: variantName,
511
+ uri: `musea://variant/${encodedPath}/${encodeURIComponent(variantName)}`
512
+ }))
513
+ };
514
+ }
515
+ function addScore(reasons, reason, amount, scoreRef) {
516
+ reasons.add(reason);
517
+ scoreRef.value += amount;
518
+ }
519
+ function getComponentCandidates(info) {
520
+ const candidates = /* @__PURE__ */ new Set();
521
+ if (info.component) {
522
+ candidates.add(info.component);
523
+ candidates.add(path.basename(info.component));
524
+ candidates.add(path.basename(info.component, path.extname(info.component)));
525
+ }
526
+ return Array.from(candidates);
527
+ }
528
+ function scoreArtInfo(projectRoot, info, query) {
529
+ const normalizedQuery = normalize(query);
530
+ if (!normalizedQuery) return null;
531
+ const relativePath = toProjectPath(projectRoot, info.path);
532
+ const relativePathNorm = normalizePathLike(relativePath);
533
+ const titleNorm = normalize(info.title);
534
+ const descriptionNorm = normalize(info.description);
535
+ const categoryNorm = normalize(info.category);
536
+ const tagNorms = info.tags.map((tag) => normalize(tag));
537
+ const variantNorms = info.variantNames.map((variantName) => normalize(variantName));
538
+ const componentNorms = getComponentCandidates(info).map((component) => normalizePathLike(component));
539
+ const reasons = /* @__PURE__ */ new Set();
540
+ const scoreRef = { value: 0 };
541
+ if (relativePathNorm === normalizePathLike(query)) addScore(reasons, "exact path match", 220, scoreRef);
542
+ else if (relativePathNorm.includes(normalizePathLike(query))) addScore(reasons, "path match", 70, scoreRef);
543
+ if (titleNorm === normalizedQuery) addScore(reasons, "exact title match", 200, scoreRef);
544
+ else if (titleNorm.startsWith(normalizedQuery)) addScore(reasons, "title prefix match", 140, scoreRef);
545
+ else if (titleNorm.includes(normalizedQuery)) addScore(reasons, "title match", 110, scoreRef);
546
+ if (categoryNorm === normalizedQuery) addScore(reasons, "exact category match", 120, scoreRef);
547
+ else if (categoryNorm.includes(normalizedQuery)) addScore(reasons, "category match", 55, scoreRef);
548
+ if (descriptionNorm.includes(normalizedQuery)) addScore(reasons, "description match", 60, scoreRef);
549
+ for (const tag of tagNorms) {
550
+ if (tag === normalizedQuery) {
551
+ addScore(reasons, "exact tag match", 130, scoreRef);
552
+ break;
553
+ }
554
+ if (tag.includes(normalizedQuery)) {
555
+ addScore(reasons, "tag match", 80, scoreRef);
556
+ break;
557
+ }
558
+ }
559
+ for (const variant of variantNorms) {
560
+ if (variant === normalizedQuery) {
561
+ addScore(reasons, "exact variant match", 130, scoreRef);
562
+ break;
563
+ }
564
+ if (variant.includes(normalizedQuery)) {
565
+ addScore(reasons, "variant match", 90, scoreRef);
566
+ break;
567
+ }
568
+ }
569
+ for (const component of componentNorms) {
570
+ if (component === normalizePathLike(query)) {
571
+ addScore(reasons, "exact component match", 180, scoreRef);
572
+ break;
573
+ }
574
+ if (component.includes(normalizePathLike(query))) {
575
+ addScore(reasons, "component match", 100, scoreRef);
576
+ break;
577
+ }
578
+ }
579
+ const terms = tokenize(query);
580
+ for (const term of terms) {
581
+ if (term === normalizedQuery) continue;
582
+ if (titleNorm.includes(term)) addScore(reasons, `title contains "${term}"`, 18, scoreRef);
583
+ if (descriptionNorm.includes(term)) addScore(reasons, `description contains "${term}"`, 10, scoreRef);
584
+ if (categoryNorm.includes(term)) addScore(reasons, `category contains "${term}"`, 10, scoreRef);
585
+ if (tagNorms.some((tag) => tag.includes(term))) addScore(reasons, `tag contains "${term}"`, 16, scoreRef);
586
+ if (variantNorms.some((variant) => variant.includes(term))) addScore(reasons, `variant contains "${term}"`, 14, scoreRef);
587
+ if (componentNorms.some((component) => component.includes(term))) addScore(reasons, `component contains "${term}"`, 14, scoreRef);
588
+ }
589
+ if (scoreRef.value <= 0) return null;
590
+ return {
591
+ info,
592
+ relativePath,
593
+ score: scoreRef.value,
594
+ reasons: Array.from(reasons).slice(0, 5)
595
+ };
596
+ }
597
+ function compareArtResults(left, right) {
598
+ 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);
599
+ }
600
+ async function searchArtInfos(ctx, query, filters) {
601
+ const arts = Array.from((await ctx.scanArtFiles()).values());
602
+ const category = normalize(filters?.category);
603
+ const tag = normalize(filters?.tag);
604
+ const status = normalize(filters?.status);
605
+ const componentFilter = normalize(filters?.component);
606
+ return arts.filter((info) => {
607
+ if (category && normalize(info.category) !== category) return false;
608
+ if (tag && !info.tags.some((item) => normalize(item) === tag)) return false;
609
+ if (status && normalize(info.status) !== status) return false;
610
+ if (componentFilter && !getComponentCandidates(info).some((candidate) => normalizePathLike(candidate).includes(componentFilter))) return false;
611
+ return true;
612
+ }).map((info) => scoreArtInfo(ctx.projectRoot, info, query)).filter((result) => result != null).sort(compareArtResults).slice(0, filters?.limit ?? 10);
613
+ }
614
+ function buildAlternatives(results) {
615
+ return results.slice(1, 4).map((result) => ({
616
+ path: result.relativePath,
617
+ title: result.info.title,
618
+ component: result.info.component,
619
+ score: result.score,
620
+ reasons: result.reasons
621
+ }));
622
+ }
623
+ async function resolveArtReference(ctx, args) {
624
+ const arts = Array.from((await ctx.scanArtFiles()).values());
625
+ const pathArg = typeof args?.path === "string" ? args.path : void 0;
626
+ const titleArg = typeof args?.title === "string" ? args.title : void 0;
627
+ const componentArg = typeof args?.component === "string" ? args.component : void 0;
628
+ const queryArg = typeof args?.query === "string" ? args.query : void 0;
629
+ const refArg = typeof args?.ref === "string" ? args.ref : void 0;
630
+ if (pathArg) {
631
+ const resolvedPath = path.isAbsolute(pathArg) ? pathArg : path.resolve(ctx.projectRoot, pathArg);
632
+ const normalizedResolvedPath = normalizePathLike(resolvedPath);
633
+ const normalizedRelativePath = normalizePathLike(path.relative(ctx.projectRoot, resolvedPath));
634
+ const directMatch = arts.find((info) => {
635
+ const infoRelativePath = normalizePathLike(path.relative(ctx.projectRoot, info.path));
636
+ return normalizePathLike(info.path) === normalizedResolvedPath || infoRelativePath === normalizedRelativePath;
637
+ });
638
+ if (directMatch) return {
639
+ info: directMatch,
640
+ absolutePath: directMatch.path,
641
+ relativePath: toProjectPath(ctx.projectRoot, directMatch.path),
642
+ matchedBy: "path",
643
+ matchValue: pathArg,
644
+ score: 999,
645
+ reasons: ["exact path match"],
646
+ alternatives: []
647
+ };
648
+ }
649
+ if (titleArg) {
650
+ const matches = arts.filter((info) => normalize(info.title) === normalize(titleArg));
651
+ if (matches.length > 0) {
652
+ const primary = matches[0];
653
+ return {
654
+ info: primary,
655
+ absolutePath: primary.path,
656
+ relativePath: toProjectPath(ctx.projectRoot, primary.path),
657
+ matchedBy: "title",
658
+ matchValue: titleArg,
659
+ score: 950,
660
+ reasons: ["exact title match"],
661
+ alternatives: matches.slice(1, 4).map((info) => ({
662
+ path: toProjectPath(ctx.projectRoot, info.path),
663
+ title: info.title,
664
+ component: info.component,
665
+ score: 900,
666
+ reasons: ["exact title match"]
667
+ }))
668
+ };
669
+ }
670
+ }
671
+ if (componentArg) {
672
+ const normalizedComponent = normalizePathLike(componentArg);
673
+ const matches = arts.filter((info) => getComponentCandidates(info).some((candidate) => normalizePathLike(candidate) === normalizedComponent));
674
+ if (matches.length > 0) {
675
+ const primary = matches[0];
676
+ return {
677
+ info: primary,
678
+ absolutePath: primary.path,
679
+ relativePath: toProjectPath(ctx.projectRoot, primary.path),
680
+ matchedBy: "component",
681
+ matchValue: componentArg,
682
+ score: 930,
683
+ reasons: ["exact component match"],
684
+ alternatives: matches.slice(1, 4).map((info) => ({
685
+ path: toProjectPath(ctx.projectRoot, info.path),
686
+ title: info.title,
687
+ component: info.component,
688
+ score: 880,
689
+ reasons: ["exact component match"]
690
+ }))
691
+ };
692
+ }
693
+ }
694
+ const queryValue = queryArg ?? refArg ?? pathArg ?? titleArg ?? componentArg;
695
+ if (!queryValue) throw new McpError(ErrorCode.InvalidParams, "Provide one of: path, title, component, query, or ref");
696
+ const results = await searchArtInfos(ctx, queryValue, { limit: 4 });
697
+ if (results.length === 0) throw new McpError(ErrorCode.InvalidParams, `No component matched "${queryValue}". Try list_components or search_components first.`);
698
+ const primary = results[0];
699
+ return {
700
+ info: primary.info,
701
+ absolutePath: primary.info.path,
702
+ relativePath: primary.relativePath,
703
+ matchedBy: queryArg ? "query" : "ref",
704
+ matchValue: queryValue,
705
+ score: primary.score,
706
+ reasons: primary.reasons,
707
+ alternatives: buildAlternatives(results)
708
+ };
709
+ }
710
+ function resolveComponentSourcePath(artAbsolutePath, componentReference) {
711
+ if (!componentReference) return null;
712
+ if (path.isAbsolute(componentReference)) return componentReference;
713
+ return path.resolve(path.dirname(artAbsolutePath), componentReference);
714
+ }
715
+ async function getComponentSourceDescriptor(ctx, resolved) {
716
+ const componentPath = resolveComponentSourcePath(resolved.absolutePath, resolved.info.component);
717
+ if (!componentPath) return {
718
+ reference: resolved.info.component,
719
+ exists: false,
720
+ error: "This art file does not declare a component source."
721
+ };
722
+ try {
723
+ await fs.promises.access(componentPath, fs.constants.R_OK);
724
+ return {
725
+ reference: resolved.info.component,
726
+ absolutePath: componentPath,
727
+ path: toProjectPath(ctx.projectRoot, componentPath),
728
+ exists: true
729
+ };
730
+ } catch {
731
+ return {
732
+ reference: resolved.info.component,
733
+ absolutePath: componentPath,
734
+ path: toProjectPath(ctx.projectRoot, componentPath),
735
+ exists: false,
736
+ error: `Component source not found: ${toProjectPath(ctx.projectRoot, componentPath)}`
737
+ };
738
+ }
739
+ }
740
+ function normalizeDefaultValue(value) {
741
+ if (value === "true") return true;
742
+ if (value === "false") return false;
743
+ if (typeof value === "string" && (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'"))) return value.slice(1, -1);
744
+ return value;
745
+ }
746
+ function inferControlType(type) {
747
+ const normalizedType = type.toLowerCase();
748
+ if (normalizedType === "boolean") return "boolean";
749
+ if (normalizedType === "number") return "number";
750
+ if (normalizedType.includes("|") && !normalizedType.includes("=>")) return "select";
751
+ return "text";
752
+ }
753
+ function extractOptionsFromType(type) {
754
+ const options = [];
755
+ for (const match of type.matchAll(/["']([^"']+)["']/g)) options.push({
756
+ label: match[1],
757
+ value: match[1]
758
+ });
759
+ return options;
760
+ }
761
+ function buildPaletteFromAnalysis(title, analysis) {
762
+ const controls = analysis.props.map((prop) => {
763
+ const control = inferControlType(prop.type);
764
+ return {
765
+ name: prop.name,
766
+ control,
767
+ defaultValue: normalizeDefaultValue(prop.defaultValue),
768
+ description: void 0,
769
+ required: prop.required,
770
+ options: control === "select" ? extractOptionsFromType(prop.type) : [],
771
+ range: void 0,
772
+ group: void 0
773
+ };
774
+ });
775
+ return {
776
+ title,
777
+ controls,
778
+ groups: [],
779
+ json: JSON.stringify({
780
+ title,
781
+ controls
782
+ }, null, 2),
783
+ typescript: `export interface ${title.replace(/\s+/g, "")}Props {\n${controls.map((control) => {
784
+ const type = control.control === "boolean" ? "boolean" : control.control === "number" ? "number" : control.control === "select" && control.options.length > 0 ? control.options.map((option) => `"${String(option.value)}"`).join(" | ") : "string";
785
+ return ` ${control.name}${control.required ? "" : "?"}: ${type};`;
786
+ }).join("\n")}\n}\n`
787
+ };
788
+ }
789
+ async function analyzeResolvedComponent(ctx, binding, resolved) {
790
+ const sourceDescriptor = await getComponentSourceDescriptor(ctx, resolved);
791
+ if (!sourceDescriptor.exists || !sourceDescriptor.absolutePath) return {
792
+ source: sourceDescriptor,
793
+ analysis: null
794
+ };
795
+ if (!binding.analyzeSfc) return {
796
+ source: {
797
+ ...sourceDescriptor,
798
+ error: "analyzeSfc is not available in the native binding."
799
+ },
800
+ analysis: null
801
+ };
802
+ const source = await fs.promises.readFile(sourceDescriptor.absolutePath, "utf-8");
803
+ const analysis = binding.analyzeSfc(source, { filename: sourceDescriptor.absolutePath });
804
+ return {
805
+ source: sourceDescriptor,
806
+ analysis: {
807
+ path: sourceDescriptor.path ?? sourceDescriptor.absolutePath,
808
+ props: analysis.props.map((prop) => ({
809
+ name: prop.name,
810
+ type: prop.type,
811
+ required: prop.required,
812
+ defaultValue: prop.default_value
813
+ })),
814
+ emits: analysis.emits
815
+ }
816
+ };
817
+ }
818
+ function formatGeneratedMarkdown(markdown, componentName) {
819
+ let formatted = markdown.replace(/<Self(\s|>|\/)/g, `<${componentName}$1`).replace(/<\/Self>/g, `</${componentName}>`);
820
+ formatted = formatted.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
821
+ const lines = code.split("\n");
822
+ let minIndent = Infinity;
823
+ for (const line of lines) if (line.trim()) {
824
+ const indent = line.match(/^(\s*)/)?.[1].length ?? 0;
825
+ minIndent = Math.min(minIndent, indent);
826
+ }
827
+ if (minIndent === Infinity) minIndent = 0;
828
+ return `\`\`\`${lang}\n${(minIndent > 0 ? lines.map((line) => line.slice(minIndent)) : lines).join("\n")}\`\`\``;
829
+ });
830
+ return formatted;
831
+ }
832
+ async function buildPalette(ctx, binding, resolved, source) {
833
+ let palette = null;
834
+ if (binding.generateArtPalette) {
835
+ const generated = binding.generateArtPalette(source, { filename: resolved.absolutePath });
836
+ palette = {
837
+ title: generated.title,
838
+ controls: generated.controls.map((control) => ({
839
+ name: control.name,
840
+ control: control.control,
841
+ defaultValue: control.default_value,
842
+ description: control.description,
843
+ required: control.required,
844
+ options: control.options,
845
+ range: control.range,
846
+ group: control.group
847
+ })),
848
+ groups: generated.groups,
849
+ json: generated.json,
850
+ typescript: generated.typescript
851
+ };
852
+ }
853
+ if (palette && palette.controls.length > 0) return palette;
854
+ const { analysis } = await analyzeResolvedComponent(ctx, binding, resolved);
855
+ if (!analysis || analysis.props.length === 0) return palette;
856
+ return buildPaletteFromAnalysis(resolved.info.title, analysis);
857
+ }
858
+ async function buildDocumentation(binding, resolved, source, options) {
859
+ if (!binding.generateArtDoc) return null;
860
+ const doc = binding.generateArtDoc(source, { filename: resolved.absolutePath }, {
861
+ include_source: options?.includeSource,
862
+ include_templates: options?.includeTemplates,
863
+ include_metadata: true
864
+ });
865
+ return {
866
+ markdown: formatGeneratedMarkdown(doc.markdown, resolved.info.title || "Component"),
867
+ title: doc.title,
868
+ category: doc.category,
869
+ variantCount: doc.variant_count
870
+ };
871
+ }
872
+ async function buildComponentDetails(ctx, binding, resolved, options) {
873
+ const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
874
+ const parsed = binding.parseArt(source, { filename: resolved.absolutePath });
875
+ const componentState = await analyzeResolvedComponent(ctx, binding, resolved);
876
+ const palette = options?.includePalette === false ? null : await buildPalette(ctx, binding, resolved, source);
877
+ const documentation = options?.includeDocumentation === true ? await buildDocumentation(binding, resolved, source) : null;
878
+ const resourceUris = buildResourceUris$1(resolved.relativePath, parsed.variants.map((variant) => variant.name), Boolean(componentState.source.reference));
879
+ return {
880
+ path: resolved.relativePath,
881
+ match: {
882
+ matchedBy: resolved.matchedBy,
883
+ matchValue: resolved.matchValue,
884
+ score: resolved.score,
885
+ reasons: resolved.reasons,
886
+ alternatives: resolved.alternatives
887
+ },
888
+ metadata: parsed.metadata,
889
+ variants: parsed.variants.map((variant) => ({
890
+ name: variant.name,
891
+ template: variant.template,
892
+ isDefault: variant.is_default,
893
+ skipVrt: variant.skip_vrt
894
+ })),
895
+ defaultVariant: parsed.variants.find((variant) => variant.is_default)?.name,
896
+ variantNames: parsed.variants.map((variant) => variant.name),
897
+ hasScriptSetup: parsed.has_script_setup,
898
+ hasScript: parsed.has_script,
899
+ styleCount: parsed.style_count,
900
+ componentSource: componentState.source,
901
+ componentAnalysis: options?.includeAnalysis === false ? void 0 : componentState.analysis ?? {
902
+ path: componentState.source.path,
903
+ props: [],
904
+ emits: [],
905
+ error: componentState.source.error
906
+ },
907
+ palette,
908
+ documentation,
909
+ resources: resourceUris
910
+ };
911
+ }
912
+ function buildCatalogMarkdown(arts, projectRoot) {
913
+ const grouped = /* @__PURE__ */ new Map();
914
+ for (const art of arts) {
915
+ const category = art.category || "Uncategorized";
916
+ const list = grouped.get(category) ?? [];
917
+ list.push(art);
918
+ grouped.set(category, list);
919
+ }
920
+ let markdown = "# Musea Component Catalog\n\n";
921
+ for (const [category, items] of Array.from(grouped.entries()).sort(([left], [right]) => left.localeCompare(right))) {
922
+ markdown += `## ${category}\n\n`;
923
+ for (const item of items.sort((left, right) => left.title.localeCompare(right.title))) {
924
+ const relativePath = toProjectPath(projectRoot, item.path);
925
+ markdown += `- **${item.title}** \`${relativePath}\``;
926
+ if (item.description) markdown += ` — ${item.description}`;
927
+ markdown += "\n";
928
+ if (item.variantNames.length > 0) markdown += ` Variants: ${item.variantNames.join(", ")}\n`;
929
+ if (item.tags.length > 0) markdown += ` Tags: ${item.tags.join(", ")}\n`;
930
+ }
931
+ markdown += "\n";
932
+ }
933
+ return markdown;
934
+ }
935
+ function buildIndexSummary(ctx, arts) {
936
+ const categories = /* @__PURE__ */ new Map();
937
+ const tags = /* @__PURE__ */ new Map();
938
+ for (const art of arts) {
939
+ categories.set(art.category || "Uncategorized", (categories.get(art.category || "Uncategorized") ?? 0) + 1);
940
+ for (const tag of art.tags) tags.set(tag, (tags.get(tag) ?? 0) + 1);
941
+ }
942
+ return {
943
+ componentCount: arts.length,
944
+ categories: Array.from(categories.entries()).map(([name, count]) => ({
945
+ name,
946
+ count
947
+ })).sort((left, right) => left.name.localeCompare(right.name)),
948
+ tags: Array.from(tags.entries()).map(([name, count]) => ({
949
+ name,
950
+ count
951
+ })).sort((left, right) => right.count - left.count || left.name.localeCompare(right.name)),
952
+ components: arts.slice().sort((left, right) => left.title.localeCompare(right.title)).map((art) => ({
953
+ path: toProjectPath(ctx.projectRoot, art.path),
954
+ title: art.title,
955
+ description: art.description,
956
+ component: art.component,
957
+ category: art.category,
958
+ status: art.status,
959
+ tags: art.tags,
960
+ variantCount: art.variantCount,
961
+ variantNames: art.variantNames,
962
+ defaultVariant: art.defaultVariant
963
+ }))
964
+ };
965
+ }
966
+ function getProjectPath(projectRoot, absolutePath) {
967
+ return toProjectPath(projectRoot, absolutePath);
968
+ }
969
+ //#endregion
970
+ //#region src/tools/handler/analysis.ts
971
+ /**
972
+ * MCP tool handlers for component analysis.
973
+ *
974
+ * Handles `analyze_component` and `get_palette` tool calls.
975
+ */
976
+ async function handleAnalyzeComponent(ctx, binding, args) {
977
+ const directPath = args?.path;
978
+ if (directPath?.endsWith(".vue") && !directPath.endsWith(".art.vue")) {
979
+ if (!binding.analyzeSfc) throw new McpError(ErrorCode.InternalError, "analyzeSfc not available in native binding");
980
+ const absolutePath = path.resolve(ctx.projectRoot, directPath);
981
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
982
+ const analysis = binding.analyzeSfc(source, { filename: absolutePath });
983
+ return { content: [{
984
+ type: "text",
985
+ text: JSON.stringify({
986
+ path: directPath,
987
+ props: analysis.props.map((prop) => ({
988
+ name: prop.name,
989
+ type: prop.type,
990
+ required: prop.required,
991
+ defaultValue: prop.default_value
992
+ })),
993
+ emits: analysis.emits
994
+ }, null, 2)
995
+ }] };
996
+ }
997
+ const resolved = await resolveArtReference(ctx, args);
998
+ const { source, analysis } = await analyzeResolvedComponent(ctx, binding, resolved);
999
+ if (!analysis) throw new McpError(ErrorCode.InvalidParams, source.error ?? `Could not analyze component source for "${resolved.info.title}"`);
1000
+ return { content: [{
1001
+ type: "text",
1002
+ text: JSON.stringify({
1003
+ component: {
1004
+ title: resolved.info.title,
1005
+ artPath: resolved.relativePath,
1006
+ componentReference: resolved.info.component,
1007
+ componentPath: source.path
1008
+ },
1009
+ match: {
1010
+ matchedBy: resolved.matchedBy,
1011
+ matchValue: resolved.matchValue,
1012
+ score: resolved.score,
1013
+ reasons: resolved.reasons,
1014
+ alternatives: resolved.alternatives
1015
+ },
1016
+ props: analysis.props,
1017
+ emits: analysis.emits
1018
+ }, null, 2)
1019
+ }] };
1020
+ }
1021
+ async function handleGetPalette(ctx, binding, args) {
1022
+ const resolved = await resolveArtReference(ctx, args);
1023
+ const palette = await buildPalette(ctx, binding, resolved, await fs.promises.readFile(resolved.absolutePath, "utf-8"));
1024
+ if (!palette) throw new McpError(ErrorCode.InvalidParams, `Could not infer a palette for "${resolved.info.title}"`);
1025
+ return { content: [{
1026
+ type: "text",
1027
+ text: JSON.stringify({
1028
+ component: {
1029
+ title: resolved.info.title,
1030
+ path: resolved.relativePath,
1031
+ componentReference: resolved.info.component
1032
+ },
1033
+ match: {
1034
+ matchedBy: resolved.matchedBy,
1035
+ matchValue: resolved.matchValue,
1036
+ score: resolved.score,
1037
+ reasons: resolved.reasons,
1038
+ alternatives: resolved.alternatives
1039
+ },
1040
+ title: palette.title,
1041
+ controls: palette.controls,
1042
+ groups: palette.groups,
1043
+ json: palette.json,
1044
+ typescript: palette.typescript
1045
+ }, null, 2)
1046
+ }] };
1047
+ }
1048
+ //#endregion
1049
+ //#region src/tools/handler/registry.ts
1050
+ /**
1051
+ * MCP tool handlers for the component registry.
1052
+ *
1053
+ * Handles `list_components`, `get_component`, `get_variant`, `search_components`,
1054
+ * and `recommend_components` tool calls.
1055
+ */
1056
+ function buildResourceUris(relativePath, variantNames, hasComponent) {
1057
+ const encodedPath = encodeURIComponent(relativePath);
1058
+ return {
1059
+ component: `musea://component/${encodedPath}`,
1060
+ docs: `musea://docs/${encodedPath}`,
1061
+ source: `musea://source/${encodedPath}`,
1062
+ componentSource: hasComponent ? `musea://component-source/${encodedPath}` : void 0,
1063
+ variants: variantNames.map((variantName) => ({
1064
+ name: variantName,
1065
+ uri: `musea://variant/${encodedPath}/${encodeURIComponent(variantName)}`
1066
+ }))
1067
+ };
1068
+ }
1069
+ async function handleListComponents(ctx, args) {
1070
+ const arts = Array.from((await ctx.scanArtFiles()).values());
1071
+ const category = typeof args?.category === "string" ? args.category.toLowerCase() : void 0;
1072
+ const tag = typeof args?.tag === "string" ? args.tag.toLowerCase() : void 0;
1073
+ const status = typeof args?.status === "string" ? args.status.toLowerCase() : void 0;
1074
+ const component = typeof args?.component === "string" ? args.component.toLowerCase() : void 0;
1075
+ const limit = typeof args?.limit === "number" ? args.limit : void 0;
1076
+ const includeVariants = args?.includeVariants === true;
1077
+ const sortBy = typeof args?.sortBy === "string" ? args.sortBy : "title";
1078
+ let results = arts.filter((info) => {
1079
+ if (category && info.category?.toLowerCase() !== category) return false;
1080
+ if (tag && !info.tags.some((item) => item.toLowerCase() === tag)) return false;
1081
+ if (status && info.status.toLowerCase() !== status) return false;
1082
+ if (component && ![
1083
+ info.component,
1084
+ info.component ? info.component.split("/").at(-1) : void 0,
1085
+ info.component ? info.component.split("/").at(-1)?.replace(/\.\w+$/, "") : void 0
1086
+ ].filter((value) => Boolean(value)).some((value) => value.toLowerCase().includes(component))) return false;
1087
+ return true;
1088
+ });
1089
+ results = results.sort((left, right) => {
1090
+ if (sortBy === "category") return (left.category || "").localeCompare(right.category || "") || left.title.localeCompare(right.title);
1091
+ if (sortBy === "status") return left.status.localeCompare(right.status) || left.title.localeCompare(right.title);
1092
+ if (sortBy === "variants") return right.variantCount - left.variantCount || left.title.localeCompare(right.title);
1093
+ return left.title.localeCompare(right.title);
1094
+ });
1095
+ if (typeof limit === "number") results = results.slice(0, limit);
1096
+ return { content: [{
1097
+ type: "text",
1098
+ text: JSON.stringify(results.map((info) => {
1099
+ const relativePath = getProjectPath(ctx.projectRoot, info.path);
1100
+ return {
1101
+ path: relativePath,
1102
+ title: info.title,
1103
+ description: info.description,
1104
+ component: info.component,
1105
+ category: info.category,
1106
+ status: info.status,
1107
+ order: info.order,
1108
+ tags: info.tags,
1109
+ variantCount: info.variantCount,
1110
+ variantNames: info.variantNames,
1111
+ defaultVariant: info.defaultVariant,
1112
+ variants: includeVariants ? info.variantNames.map((variantName) => ({
1113
+ name: variantName,
1114
+ isDefault: variantName === info.defaultVariant,
1115
+ uri: `musea://variant/${encodeURIComponent(relativePath)}/${encodeURIComponent(variantName)}`
1116
+ })) : void 0,
1117
+ resources: buildResourceUris(relativePath, info.variantNames, Boolean(info.component))
1118
+ };
1119
+ }), null, 2)
1120
+ }] };
1121
+ }
1122
+ async function handleGetComponent(ctx, binding, args) {
1123
+ const details = await buildComponentDetails(ctx, binding, await resolveArtReference(ctx, args), {
1124
+ includeAnalysis: args?.includeAnalysis !== false,
1125
+ includePalette: args?.includePalette !== false,
1126
+ includeDocumentation: args?.includeDocumentation === true
1127
+ });
1128
+ return { content: [{
1129
+ type: "text",
1130
+ text: JSON.stringify(details, null, 2)
1131
+ }] };
1132
+ }
1133
+ async function handleGetVariant(ctx, binding, args) {
1134
+ const variantName = args?.variant;
1135
+ if (!variantName) throw new McpError(ErrorCode.InvalidParams, "variant is required");
1136
+ const resolved = await resolveArtReference(ctx, args);
1137
+ const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
1138
+ const parsed = binding.parseArt(source, { filename: resolved.absolutePath });
1139
+ const variant = parsed.variants.find((item) => item.name.toLowerCase() === variantName.toLowerCase());
1140
+ if (!variant) throw new McpError(ErrorCode.InvalidParams, `Variant "${variantName}" not found in "${resolved.info.title}"`);
1141
+ const analysis = args?.includeAnalysis === true ? await analyzeResolvedComponent(ctx, binding, resolved) : null;
1142
+ return { content: [{
1143
+ type: "text",
1144
+ text: JSON.stringify({
1145
+ component: {
1146
+ title: resolved.info.title,
1147
+ path: resolved.relativePath,
1148
+ componentReference: resolved.info.component
1149
+ },
1150
+ match: {
1151
+ matchedBy: resolved.matchedBy,
1152
+ matchValue: resolved.matchValue,
1153
+ score: resolved.score,
1154
+ reasons: resolved.reasons,
1155
+ alternatives: resolved.alternatives
1156
+ },
1157
+ variant: {
1158
+ name: variant.name,
1159
+ template: variant.template,
1160
+ isDefault: variant.is_default,
1161
+ skipVrt: variant.skip_vrt
1162
+ },
1163
+ relatedVariants: parsed.variants.map((item) => item.name),
1164
+ componentAnalysis: analysis?.analysis == null ? void 0 : {
1165
+ path: analysis.analysis.path,
1166
+ props: analysis.analysis.props,
1167
+ emits: analysis.analysis.emits
1168
+ }
1169
+ }, null, 2)
1170
+ }] };
1171
+ }
1172
+ async function handleSearchComponents(ctx, args) {
1173
+ const query = args?.query;
1174
+ if (!query) throw new McpError(ErrorCode.InvalidParams, "query is required");
1175
+ const results = await searchArtInfos(ctx, query, {
1176
+ category: args?.category,
1177
+ tag: args?.tag,
1178
+ status: args?.status,
1179
+ component: args?.component,
1180
+ limit: args?.limit ?? 10
1181
+ });
1182
+ return { content: [{
1183
+ type: "text",
1184
+ text: JSON.stringify(results.map((result) => ({
1185
+ score: result.score,
1186
+ reasons: result.reasons,
1187
+ path: result.relativePath,
1188
+ title: result.info.title,
1189
+ description: result.info.description,
1190
+ component: result.info.component,
1191
+ category: result.info.category,
1192
+ status: result.info.status,
1193
+ tags: result.info.tags,
1194
+ variantCount: result.info.variantCount,
1195
+ variantNames: result.info.variantNames,
1196
+ defaultVariant: result.info.defaultVariant,
1197
+ resources: buildResourceUris(result.relativePath, result.info.variantNames, Boolean(result.info.component))
1198
+ })), null, 2)
1199
+ }] };
1200
+ }
1201
+ async function handleRecommendComponents(ctx, args) {
1202
+ const task = args?.task;
1203
+ if (!task) throw new McpError(ErrorCode.InvalidParams, "task is required");
1204
+ const results = await searchArtInfos(ctx, task, {
1205
+ category: args?.category,
1206
+ tag: args?.tag,
1207
+ status: args?.status,
1208
+ component: args?.component,
1209
+ limit: args?.limit ?? 5
1210
+ });
1211
+ return { content: [{
1212
+ type: "text",
1213
+ text: JSON.stringify({
1214
+ task,
1215
+ recommendations: results.map((result) => ({
1216
+ title: result.info.title,
1217
+ path: result.relativePath,
1218
+ component: result.info.component,
1219
+ category: result.info.category,
1220
+ status: result.info.status,
1221
+ tags: result.info.tags,
1222
+ variantNames: result.info.variantNames,
1223
+ defaultVariant: result.info.defaultVariant,
1224
+ score: result.score,
1225
+ why: result.reasons,
1226
+ resources: buildResourceUris(result.relativePath, result.info.variantNames, Boolean(result.info.component))
1227
+ }))
1228
+ }, null, 2)
1229
+ }] };
1230
+ }
1231
+ //#endregion
1232
+ //#region src/tokens.ts
1233
+ async function parseTokensFromPath(tokensPath) {
1234
+ if ((await fs.promises.stat(tokensPath)).isDirectory()) {
1235
+ const entries = await fs.promises.readdir(tokensPath, { withFileTypes: true });
1236
+ const categories = [];
1237
+ for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
1238
+ const filePath = path.join(tokensPath, entry.name);
1239
+ const content = await fs.promises.readFile(filePath, "utf-8");
1240
+ const tokens = JSON.parse(content);
1241
+ const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
1242
+ categories.push({
1243
+ name: formatCategoryName(categoryName),
1244
+ tokens: extractTokenValues(tokens),
1245
+ subcategories: extractSubcats(tokens)
1246
+ });
1247
+ }
1248
+ return categories;
1249
+ }
1250
+ const content = await fs.promises.readFile(tokensPath, "utf-8");
1251
+ return flattenTokenStructure(JSON.parse(content));
1252
+ }
1253
+ function generateTokensMarkdown(categories) {
1254
+ const renderCategory = (category, level = 2) => {
1255
+ let md = `\n${"#".repeat(level)} ${category.name}\n\n`;
1256
+ if (Object.keys(category.tokens).length > 0) {
1257
+ md += "| Token | Value | Description |\n";
1258
+ md += "|-------|-------|-------------|\n";
1259
+ for (const [name, token] of Object.entries(category.tokens)) md += `| \`${name}\` | \`${token.value}\` | ${token.description || "-"} |\n`;
1260
+ md += "\n";
1261
+ }
1262
+ if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
1263
+ return md;
1264
+ };
1265
+ let markdown = "# Design Tokens\n";
1266
+ for (const category of categories) markdown += renderCategory(category);
1267
+ return markdown;
1268
+ }
1269
+ function flattenTokenCategories(categories, parentPath = []) {
1270
+ const flattened = [];
1271
+ for (const category of categories) {
1272
+ const categoryPath = [...parentPath, category.name];
1273
+ for (const [name, token] of Object.entries(category.tokens)) flattened.push({
1274
+ name,
1275
+ path: [...categoryPath, name].join("."),
1276
+ categoryPath,
1277
+ value: token.value,
1278
+ type: token.type,
1279
+ description: token.description
1280
+ });
1281
+ if (category.subcategories) flattened.push(...flattenTokenCategories(category.subcategories, categoryPath));
1282
+ }
1283
+ return flattened;
1284
+ }
1285
+ function isTokenLeaf(value) {
1286
+ if (typeof value !== "object" || value === null) return false;
1287
+ const obj = value;
1288
+ return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number");
1289
+ }
1290
+ function extractTokenValues(obj) {
1291
+ const tokens = {};
1292
+ for (const [key, value] of Object.entries(obj)) if (isTokenLeaf(value)) {
1293
+ const raw = value;
1294
+ tokens[key] = {
1295
+ value: raw.value,
1296
+ type: raw.type,
1297
+ description: raw.description
1298
+ };
1299
+ }
1300
+ return tokens;
1301
+ }
1302
+ function extractSubcats(obj) {
1303
+ const subcategories = [];
1304
+ for (const [key, value] of Object.entries(obj)) if (!isTokenLeaf(value) && typeof value === "object" && value !== null) {
1305
+ const tokens = extractTokenValues(value);
1306
+ const nested = extractSubcats(value);
1307
+ if (Object.keys(tokens).length > 0 || nested && nested.length > 0) subcategories.push({
1308
+ name: formatCategoryName(key),
1309
+ tokens,
1310
+ subcategories: nested
1311
+ });
1312
+ }
1313
+ return subcategories.length > 0 ? subcategories : void 0;
1314
+ }
1315
+ function flattenTokenStructure(tokens) {
1316
+ const categories = [];
1317
+ for (const [key, value] of Object.entries(tokens)) {
1318
+ if (isTokenLeaf(value)) continue;
1319
+ if (typeof value === "object" && value !== null) {
1320
+ const categoryTokens = extractTokenValues(value);
1321
+ const subcategories = flattenTokenStructure(value);
1322
+ if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
1323
+ name: formatCategoryName(key),
1324
+ tokens: categoryTokens,
1325
+ subcategories: subcategories.length > 0 ? subcategories : void 0
1326
+ });
1327
+ }
1328
+ }
1329
+ return categories;
1330
+ }
1331
+ function formatCategoryName(name) {
1332
+ return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
1333
+ }
1334
+ //#endregion
1335
+ //#region src/tools/handler/generation.ts
1336
+ /**
1337
+ * MCP tool handlers for code generation.
1338
+ *
1339
+ * Handles `generate_variants`, `generate_csf`, `generate_docs`,
1340
+ * `generate_catalog`, `get_tokens`, and `search_tokens` tool calls.
1341
+ */
1342
+ async function handleGenerateVariants(ctx, binding, args) {
1343
+ const componentRelPath = args?.componentPath;
1344
+ if (!componentRelPath) throw new McpError(ErrorCode.InvalidParams, "componentPath is required");
1345
+ if (!binding.analyzeSfc) throw new McpError(ErrorCode.InternalError, "analyzeSfc not available in native binding");
1346
+ if (!binding.generateVariants) throw new McpError(ErrorCode.InternalError, "generateVariants not available in native binding");
1347
+ const absolutePath = path.resolve(ctx.projectRoot, componentRelPath);
1348
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
1349
+ const props = binding.analyzeSfc(source, { filename: absolutePath }).props.map((prop) => ({
1350
+ name: prop.name,
1351
+ prop_type: prop.type,
1352
+ required: prop.required,
1353
+ default_value: prop.default_value
1354
+ }));
1355
+ const relPath = `./${path.basename(absolutePath)}`;
1356
+ const result = binding.generateVariants(relPath, props, {
1357
+ max_variants: args?.maxVariants,
1358
+ include_default: args?.includeDefault,
1359
+ include_boolean_toggles: args?.includeBooleanToggles,
1360
+ include_enum_variants: args?.includeEnumVariants
1361
+ });
1362
+ return { content: [{
1363
+ type: "text",
1364
+ text: JSON.stringify({
1365
+ componentPath: componentRelPath,
1366
+ componentName: result.component_name,
1367
+ artFileContent: result.art_file_content,
1368
+ variants: result.variants.map((variant) => ({
1369
+ name: variant.name,
1370
+ isDefault: variant.is_default,
1371
+ props: variant.props,
1372
+ description: variant.description
1373
+ }))
1374
+ }, null, 2)
1375
+ }] };
1376
+ }
1377
+ async function handleGenerateCsf(ctx, binding, args) {
1378
+ const resolved = await resolveArtReference(ctx, args);
1379
+ const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
1380
+ const csf = binding.artToCsf(source, { filename: resolved.absolutePath });
1381
+ return { content: [{
1382
+ type: "text",
1383
+ text: JSON.stringify({
1384
+ component: {
1385
+ title: resolved.info.title,
1386
+ path: resolved.relativePath
1387
+ },
1388
+ match: {
1389
+ matchedBy: resolved.matchedBy,
1390
+ matchValue: resolved.matchValue,
1391
+ score: resolved.score,
1392
+ reasons: resolved.reasons,
1393
+ alternatives: resolved.alternatives
1394
+ },
1395
+ filename: csf.filename,
1396
+ code: csf.code
1397
+ }, null, 2)
1398
+ }] };
1399
+ }
1400
+ async function handleGenerateDocs(ctx, binding, args) {
1401
+ const resolved = await resolveArtReference(ctx, args);
1402
+ const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
1403
+ if (!binding.generateArtDoc) throw new McpError(ErrorCode.InternalError, "generateArtDoc not available in native binding");
1404
+ const doc = binding.generateArtDoc(source, { filename: resolved.absolutePath }, {
1405
+ include_source: args?.includeSource,
1406
+ include_templates: args?.includeTemplates,
1407
+ include_metadata: true
1408
+ });
1409
+ const formattedDoc = await buildDocumentation(binding, resolved, source, {
1410
+ includeSource: args?.includeSource,
1411
+ includeTemplates: args?.includeTemplates
1412
+ });
1413
+ return { content: [{
1414
+ type: "text",
1415
+ text: JSON.stringify({
1416
+ component: {
1417
+ title: resolved.info.title,
1418
+ path: resolved.relativePath
1419
+ },
1420
+ match: {
1421
+ matchedBy: resolved.matchedBy,
1422
+ matchValue: resolved.matchValue,
1423
+ score: resolved.score,
1424
+ reasons: resolved.reasons,
1425
+ alternatives: resolved.alternatives
1426
+ },
1427
+ markdown: formattedDoc?.markdown ?? doc.markdown,
1428
+ title: doc.title,
1429
+ category: doc.category,
1430
+ variantCount: doc.variant_count
1431
+ }, null, 2)
1432
+ }] };
1433
+ }
1434
+ async function handleGenerateCatalog(ctx, binding, args) {
1435
+ const arts = await ctx.scanArtFiles();
1436
+ if (binding.generateArtCatalog) {
1437
+ const sources = [];
1438
+ for (const [filePath] of arts) {
1439
+ const source = await fs.promises.readFile(filePath, "utf-8");
1440
+ sources.push(source);
1441
+ }
1442
+ const catalog = binding.generateArtCatalog(sources, {
1443
+ include_source: args?.includeSource,
1444
+ include_templates: args?.includeTemplates,
1445
+ include_metadata: true
1446
+ });
1447
+ return { content: [{
1448
+ type: "text",
1449
+ text: JSON.stringify({
1450
+ markdown: catalog.markdown,
1451
+ componentCount: catalog.component_count,
1452
+ categories: catalog.categories,
1453
+ tags: catalog.tags
1454
+ }, null, 2)
1455
+ }] };
1456
+ }
1457
+ const allArts = Array.from(arts.values());
1458
+ return { content: [{
1459
+ type: "text",
1460
+ text: JSON.stringify({
1461
+ markdown: buildCatalogMarkdown(allArts, ctx.projectRoot),
1462
+ componentCount: allArts.length,
1463
+ categories: Array.from(new Set(allArts.map((art) => art.category || "Uncategorized"))),
1464
+ tags: Array.from(new Set(allArts.flatMap((art) => art.tags)))
1465
+ }, null, 2)
1466
+ }] };
1467
+ }
1468
+ async function handleGetTokens(ctx, args) {
1469
+ const inputPath = args?.tokensPath;
1470
+ const format = args?.format ?? "json";
1471
+ let resolvedPath;
1472
+ if (inputPath) resolvedPath = path.resolve(ctx.projectRoot, inputPath);
1473
+ else resolvedPath = await ctx.resolveTokensPath();
1474
+ if (!resolvedPath) throw new McpError(ErrorCode.InvalidParams, "No tokens path provided and none auto-detected. Looked for: tokens/, design-tokens/, style-dictionary/ directories.");
1475
+ const categories = await parseTokensFromPath(resolvedPath);
1476
+ const flattened = flattenTokenCategories(categories);
1477
+ if (format === "markdown") return { content: [{
1478
+ type: "text",
1479
+ text: generateTokensMarkdown(categories)
1480
+ }] };
1481
+ return { content: [{
1482
+ type: "text",
1483
+ text: JSON.stringify({
1484
+ source: path.relative(ctx.projectRoot, resolvedPath),
1485
+ categoryCount: categories.length,
1486
+ tokenCount: flattened.length,
1487
+ categories
1488
+ }, null, 2)
1489
+ }] };
1490
+ }
1491
+ async function handleSearchTokens(ctx, args) {
1492
+ const query = args?.query;
1493
+ if (!query) throw new McpError(ErrorCode.InvalidParams, "query is required");
1494
+ const inputPath = args?.tokensPath;
1495
+ const typeFilter = typeof args?.type === "string" ? args.type.toLowerCase() : void 0;
1496
+ const limit = typeof args?.limit === "number" ? args.limit : 20;
1497
+ const resolvedPath = inputPath ? path.resolve(ctx.projectRoot, inputPath) : await ctx.resolveTokensPath();
1498
+ if (!resolvedPath) throw new McpError(ErrorCode.InvalidParams, "No tokens path provided and none auto-detected. Looked for: tokens/, design-tokens/, style-dictionary/ directories.");
1499
+ const flattened = flattenTokenCategories(await parseTokensFromPath(resolvedPath));
1500
+ const normalizedQuery = query.toLowerCase();
1501
+ const allMatches = flattened.filter((token) => {
1502
+ if (typeFilter && token.type?.toLowerCase() !== typeFilter) return false;
1503
+ return token.name.toLowerCase().includes(normalizedQuery) || token.path.toLowerCase().includes(normalizedQuery) || token.categoryPath.some((segment) => segment.toLowerCase().includes(normalizedQuery)) || String(token.value).toLowerCase().includes(normalizedQuery) || token.description?.toLowerCase().includes(normalizedQuery);
1504
+ });
1505
+ const matches = allMatches.slice(0, limit);
1506
+ return { content: [{
1507
+ type: "text",
1508
+ text: JSON.stringify({
1509
+ query,
1510
+ source: path.relative(ctx.projectRoot, resolvedPath),
1511
+ totalMatches: allMatches.length,
1512
+ matches
1513
+ }, null, 2)
1514
+ }] };
1515
+ }
1516
+ //#endregion
1517
+ //#region src/tools/handler/index.ts
1518
+ /**
1519
+ * MCP tool call handler for Musea.
1520
+ *
1521
+ * Routes incoming tool calls to the appropriate handler logic based on
1522
+ * the tool name, using the native Rust binding and server context.
1523
+ */
1524
+ async function handleToolCall(ctx, name, args) {
1525
+ const binding = ctx.loadNative();
1526
+ switch (name) {
1527
+ case "analyze_component": return handleAnalyzeComponent(ctx, binding, args);
1528
+ case "get_palette": return handleGetPalette(ctx, binding, args);
1529
+ case "list_components": return handleListComponents(ctx, args);
1530
+ case "get_component": return handleGetComponent(ctx, binding, args);
1531
+ case "get_variant": return handleGetVariant(ctx, binding, args);
1532
+ case "search_components": return handleSearchComponents(ctx, args);
1533
+ case "recommend_components": return handleRecommendComponents(ctx, args);
1534
+ case "generate_variants": return handleGenerateVariants(ctx, binding, args);
1535
+ case "generate_csf": return handleGenerateCsf(ctx, binding, args);
1536
+ case "generate_docs": return handleGenerateDocs(ctx, binding, args);
1537
+ case "generate_catalog": return handleGenerateCatalog(ctx, binding, args);
1538
+ case "get_tokens": return handleGetTokens(ctx, args);
1539
+ case "search_tokens": return handleSearchTokens(ctx, args);
1540
+ default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
1541
+ }
1542
+ }
1543
+ //#endregion
1544
+ //#region src/resources.ts
1545
+ function componentUri(relativePath) {
1546
+ return `musea://component/${encodeURIComponent(relativePath)}`;
1547
+ }
1548
+ function docsUri(relativePath) {
1549
+ return `musea://docs/${encodeURIComponent(relativePath)}`;
1550
+ }
1551
+ function sourceUri(relativePath) {
1552
+ return `musea://source/${encodeURIComponent(relativePath)}`;
1553
+ }
1554
+ function componentSourceUri(relativePath) {
1555
+ return `musea://component-source/${encodeURIComponent(relativePath)}`;
1556
+ }
1557
+ function variantUri(relativePath, variantName) {
1558
+ return `musea://variant/${encodeURIComponent(relativePath)}/${encodeURIComponent(variantName)}`;
1559
+ }
1560
+ async function listResources(ctx) {
1561
+ const arts = Array.from((await ctx.scanArtFiles()).values());
1562
+ const resources = [{
1563
+ uri: "musea://index",
1564
+ name: "Component Index",
1565
+ description: "JSON summary of the project component registry",
1566
+ mimeType: "application/json"
1567
+ }, {
1568
+ uri: "musea://catalog",
1569
+ name: "Component Catalog",
1570
+ description: "Markdown catalog for the whole design system",
1571
+ mimeType: "text/markdown"
1572
+ }];
1573
+ for (const info of arts) {
1574
+ const relativePath = getProjectPath(ctx.projectRoot, info.path);
1575
+ resources.push({
1576
+ uri: componentUri(relativePath),
1577
+ name: info.title,
1578
+ description: info.description || `${info.category || "Component"} • ${info.variantCount} variant(s) • ${info.status}`,
1579
+ mimeType: "application/json"
1580
+ });
1581
+ resources.push({
1582
+ uri: docsUri(relativePath),
1583
+ name: `${info.title} — Documentation`,
1584
+ description: `Generated Markdown docs for ${info.title}`,
1585
+ mimeType: "text/markdown"
1586
+ });
1587
+ resources.push({
1588
+ uri: sourceUri(relativePath),
1589
+ name: `${info.title} — Art Source`,
1590
+ description: `Raw .art.vue source for ${info.title}`,
1591
+ mimeType: "text/plain"
1592
+ });
1593
+ if (info.component) resources.push({
1594
+ uri: componentSourceUri(relativePath),
1595
+ name: `${info.title} — Component Source`,
1596
+ description: `Resolved Vue component source for ${info.title}`,
1597
+ mimeType: "text/plain"
1598
+ });
1599
+ for (const variantName of info.variantNames) resources.push({
1600
+ uri: variantUri(relativePath, variantName),
1601
+ name: `${info.title} — ${variantName}`,
1602
+ description: `Variant details for ${variantName}`,
1603
+ mimeType: "application/json"
1604
+ });
1605
+ }
1606
+ if (await ctx.resolveTokensPath()) {
1607
+ resources.push({
1608
+ uri: "musea://tokens",
1609
+ name: "Design Tokens",
1610
+ description: "Project design tokens as JSON",
1611
+ mimeType: "application/json"
1612
+ });
1613
+ resources.push({
1614
+ uri: "musea://tokens/markdown",
1615
+ name: "Design Tokens — Markdown",
1616
+ description: "Project design tokens as Markdown tables",
1617
+ mimeType: "text/markdown"
1618
+ });
1619
+ }
1620
+ return { resources };
1621
+ }
1622
+ async function readResource(ctx, uri) {
1623
+ if (uri === "musea://index") {
1624
+ const arts = Array.from((await ctx.scanArtFiles()).values());
1625
+ return { contents: [{
1626
+ uri,
1627
+ mimeType: "application/json",
1628
+ text: JSON.stringify(buildIndexSummary(ctx, arts), null, 2)
1629
+ }] };
1630
+ }
1631
+ if (uri === "musea://catalog") {
1632
+ const binding = ctx.loadNative();
1633
+ const arts = Array.from((await ctx.scanArtFiles()).values());
1634
+ if (binding.generateArtCatalog) {
1635
+ const sources = await Promise.all(arts.map((art) => fs.promises.readFile(art.path, "utf-8")));
1636
+ return { contents: [{
1637
+ uri,
1638
+ mimeType: "text/markdown",
1639
+ text: binding.generateArtCatalog(sources, { include_metadata: true }).markdown
1640
+ }] };
1641
+ }
1642
+ return { contents: [{
1643
+ uri,
1644
+ mimeType: "text/markdown",
1645
+ text: buildCatalogMarkdown(arts, ctx.projectRoot)
1646
+ }] };
1647
+ }
1648
+ if (uri.startsWith("musea://component/")) {
1649
+ const relativePath = decodeURIComponent(uri.slice(18));
1650
+ const details = await buildComponentDetails(ctx, ctx.loadNative(), await resolveArtReference(ctx, { path: relativePath }), {
1651
+ includeAnalysis: true,
1652
+ includePalette: true,
1653
+ includeDocumentation: false
1654
+ });
1655
+ return { contents: [{
1656
+ uri,
1657
+ mimeType: "application/json",
1658
+ text: JSON.stringify(details, null, 2)
1659
+ }] };
1660
+ }
1661
+ if (uri.startsWith("musea://docs/")) {
1662
+ const relativePath = decodeURIComponent(uri.slice(13));
1663
+ const binding = ctx.loadNative();
1664
+ const resolved = await resolveArtReference(ctx, { path: relativePath });
1665
+ const documentation = await buildDocumentation(binding, resolved, await fs.promises.readFile(resolved.absolutePath, "utf-8"));
1666
+ if (!documentation) throw new McpError(ErrorCode.InternalError, "generateArtDoc not available in native binding");
1667
+ return { contents: [{
1668
+ uri,
1669
+ mimeType: "text/markdown",
1670
+ text: documentation.markdown
1671
+ }] };
1672
+ }
1673
+ if (uri.startsWith("musea://source/")) {
1674
+ const relativePath = decodeURIComponent(uri.slice(15));
1675
+ const absolutePath = path.resolve(ctx.projectRoot, relativePath);
1676
+ return { contents: [{
1677
+ uri,
1678
+ mimeType: "text/plain",
1679
+ text: await fs.promises.readFile(absolutePath, "utf-8")
1680
+ }] };
1681
+ }
1682
+ if (uri.startsWith("musea://component-source/")) {
1683
+ const relativePath = decodeURIComponent(uri.slice(25));
1684
+ const componentSource = (await buildComponentDetails(ctx, ctx.loadNative(), await resolveArtReference(ctx, { path: relativePath }), {
1685
+ includeAnalysis: false,
1686
+ includePalette: false,
1687
+ includeDocumentation: false
1688
+ })).componentSource;
1689
+ if (!componentSource?.path || componentSource.exists !== true) throw new McpError(ErrorCode.InvalidRequest, componentSource?.error ?? "Component source not available for this art file");
1690
+ const absolutePath = path.resolve(ctx.projectRoot, componentSource.path);
1691
+ return { contents: [{
1692
+ uri,
1693
+ mimeType: "text/plain",
1694
+ text: await fs.promises.readFile(absolutePath, "utf-8")
1695
+ }] };
1696
+ }
1697
+ if (uri.startsWith("musea://variant/")) {
1698
+ const rest = uri.slice(16);
1699
+ const separatorIndex = rest.indexOf("/");
1700
+ if (separatorIndex === -1) throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`);
1701
+ const relativePath = decodeURIComponent(rest.slice(0, separatorIndex));
1702
+ const variantName = decodeURIComponent(rest.slice(separatorIndex + 1));
1703
+ const binding = ctx.loadNative();
1704
+ const resolved = await resolveArtReference(ctx, { path: relativePath });
1705
+ const source = await fs.promises.readFile(resolved.absolutePath, "utf-8");
1706
+ const parsed = binding.parseArt(source, { filename: resolved.absolutePath });
1707
+ const variant = parsed.variants.find((item) => item.name.toLowerCase() === variantName.toLowerCase());
1708
+ if (!variant) throw new McpError(ErrorCode.InvalidRequest, `Variant "${variantName}" not found in "${resolved.info.title}"`);
1709
+ return { contents: [{
1710
+ uri,
1711
+ mimeType: "application/json",
1712
+ text: JSON.stringify({
1713
+ component: {
1714
+ title: resolved.info.title,
1715
+ path: resolved.relativePath,
1716
+ componentReference: resolved.info.component
1717
+ },
1718
+ variant: {
1719
+ name: variant.name,
1720
+ template: variant.template,
1721
+ isDefault: variant.is_default,
1722
+ skipVrt: variant.skip_vrt
1723
+ },
1724
+ relatedVariants: parsed.variants.map((item) => item.name)
1725
+ }, null, 2)
1726
+ }] };
1727
+ }
1728
+ if (uri === "musea://tokens" || uri === "musea://tokens/markdown") {
1729
+ const resolvedTokensPath = await ctx.resolveTokensPath();
1730
+ if (!resolvedTokensPath) throw new McpError(ErrorCode.InternalError, "No tokens path configured or auto-detected");
1731
+ const categories = await parseTokensFromPath(resolvedTokensPath);
1732
+ if (uri === "musea://tokens/markdown") return { contents: [{
1733
+ uri,
1734
+ mimeType: "text/markdown",
1735
+ text: generateTokensMarkdown(categories)
1736
+ }] };
1737
+ return { contents: [{
1738
+ uri,
1739
+ mimeType: "application/json",
1740
+ text: JSON.stringify({
1741
+ source: path.relative(ctx.projectRoot, resolvedTokensPath),
1742
+ categoryCount: categories.length,
1743
+ tokenCount: flattenTokenCategories(categories).length,
1744
+ categories
1745
+ }, null, 2)
1746
+ }] };
1747
+ }
1748
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`);
1749
+ }
1750
+ //#endregion
1751
+ //#region src/index.ts
1752
+ /**
1753
+ * Musea MCP Server — Vue.js design system toolkit.
1754
+ *
1755
+ * Provides AI assistants with tools to:
1756
+ * - Analyze Vue SFC components (props, emits)
1757
+ * - Browse and search a component registry
1758
+ * - Generate documentation, variants, and Storybook stories
1759
+ * - Read and format design tokens
1760
+ */
1761
+ function createMuseaServer(config) {
1762
+ const server = new Server({
1763
+ name: "musea-mcp-server",
1764
+ version: "0.0.1-alpha.11"
1765
+ }, { capabilities: {
1766
+ resources: {},
1767
+ tools: {}
1768
+ } });
1769
+ const projectRoot = config.projectRoot;
1770
+ const include = config.include ?? ["**/*.art.vue"];
1771
+ const exclude = config.exclude ?? ["node_modules/**", "dist/**"];
1772
+ const tokensPath = config.tokensPath;
1773
+ let artCache = /* @__PURE__ */ new Map();
1774
+ let lastScanTime = 0;
1775
+ async function scanArtFiles() {
1776
+ const now = Date.now();
1777
+ if (now - lastScanTime < 5e3 && artCache.size > 0) return artCache;
1778
+ const binding = loadNative();
1779
+ const files = await findArtFiles(projectRoot, include, exclude);
1780
+ artCache = /* @__PURE__ */ new Map();
1781
+ for (const file of files) try {
1782
+ const source = await fs.promises.readFile(file, "utf-8");
1783
+ const parsed = binding.parseArt(source, { filename: file });
1784
+ artCache.set(file, {
1785
+ path: file,
1786
+ title: parsed.metadata.title,
1787
+ description: parsed.metadata.description,
1788
+ component: parsed.metadata.component,
1789
+ category: parsed.metadata.category,
1790
+ tags: parsed.metadata.tags,
1791
+ status: parsed.metadata.status,
1792
+ order: parsed.metadata.order,
1793
+ variantCount: parsed.variants.length,
1794
+ variantNames: parsed.variants.map((variant) => variant.name),
1795
+ defaultVariant: parsed.variants.find((variant) => variant.is_default)?.name
1796
+ });
1797
+ } catch (e) {
1798
+ console.error(`Failed to parse ${file}:`, e);
1799
+ }
1800
+ lastScanTime = now;
1801
+ return artCache;
1802
+ }
1803
+ async function resolveTokensPath() {
1804
+ if (tokensPath) return path.resolve(projectRoot, tokensPath);
1805
+ for (const dir of [
1806
+ "tokens",
1807
+ "design-tokens",
1808
+ "style-dictionary"
1809
+ ]) {
1810
+ const candidate = path.join(projectRoot, dir);
1811
+ try {
1812
+ const stat = await fs.promises.stat(candidate);
1813
+ if (stat.isDirectory() || stat.isFile()) return candidate;
1814
+ } catch {}
1815
+ }
1816
+ return null;
1817
+ }
1818
+ const ctx = {
1819
+ projectRoot,
1820
+ loadNative,
1821
+ scanArtFiles,
1822
+ resolveTokensPath
1823
+ };
1824
+ server.setRequestHandler(ListResourcesRequestSchema, () => listResources(ctx));
1825
+ server.setRequestHandler(ReadResourceRequestSchema, (req) => readResource(ctx, req.params.uri));
1826
+ server.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: toolDefinitions }));
1827
+ server.setRequestHandler(CallToolRequestSchema, (req) => handleToolCall(ctx, req.params.name, req.params.arguments));
1828
+ return server;
1829
+ }
1830
+ async function startServer(projectRoot, options) {
1831
+ const server = createMuseaServer({
1832
+ projectRoot,
1833
+ tokensPath: options?.tokensPath
1834
+ });
1835
+ const transport = new StdioServerTransport();
1836
+ await server.connect(transport);
1837
+ console.error("[musea-mcp] Server started");
1838
+ }
1839
+ //#endregion
1840
+ export { startServer as n, createMuseaServer as t };