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