@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 +8 -3
- package/dist/index.d.ts +4 -10
- package/dist/index.js +1 -1
- package/dist/src-ZT3oDiIm.js +740 -0
- package/package.json +5 -4
- package/dist/src-DMEJQckJ.js +0 -313
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { startServer } from "./src-
|
|
2
|
+
import { startServer } from "./src-ZT3oDiIm.js";
|
|
3
3
|
|
|
4
4
|
//#region src/cli.ts
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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.
|
|
4
|
-
"description": "MCP server for
|
|
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
|
-
"
|
|
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.
|
|
38
|
+
"@vizejs/native": "0.0.1-alpha.75"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^22.14.0",
|
package/dist/src-DMEJQckJ.js
DELETED
|
@@ -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 };
|