@fragments-sdk/cli 0.15.9 → 0.15.10
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 +6 -6
- package/dist/mcp-bin.js +779 -132
- package/dist/mcp-bin.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/label.contract.json +1 -1
- package/src/commands/__fixtures__/shadcn-label-wrapper/src/components/ui/primitive.contract.json +1 -1
- package/src/mcp/__tests__/server.integration.test.ts +342 -0
- package/src/mcp/server.ts +924 -138
- package/src/mcp/version.ts +17 -0
package/dist/mcp-bin.js
CHANGED
|
@@ -18,10 +18,26 @@ import {
|
|
|
18
18
|
ListToolsRequestSchema
|
|
19
19
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
20
|
import { buildMcpTools, buildToolNames, CLI_TOOL_EXTENSIONS } from "@fragments-sdk/context/mcp-tools";
|
|
21
|
+
import { ComponentGraphEngine, deserializeGraph } from "@fragments-sdk/context/graph";
|
|
21
22
|
import { readFile } from "fs/promises";
|
|
22
|
-
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
23
|
+
import { existsSync, readFileSync as readFileSync2, readdirSync } from "fs";
|
|
23
24
|
import { join, dirname, resolve } from "path";
|
|
24
25
|
import { createRequire } from "module";
|
|
26
|
+
|
|
27
|
+
// src/mcp/version.ts
|
|
28
|
+
import { readFileSync } from "fs";
|
|
29
|
+
function readPackageVersion() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = readFileSync(new URL("../../package.json", import.meta.url), "utf-8");
|
|
32
|
+
const pkg = JSON.parse(raw);
|
|
33
|
+
return pkg.version ?? "0.0.0";
|
|
34
|
+
} catch {
|
|
35
|
+
return "0.0.0";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
var MCP_SERVER_VERSION = readPackageVersion();
|
|
39
|
+
|
|
40
|
+
// src/mcp/server.ts
|
|
25
41
|
var _service = null;
|
|
26
42
|
async function getService() {
|
|
27
43
|
if (!_service) {
|
|
@@ -35,7 +51,78 @@ async function getService() {
|
|
|
35
51
|
}
|
|
36
52
|
return _service;
|
|
37
53
|
}
|
|
38
|
-
var TOOL_NAMES = buildToolNames(
|
|
54
|
+
var TOOL_NAMES = buildToolNames();
|
|
55
|
+
var TOOLS = buildMcpTools(void 0, CLI_TOOL_EXTENSIONS);
|
|
56
|
+
var DISCOVER_SYNONYM_MAP = {
|
|
57
|
+
form: ["input", "field", "submit", "validation"],
|
|
58
|
+
input: ["form", "field", "text", "entry"],
|
|
59
|
+
button: ["action", "click", "submit", "trigger"],
|
|
60
|
+
action: ["button", "click", "trigger"],
|
|
61
|
+
submit: ["button", "form", "action", "send"],
|
|
62
|
+
alert: ["notification", "message", "warning", "error", "feedback"],
|
|
63
|
+
notification: ["alert", "message", "toast"],
|
|
64
|
+
feedback: ["form", "comment", "review", "rating"],
|
|
65
|
+
card: ["container", "panel", "box", "content"],
|
|
66
|
+
toggle: ["switch", "checkbox", "boolean", "on/off"],
|
|
67
|
+
switch: ["toggle", "checkbox", "boolean"],
|
|
68
|
+
badge: ["tag", "label", "status", "indicator"],
|
|
69
|
+
status: ["badge", "indicator", "state"],
|
|
70
|
+
login: ["auth", "signin", "authentication", "form"],
|
|
71
|
+
auth: ["login", "signin", "authentication"],
|
|
72
|
+
chat: ["message", "conversation", "ai"],
|
|
73
|
+
table: ["data", "grid", "list", "rows"],
|
|
74
|
+
layout: ["stack", "grid", "box", "container", "page"],
|
|
75
|
+
landing: ["page", "hero", "marketing", "section", "layout"],
|
|
76
|
+
hero: ["landing", "marketing", "banner", "headline", "section"],
|
|
77
|
+
marketing: ["landing", "hero", "pricing", "testimonial", "cta"]
|
|
78
|
+
};
|
|
79
|
+
var TOKEN_CATEGORY_ALIASES = {
|
|
80
|
+
colors: ["color", "colors", "accent", "background", "foreground", "semantic", "theme"],
|
|
81
|
+
spacing: ["spacing", "space", "spaces", "padding", "margin", "gap", "inset"],
|
|
82
|
+
typography: ["typography", "type", "font", "fonts", "letter", "line-height"],
|
|
83
|
+
surfaces: ["surface", "surfaces", "canvas", "backgrounds"],
|
|
84
|
+
shadows: ["shadow", "shadows", "elevation"],
|
|
85
|
+
radius: ["radius", "radii", "corner", "corners", "round", "rounded", "rounding"],
|
|
86
|
+
borders: ["border", "borders", "stroke", "outline"],
|
|
87
|
+
text: ["text", "copy", "content"],
|
|
88
|
+
focus: ["focus", "ring", "focus-ring"],
|
|
89
|
+
layout: ["layout", "container", "grid", "breakpoint"],
|
|
90
|
+
code: ["code"],
|
|
91
|
+
"component-sizing": ["component-sizing", "sizing", "size", "sizes"]
|
|
92
|
+
};
|
|
93
|
+
var FRIENDLY_TOKEN_CATEGORY_ORDER = [
|
|
94
|
+
"colors",
|
|
95
|
+
"spacing",
|
|
96
|
+
"typography",
|
|
97
|
+
"surfaces",
|
|
98
|
+
"shadows",
|
|
99
|
+
"radius",
|
|
100
|
+
"borders",
|
|
101
|
+
"text",
|
|
102
|
+
"focus",
|
|
103
|
+
"layout",
|
|
104
|
+
"code",
|
|
105
|
+
"component-sizing"
|
|
106
|
+
];
|
|
107
|
+
var STYLE_QUERY_TERMS = /* @__PURE__ */ new Set([
|
|
108
|
+
"color",
|
|
109
|
+
"colors",
|
|
110
|
+
"spacing",
|
|
111
|
+
"padding",
|
|
112
|
+
"margin",
|
|
113
|
+
"font",
|
|
114
|
+
"border",
|
|
115
|
+
"radius",
|
|
116
|
+
"shadow",
|
|
117
|
+
"variable",
|
|
118
|
+
"token",
|
|
119
|
+
"css",
|
|
120
|
+
"theme",
|
|
121
|
+
"background",
|
|
122
|
+
"hover",
|
|
123
|
+
"surface",
|
|
124
|
+
"focus"
|
|
125
|
+
]);
|
|
39
126
|
var PLACEHOLDER_PATTERNS = [
|
|
40
127
|
/^\w+ component is needed$/i,
|
|
41
128
|
/^Alternative component is more appropriate$/i,
|
|
@@ -47,6 +134,168 @@ function filterPlaceholders(items) {
|
|
|
47
134
|
(item) => !PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(item.trim()))
|
|
48
135
|
);
|
|
49
136
|
}
|
|
137
|
+
function parsePositiveLimit(value, defaultValue, max) {
|
|
138
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
139
|
+
return defaultValue;
|
|
140
|
+
}
|
|
141
|
+
return Math.min(Math.max(Math.floor(value), 1), max);
|
|
142
|
+
}
|
|
143
|
+
function resolveVerbosity(value, compactAlias = false) {
|
|
144
|
+
if (value === "compact" || value === "standard" || value === "full") {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
return compactAlias ? "compact" : "standard";
|
|
148
|
+
}
|
|
149
|
+
function normalizeSearchText(value) {
|
|
150
|
+
return (value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
151
|
+
}
|
|
152
|
+
function splitSearchTerms(value) {
|
|
153
|
+
return Array.from(new Set(normalizeSearchText(value).split(/\s+/).filter(Boolean)));
|
|
154
|
+
}
|
|
155
|
+
function expandSearchTerms(terms) {
|
|
156
|
+
const expanded = new Set(terms);
|
|
157
|
+
for (const term of terms) {
|
|
158
|
+
for (const synonym of DISCOVER_SYNONYM_MAP[term] ?? []) {
|
|
159
|
+
expanded.add(synonym);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return Array.from(expanded);
|
|
163
|
+
}
|
|
164
|
+
function countTermMatches(haystack, terms) {
|
|
165
|
+
return terms.reduce((count, term) => count + (haystack.includes(term) ? 1 : 0), 0);
|
|
166
|
+
}
|
|
167
|
+
function truncateCodePreview(code, previewLines = 20, threshold = 30) {
|
|
168
|
+
const lines = code.split("\n");
|
|
169
|
+
if (lines.length <= threshold) {
|
|
170
|
+
return code;
|
|
171
|
+
}
|
|
172
|
+
return `${lines.slice(0, previewLines).join("\n")}
|
|
173
|
+
// ... truncated (${lines.length} lines total)`;
|
|
174
|
+
}
|
|
175
|
+
function assignConfidence(score) {
|
|
176
|
+
if (score >= 25) return "high";
|
|
177
|
+
if (score >= 15) return "medium";
|
|
178
|
+
return "low";
|
|
179
|
+
}
|
|
180
|
+
function canonicalizeTokenCategory(categoryKey) {
|
|
181
|
+
const normalized = normalizeSearchText(categoryKey);
|
|
182
|
+
for (const canonical of FRIENDLY_TOKEN_CATEGORY_ORDER) {
|
|
183
|
+
if (normalized === canonical) {
|
|
184
|
+
return canonical;
|
|
185
|
+
}
|
|
186
|
+
const aliases = TOKEN_CATEGORY_ALIASES[canonical] ?? [];
|
|
187
|
+
if (normalized.includes(canonical) || aliases.some((alias) => normalized.includes(alias))) {
|
|
188
|
+
return canonical;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return void 0;
|
|
192
|
+
}
|
|
193
|
+
function resolveTokenCategoryKeys(tokenData, requestedCategory) {
|
|
194
|
+
if (!requestedCategory) {
|
|
195
|
+
return { keys: Object.keys(tokenData.categories) };
|
|
196
|
+
}
|
|
197
|
+
const normalized = normalizeSearchText(requestedCategory);
|
|
198
|
+
if (!normalized) {
|
|
199
|
+
return { keys: Object.keys(tokenData.categories) };
|
|
200
|
+
}
|
|
201
|
+
const keys = Object.keys(tokenData.categories);
|
|
202
|
+
const exactRawMatches = keys.filter((key) => normalizeSearchText(key) === normalized);
|
|
203
|
+
if (exactRawMatches.length > 0) {
|
|
204
|
+
return { keys: exactRawMatches, canonical: canonicalizeTokenCategory(exactRawMatches[0]) };
|
|
205
|
+
}
|
|
206
|
+
const canonical = FRIENDLY_TOKEN_CATEGORY_ORDER.find((candidate) => {
|
|
207
|
+
if (candidate === normalized) return true;
|
|
208
|
+
return (TOKEN_CATEGORY_ALIASES[candidate] ?? []).includes(normalized);
|
|
209
|
+
});
|
|
210
|
+
if (canonical) {
|
|
211
|
+
const aliases = [canonical, ...TOKEN_CATEGORY_ALIASES[canonical] ?? []];
|
|
212
|
+
const aliasMatches = keys.filter((key) => {
|
|
213
|
+
const normalizedKey = normalizeSearchText(key);
|
|
214
|
+
return aliases.some((alias) => normalizedKey.includes(alias));
|
|
215
|
+
});
|
|
216
|
+
if (aliasMatches.length > 0) {
|
|
217
|
+
return { keys: aliasMatches, canonical };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const partialMatches = keys.filter((key) => normalizeSearchText(key).includes(normalized));
|
|
221
|
+
return { keys: partialMatches, canonical };
|
|
222
|
+
}
|
|
223
|
+
function summarizeFriendlyTokenCategories(tokenData) {
|
|
224
|
+
const counts = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const [rawCategory, tokens] of Object.entries(tokenData.categories)) {
|
|
226
|
+
const canonical = canonicalizeTokenCategory(rawCategory) ?? rawCategory;
|
|
227
|
+
counts.set(canonical, (counts.get(canonical) ?? 0) + tokens.length);
|
|
228
|
+
}
|
|
229
|
+
const ordered = [];
|
|
230
|
+
for (const category of FRIENDLY_TOKEN_CATEGORY_ORDER) {
|
|
231
|
+
const count = counts.get(category);
|
|
232
|
+
if (count) {
|
|
233
|
+
ordered.push({ category, count });
|
|
234
|
+
counts.delete(category);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
for (const [category, count] of counts) {
|
|
238
|
+
ordered.push({ category, count });
|
|
239
|
+
}
|
|
240
|
+
return ordered;
|
|
241
|
+
}
|
|
242
|
+
function limitTokensPerCategory(categories, limit) {
|
|
243
|
+
if (limit === void 0) {
|
|
244
|
+
return {
|
|
245
|
+
categories,
|
|
246
|
+
total: Object.values(categories).reduce((sum, entries) => sum + entries.length, 0)
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
const limited = {};
|
|
250
|
+
let total = 0;
|
|
251
|
+
for (const [category, entries] of Object.entries(categories)) {
|
|
252
|
+
const sliced = entries.slice(0, limit);
|
|
253
|
+
if (sliced.length === 0) continue;
|
|
254
|
+
limited[category] = sliced;
|
|
255
|
+
total += sliced.length;
|
|
256
|
+
}
|
|
257
|
+
return { categories: limited, total };
|
|
258
|
+
}
|
|
259
|
+
function scoreBlockMatch(block, query, preferredComponents = []) {
|
|
260
|
+
const normalizedQuery = normalizeSearchText(query);
|
|
261
|
+
if (!normalizedQuery) {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
const terms = splitSearchTerms(query);
|
|
265
|
+
const expandedTerms = expandSearchTerms(terms);
|
|
266
|
+
const synonymOnlyTerms = expandedTerms.filter((term) => !terms.includes(term));
|
|
267
|
+
const nameText = normalizeSearchText(block.name);
|
|
268
|
+
const descriptionText = normalizeSearchText(block.description);
|
|
269
|
+
const tagsText = normalizeSearchText((block.tags ?? []).join(" "));
|
|
270
|
+
const componentsText = normalizeSearchText(block.components.join(" "));
|
|
271
|
+
const categoryText = normalizeSearchText(block.category);
|
|
272
|
+
let score = 0;
|
|
273
|
+
if (nameText === normalizedQuery) score += 120;
|
|
274
|
+
else if (nameText.includes(normalizedQuery)) score += 90;
|
|
275
|
+
if (tagsText.includes(normalizedQuery)) score += 70;
|
|
276
|
+
if (descriptionText.includes(normalizedQuery)) score += 55;
|
|
277
|
+
if (categoryText.includes(normalizedQuery)) score += 40;
|
|
278
|
+
if (componentsText.includes(normalizedQuery)) score += 25;
|
|
279
|
+
score += countTermMatches(nameText, terms) * 30;
|
|
280
|
+
score += countTermMatches(tagsText, terms) * 22;
|
|
281
|
+
score += countTermMatches(descriptionText, terms) * 16;
|
|
282
|
+
score += countTermMatches(categoryText, terms) * 14;
|
|
283
|
+
score += countTermMatches(componentsText, terms) * 10;
|
|
284
|
+
score += countTermMatches(nameText, synonymOnlyTerms) * 12;
|
|
285
|
+
score += countTermMatches(tagsText, synonymOnlyTerms) * 10;
|
|
286
|
+
score += countTermMatches(descriptionText, synonymOnlyTerms) * 6;
|
|
287
|
+
const preferredMatches = block.components.filter(
|
|
288
|
+
(component) => preferredComponents.some((preferred) => preferred.toLowerCase() === component.toLowerCase())
|
|
289
|
+
);
|
|
290
|
+
score += preferredMatches.length * 18;
|
|
291
|
+
return score;
|
|
292
|
+
}
|
|
293
|
+
function rankBlocks(blocks, query, preferredComponents = []) {
|
|
294
|
+
return blocks.map((block) => ({
|
|
295
|
+
block,
|
|
296
|
+
score: scoreBlockMatch(block, query, preferredComponents)
|
|
297
|
+
})).filter((entry) => entry.score > 0).sort((a, b) => b.score - a.score || a.block.name.localeCompare(b.block.name));
|
|
298
|
+
}
|
|
50
299
|
function resolveWorkspaceGlob(baseDir, pattern) {
|
|
51
300
|
const parts = pattern.split("/");
|
|
52
301
|
let dirs = [baseDir];
|
|
@@ -77,7 +326,7 @@ function getWorkspaceDirs(rootDir) {
|
|
|
77
326
|
const rootPkgPath = join(rootDir, "package.json");
|
|
78
327
|
if (existsSync(rootPkgPath)) {
|
|
79
328
|
try {
|
|
80
|
-
const rootPkg = JSON.parse(
|
|
329
|
+
const rootPkg = JSON.parse(readFileSync2(rootPkgPath, "utf-8"));
|
|
81
330
|
const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
|
|
82
331
|
if (Array.isArray(workspaces)) {
|
|
83
332
|
for (const pattern of workspaces) {
|
|
@@ -91,7 +340,7 @@ function getWorkspaceDirs(rootDir) {
|
|
|
91
340
|
const pnpmWsPath = join(rootDir, "pnpm-workspace.yaml");
|
|
92
341
|
if (existsSync(pnpmWsPath)) {
|
|
93
342
|
try {
|
|
94
|
-
const content =
|
|
343
|
+
const content = readFileSync2(pnpmWsPath, "utf-8");
|
|
95
344
|
const lines = content.split("\n");
|
|
96
345
|
let inPackages = false;
|
|
97
346
|
for (const line of lines) {
|
|
@@ -124,7 +373,7 @@ function resolveDepPackageJson(localRequire, depName) {
|
|
|
124
373
|
while (true) {
|
|
125
374
|
const candidate = join(dir, "package.json");
|
|
126
375
|
if (existsSync(candidate)) {
|
|
127
|
-
const pkg = JSON.parse(
|
|
376
|
+
const pkg = JSON.parse(readFileSync2(candidate, "utf-8"));
|
|
128
377
|
if (pkg.name === depName) return candidate;
|
|
129
378
|
}
|
|
130
379
|
const parent = dirname(dir);
|
|
@@ -139,7 +388,7 @@ function findFragmentsInDeps(dir, found) {
|
|
|
139
388
|
const pkgJsonPath = join(dir, "package.json");
|
|
140
389
|
if (!existsSync(pkgJsonPath)) return;
|
|
141
390
|
try {
|
|
142
|
-
const pkgJson = JSON.parse(
|
|
391
|
+
const pkgJson = JSON.parse(readFileSync2(pkgJsonPath, "utf-8"));
|
|
143
392
|
const allDeps = {
|
|
144
393
|
...pkgJson.dependencies,
|
|
145
394
|
...pkgJson.devDependencies
|
|
@@ -149,7 +398,7 @@ function findFragmentsInDeps(dir, found) {
|
|
|
149
398
|
try {
|
|
150
399
|
const depPkgPath = resolveDepPackageJson(localRequire, depName);
|
|
151
400
|
if (!depPkgPath) continue;
|
|
152
|
-
const depPkg = JSON.parse(
|
|
401
|
+
const depPkg = JSON.parse(readFileSync2(depPkgPath, "utf-8"));
|
|
153
402
|
if (depPkg.fragments) {
|
|
154
403
|
const fragmentsPath = join(dirname(depPkgPath), depPkg.fragments);
|
|
155
404
|
if (existsSync(fragmentsPath) && !found.includes(fragmentsPath)) {
|
|
@@ -185,12 +434,11 @@ function findFragmentsJson(startDir) {
|
|
|
185
434
|
}
|
|
186
435
|
return found;
|
|
187
436
|
}
|
|
188
|
-
var TOOLS = buildMcpTools(BRAND.nameLower, CLI_TOOL_EXTENSIONS);
|
|
189
437
|
function createMcpServer(config) {
|
|
190
438
|
const server = new Server(
|
|
191
439
|
{
|
|
192
440
|
name: `${BRAND.nameLower}-mcp`,
|
|
193
|
-
version:
|
|
441
|
+
version: MCP_SERVER_VERSION
|
|
194
442
|
},
|
|
195
443
|
{
|
|
196
444
|
capabilities: {
|
|
@@ -327,21 +575,24 @@ function createMcpServer(config) {
|
|
|
327
575
|
const data = await loadFragments();
|
|
328
576
|
const useCase = args2?.useCase ?? void 0;
|
|
329
577
|
const componentForAlts = args2?.component ?? void 0;
|
|
330
|
-
const category = args2?.category
|
|
331
|
-
const search = args2?.search
|
|
578
|
+
const category = normalizeSearchText(args2?.category) || void 0;
|
|
579
|
+
const search = normalizeSearchText(args2?.search) || void 0;
|
|
332
580
|
const status = args2?.status ?? void 0;
|
|
333
581
|
const format = args2?.format ?? "markdown";
|
|
334
582
|
const compact = args2?.compact ?? false;
|
|
335
583
|
const includeCode = args2?.includeCode ?? false;
|
|
336
584
|
const includeRelations = args2?.includeRelations ?? false;
|
|
337
|
-
|
|
585
|
+
const verbosity = resolveVerbosity(args2?.verbosity, compact);
|
|
586
|
+
const listLimit = parsePositiveLimit(args2?.limit, void 0, 50);
|
|
587
|
+
const suggestLimit = parsePositiveLimit(args2?.limit, 10, 25) ?? 10;
|
|
588
|
+
if (args2?.format && !useCase && !componentForAlts && !category && !search && !status) {
|
|
338
589
|
const fragments2 = Object.values(data.fragments);
|
|
339
590
|
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
340
591
|
const { content: ctxContent, tokenEstimate } = generateContext(fragments2, {
|
|
341
592
|
format,
|
|
342
|
-
compact,
|
|
593
|
+
compact: verbosity === "compact",
|
|
343
594
|
include: {
|
|
344
|
-
code: includeCode,
|
|
595
|
+
code: includeCode || verbosity === "full",
|
|
345
596
|
relations: includeRelations
|
|
346
597
|
}
|
|
347
598
|
}, allBlocks);
|
|
@@ -356,29 +607,9 @@ function createMcpServer(config) {
|
|
|
356
607
|
if (useCase) {
|
|
357
608
|
const useCaseLower = useCase.toLowerCase();
|
|
358
609
|
const context = args2?.context?.toLowerCase() ?? "";
|
|
359
|
-
const searchTerms = `${useCaseLower} ${context}
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
"input": ["form", "field", "text", "entry"],
|
|
363
|
-
"button": ["action", "click", "submit", "trigger"],
|
|
364
|
-
"action": ["button", "click", "trigger"],
|
|
365
|
-
"alert": ["notification", "message", "warning", "error", "feedback"],
|
|
366
|
-
"notification": ["alert", "message", "toast"],
|
|
367
|
-
"card": ["container", "panel", "box", "content"],
|
|
368
|
-
"toggle": ["switch", "checkbox", "boolean", "on/off"],
|
|
369
|
-
"switch": ["toggle", "checkbox", "boolean"],
|
|
370
|
-
"badge": ["tag", "label", "status", "indicator"],
|
|
371
|
-
"status": ["badge", "indicator", "state"],
|
|
372
|
-
"login": ["auth", "signin", "authentication", "form"],
|
|
373
|
-
"auth": ["login", "signin", "authentication"]
|
|
374
|
-
};
|
|
375
|
-
const expandedTerms = new Set(searchTerms);
|
|
376
|
-
searchTerms.forEach((term) => {
|
|
377
|
-
const synonyms = synonymMap[term];
|
|
378
|
-
if (synonyms) {
|
|
379
|
-
synonyms.forEach((syn) => expandedTerms.add(syn));
|
|
380
|
-
}
|
|
381
|
-
});
|
|
610
|
+
const searchTerms = splitSearchTerms(`${useCaseLower} ${context}`);
|
|
611
|
+
const expandedTerms = expandSearchTerms(searchTerms);
|
|
612
|
+
const synonymOnlyTerms = expandedTerms.filter((term) => !searchTerms.includes(term));
|
|
382
613
|
const scored = Object.values(data.fragments).map((s) => {
|
|
383
614
|
let score = 0;
|
|
384
615
|
const reasons = [];
|
|
@@ -386,7 +617,7 @@ function createMcpServer(config) {
|
|
|
386
617
|
if (searchTerms.some((term) => nameLower.includes(term))) {
|
|
387
618
|
score += 15;
|
|
388
619
|
reasons.push(`Name matches search`);
|
|
389
|
-
} else if (
|
|
620
|
+
} else if (expandedTerms.some((term) => nameLower.includes(term))) {
|
|
390
621
|
score += 8;
|
|
391
622
|
reasons.push(`Name matches related term`);
|
|
392
623
|
}
|
|
@@ -410,9 +641,7 @@ function createMcpServer(config) {
|
|
|
410
641
|
score += whenMatches.length * 10;
|
|
411
642
|
reasons.push(`Use cases match: "${whenMatches.join(", ")}"`);
|
|
412
643
|
}
|
|
413
|
-
const expandedWhenMatches =
|
|
414
|
-
(term) => !searchTerms.includes(term) && whenUsed.includes(term)
|
|
415
|
-
);
|
|
644
|
+
const expandedWhenMatches = synonymOnlyTerms.filter((term) => whenUsed.includes(term));
|
|
416
645
|
if (expandedWhenMatches.length > 0) {
|
|
417
646
|
score += expandedWhenMatches.length * 5;
|
|
418
647
|
reasons.push(`Related use cases: "${expandedWhenMatches.join(", ")}"`);
|
|
@@ -466,18 +695,17 @@ function createMcpServer(config) {
|
|
|
466
695
|
if (count < 2 || suggestions.length < 3) {
|
|
467
696
|
suggestions.push(item);
|
|
468
697
|
categoryCount[cat] = count + 1;
|
|
469
|
-
if (suggestions.length >=
|
|
698
|
+
if (suggestions.length >= suggestLimit) break;
|
|
470
699
|
}
|
|
471
700
|
}
|
|
472
701
|
const compositionHint = suggestions.length >= 2 ? `These components work well together. For example, ${suggestions[0].component} can be combined with ${suggestions.slice(1, 3).map((s) => s.component).join(" and ")}.` : void 0;
|
|
473
|
-
const
|
|
474
|
-
const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
|
|
702
|
+
const isStyleQuery = splitSearchTerms(useCaseLower).some((term) => STYLE_QUERY_TERMS.has(term));
|
|
475
703
|
const noMatch = suggestions.length === 0;
|
|
476
704
|
const weakMatch = !noMatch && suggestions.every((s) => s.confidence === "low");
|
|
477
705
|
let recommendation;
|
|
478
706
|
let nextStep;
|
|
479
707
|
if (noMatch) {
|
|
480
|
-
recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${TOOL_NAMES.tokens} to find CSS custom properties.` :
|
|
708
|
+
recommendation = isStyleQuery ? `No matching components found. Your query seems styling-related \u2014 try ${TOOL_NAMES.tokens} to find CSS custom properties.` : `No matching components found. Try different keywords or browse all components with ${TOOL_NAMES.discover}.`;
|
|
481
709
|
nextStep = isStyleQuery ? `Use ${TOOL_NAMES.tokens}(search: "${searchTerms[0]}") to find design tokens.` : void 0;
|
|
482
710
|
} else if (weakMatch) {
|
|
483
711
|
recommendation = `Weak matches only \u2014 ${suggestions[0].component} might work but confidence is low.${isStyleQuery ? ` If you need a CSS variable, try ${TOOL_NAMES.tokens}.` : ""}`;
|
|
@@ -486,13 +714,20 @@ function createMcpServer(config) {
|
|
|
486
714
|
recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
|
|
487
715
|
nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
|
|
488
716
|
}
|
|
717
|
+
const suggestionResults = suggestions.map(
|
|
718
|
+
({ score, ...rest }) => verbosity === "full" ? { ...rest, score } : rest
|
|
719
|
+
);
|
|
489
720
|
return {
|
|
490
721
|
content: [{
|
|
491
722
|
type: "text",
|
|
492
723
|
text: JSON.stringify({
|
|
493
724
|
useCase,
|
|
494
725
|
context: context || void 0,
|
|
495
|
-
suggestions:
|
|
726
|
+
suggestions: verbosity === "compact" ? suggestionResults.map((suggestion) => ({
|
|
727
|
+
component: suggestion.component,
|
|
728
|
+
description: suggestion.description,
|
|
729
|
+
confidence: suggestion.confidence
|
|
730
|
+
})) : suggestionResults,
|
|
496
731
|
noMatch,
|
|
497
732
|
weakMatch,
|
|
498
733
|
recommendation,
|
|
@@ -507,7 +742,7 @@ function createMcpServer(config) {
|
|
|
507
742
|
(s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
|
|
508
743
|
);
|
|
509
744
|
if (!fragment) {
|
|
510
|
-
throw new Error(`Component "${componentForAlts}" not found. Use
|
|
745
|
+
throw new Error(`Component "${componentForAlts}" not found. Use ${TOOL_NAMES.discover} to see available components.`);
|
|
511
746
|
}
|
|
512
747
|
const relations = fragment.relations ?? [];
|
|
513
748
|
const referencedBy = Object.values(data.fragments).filter(
|
|
@@ -542,7 +777,7 @@ function createMcpServer(config) {
|
|
|
542
777
|
};
|
|
543
778
|
}
|
|
544
779
|
const fragments = Object.values(data.fragments).filter((s) => {
|
|
545
|
-
if (category && s.meta.category !== category) return false;
|
|
780
|
+
if (category && normalizeSearchText(s.meta.category) !== category) return false;
|
|
546
781
|
if (status && (s.meta.status ?? "stable") !== status) return false;
|
|
547
782
|
if (search) {
|
|
548
783
|
const nameMatch = s.meta.name.toLowerCase().includes(search);
|
|
@@ -551,22 +786,46 @@ function createMcpServer(config) {
|
|
|
551
786
|
if (!nameMatch && !descMatch && !tagMatch) return false;
|
|
552
787
|
}
|
|
553
788
|
return true;
|
|
554
|
-
})
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
789
|
+
});
|
|
790
|
+
const total = fragments.length;
|
|
791
|
+
const limitedFragments = listLimit === void 0 ? fragments : fragments.slice(0, listLimit);
|
|
792
|
+
const formattedFragments = limitedFragments.map((s) => {
|
|
793
|
+
const base = {
|
|
794
|
+
name: s.meta.name,
|
|
795
|
+
category: s.meta.category,
|
|
796
|
+
description: s.meta.description,
|
|
797
|
+
status: s.meta.status ?? "stable",
|
|
798
|
+
variantCount: s.variants.length
|
|
799
|
+
};
|
|
800
|
+
if (verbosity === "compact") {
|
|
801
|
+
return base;
|
|
802
|
+
}
|
|
803
|
+
if (verbosity === "full") {
|
|
804
|
+
return {
|
|
805
|
+
...base,
|
|
806
|
+
tags: s.meta.tags ?? [],
|
|
807
|
+
usage: {
|
|
808
|
+
when: filterPlaceholders(s.usage?.when).slice(0, 3),
|
|
809
|
+
whenNot: filterPlaceholders(s.usage?.whenNot).slice(0, 2)
|
|
810
|
+
},
|
|
811
|
+
relations: s.relations ?? [],
|
|
812
|
+
codeExample: s.variants[0]?.code
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return {
|
|
816
|
+
...base,
|
|
817
|
+
tags: s.meta.tags ?? []
|
|
818
|
+
};
|
|
819
|
+
});
|
|
562
820
|
return {
|
|
563
821
|
content: [{
|
|
564
822
|
type: "text",
|
|
565
823
|
text: JSON.stringify({
|
|
566
|
-
total
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
824
|
+
total,
|
|
825
|
+
returned: formattedFragments.length,
|
|
826
|
+
fragments: formattedFragments,
|
|
827
|
+
categories: [...new Set(fragments.map((s) => s.meta.category))],
|
|
828
|
+
hint: total === 0 ? "No components found. Try broader search terms or check available categories." : total > 5 ? `Use ${TOOL_NAMES.discover} with useCase for recommendations, or ${TOOL_NAMES.inspect} for details on a specific component.` : void 0
|
|
570
829
|
}, null, 2)
|
|
571
830
|
}]
|
|
572
831
|
};
|
|
@@ -588,7 +847,7 @@ function createMcpServer(config) {
|
|
|
588
847
|
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
589
848
|
);
|
|
590
849
|
if (!fragment) {
|
|
591
|
-
throw new Error(`Component "${componentName}" not found. Use
|
|
850
|
+
throw new Error(`Component "${componentName}" not found. Use ${TOOL_NAMES.discover} to see available components.`);
|
|
592
851
|
}
|
|
593
852
|
const pkgName = await getPackageName(fragment.meta.name);
|
|
594
853
|
let variants = fragment.variants;
|
|
@@ -691,9 +950,11 @@ function createMcpServer(config) {
|
|
|
691
950
|
case TOOL_NAMES.blocks: {
|
|
692
951
|
const data = await loadFragments();
|
|
693
952
|
const blockName = args2?.name;
|
|
694
|
-
const search = args2?.search
|
|
953
|
+
const search = args2?.search ?? void 0;
|
|
695
954
|
const component = args2?.component?.toLowerCase() ?? void 0;
|
|
696
|
-
const category = args2?.category
|
|
955
|
+
const category = normalizeSearchText(args2?.category) || void 0;
|
|
956
|
+
const blocksLimit = parsePositiveLimit(args2?.limit, void 0, 50);
|
|
957
|
+
const verbosity = resolveVerbosity(args2?.verbosity);
|
|
697
958
|
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
698
959
|
if (allBlocks.length === 0) {
|
|
699
960
|
return {
|
|
@@ -709,21 +970,10 @@ function createMcpServer(config) {
|
|
|
709
970
|
}
|
|
710
971
|
let filtered = allBlocks;
|
|
711
972
|
if (blockName) {
|
|
712
|
-
filtered = filtered.filter(
|
|
713
|
-
(b) => b.name.toLowerCase() === blockName.toLowerCase()
|
|
714
|
-
);
|
|
973
|
+
filtered = filtered.filter((b) => b.name.toLowerCase() === blockName.toLowerCase());
|
|
715
974
|
}
|
|
716
975
|
if (search) {
|
|
717
|
-
filtered = filtered.
|
|
718
|
-
const haystack = [
|
|
719
|
-
b.name,
|
|
720
|
-
b.description,
|
|
721
|
-
...b.tags ?? [],
|
|
722
|
-
...b.components,
|
|
723
|
-
b.category
|
|
724
|
-
].join(" ").toLowerCase();
|
|
725
|
-
return haystack.includes(search);
|
|
726
|
-
});
|
|
976
|
+
filtered = rankBlocks(filtered, search).map(({ block }) => block);
|
|
727
977
|
}
|
|
728
978
|
if (component) {
|
|
729
979
|
filtered = filtered.filter(
|
|
@@ -732,15 +982,42 @@ function createMcpServer(config) {
|
|
|
732
982
|
}
|
|
733
983
|
if (category) {
|
|
734
984
|
filtered = filtered.filter(
|
|
735
|
-
(b) => b.category
|
|
985
|
+
(b) => normalizeSearchText(b.category) === category
|
|
736
986
|
);
|
|
737
987
|
}
|
|
988
|
+
if (!search) {
|
|
989
|
+
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name));
|
|
990
|
+
}
|
|
991
|
+
const total = filtered.length;
|
|
992
|
+
if (blocksLimit !== void 0) {
|
|
993
|
+
filtered = filtered.slice(0, blocksLimit);
|
|
994
|
+
}
|
|
995
|
+
const blocks = filtered.map((block) => {
|
|
996
|
+
const base = {
|
|
997
|
+
name: block.name,
|
|
998
|
+
description: block.description,
|
|
999
|
+
category: block.category,
|
|
1000
|
+
components: block.components,
|
|
1001
|
+
tags: block.tags
|
|
1002
|
+
};
|
|
1003
|
+
if (verbosity === "compact") {
|
|
1004
|
+
return base;
|
|
1005
|
+
}
|
|
1006
|
+
return {
|
|
1007
|
+
...base,
|
|
1008
|
+
code: verbosity === "full" ? block.code : truncateCodePreview(block.code)
|
|
1009
|
+
};
|
|
1010
|
+
});
|
|
738
1011
|
return {
|
|
739
1012
|
content: [{
|
|
740
1013
|
type: "text",
|
|
741
1014
|
text: JSON.stringify({
|
|
742
|
-
total
|
|
743
|
-
|
|
1015
|
+
total,
|
|
1016
|
+
returned: blocks.length,
|
|
1017
|
+
blocks,
|
|
1018
|
+
...total === 0 && {
|
|
1019
|
+
hint: "No blocks matching your query. Try broader search terms."
|
|
1020
|
+
}
|
|
744
1021
|
}, null, 2)
|
|
745
1022
|
}]
|
|
746
1023
|
};
|
|
@@ -750,8 +1027,9 @@ function createMcpServer(config) {
|
|
|
750
1027
|
// ================================================================
|
|
751
1028
|
case TOOL_NAMES.tokens: {
|
|
752
1029
|
const data = await loadFragments();
|
|
753
|
-
const category = args2?.category
|
|
754
|
-
const search = args2?.search
|
|
1030
|
+
const category = args2?.category ?? void 0;
|
|
1031
|
+
const search = normalizeSearchText(args2?.search) || void 0;
|
|
1032
|
+
const tokensLimit = parsePositiveLimit(args2?.limit, search ? 25 : void 0, 100);
|
|
755
1033
|
const tokenData = data.tokens;
|
|
756
1034
|
if (!tokenData || tokenData.total === 0) {
|
|
757
1035
|
return {
|
|
@@ -766,24 +1044,31 @@ function createMcpServer(config) {
|
|
|
766
1044
|
};
|
|
767
1045
|
}
|
|
768
1046
|
const filteredCategories = {};
|
|
769
|
-
|
|
1047
|
+
const resolvedCategory = resolveTokenCategoryKeys(tokenData, category);
|
|
1048
|
+
const searchCategoryKeys = search ? resolveTokenCategoryKeys(tokenData, search).keys : [];
|
|
770
1049
|
for (const [cat, tokens] of Object.entries(tokenData.categories)) {
|
|
771
|
-
if (category && cat
|
|
1050
|
+
if (category && !resolvedCategory.keys.includes(cat)) continue;
|
|
772
1051
|
let filtered = tokens;
|
|
773
1052
|
if (search) {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
)
|
|
1053
|
+
const normalizedCategory = normalizeSearchText(cat);
|
|
1054
|
+
const searchMatchesCategory = searchCategoryKeys.includes(cat);
|
|
1055
|
+
if (!searchMatchesCategory) {
|
|
1056
|
+
filtered = tokens.filter(
|
|
1057
|
+
(token) => token.name.toLowerCase().includes(search) || token.description && token.description.toLowerCase().includes(search) || normalizedCategory.includes(search)
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
777
1060
|
}
|
|
778
1061
|
if (filtered.length > 0) {
|
|
779
1062
|
filteredCategories[cat] = filtered;
|
|
780
|
-
filteredTotal += filtered.length;
|
|
781
1063
|
}
|
|
782
1064
|
}
|
|
1065
|
+
const limited = limitTokensPerCategory(filteredCategories, tokensLimit);
|
|
1066
|
+
const filteredTotal = limited.total;
|
|
1067
|
+
const friendlyCategories = summarizeFriendlyTokenCategories(tokenData);
|
|
1068
|
+
const availableCategoryNames = friendlyCategories.map((entry) => entry.category);
|
|
783
1069
|
let hint;
|
|
784
1070
|
if (filteredTotal === 0) {
|
|
785
|
-
|
|
786
|
-
hint = search ? `No tokens matching "${search}". Try: ${availableCategories.join(", ")}` : category ? `Category "${category}" not found. Available: ${availableCategories.join(", ")}` : void 0;
|
|
1071
|
+
hint = search ? `No tokens matching "${search}". Try categories like: ${availableCategoryNames.join(", ")}` : category ? `Category "${category}" not found. Try categories like: ${availableCategoryNames.join(", ")}` : void 0;
|
|
787
1072
|
} else if (!category && !search) {
|
|
788
1073
|
hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
|
|
789
1074
|
}
|
|
@@ -794,12 +1079,10 @@ function createMcpServer(config) {
|
|
|
794
1079
|
prefix: tokenData.prefix,
|
|
795
1080
|
total: filteredTotal,
|
|
796
1081
|
totalAvailable: tokenData.total,
|
|
797
|
-
categories:
|
|
1082
|
+
categories: limited.categories,
|
|
798
1083
|
...hint && { hint },
|
|
799
1084
|
...!category && !search && {
|
|
800
|
-
availableCategories:
|
|
801
|
-
([cat, tokens]) => ({ category: cat, count: tokens.length })
|
|
802
|
-
)
|
|
1085
|
+
availableCategories: friendlyCategories
|
|
803
1086
|
}
|
|
804
1087
|
}, null, 2)
|
|
805
1088
|
}]
|
|
@@ -815,58 +1098,55 @@ function createMcpServer(config) {
|
|
|
815
1098
|
throw new Error("useCase is required");
|
|
816
1099
|
}
|
|
817
1100
|
const useCaseLower = useCase.toLowerCase();
|
|
818
|
-
const searchTerms = useCaseLower
|
|
819
|
-
const
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
"alert": ["notification", "message", "warning", "error", "feedback"],
|
|
824
|
-
"notification": ["alert", "message", "toast"],
|
|
825
|
-
"card": ["container", "panel", "box", "content"],
|
|
826
|
-
"toggle": ["switch", "checkbox", "boolean"],
|
|
827
|
-
"badge": ["tag", "label", "status", "indicator"],
|
|
828
|
-
"login": ["auth", "signin", "authentication", "form"],
|
|
829
|
-
"chat": ["message", "conversation", "ai"],
|
|
830
|
-
"table": ["data", "grid", "list", "rows"]
|
|
831
|
-
};
|
|
832
|
-
const expandedTerms = new Set(searchTerms);
|
|
833
|
-
searchTerms.forEach((term) => {
|
|
834
|
-
const synonyms = synonymMap[term];
|
|
835
|
-
if (synonyms) synonyms.forEach((syn) => expandedTerms.add(syn));
|
|
836
|
-
});
|
|
1101
|
+
const searchTerms = splitSearchTerms(useCaseLower);
|
|
1102
|
+
const expandedTerms = expandSearchTerms(searchTerms);
|
|
1103
|
+
const synonymOnlyTerms = expandedTerms.filter((term) => !searchTerms.includes(term));
|
|
1104
|
+
const verbosity = resolveVerbosity(args2?.verbosity);
|
|
1105
|
+
const componentLimit = parsePositiveLimit(args2?.limit, 5, 15) ?? 5;
|
|
837
1106
|
const scored = Object.values(data.fragments).map((s) => {
|
|
838
1107
|
let score = 0;
|
|
839
1108
|
const nameLower = s.meta.name.toLowerCase();
|
|
840
1109
|
if (searchTerms.some((t) => nameLower.includes(t))) score += 15;
|
|
841
|
-
else if (
|
|
1110
|
+
else if (expandedTerms.some((t) => nameLower.includes(t))) score += 8;
|
|
842
1111
|
const desc = s.meta.description?.toLowerCase() ?? "";
|
|
843
1112
|
score += searchTerms.filter((t) => desc.includes(t)).length * 6;
|
|
844
1113
|
const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
|
|
845
1114
|
score += searchTerms.filter((t) => tags.some((tag) => tag.includes(t))).length * 4;
|
|
846
1115
|
const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
|
|
847
1116
|
score += searchTerms.filter((t) => whenUsed.includes(t)).length * 10;
|
|
848
|
-
score +=
|
|
1117
|
+
score += synonymOnlyTerms.filter((t) => whenUsed.includes(t)).length * 5;
|
|
849
1118
|
if (s.meta.category && searchTerms.some((t) => s.meta.category.toLowerCase().includes(t))) score += 8;
|
|
850
1119
|
if (s.meta.status === "stable") score += 5;
|
|
851
1120
|
if (s.meta.status === "deprecated") score -= 25;
|
|
852
1121
|
return { fragment: s, score };
|
|
853
1122
|
});
|
|
854
|
-
const topMatches = scored.filter((s) => s.score >= 8).sort((a, b) => b.score - a.score).slice(0,
|
|
1123
|
+
const topMatches = scored.filter((s) => s.score >= 8).sort((a, b) => b.score - a.score).slice(0, componentLimit);
|
|
855
1124
|
const components = await Promise.all(
|
|
856
1125
|
topMatches.map(async ({ fragment: s, score }) => {
|
|
857
1126
|
const pkgName = await getPackageName(s.meta.name);
|
|
858
|
-
const
|
|
1127
|
+
const confidence = assignConfidence(score);
|
|
1128
|
+
if (verbosity === "compact") {
|
|
1129
|
+
return {
|
|
1130
|
+
name: s.meta.name,
|
|
1131
|
+
description: s.meta.description,
|
|
1132
|
+
confidence,
|
|
1133
|
+
import: `import { ${s.meta.name} } from '${pkgName}';`
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const exampleLimit = verbosity === "full" ? s.variants.length : 2;
|
|
1137
|
+
const propsLimit = verbosity === "full" ? Object.keys(s.props ?? {}).length : 5;
|
|
1138
|
+
const examples = s.variants.slice(0, exampleLimit).map((v) => ({
|
|
859
1139
|
variant: v.name,
|
|
860
1140
|
code: v.code ?? `<${s.meta.name} />`
|
|
861
1141
|
}));
|
|
862
|
-
const propsSummary = Object.entries(s.props ?? {}).slice(0,
|
|
1142
|
+
const propsSummary = Object.entries(s.props ?? {}).slice(0, propsLimit).map(
|
|
863
1143
|
([name2, p]) => `${name2}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
|
|
864
1144
|
);
|
|
865
1145
|
return {
|
|
866
1146
|
name: s.meta.name,
|
|
867
1147
|
category: s.meta.category,
|
|
868
1148
|
description: s.meta.description,
|
|
869
|
-
confidence
|
|
1149
|
+
confidence,
|
|
870
1150
|
import: `import { ${s.meta.name} } from '${pkgName}';`,
|
|
871
1151
|
props: propsSummary,
|
|
872
1152
|
examples,
|
|
@@ -876,21 +1156,39 @@ function createMcpServer(config) {
|
|
|
876
1156
|
})
|
|
877
1157
|
);
|
|
878
1158
|
const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
|
|
879
|
-
const
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1159
|
+
const preferredComponents = topMatches.map(({ fragment }) => fragment.meta.name);
|
|
1160
|
+
const matchingBlocks = rankBlocks(allBlocks, useCase, preferredComponents).slice(0, 2).map(({ block }) => {
|
|
1161
|
+
const base = {
|
|
1162
|
+
name: block.name,
|
|
1163
|
+
description: block.description,
|
|
1164
|
+
components: block.components
|
|
1165
|
+
};
|
|
1166
|
+
if (verbosity === "compact") {
|
|
1167
|
+
return base;
|
|
1168
|
+
}
|
|
1169
|
+
return {
|
|
1170
|
+
...base,
|
|
1171
|
+
code: verbosity === "full" ? block.code : truncateCodePreview(block.code)
|
|
1172
|
+
};
|
|
1173
|
+
});
|
|
883
1174
|
const tokenData = data.tokens;
|
|
884
1175
|
let relevantTokens;
|
|
885
1176
|
if (tokenData) {
|
|
886
|
-
const
|
|
887
|
-
|
|
888
|
-
|
|
1177
|
+
const styleCategories = Array.from(new Set(searchTerms.flatMap((term) => {
|
|
1178
|
+
return FRIENDLY_TOKEN_CATEGORY_ORDER.filter((categoryName) => {
|
|
1179
|
+
if (categoryName === term) return true;
|
|
1180
|
+
return (TOKEN_CATEGORY_ALIASES[categoryName] ?? []).includes(term);
|
|
1181
|
+
});
|
|
1182
|
+
})));
|
|
1183
|
+
if (styleCategories.length > 0) {
|
|
889
1184
|
relevantTokens = {};
|
|
890
|
-
for (const
|
|
891
|
-
const
|
|
892
|
-
|
|
893
|
-
|
|
1185
|
+
for (const categoryName of styleCategories) {
|
|
1186
|
+
const matchingCategoryKeys = resolveTokenCategoryKeys(tokenData, categoryName).keys;
|
|
1187
|
+
for (const key of matchingCategoryKeys) {
|
|
1188
|
+
const tokens = tokenData.categories[key];
|
|
1189
|
+
if (tokens && tokens.length > 0) {
|
|
1190
|
+
relevantTokens[key] = tokens.slice(0, 5).map((token) => token.name);
|
|
1191
|
+
}
|
|
894
1192
|
}
|
|
895
1193
|
}
|
|
896
1194
|
if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
|
|
@@ -903,7 +1201,7 @@ function createMcpServer(config) {
|
|
|
903
1201
|
useCase,
|
|
904
1202
|
components,
|
|
905
1203
|
blocks: matchingBlocks.length > 0 ? matchingBlocks : void 0,
|
|
906
|
-
tokens: relevantTokens,
|
|
1204
|
+
tokens: verbosity === "compact" ? void 0 : relevantTokens,
|
|
907
1205
|
noMatch: components.length === 0,
|
|
908
1206
|
summary: components.length > 0 ? `Found ${components.length} component(s) for "${useCase}". ${matchingBlocks.length > 0 ? `Plus ${matchingBlocks.length} ready-to-use block(s).` : ""}` : `No components match "${useCase}". Try ${TOOL_NAMES.discover} with different terms${tokenData ? ` or ${TOOL_NAMES.tokens} for CSS variables` : ""}.`
|
|
909
1207
|
}, null, 2)
|
|
@@ -1126,7 +1424,7 @@ Suggestion: ${result.suggestion}` : ""}`
|
|
|
1126
1424
|
(s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1127
1425
|
);
|
|
1128
1426
|
if (!fragment) {
|
|
1129
|
-
throw new Error(`Component "${componentName}" not found. Use
|
|
1427
|
+
throw new Error(`Component "${componentName}" not found. Use ${TOOL_NAMES.discover} to see available components.`);
|
|
1130
1428
|
}
|
|
1131
1429
|
const baseUrl = config.viewerUrl ?? "http://localhost:6006";
|
|
1132
1430
|
const fixUrl = `${baseUrl}/fragments/fix`;
|
|
@@ -1160,7 +1458,7 @@ Suggestion: ${result.suggestion}` : ""}`
|
|
|
1160
1458
|
patches: result.patches,
|
|
1161
1459
|
summary: result.summary,
|
|
1162
1460
|
patchCount: result.patches.length,
|
|
1163
|
-
nextStep: result.patches.length > 0 ?
|
|
1461
|
+
nextStep: result.patches.length > 0 ? `Apply patches using your editor or \`patch\` command, then run ${TOOL_NAMES.render} with baseline:true to confirm fixes.` : void 0
|
|
1164
1462
|
}, null, 2)
|
|
1165
1463
|
}]
|
|
1166
1464
|
};
|
|
@@ -1175,6 +1473,355 @@ Suggestion: ${result.suggestion}` : ""}`
|
|
|
1175
1473
|
}
|
|
1176
1474
|
}
|
|
1177
1475
|
// ================================================================
|
|
1476
|
+
// GRAPH — query component relationship graph
|
|
1477
|
+
// ================================================================
|
|
1478
|
+
case TOOL_NAMES.graph: {
|
|
1479
|
+
const data = await loadFragments();
|
|
1480
|
+
const mode = args2?.mode ?? "health";
|
|
1481
|
+
const componentName = args2?.component;
|
|
1482
|
+
const target = args2?.target;
|
|
1483
|
+
const edgeTypes = args2?.edgeTypes;
|
|
1484
|
+
const maxDepth = args2?.maxDepth ?? 3;
|
|
1485
|
+
if (!data.graph) {
|
|
1486
|
+
return {
|
|
1487
|
+
content: [{
|
|
1488
|
+
type: "text",
|
|
1489
|
+
text: JSON.stringify({
|
|
1490
|
+
error: "No graph data available. Run `fragments build` to generate the component graph.",
|
|
1491
|
+
hint: "The graph is built automatically during `fragments build` and embedded in fragments.json."
|
|
1492
|
+
})
|
|
1493
|
+
}],
|
|
1494
|
+
isError: true
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const graph = deserializeGraph(data.graph);
|
|
1498
|
+
const blocks = data.blocks ? Object.fromEntries(
|
|
1499
|
+
Object.entries(data.blocks).map(([key, value]) => [key, { components: value.components }])
|
|
1500
|
+
) : void 0;
|
|
1501
|
+
const engine = new ComponentGraphEngine(graph, blocks);
|
|
1502
|
+
const requireComponent = (modeName) => {
|
|
1503
|
+
if (!componentName) {
|
|
1504
|
+
return `component is required for ${modeName} mode`;
|
|
1505
|
+
}
|
|
1506
|
+
if (!engine.hasNode(componentName)) {
|
|
1507
|
+
return `Component "${componentName}" not found in graph.`;
|
|
1508
|
+
}
|
|
1509
|
+
return void 0;
|
|
1510
|
+
};
|
|
1511
|
+
switch (mode) {
|
|
1512
|
+
case "health": {
|
|
1513
|
+
const health = engine.getHealth();
|
|
1514
|
+
return {
|
|
1515
|
+
content: [{
|
|
1516
|
+
type: "text",
|
|
1517
|
+
text: JSON.stringify({
|
|
1518
|
+
mode,
|
|
1519
|
+
...health,
|
|
1520
|
+
summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
|
|
1521
|
+
}, null, 2)
|
|
1522
|
+
}]
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
case "dependencies": {
|
|
1526
|
+
const error = requireComponent(mode);
|
|
1527
|
+
if (error) throw new Error(error);
|
|
1528
|
+
const dependencies = engine.dependencies(componentName, edgeTypes);
|
|
1529
|
+
return {
|
|
1530
|
+
content: [{
|
|
1531
|
+
type: "text",
|
|
1532
|
+
text: JSON.stringify({
|
|
1533
|
+
mode,
|
|
1534
|
+
component: componentName,
|
|
1535
|
+
count: dependencies.length,
|
|
1536
|
+
dependencies: dependencies.map((edge) => ({
|
|
1537
|
+
component: edge.target,
|
|
1538
|
+
type: edge.type,
|
|
1539
|
+
weight: edge.weight,
|
|
1540
|
+
note: edge.note,
|
|
1541
|
+
provenance: edge.provenance
|
|
1542
|
+
}))
|
|
1543
|
+
}, null, 2)
|
|
1544
|
+
}]
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
case "dependents": {
|
|
1548
|
+
const error = requireComponent(mode);
|
|
1549
|
+
if (error) throw new Error(error);
|
|
1550
|
+
const dependents = engine.dependents(componentName, edgeTypes);
|
|
1551
|
+
return {
|
|
1552
|
+
content: [{
|
|
1553
|
+
type: "text",
|
|
1554
|
+
text: JSON.stringify({
|
|
1555
|
+
mode,
|
|
1556
|
+
component: componentName,
|
|
1557
|
+
count: dependents.length,
|
|
1558
|
+
dependents: dependents.map((edge) => ({
|
|
1559
|
+
component: edge.source,
|
|
1560
|
+
type: edge.type,
|
|
1561
|
+
weight: edge.weight,
|
|
1562
|
+
note: edge.note,
|
|
1563
|
+
provenance: edge.provenance
|
|
1564
|
+
}))
|
|
1565
|
+
}, null, 2)
|
|
1566
|
+
}]
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
case "impact": {
|
|
1570
|
+
const error = requireComponent(mode);
|
|
1571
|
+
if (error) throw new Error(error);
|
|
1572
|
+
const impact = engine.impact(componentName, maxDepth);
|
|
1573
|
+
return {
|
|
1574
|
+
content: [{
|
|
1575
|
+
type: "text",
|
|
1576
|
+
text: JSON.stringify({
|
|
1577
|
+
mode,
|
|
1578
|
+
...impact,
|
|
1579
|
+
summary: `Changing ${componentName} affects ${impact.totalAffected} component(s) and ${impact.affectedBlocks.length} block(s)`
|
|
1580
|
+
}, null, 2)
|
|
1581
|
+
}]
|
|
1582
|
+
};
|
|
1583
|
+
}
|
|
1584
|
+
case "path": {
|
|
1585
|
+
if (!componentName || !target) {
|
|
1586
|
+
throw new Error("component and target are required for path mode");
|
|
1587
|
+
}
|
|
1588
|
+
const path = engine.path(componentName, target);
|
|
1589
|
+
return {
|
|
1590
|
+
content: [{
|
|
1591
|
+
type: "text",
|
|
1592
|
+
text: JSON.stringify({
|
|
1593
|
+
mode,
|
|
1594
|
+
from: componentName,
|
|
1595
|
+
to: target,
|
|
1596
|
+
...path,
|
|
1597
|
+
edges: path.edges.map((edge) => ({
|
|
1598
|
+
source: edge.source,
|
|
1599
|
+
target: edge.target,
|
|
1600
|
+
type: edge.type
|
|
1601
|
+
}))
|
|
1602
|
+
}, null, 2)
|
|
1603
|
+
}]
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
case "composition": {
|
|
1607
|
+
const error = requireComponent(mode);
|
|
1608
|
+
if (error) throw new Error(error);
|
|
1609
|
+
const composition = engine.composition(componentName);
|
|
1610
|
+
return {
|
|
1611
|
+
content: [{
|
|
1612
|
+
type: "text",
|
|
1613
|
+
text: JSON.stringify({
|
|
1614
|
+
mode,
|
|
1615
|
+
...composition
|
|
1616
|
+
}, null, 2)
|
|
1617
|
+
}]
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
case "alternatives": {
|
|
1621
|
+
const error = requireComponent(mode);
|
|
1622
|
+
if (error) throw new Error(error);
|
|
1623
|
+
const alternatives = engine.alternatives(componentName);
|
|
1624
|
+
return {
|
|
1625
|
+
content: [{
|
|
1626
|
+
type: "text",
|
|
1627
|
+
text: JSON.stringify({
|
|
1628
|
+
mode,
|
|
1629
|
+
component: componentName,
|
|
1630
|
+
count: alternatives.length,
|
|
1631
|
+
alternatives
|
|
1632
|
+
}, null, 2)
|
|
1633
|
+
}]
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
case "islands": {
|
|
1637
|
+
const islands = engine.islands();
|
|
1638
|
+
return {
|
|
1639
|
+
content: [{
|
|
1640
|
+
type: "text",
|
|
1641
|
+
text: JSON.stringify({
|
|
1642
|
+
mode,
|
|
1643
|
+
count: islands.length,
|
|
1644
|
+
islands: islands.map((island, index) => ({
|
|
1645
|
+
id: index + 1,
|
|
1646
|
+
size: island.length,
|
|
1647
|
+
components: island
|
|
1648
|
+
}))
|
|
1649
|
+
}, null, 2)
|
|
1650
|
+
}]
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
default:
|
|
1654
|
+
throw new Error(`Unknown mode: "${mode}". Valid modes: dependencies, dependents, impact, path, composition, alternatives, islands, health`);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
// ================================================================
|
|
1658
|
+
// A11Y — run accessibility audit against viewer endpoint
|
|
1659
|
+
// ================================================================
|
|
1660
|
+
case TOOL_NAMES.a11y: {
|
|
1661
|
+
const data = await loadFragments();
|
|
1662
|
+
const componentName = args2?.component;
|
|
1663
|
+
const variantName = args2?.variant ?? void 0;
|
|
1664
|
+
const standard = args2?.standard ?? "AA";
|
|
1665
|
+
if (!componentName) {
|
|
1666
|
+
throw new Error("component is required");
|
|
1667
|
+
}
|
|
1668
|
+
const fragment = Object.values(data.fragments).find(
|
|
1669
|
+
(entry) => entry.meta.name.toLowerCase() === componentName.toLowerCase()
|
|
1670
|
+
);
|
|
1671
|
+
if (!fragment) {
|
|
1672
|
+
throw new Error(`Component "${componentName}" not found. Use ${TOOL_NAMES.discover} to see available components.`);
|
|
1673
|
+
}
|
|
1674
|
+
const baseUrl = config.viewerUrl ?? "http://localhost:6006";
|
|
1675
|
+
const auditUrl = `${baseUrl}/fragments/a11y`;
|
|
1676
|
+
try {
|
|
1677
|
+
const response = await fetch(auditUrl, {
|
|
1678
|
+
method: "POST",
|
|
1679
|
+
headers: { "Content-Type": "application/json" },
|
|
1680
|
+
body: JSON.stringify({
|
|
1681
|
+
component: componentName,
|
|
1682
|
+
variant: variantName,
|
|
1683
|
+
standard
|
|
1684
|
+
})
|
|
1685
|
+
});
|
|
1686
|
+
const result = await response.json();
|
|
1687
|
+
if (!response.ok || result.error) {
|
|
1688
|
+
throw new Error(result.error ?? "A11y audit failed");
|
|
1689
|
+
}
|
|
1690
|
+
const variants = result.results ?? [];
|
|
1691
|
+
const summary = variants.reduce(
|
|
1692
|
+
(acc, variant) => {
|
|
1693
|
+
acc.totalViolations += variant.violations;
|
|
1694
|
+
acc.totalPasses += variant.passes;
|
|
1695
|
+
acc.totalIncomplete += variant.incomplete;
|
|
1696
|
+
acc.critical += variant.summary.critical;
|
|
1697
|
+
acc.serious += variant.summary.serious;
|
|
1698
|
+
acc.moderate += variant.summary.moderate;
|
|
1699
|
+
acc.minor += variant.summary.minor;
|
|
1700
|
+
return acc;
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
totalViolations: 0,
|
|
1704
|
+
totalPasses: 0,
|
|
1705
|
+
totalIncomplete: 0,
|
|
1706
|
+
critical: 0,
|
|
1707
|
+
serious: 0,
|
|
1708
|
+
moderate: 0,
|
|
1709
|
+
minor: 0
|
|
1710
|
+
}
|
|
1711
|
+
);
|
|
1712
|
+
const totalChecks = summary.totalPasses + summary.totalViolations + summary.totalIncomplete;
|
|
1713
|
+
const wcagScore = totalChecks > 0 ? Math.round(summary.totalPasses / totalChecks * 100) : 100;
|
|
1714
|
+
return {
|
|
1715
|
+
content: [{
|
|
1716
|
+
type: "text",
|
|
1717
|
+
text: JSON.stringify({
|
|
1718
|
+
component: componentName,
|
|
1719
|
+
standard,
|
|
1720
|
+
wcagScore,
|
|
1721
|
+
passed: summary.critical === 0 && summary.serious === 0,
|
|
1722
|
+
summary,
|
|
1723
|
+
variants
|
|
1724
|
+
}, null, 2)
|
|
1725
|
+
}]
|
|
1726
|
+
};
|
|
1727
|
+
} catch (error) {
|
|
1728
|
+
return {
|
|
1729
|
+
content: [{
|
|
1730
|
+
type: "text",
|
|
1731
|
+
text: `Failed to run accessibility audit: ${error instanceof Error ? error.message : "Unknown error"}. Make sure the Fragments dev server is running.`
|
|
1732
|
+
}],
|
|
1733
|
+
isError: true
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
// ================================================================
|
|
1738
|
+
// GENERATE_UI — delegate to playground generation endpoint
|
|
1739
|
+
// ================================================================
|
|
1740
|
+
case TOOL_NAMES.generate_ui: {
|
|
1741
|
+
const prompt = args2?.prompt;
|
|
1742
|
+
if (!prompt) {
|
|
1743
|
+
throw new Error("prompt is required");
|
|
1744
|
+
}
|
|
1745
|
+
const currentTree = args2?.currentTree;
|
|
1746
|
+
const playgroundUrl = "https://usefragments.com";
|
|
1747
|
+
const response = await fetch(`${playgroundUrl}/api/playground/generate`, {
|
|
1748
|
+
method: "POST",
|
|
1749
|
+
headers: { "Content-Type": "application/json" },
|
|
1750
|
+
body: JSON.stringify({
|
|
1751
|
+
prompt,
|
|
1752
|
+
...currentTree && { currentSpec: currentTree }
|
|
1753
|
+
})
|
|
1754
|
+
});
|
|
1755
|
+
if (!response.ok) {
|
|
1756
|
+
throw new Error(`Playground API error (${response.status}): ${await response.text()}`);
|
|
1757
|
+
}
|
|
1758
|
+
return {
|
|
1759
|
+
content: [{
|
|
1760
|
+
type: "text",
|
|
1761
|
+
text: await response.text()
|
|
1762
|
+
}]
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
// ================================================================
|
|
1766
|
+
// GOVERN — validate generated UI specs against governance policies
|
|
1767
|
+
// ================================================================
|
|
1768
|
+
case TOOL_NAMES.govern: {
|
|
1769
|
+
const data = await loadFragments();
|
|
1770
|
+
const spec = args2?.spec;
|
|
1771
|
+
if (!spec || typeof spec !== "object") {
|
|
1772
|
+
return {
|
|
1773
|
+
content: [{
|
|
1774
|
+
type: "text",
|
|
1775
|
+
text: JSON.stringify({
|
|
1776
|
+
error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
|
|
1777
|
+
})
|
|
1778
|
+
}],
|
|
1779
|
+
isError: true
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
const {
|
|
1783
|
+
handleGovernTool,
|
|
1784
|
+
formatVerdict,
|
|
1785
|
+
universal,
|
|
1786
|
+
fragments: fragmentsPreset
|
|
1787
|
+
} = await import("@fragments-sdk/govern");
|
|
1788
|
+
const policyOverrides = args2?.policy;
|
|
1789
|
+
const format = args2?.format ?? "json";
|
|
1790
|
+
const tokenPrefix = data.tokens?.prefix;
|
|
1791
|
+
const basePolicy = tokenPrefix && tokenPrefix.includes("fui") ? { rules: fragmentsPreset().rules } : { rules: universal().rules };
|
|
1792
|
+
const engineOptions = data.tokens ? { tokenData: data.tokens } : void 0;
|
|
1793
|
+
const input = {
|
|
1794
|
+
spec,
|
|
1795
|
+
policy: policyOverrides,
|
|
1796
|
+
format
|
|
1797
|
+
};
|
|
1798
|
+
try {
|
|
1799
|
+
const verdict = await handleGovernTool(input, basePolicy, engineOptions);
|
|
1800
|
+
return {
|
|
1801
|
+
content: [{
|
|
1802
|
+
type: "text",
|
|
1803
|
+
text: format === "summary" ? formatVerdict(verdict, "summary") : JSON.stringify(verdict)
|
|
1804
|
+
}],
|
|
1805
|
+
_meta: {
|
|
1806
|
+
score: verdict.score,
|
|
1807
|
+
passed: verdict.passed,
|
|
1808
|
+
violationCount: verdict.results.reduce((sum, result) => sum + result.violations.length, 0)
|
|
1809
|
+
}
|
|
1810
|
+
};
|
|
1811
|
+
} catch (error) {
|
|
1812
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1813
|
+
return {
|
|
1814
|
+
content: [{
|
|
1815
|
+
type: "text",
|
|
1816
|
+
text: JSON.stringify({
|
|
1817
|
+
error: message
|
|
1818
|
+
})
|
|
1819
|
+
}],
|
|
1820
|
+
isError: true
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
// ================================================================
|
|
1178
1825
|
// PERF — query performance data
|
|
1179
1826
|
// ================================================================
|
|
1180
1827
|
case TOOL_NAMES.perf: {
|