@vizejs/musea-mcp-server 0.0.1-alpha.100

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/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # @vizejs/musea-mcp-server
2
+
3
+ MCP (Model Context Protocol) server for Musea design system integration.
4
+
5
+ ## Features
6
+
7
+ - **AI Integration** - Connect Musea to AI assistants
8
+ - **Component Discovery** - Query available components
9
+ - **Design Token Access** - Retrieve design system tokens
10
+ - **Documentation** - Access component docs via MCP
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install -g @vizejs/musea-mcp-server
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### With Claude Desktop
21
+
22
+ Add to `claude_desktop_config.json`:
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "musea": {
28
+ "command": "musea-mcp-server",
29
+ "args": ["--project", "/path/to/project"]
30
+ }
31
+ }
32
+ }
33
+ ```
34
+
35
+ ### Standalone
36
+
37
+ ```bash
38
+ musea-mcp-server --project ./my-vue-app
39
+ ```
40
+
41
+ ## MCP Tools
42
+
43
+ - `list_components` - List all components
44
+ - `get_component` - Get component details
45
+ - `get_variants` - Get component variants
46
+ - `get_tokens` - Get design tokens
47
+
48
+ ## License
49
+
50
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ export { };
package/dist/cli.js ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { startServer } from "./src-ZT3oDiIm.js";
3
+
4
+ //#region src/cli.ts
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];
10
+ console.error(`[musea-mcp] Starting server for project: ${projectRoot}`);
11
+ if (tokensPath) console.error(`[musea-mcp] Tokens path: ${tokensPath}`);
12
+ startServer(projectRoot, { tokensPath }).catch((error) => {
13
+ console.error("[musea-mcp] Failed to start:", error);
14
+ process.exit(1);
15
+ });
16
+
17
+ //#endregion
@@ -0,0 +1,14 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+
3
+ //#region src/index.d.ts
4
+
5
+ declare function createMuseaServer(config: {
6
+ projectRoot: string;
7
+ include?: string[];
8
+ exclude?: string[];
9
+ tokensPath?: string;
10
+ }): Server;
11
+ declare function startServer(projectRoot: string, options?: {
12
+ tokensPath?: string;
13
+ }): Promise<void>; //#endregion
14
+ export { createMuseaServer, createMuseaServer as default, startServer };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import { createMuseaServer, src_default, startServer } from "./src-ZT3oDiIm.js";
2
+
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 ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@vizejs/musea-mcp-server",
3
+ "version": "0.0.1-alpha.100",
4
+ "description": "MCP server for building Vue.js design systems - component analysis, documentation, variant generation, and design tokens",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "bin": {
9
+ "musea-mcp": "./dist/cli.js"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "keywords": [
21
+ "mcp",
22
+ "model-context-protocol",
23
+ "vue",
24
+ "design-system",
25
+ "component-analysis",
26
+ "musea",
27
+ "ai"
28
+ ],
29
+ "author": "ubugeeei",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/ubugeeei/vize.git",
34
+ "directory": "npm/musea-mcp-server"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^0.5.0",
38
+ "@vizejs/native": "0.0.1-alpha.100"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^22.14.0",
42
+ "tsdown": "^0.9.0",
43
+ "typescript": "^5.7.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "scripts": {
49
+ "build": "tsdown",
50
+ "dev": "tsdown --watch",
51
+ "start": "node dist/cli.js",
52
+ "lint": "oxlint --deny-warnings --type-aware --tsconfig tsconfig.json",
53
+ "lint:fix": "oxlint --type-aware --tsconfig tsconfig.json --fix",
54
+ "fmt": "oxfmt --write src",
55
+ "fmt:check": "oxfmt src"
56
+ }
57
+ }