@vizejs/musea-mcp-server 0.0.1-alpha.74 → 0.0.1-alpha.75

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { startServer } from "./src-DMEJQckJ.js";
2
+ import { startServer } from "./src-ZT3oDiIm.js";
3
3
 
4
4
  //#region src/cli.ts
5
- const projectRoot = process.argv[2] || process.env.MUSEA_PROJECT_ROOT || process.cwd();
5
+ let projectRoot = process.env.MUSEA_PROJECT_ROOT || process.cwd();
6
+ let tokensPath = process.env.MUSEA_TOKENS_PATH;
7
+ const args = process.argv.slice(2);
8
+ for (let i = 0; i < args.length; i++) if (args[i] === "--tokens-path" && i + 1 < args.length) tokensPath = args[++i];
9
+ else if (!args[i].startsWith("--")) projectRoot = args[i];
6
10
  console.error(`[musea-mcp] Starting server for project: ${projectRoot}`);
7
- startServer(projectRoot).catch((error) => {
11
+ if (tokensPath) console.error(`[musea-mcp] Tokens path: ${tokensPath}`);
12
+ startServer(projectRoot, { tokensPath }).catch((error) => {
8
13
  console.error("[musea-mcp] Failed to start:", error);
9
14
  process.exit(1);
10
15
  });
package/dist/index.d.ts CHANGED
@@ -1,20 +1,14 @@
1
1
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
 
3
3
  //#region src/index.d.ts
4
- /**
5
- * Create and configure the MCP server.
6
- */
7
4
 
8
- /**
9
- * Create and configure the MCP server.
10
- */
11
5
  declare function createMuseaServer(config: {
12
6
  projectRoot: string;
13
7
  include?: string[];
14
8
  exclude?: string[];
9
+ tokensPath?: string;
15
10
  }): Server;
16
- /**
17
- * Start the MCP server with stdio transport.
18
- */
19
- declare function startServer(projectRoot: string): Promise<void>; //#endregion
11
+ declare function startServer(projectRoot: string, options?: {
12
+ tokensPath?: string;
13
+ }): Promise<void>; //#endregion
20
14
  export { createMuseaServer, createMuseaServer as default, startServer };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { createMuseaServer, src_default, startServer } from "./src-DMEJQckJ.js";
1
+ import { createMuseaServer, src_default, startServer } from "./src-ZT3oDiIm.js";
2
2
 
3
3
  export { createMuseaServer, src_default as default, startServer };
@@ -0,0 +1,740 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { createRequire } from "node:module";
7
+
8
+ //#region src/native.ts
9
+ let native = null;
10
+ function loadNative() {
11
+ if (native) return native;
12
+ const require = createRequire(import.meta.url);
13
+ try {
14
+ native = require("@vizejs/native");
15
+ return native;
16
+ } catch (e) {
17
+ throw new Error(`Failed to load @vizejs/native. Make sure it's installed: ${String(e)}`);
18
+ }
19
+ }
20
+
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 = 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
+ }
44
+ }
45
+ await scan(root);
46
+ return files;
47
+ }
48
+ function matchGlob(filepath, pattern) {
49
+ const regex = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*").replace(/\./g, "\\.");
50
+ return new RegExp(`^${regex}$`).test(filepath);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/tokens.ts
55
+ async function parseTokensFromPath(tokensPath) {
56
+ const stat = await fs.promises.stat(tokensPath);
57
+ if (stat.isDirectory()) {
58
+ const entries = await fs.promises.readdir(tokensPath, { withFileTypes: true });
59
+ const categories = [];
60
+ for (const entry of entries) if (entry.isFile() && (entry.name.endsWith(".json") || entry.name.endsWith(".tokens.json"))) {
61
+ const filePath = path.join(tokensPath, entry.name);
62
+ const content$1 = await fs.promises.readFile(filePath, "utf-8");
63
+ const tokens$1 = JSON.parse(content$1);
64
+ const categoryName = path.basename(entry.name, path.extname(entry.name)).replace(".tokens", "");
65
+ categories.push({
66
+ name: formatCategoryName(categoryName),
67
+ tokens: extractTokenValues(tokens$1),
68
+ subcategories: extractSubcats(tokens$1)
69
+ });
70
+ }
71
+ return categories;
72
+ }
73
+ const content = await fs.promises.readFile(tokensPath, "utf-8");
74
+ const tokens = JSON.parse(content);
75
+ return flattenTokenStructure(tokens);
76
+ }
77
+ function generateTokensMarkdown(categories) {
78
+ const renderCategory = (category, level = 2) => {
79
+ const heading = "#".repeat(level);
80
+ let md = `\n${heading} ${category.name}\n\n`;
81
+ if (Object.keys(category.tokens).length > 0) {
82
+ md += "| Token | Value | Description |\n";
83
+ md += "|-------|-------|-------------|\n";
84
+ for (const [name, token] of Object.entries(category.tokens)) md += `| \`${name}\` | \`${token.value}\` | ${token.description || "-"} |\n`;
85
+ md += "\n";
86
+ }
87
+ if (category.subcategories) for (const sub of category.subcategories) md += renderCategory(sub, level + 1);
88
+ return md;
89
+ };
90
+ let markdown = "# Design Tokens\n";
91
+ for (const category of categories) markdown += renderCategory(category);
92
+ return markdown;
93
+ }
94
+ function isTokenLeaf(value) {
95
+ if (typeof value !== "object" || value === null) return false;
96
+ const obj = value;
97
+ return "value" in obj && (typeof obj.value === "string" || typeof obj.value === "number");
98
+ }
99
+ function extractTokenValues(obj) {
100
+ const tokens = {};
101
+ for (const [key, value] of Object.entries(obj)) if (isTokenLeaf(value)) {
102
+ const raw = value;
103
+ tokens[key] = {
104
+ value: raw.value,
105
+ type: raw.type,
106
+ description: raw.description
107
+ };
108
+ }
109
+ return tokens;
110
+ }
111
+ function extractSubcats(obj) {
112
+ const subcategories = [];
113
+ for (const [key, value] of Object.entries(obj)) if (!isTokenLeaf(value) && typeof value === "object" && value !== null) {
114
+ const tokens = extractTokenValues(value);
115
+ const nested = extractSubcats(value);
116
+ if (Object.keys(tokens).length > 0 || nested && nested.length > 0) subcategories.push({
117
+ name: formatCategoryName(key),
118
+ tokens,
119
+ subcategories: nested
120
+ });
121
+ }
122
+ return subcategories.length > 0 ? subcategories : void 0;
123
+ }
124
+ function flattenTokenStructure(tokens) {
125
+ const categories = [];
126
+ for (const [key, value] of Object.entries(tokens)) {
127
+ if (isTokenLeaf(value)) continue;
128
+ if (typeof value === "object" && value !== null) {
129
+ const categoryTokens = extractTokenValues(value);
130
+ const subcategories = flattenTokenStructure(value);
131
+ if (Object.keys(categoryTokens).length > 0 || subcategories.length > 0) categories.push({
132
+ name: formatCategoryName(key),
133
+ tokens: categoryTokens,
134
+ subcategories: subcategories.length > 0 ? subcategories : void 0
135
+ });
136
+ }
137
+ }
138
+ return categories;
139
+ }
140
+ function formatCategoryName(name) {
141
+ return name.replace(/[-_]/g, " ").replace(/([a-z])([A-Z])/g, "$1 $2").split(" ").map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
142
+ }
143
+
144
+ //#endregion
145
+ //#region src/tools.ts
146
+ const toolDefinitions = [
147
+ {
148
+ name: "analyze_component",
149
+ description: "Statically analyze a Vue SFC to extract its props and emits. Useful for understanding a component's public API when building or reviewing a design system.",
150
+ inputSchema: {
151
+ type: "object",
152
+ properties: { path: {
153
+ type: "string",
154
+ description: "Path to the .vue component file (relative to project root)"
155
+ } },
156
+ required: ["path"]
157
+ }
158
+ },
159
+ {
160
+ name: "get_palette",
161
+ description: "Derive an interactive props palette (control types, defaults, ranges, options) for a component described by an Art file. Helps to understand how props can be tweaked in a design system playground.",
162
+ inputSchema: {
163
+ type: "object",
164
+ properties: { path: {
165
+ type: "string",
166
+ description: "Path to the .art.vue file (relative to project root)"
167
+ } },
168
+ required: ["path"]
169
+ }
170
+ },
171
+ {
172
+ name: "list_components",
173
+ description: "List components registered in the design system. Returns titles, categories, tags, and variant counts.",
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ category: {
178
+ type: "string",
179
+ description: "Filter by category"
180
+ },
181
+ tag: {
182
+ type: "string",
183
+ description: "Filter by tag"
184
+ }
185
+ }
186
+ }
187
+ },
188
+ {
189
+ name: "get_component",
190
+ description: "Get full details of a design-system component: metadata, variant list, and script/style information.",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: { path: {
194
+ type: "string",
195
+ description: "Path to the .art.vue file (relative to project root)"
196
+ } },
197
+ required: ["path"]
198
+ }
199
+ },
200
+ {
201
+ name: "get_variant",
202
+ description: "Retrieve a single variant (template and metadata) from a component.",
203
+ inputSchema: {
204
+ type: "object",
205
+ properties: {
206
+ path: {
207
+ type: "string",
208
+ description: "Path to the .art.vue file"
209
+ },
210
+ variant: {
211
+ type: "string",
212
+ description: "Variant name"
213
+ }
214
+ },
215
+ required: ["path", "variant"]
216
+ }
217
+ },
218
+ {
219
+ name: "search_components",
220
+ description: "Full-text search over component titles, descriptions, and tags.",
221
+ inputSchema: {
222
+ type: "object",
223
+ properties: { query: {
224
+ type: "string",
225
+ description: "Search query"
226
+ } },
227
+ required: ["query"]
228
+ }
229
+ },
230
+ {
231
+ name: "generate_variants",
232
+ description: "Analyze a Vue component's props and auto-generate an .art.vue file containing appropriate variant combinations (default, boolean toggles, enum values, etc.).",
233
+ inputSchema: {
234
+ type: "object",
235
+ properties: {
236
+ componentPath: {
237
+ type: "string",
238
+ description: "Path to the .vue component file (relative to project root)"
239
+ },
240
+ maxVariants: {
241
+ type: "number",
242
+ description: "Maximum number of variants to generate (default: 20)"
243
+ },
244
+ includeDefault: {
245
+ type: "boolean",
246
+ description: "Include a default variant (default: true)"
247
+ },
248
+ includeBooleanToggles: {
249
+ type: "boolean",
250
+ description: "Generate variants that toggle each boolean prop (default: true)"
251
+ },
252
+ includeEnumVariants: {
253
+ type: "boolean",
254
+ description: "Generate one variant per enum/union value (default: true)"
255
+ }
256
+ },
257
+ required: ["componentPath"]
258
+ }
259
+ },
260
+ {
261
+ name: "generate_csf",
262
+ description: "Convert an .art.vue file into Storybook CSF 3.0 code for integration with existing Storybook setups.",
263
+ inputSchema: {
264
+ type: "object",
265
+ properties: { path: {
266
+ type: "string",
267
+ description: "Path to the .art.vue file"
268
+ } },
269
+ required: ["path"]
270
+ }
271
+ },
272
+ {
273
+ name: "generate_docs",
274
+ description: "Generate Markdown documentation for a design-system component from its .art.vue definition.",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {
278
+ path: {
279
+ type: "string",
280
+ description: "Path to the .art.vue file (relative to project root)"
281
+ },
282
+ includeSource: {
283
+ type: "boolean",
284
+ description: "Embed source code in the output (default: false)"
285
+ },
286
+ includeTemplates: {
287
+ type: "boolean",
288
+ description: "Embed variant templates in the output (default: false)"
289
+ }
290
+ },
291
+ required: ["path"]
292
+ }
293
+ },
294
+ {
295
+ name: "generate_catalog",
296
+ description: "Produce a single Markdown catalog covering every component in the design system, grouped by category.",
297
+ inputSchema: {
298
+ type: "object",
299
+ properties: {
300
+ includeSource: {
301
+ type: "boolean",
302
+ description: "Embed source code in the catalog (default: false)"
303
+ },
304
+ includeTemplates: {
305
+ type: "boolean",
306
+ description: "Embed variant templates in the catalog (default: false)"
307
+ }
308
+ }
309
+ }
310
+ },
311
+ {
312
+ name: "get_tokens",
313
+ description: "Read design tokens (colors, spacing, typography, etc.) from a Style Dictionary–compatible JSON file or directory. Auto-detects common paths if not specified.",
314
+ inputSchema: {
315
+ type: "object",
316
+ properties: {
317
+ tokensPath: {
318
+ type: "string",
319
+ description: "Path to tokens JSON file or directory (relative to project root). Auto-detects tokens/, design-tokens/, or style-dictionary/ if omitted."
320
+ },
321
+ format: {
322
+ type: "string",
323
+ enum: ["json", "markdown"],
324
+ description: "Output format (default: json)"
325
+ }
326
+ }
327
+ }
328
+ }
329
+ ];
330
+ async function handleToolCall(ctx, name, args) {
331
+ const binding = ctx.loadNative();
332
+ switch (name) {
333
+ case "analyze_component": {
334
+ const vuePath = args?.path;
335
+ if (!vuePath) throw new McpError(ErrorCode.InvalidParams, "path is required");
336
+ if (!binding.analyzeSfc) throw new McpError(ErrorCode.InternalError, "analyzeSfc not available in native binding");
337
+ const absolutePath = path.resolve(ctx.projectRoot, vuePath);
338
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
339
+ const analysis = binding.analyzeSfc(source, { filename: absolutePath });
340
+ return { content: [{
341
+ type: "text",
342
+ text: JSON.stringify({
343
+ props: analysis.props.map((p) => ({
344
+ name: p.name,
345
+ type: p.type,
346
+ required: p.required,
347
+ defaultValue: p.default_value
348
+ })),
349
+ emits: analysis.emits
350
+ }, null, 2)
351
+ }] };
352
+ }
353
+ case "get_palette": {
354
+ const artPath = args?.path;
355
+ if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
356
+ if (!binding.generateArtPalette) throw new McpError(ErrorCode.InternalError, "generateArtPalette not available in native binding");
357
+ const absolutePath = path.resolve(ctx.projectRoot, artPath);
358
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
359
+ const palette = binding.generateArtPalette(source, { filename: absolutePath });
360
+ return { content: [{
361
+ type: "text",
362
+ text: JSON.stringify({
363
+ title: palette.title,
364
+ controls: palette.controls.map((c) => ({
365
+ name: c.name,
366
+ control: c.control,
367
+ defaultValue: c.default_value,
368
+ description: c.description,
369
+ required: c.required,
370
+ options: c.options,
371
+ range: c.range,
372
+ group: c.group
373
+ })),
374
+ groups: palette.groups,
375
+ json: palette.json,
376
+ typescript: palette.typescript
377
+ }, null, 2)
378
+ }] };
379
+ }
380
+ case "list_components": {
381
+ const arts = await ctx.scanArtFiles();
382
+ let results = Array.from(arts.values());
383
+ if (args?.category) results = results.filter((a) => a.category?.toLowerCase() === args.category.toLowerCase());
384
+ if (args?.tag) results = results.filter((a) => a.tags.some((t) => t.toLowerCase() === args.tag.toLowerCase()));
385
+ return { content: [{
386
+ type: "text",
387
+ text: JSON.stringify(results.map((r) => ({
388
+ path: path.relative(ctx.projectRoot, r.path),
389
+ title: r.title,
390
+ description: r.description,
391
+ component: r.component,
392
+ category: r.category,
393
+ tags: r.tags,
394
+ variantCount: r.variantCount
395
+ })), null, 2)
396
+ }] };
397
+ }
398
+ case "get_component": {
399
+ const artPath = args?.path;
400
+ if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
401
+ const absolutePath = path.resolve(ctx.projectRoot, artPath);
402
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
403
+ const parsed = binding.parseArt(source, { filename: absolutePath });
404
+ return { content: [{
405
+ type: "text",
406
+ text: JSON.stringify({
407
+ metadata: parsed.metadata,
408
+ variants: parsed.variants.map((v) => ({
409
+ name: v.name,
410
+ template: v.template,
411
+ isDefault: v.is_default,
412
+ skipVrt: v.skip_vrt
413
+ })),
414
+ hasScriptSetup: parsed.has_script_setup,
415
+ hasScript: parsed.has_script,
416
+ styleCount: parsed.style_count
417
+ }, null, 2)
418
+ }] };
419
+ }
420
+ case "get_variant": {
421
+ const artPath = args?.path;
422
+ const variantName = args?.variant;
423
+ if (!artPath || !variantName) throw new McpError(ErrorCode.InvalidParams, "path and variant are required");
424
+ const absolutePath = path.resolve(ctx.projectRoot, artPath);
425
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
426
+ const parsed = binding.parseArt(source, { filename: absolutePath });
427
+ const variant = parsed.variants.find((v) => v.name.toLowerCase() === variantName.toLowerCase());
428
+ if (!variant) throw new McpError(ErrorCode.InvalidParams, `Variant "${variantName}" not found`);
429
+ return { content: [{
430
+ type: "text",
431
+ text: JSON.stringify({
432
+ name: variant.name,
433
+ template: variant.template,
434
+ isDefault: variant.is_default,
435
+ skipVrt: variant.skip_vrt
436
+ }, null, 2)
437
+ }] };
438
+ }
439
+ case "search_components": {
440
+ const query = (args?.query)?.toLowerCase();
441
+ if (!query) throw new McpError(ErrorCode.InvalidParams, "query is required");
442
+ const arts = await ctx.scanArtFiles();
443
+ const results = Array.from(arts.values()).filter((a) => a.title.toLowerCase().includes(query) || a.description?.toLowerCase().includes(query) || a.tags.some((t) => t.toLowerCase().includes(query)));
444
+ return { content: [{
445
+ type: "text",
446
+ text: JSON.stringify(results.map((r) => ({
447
+ path: path.relative(ctx.projectRoot, r.path),
448
+ title: r.title,
449
+ description: r.description,
450
+ component: r.component,
451
+ category: r.category,
452
+ tags: r.tags
453
+ })), null, 2)
454
+ }] };
455
+ }
456
+ case "generate_variants": {
457
+ const componentRelPath = args?.componentPath;
458
+ if (!componentRelPath) throw new McpError(ErrorCode.InvalidParams, "componentPath is required");
459
+ if (!binding.analyzeSfc) throw new McpError(ErrorCode.InternalError, "analyzeSfc not available in native binding");
460
+ if (!binding.generateVariants) throw new McpError(ErrorCode.InternalError, "generateVariants not available in native binding");
461
+ const absolutePath = path.resolve(ctx.projectRoot, componentRelPath);
462
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
463
+ const analysis = binding.analyzeSfc(source, { filename: absolutePath });
464
+ const props = analysis.props.map((p) => ({
465
+ name: p.name,
466
+ prop_type: p.type,
467
+ required: p.required,
468
+ default_value: p.default_value
469
+ }));
470
+ const relPath = `./${path.basename(absolutePath)}`;
471
+ const result = binding.generateVariants(relPath, props, {
472
+ max_variants: args?.maxVariants,
473
+ include_default: args?.includeDefault,
474
+ include_boolean_toggles: args?.includeBooleanToggles,
475
+ include_enum_variants: args?.includeEnumVariants
476
+ });
477
+ return { content: [{
478
+ type: "text",
479
+ text: JSON.stringify({
480
+ componentName: result.component_name,
481
+ artFileContent: result.art_file_content,
482
+ variants: result.variants.map((v) => ({
483
+ name: v.name,
484
+ isDefault: v.is_default,
485
+ props: v.props,
486
+ description: v.description
487
+ }))
488
+ }, null, 2)
489
+ }] };
490
+ }
491
+ case "generate_csf": {
492
+ const artPath = args?.path;
493
+ if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
494
+ const absolutePath = path.resolve(ctx.projectRoot, artPath);
495
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
496
+ const csf = binding.artToCsf(source, { filename: absolutePath });
497
+ return { content: [{
498
+ type: "text",
499
+ text: csf.code
500
+ }] };
501
+ }
502
+ case "generate_docs": {
503
+ const artPath = args?.path;
504
+ if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
505
+ if (!binding.generateArtDoc) throw new McpError(ErrorCode.InternalError, "generateArtDoc not available in native binding");
506
+ const absolutePath = path.resolve(ctx.projectRoot, artPath);
507
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
508
+ const doc = binding.generateArtDoc(source, { filename: absolutePath }, {
509
+ include_source: args?.includeSource,
510
+ include_templates: args?.includeTemplates,
511
+ include_metadata: true
512
+ });
513
+ return { content: [{
514
+ type: "text",
515
+ text: JSON.stringify({
516
+ markdown: doc.markdown,
517
+ title: doc.title,
518
+ category: doc.category,
519
+ variantCount: doc.variant_count
520
+ }, null, 2)
521
+ }] };
522
+ }
523
+ case "generate_catalog": {
524
+ if (!binding.generateArtCatalog) throw new McpError(ErrorCode.InternalError, "generateArtCatalog not available in native binding");
525
+ const arts = await ctx.scanArtFiles();
526
+ const sources = [];
527
+ for (const [filePath] of arts) {
528
+ const source = await fs.promises.readFile(filePath, "utf-8");
529
+ sources.push(source);
530
+ }
531
+ const catalog = binding.generateArtCatalog(sources, {
532
+ include_source: args?.includeSource,
533
+ include_templates: args?.includeTemplates,
534
+ include_metadata: true
535
+ });
536
+ return { content: [{
537
+ type: "text",
538
+ text: JSON.stringify({
539
+ markdown: catalog.markdown,
540
+ componentCount: catalog.component_count,
541
+ categories: catalog.categories,
542
+ tags: catalog.tags
543
+ }, null, 2)
544
+ }] };
545
+ }
546
+ case "get_tokens": {
547
+ const inputPath = args?.tokensPath;
548
+ const format = args?.format ?? "json";
549
+ let resolvedPath;
550
+ if (inputPath) resolvedPath = path.resolve(ctx.projectRoot, inputPath);
551
+ else resolvedPath = await ctx.resolveTokensPath();
552
+ if (!resolvedPath) throw new McpError(ErrorCode.InvalidParams, "No tokens path provided and none auto-detected. Looked for: tokens/, design-tokens/, style-dictionary/ directories.");
553
+ const categories = await parseTokensFromPath(resolvedPath);
554
+ if (format === "markdown") return { content: [{
555
+ type: "text",
556
+ text: generateTokensMarkdown(categories)
557
+ }] };
558
+ return { content: [{
559
+ type: "text",
560
+ text: JSON.stringify({ categories }, null, 2)
561
+ }] };
562
+ }
563
+ default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
564
+ }
565
+ }
566
+
567
+ //#endregion
568
+ //#region src/resources.ts
569
+ async function listResources(ctx) {
570
+ const arts = await ctx.scanArtFiles();
571
+ const resources = [];
572
+ for (const [filePath, info] of arts) {
573
+ const relativePath = path.relative(ctx.projectRoot, filePath);
574
+ resources.push({
575
+ uri: `musea://component/${encodeURIComponent(relativePath)}`,
576
+ name: info.title,
577
+ description: info.description || `${info.category || "Component"} — ${info.variantCount} variant(s)`,
578
+ mimeType: "application/json"
579
+ });
580
+ resources.push({
581
+ uri: `musea://docs/${encodeURIComponent(relativePath)}`,
582
+ name: `${info.title} — Documentation`,
583
+ description: `Markdown docs for ${info.title}`,
584
+ mimeType: "text/markdown"
585
+ });
586
+ }
587
+ const resolvedTokensPath = await ctx.resolveTokensPath();
588
+ if (resolvedTokensPath) resources.push({
589
+ uri: "musea://tokens",
590
+ name: "Design Tokens",
591
+ description: "Project design tokens (colors, spacing, typography, …)",
592
+ mimeType: "application/json"
593
+ });
594
+ return { resources };
595
+ }
596
+ async function readResource(ctx, uri) {
597
+ if (uri.startsWith("musea://component/")) {
598
+ const relativePath = decodeURIComponent(uri.slice(18));
599
+ const absolutePath = path.resolve(ctx.projectRoot, relativePath);
600
+ try {
601
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
602
+ const binding = ctx.loadNative();
603
+ const parsed = binding.parseArt(source, { filename: absolutePath });
604
+ return { contents: [{
605
+ uri,
606
+ mimeType: "application/json",
607
+ text: JSON.stringify({
608
+ path: relativePath,
609
+ metadata: parsed.metadata,
610
+ variants: parsed.variants.map((v) => ({
611
+ name: v.name,
612
+ template: v.template,
613
+ isDefault: v.is_default,
614
+ skipVrt: v.skip_vrt
615
+ })),
616
+ hasScriptSetup: parsed.has_script_setup,
617
+ hasScript: parsed.has_script,
618
+ styleCount: parsed.style_count
619
+ }, null, 2)
620
+ }] };
621
+ } catch (e) {
622
+ throw new McpError(ErrorCode.InternalError, `Failed to read component: ${String(e)}`);
623
+ }
624
+ }
625
+ if (uri.startsWith("musea://docs/")) {
626
+ const relativePath = decodeURIComponent(uri.slice(13));
627
+ const absolutePath = path.resolve(ctx.projectRoot, relativePath);
628
+ try {
629
+ const source = await fs.promises.readFile(absolutePath, "utf-8");
630
+ const binding = ctx.loadNative();
631
+ if (!binding.generateArtDoc) throw new McpError(ErrorCode.InternalError, "generateArtDoc not available in native binding");
632
+ const doc = binding.generateArtDoc(source, { filename: absolutePath });
633
+ return { contents: [{
634
+ uri,
635
+ mimeType: "text/markdown",
636
+ text: doc.markdown
637
+ }] };
638
+ } catch (e) {
639
+ if (e instanceof McpError) throw e;
640
+ throw new McpError(ErrorCode.InternalError, `Failed to generate docs: ${String(e)}`);
641
+ }
642
+ }
643
+ if (uri === "musea://tokens") {
644
+ const resolvedTokensPath = await ctx.resolveTokensPath();
645
+ if (!resolvedTokensPath) throw new McpError(ErrorCode.InternalError, "No tokens path configured or auto-detected");
646
+ try {
647
+ const categories = await parseTokensFromPath(resolvedTokensPath);
648
+ return { contents: [{
649
+ uri,
650
+ mimeType: "application/json",
651
+ text: JSON.stringify({ categories }, null, 2)
652
+ }] };
653
+ } catch (e) {
654
+ throw new McpError(ErrorCode.InternalError, `Failed to read tokens: ${String(e)}`);
655
+ }
656
+ }
657
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`);
658
+ }
659
+
660
+ //#endregion
661
+ //#region src/index.ts
662
+ function createMuseaServer(config) {
663
+ const server = new Server({
664
+ name: "musea-mcp-server",
665
+ version: "0.0.1-alpha.11"
666
+ }, { capabilities: {
667
+ resources: {},
668
+ tools: {}
669
+ } });
670
+ const projectRoot = config.projectRoot;
671
+ const include = config.include ?? ["**/*.art.vue"];
672
+ const exclude = config.exclude ?? ["node_modules/**", "dist/**"];
673
+ const tokensPath = config.tokensPath;
674
+ let artCache = new Map();
675
+ let lastScanTime = 0;
676
+ async function scanArtFiles() {
677
+ const now = Date.now();
678
+ if (now - lastScanTime < 5e3 && artCache.size > 0) return artCache;
679
+ const binding = loadNative();
680
+ const files = await findArtFiles(projectRoot, include, exclude);
681
+ artCache = new Map();
682
+ for (const file of files) try {
683
+ const source = await fs.promises.readFile(file, "utf-8");
684
+ const parsed = binding.parseArt(source, { filename: file });
685
+ artCache.set(file, {
686
+ path: file,
687
+ title: parsed.metadata.title,
688
+ description: parsed.metadata.description,
689
+ component: parsed.metadata.component,
690
+ category: parsed.metadata.category,
691
+ tags: parsed.metadata.tags,
692
+ variantCount: parsed.variants.length
693
+ });
694
+ } catch (e) {
695
+ console.error(`Failed to parse ${file}:`, e);
696
+ }
697
+ lastScanTime = now;
698
+ return artCache;
699
+ }
700
+ async function resolveTokensPath() {
701
+ if (tokensPath) return path.resolve(projectRoot, tokensPath);
702
+ const candidates = [
703
+ "tokens",
704
+ "design-tokens",
705
+ "style-dictionary"
706
+ ];
707
+ for (const dir of candidates) {
708
+ const candidate = path.join(projectRoot, dir);
709
+ try {
710
+ const stat = await fs.promises.stat(candidate);
711
+ if (stat.isDirectory() || stat.isFile()) return candidate;
712
+ } catch {}
713
+ }
714
+ return null;
715
+ }
716
+ const ctx = {
717
+ projectRoot,
718
+ loadNative,
719
+ scanArtFiles,
720
+ resolveTokensPath
721
+ };
722
+ server.setRequestHandler(ListResourcesRequestSchema, () => listResources(ctx));
723
+ server.setRequestHandler(ReadResourceRequestSchema, (req) => readResource(ctx, req.params.uri));
724
+ server.setRequestHandler(ListToolsRequestSchema, () => Promise.resolve({ tools: toolDefinitions }));
725
+ server.setRequestHandler(CallToolRequestSchema, (req) => handleToolCall(ctx, req.params.name, req.params.arguments));
726
+ return server;
727
+ }
728
+ async function startServer(projectRoot, options) {
729
+ const server = createMuseaServer({
730
+ projectRoot,
731
+ tokensPath: options?.tokensPath
732
+ });
733
+ const transport = new StdioServerTransport();
734
+ await server.connect(transport);
735
+ console.error("[musea-mcp] Server started");
736
+ }
737
+ var src_default = createMuseaServer;
738
+
739
+ //#endregion
740
+ export { createMuseaServer, src_default, startServer };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@vizejs/musea-mcp-server",
3
- "version": "0.0.1-alpha.74",
4
- "description": "MCP server for Musea - AI-accessible component gallery",
3
+ "version": "0.0.1-alpha.75",
4
+ "description": "MCP server for building Vue.js design systems - component analysis, documentation, variant generation, and design tokens",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -21,7 +21,8 @@
21
21
  "mcp",
22
22
  "model-context-protocol",
23
23
  "vue",
24
- "component-gallery",
24
+ "design-system",
25
+ "component-analysis",
25
26
  "musea",
26
27
  "ai"
27
28
  ],
@@ -34,7 +35,7 @@
34
35
  },
35
36
  "dependencies": {
36
37
  "@modelcontextprotocol/sdk": "^0.5.0",
37
- "@vizejs/native": "0.0.1-alpha.74"
38
+ "@vizejs/native": "0.0.1-alpha.75"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@types/node": "^22.14.0",
@@ -1,313 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
4
- import fs from "node:fs";
5
- import path from "node:path";
6
- import { createRequire } from "node:module";
7
-
8
- //#region src/index.ts
9
- let native = null;
10
- function loadNative() {
11
- if (native) return native;
12
- const require = createRequire(import.meta.url);
13
- try {
14
- native = require("@vizejs/native");
15
- return native;
16
- } catch (e) {
17
- throw new Error(`Failed to load @vizejs/native. Make sure it's installed: ${String(e)}`);
18
- }
19
- }
20
- /**
21
- * Create and configure the MCP server.
22
- */
23
- function createMuseaServer(config) {
24
- const server = new Server({
25
- name: "musea-mcp-server",
26
- version: "0.0.1-alpha.11"
27
- }, { capabilities: {
28
- resources: {},
29
- tools: {}
30
- } });
31
- const projectRoot = config.projectRoot;
32
- const include = config.include ?? ["**/*.art.vue"];
33
- const exclude = config.exclude ?? ["node_modules/**", "dist/**"];
34
- let artCache = new Map();
35
- let lastScanTime = 0;
36
- async function scanArtFiles() {
37
- const now = Date.now();
38
- if (now - lastScanTime < 5e3 && artCache.size > 0) return artCache;
39
- const binding = loadNative();
40
- const files = await findArtFiles(projectRoot, include, exclude);
41
- artCache = new Map();
42
- for (const file of files) try {
43
- const source = await fs.promises.readFile(file, "utf-8");
44
- const parsed = binding.parseArt(source, { filename: file });
45
- artCache.set(file, {
46
- path: file,
47
- title: parsed.metadata.title,
48
- description: parsed.metadata.description,
49
- category: parsed.metadata.category,
50
- tags: parsed.metadata.tags,
51
- variantCount: parsed.variants.length
52
- });
53
- } catch (e) {
54
- console.error(`Failed to parse ${file}:`, e);
55
- }
56
- lastScanTime = now;
57
- return artCache;
58
- }
59
- server.setRequestHandler(ListResourcesRequestSchema, async () => {
60
- const arts = await scanArtFiles();
61
- const resources = [];
62
- for (const [filePath, info] of arts) {
63
- const relativePath = path.relative(projectRoot, filePath);
64
- resources.push({
65
- uri: `musea://art/${encodeURIComponent(relativePath)}`,
66
- name: info.title,
67
- description: info.description || `${info.category || "Component"} with ${info.variantCount} variants`,
68
- mimeType: "application/json"
69
- });
70
- }
71
- return { resources };
72
- });
73
- server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
74
- const { uri } = request.params;
75
- if (!uri.startsWith("musea://art/")) throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`);
76
- const relativePath = decodeURIComponent(uri.slice(12));
77
- const absolutePath = path.resolve(projectRoot, relativePath);
78
- try {
79
- const source = await fs.promises.readFile(absolutePath, "utf-8");
80
- const binding = loadNative();
81
- const parsed = binding.parseArt(source, { filename: absolutePath });
82
- return { contents: [{
83
- uri,
84
- mimeType: "application/json",
85
- text: JSON.stringify({
86
- path: relativePath,
87
- metadata: parsed.metadata,
88
- variants: parsed.variants.map((v) => ({
89
- name: v.name,
90
- template: v.template,
91
- isDefault: v.is_default,
92
- skipVrt: v.skip_vrt
93
- })),
94
- hasScriptSetup: parsed.has_script_setup,
95
- hasScript: parsed.has_script,
96
- styleCount: parsed.style_count
97
- }, null, 2)
98
- }] };
99
- } catch (e) {
100
- throw new McpError(ErrorCode.InternalError, `Failed to read art file: ${String(e)}`);
101
- }
102
- });
103
- server.setRequestHandler(ListToolsRequestSchema, async () => {
104
- return { tools: [
105
- {
106
- name: "list_components",
107
- description: "List all components (Art files) in the project with their metadata",
108
- inputSchema: {
109
- type: "object",
110
- properties: {
111
- category: {
112
- type: "string",
113
- description: "Filter by category"
114
- },
115
- tag: {
116
- type: "string",
117
- description: "Filter by tag"
118
- }
119
- }
120
- }
121
- },
122
- {
123
- name: "get_component",
124
- description: "Get detailed information about a specific component",
125
- inputSchema: {
126
- type: "object",
127
- properties: { path: {
128
- type: "string",
129
- description: "Path to the Art file (relative to project root)"
130
- } },
131
- required: ["path"]
132
- }
133
- },
134
- {
135
- name: "get_variant",
136
- description: "Get a specific variant from a component",
137
- inputSchema: {
138
- type: "object",
139
- properties: {
140
- path: {
141
- type: "string",
142
- description: "Path to the Art file"
143
- },
144
- variant: {
145
- type: "string",
146
- description: "Name of the variant"
147
- }
148
- },
149
- required: ["path", "variant"]
150
- }
151
- },
152
- {
153
- name: "generate_csf",
154
- description: "Generate Storybook CSF 3.0 code from an Art file",
155
- inputSchema: {
156
- type: "object",
157
- properties: { path: {
158
- type: "string",
159
- description: "Path to the Art file"
160
- } },
161
- required: ["path"]
162
- }
163
- },
164
- {
165
- name: "search_components",
166
- description: "Search components by title, description, or tags",
167
- inputSchema: {
168
- type: "object",
169
- properties: { query: {
170
- type: "string",
171
- description: "Search query"
172
- } },
173
- required: ["query"]
174
- }
175
- }
176
- ] };
177
- });
178
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
179
- const { name, arguments: args } = request.params;
180
- const binding = loadNative();
181
- switch (name) {
182
- case "list_components": {
183
- const arts = await scanArtFiles();
184
- let results = Array.from(arts.values());
185
- if (args?.category) results = results.filter((a) => a.category?.toLowerCase() === args.category.toLowerCase());
186
- if (args?.tag) results = results.filter((a) => a.tags.some((t) => t.toLowerCase() === args.tag.toLowerCase()));
187
- return { content: [{
188
- type: "text",
189
- text: JSON.stringify(results.map((r) => ({
190
- path: path.relative(projectRoot, r.path),
191
- title: r.title,
192
- description: r.description,
193
- category: r.category,
194
- tags: r.tags,
195
- variantCount: r.variantCount
196
- })), null, 2)
197
- }] };
198
- }
199
- case "get_component": {
200
- const artPath = args?.path;
201
- if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
202
- const absolutePath = path.resolve(projectRoot, artPath);
203
- const source = await fs.promises.readFile(absolutePath, "utf-8");
204
- const parsed = binding.parseArt(source, { filename: absolutePath });
205
- return { content: [{
206
- type: "text",
207
- text: JSON.stringify({
208
- metadata: parsed.metadata,
209
- variants: parsed.variants.map((v) => ({
210
- name: v.name,
211
- template: v.template,
212
- isDefault: v.is_default,
213
- skipVrt: v.skip_vrt
214
- })),
215
- hasScriptSetup: parsed.has_script_setup,
216
- hasScript: parsed.has_script,
217
- styleCount: parsed.style_count
218
- }, null, 2)
219
- }] };
220
- }
221
- case "get_variant": {
222
- const artPath = args?.path;
223
- const variantName = args?.variant;
224
- if (!artPath || !variantName) throw new McpError(ErrorCode.InvalidParams, "path and variant are required");
225
- const absolutePath = path.resolve(projectRoot, artPath);
226
- const source = await fs.promises.readFile(absolutePath, "utf-8");
227
- const parsed = binding.parseArt(source, { filename: absolutePath });
228
- const variant = parsed.variants.find((v) => v.name.toLowerCase() === variantName.toLowerCase());
229
- if (!variant) throw new McpError(ErrorCode.InvalidParams, `Variant "${variantName}" not found`);
230
- return { content: [{
231
- type: "text",
232
- text: JSON.stringify({
233
- name: variant.name,
234
- template: variant.template,
235
- isDefault: variant.is_default,
236
- skipVrt: variant.skip_vrt
237
- }, null, 2)
238
- }] };
239
- }
240
- case "generate_csf": {
241
- const artPath = args?.path;
242
- if (!artPath) throw new McpError(ErrorCode.InvalidParams, "path is required");
243
- const absolutePath = path.resolve(projectRoot, artPath);
244
- const source = await fs.promises.readFile(absolutePath, "utf-8");
245
- const csf = binding.artToCsf(source, { filename: absolutePath });
246
- return { content: [{
247
- type: "text",
248
- text: csf.code
249
- }] };
250
- }
251
- case "search_components": {
252
- const query = (args?.query)?.toLowerCase();
253
- if (!query) throw new McpError(ErrorCode.InvalidParams, "query is required");
254
- const arts = await scanArtFiles();
255
- const results = Array.from(arts.values()).filter((a) => a.title.toLowerCase().includes(query) || a.description?.toLowerCase().includes(query) || a.tags.some((t) => t.toLowerCase().includes(query)));
256
- return { content: [{
257
- type: "text",
258
- text: JSON.stringify(results.map((r) => ({
259
- path: path.relative(projectRoot, r.path),
260
- title: r.title,
261
- description: r.description,
262
- category: r.category,
263
- tags: r.tags
264
- })), null, 2)
265
- }] };
266
- }
267
- default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
268
- }
269
- });
270
- return server;
271
- }
272
- async function findArtFiles(root, include, exclude) {
273
- const files = [];
274
- async function scan(dir) {
275
- const entries = await fs.promises.readdir(dir, { withFileTypes: true });
276
- for (const entry of entries) {
277
- const fullPath = path.join(dir, entry.name);
278
- const relative = path.relative(root, fullPath);
279
- let excluded = false;
280
- for (const pattern of exclude) if (matchGlob(relative, pattern) || matchGlob(entry.name, pattern)) {
281
- excluded = true;
282
- break;
283
- }
284
- if (excluded) continue;
285
- if (entry.isDirectory()) await scan(fullPath);
286
- else if (entry.isFile() && entry.name.endsWith(".art.vue")) {
287
- for (const pattern of include) if (matchGlob(relative, pattern)) {
288
- files.push(fullPath);
289
- break;
290
- }
291
- }
292
- }
293
- }
294
- await scan(root);
295
- return files;
296
- }
297
- function matchGlob(filepath, pattern) {
298
- const regex = pattern.replace(/\*\*/g, "{{DOUBLE_STAR}}").replace(/\*/g, "[^/]*").replace(/{{DOUBLE_STAR}}/g, ".*").replace(/\./g, "\\.");
299
- return new RegExp(`^${regex}$`).test(filepath);
300
- }
301
- /**
302
- * Start the MCP server with stdio transport.
303
- */
304
- async function startServer(projectRoot) {
305
- const server = createMuseaServer({ projectRoot });
306
- const transport = new StdioServerTransport();
307
- await server.connect(transport);
308
- console.error("[musea-mcp] Server started");
309
- }
310
- var src_default = createMuseaServer;
311
-
312
- //#endregion
313
- export { createMuseaServer, src_default, startServer };