@fragments-sdk/cli 0.15.8 → 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/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(BRAND.nameLower);
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(readFileSync(rootPkgPath, "utf-8"));
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 = readFileSync(pnpmWsPath, "utf-8");
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(readFileSync(candidate, "utf-8"));
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(readFileSync(pkgJsonPath, "utf-8"));
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(readFileSync(depPkgPath, "utf-8"));
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: "0.0.1"
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 ?? void 0;
331
- const search = args2?.search?.toLowerCase() ?? void 0;
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
- if (compact || args2?.format && !useCase && !componentForAlts && !category && !search && !status) {
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}`.split(/\s+/).filter(Boolean);
360
- const synonymMap = {
361
- "form": ["input", "field", "submit", "validation"],
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 (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
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 = Array.from(expandedTerms).filter(
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 >= 5) break;
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 STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
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.` : "No matching components found. Try different keywords or browse all components with fragments_discover.";
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: suggestions.map(({ score, ...rest }) => rest),
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 fragments_discover to see available components.`);
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
- }).map((s) => ({
555
- name: s.meta.name,
556
- category: s.meta.category,
557
- description: s.meta.description,
558
- status: s.meta.status ?? "stable",
559
- variantCount: s.variants.length,
560
- tags: s.meta.tags ?? []
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: fragments.length,
567
- fragments,
568
- categories: [...new Set(fragments.map((s) => s.category))],
569
- hint: fragments.length === 0 ? "No components found. Try broader search terms or check available categories." : fragments.length > 5 ? "Use fragments_discover with useCase for recommendations, or fragments_inspect for details on a specific component." : void 0
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 fragments_discover to see available components.`);
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?.toLowerCase() ?? void 0;
953
+ const search = args2?.search ?? void 0;
695
954
  const component = args2?.component?.toLowerCase() ?? void 0;
696
- const category = args2?.category?.toLowerCase() ?? void 0;
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.filter((b) => {
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.toLowerCase() === 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: filtered.length,
743
- blocks: filtered
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?.toLowerCase() ?? void 0;
754
- const search = args2?.search?.toLowerCase() ?? void 0;
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
- let filteredTotal = 0;
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 !== category) continue;
1050
+ if (category && !resolvedCategory.keys.includes(cat)) continue;
772
1051
  let filtered = tokens;
773
1052
  if (search) {
774
- filtered = tokens.filter(
775
- (t) => t.name.toLowerCase().includes(search) || t.description && t.description.toLowerCase().includes(search)
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
- const availableCategories = Object.keys(tokenData.categories);
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: filteredCategories,
1082
+ categories: limited.categories,
798
1083
  ...hint && { hint },
799
1084
  ...!category && !search && {
800
- availableCategories: Object.entries(tokenData.categories).map(
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.split(/\s+/).filter(Boolean);
819
- const synonymMap = {
820
- "form": ["input", "field", "submit", "validation"],
821
- "input": ["form", "field", "text", "entry"],
822
- "button": ["action", "click", "submit", "trigger"],
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 (Array.from(expandedTerms).some((t) => nameLower.includes(t))) score += 8;
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 += Array.from(expandedTerms).filter((t) => !searchTerms.includes(t) && whenUsed.includes(t)).length * 5;
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, 3);
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 examples = s.variants.slice(0, 2).map((v) => ({
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, 10).map(
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: score >= 25 ? "high" : score >= 15 ? "medium" : "low",
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 matchingBlocks = allBlocks.filter((b) => {
880
- const haystack = [b.name, b.description, ...b.tags ?? [], ...b.components, b.category].join(" ").toLowerCase();
881
- return searchTerms.some((t) => haystack.includes(t)) || topMatches.some(({ fragment }) => b.components.some((c) => c.toLowerCase() === fragment.meta.name.toLowerCase()));
882
- }).slice(0, 2).map((b) => ({ name: b.name, description: b.description, components: b.components, code: b.code }));
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 STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "background", "hover", "theme"];
887
- const styleTerms = searchTerms.filter((t) => STYLE_KEYWORDS.includes(t));
888
- if (styleTerms.length > 0) {
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 [cat, tokens] of Object.entries(tokenData.categories)) {
891
- const matching = tokens.filter((t) => styleTerms.some((st) => t.name.includes(st) || cat.includes(st)));
892
- if (matching.length > 0) {
893
- relevantTokens[cat] = matching.map((t) => t.name);
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 fragments_discover to see available components.`);
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 ? "Apply patches using your editor or `patch` command, then run fragments_render with baseline:true to confirm fixes." : void 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: {