@fragments-sdk/mcp 0.5.5 → 0.5.7

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.
@@ -312,7 +312,7 @@ function estimateTokens(text) {
312
312
  return Math.ceil(text.length / 4);
313
313
  }
314
314
 
315
- // ../context/dist/chunk-HAJWPNLU.js
315
+ // ../context/dist/chunk-NECSN43I.js
316
316
  var MCP_TOOL_DEFINITIONS = [
317
317
  {
318
318
  key: "discover",
@@ -352,9 +352,18 @@ var MCP_TOOL_DEFINITIONS = [
352
352
  type: "boolean",
353
353
  description: "If true, includes code examples for each variant"
354
354
  },
355
+ limit: {
356
+ type: "number",
357
+ description: "Maximum number of results to return (default: 10 for useCase mode)"
358
+ },
355
359
  includeRelations: {
356
360
  type: "boolean",
357
361
  description: "If true, includes component relationships"
362
+ },
363
+ verbosity: {
364
+ type: "string",
365
+ enum: ["compact", "standard", "full"],
366
+ description: 'Response detail level: "compact" (names only), "standard" (default), "full" (everything including code)'
358
367
  }
359
368
  }
360
369
  },
@@ -369,7 +378,7 @@ var MCP_TOOL_DEFINITIONS = [
369
378
  fields: {
370
379
  type: "array",
371
380
  items: { type: "string" },
372
- description: 'Specific fields to return (e.g., ["meta", "usage.when", "contract.propsSummary", "props", "examples", "guidelines"]). If omitted, returns everything. Supports dot notation.'
381
+ description: 'Specific fields to return (e.g., ["meta", "guidelines.when", "contract.propsSummary", "props", "examples"]). If omitted, returns everything. Supports dot notation. Aliases: "usage" \u2192 "guidelines".'
373
382
  },
374
383
  variant: {
375
384
  type: "string",
@@ -382,6 +391,11 @@ var MCP_TOOL_DEFINITIONS = [
382
391
  maxLines: {
383
392
  type: "number",
384
393
  description: "Maximum lines per code example (truncates longer examples)"
394
+ },
395
+ verbosity: {
396
+ type: "string",
397
+ enum: ["compact", "standard", "full"],
398
+ description: 'Response detail level: "compact" (meta + prop names), "standard" (default), "full" (everything)'
385
399
  }
386
400
  },
387
401
  required: ["component"]
@@ -405,6 +419,15 @@ var MCP_TOOL_DEFINITIONS = [
405
419
  category: {
406
420
  type: "string",
407
421
  description: 'Filter by category (e.g., "authentication", "marketing", "dashboard", "settings", "ecommerce", "ai")'
422
+ },
423
+ limit: {
424
+ type: "number",
425
+ description: "Maximum number of blocks to return (default: all matching)"
426
+ },
427
+ verbosity: {
428
+ type: "string",
429
+ enum: ["compact", "standard", "full"],
430
+ description: 'Response detail level: "compact" (no code), "standard" (default, code preview for long blocks), "full" (full code)'
408
431
  }
409
432
  }
410
433
  },
@@ -419,6 +442,10 @@ var MCP_TOOL_DEFINITIONS = [
419
442
  search: {
420
443
  type: "string",
421
444
  description: 'Search token names (e.g., "accent", "hover", "padding")'
445
+ },
446
+ limit: {
447
+ type: "number",
448
+ description: "Maximum number of tokens to return per category (default: 25 for search, unlimited for category browsing)"
422
449
  }
423
450
  }
424
451
  },
@@ -429,6 +456,15 @@ var MCP_TOOL_DEFINITIONS = [
429
456
  useCase: {
430
457
  type: "string",
431
458
  description: 'What you want to implement (e.g., "login form", "data table with sorting", "streaming chat messages")'
459
+ },
460
+ limit: {
461
+ type: "number",
462
+ description: "Maximum number of components to return (default: 5)"
463
+ },
464
+ verbosity: {
465
+ type: "string",
466
+ enum: ["compact", "standard", "full"],
467
+ description: 'Response detail level: "compact" (names only), "standard" (default), "full" (all props + examples + full block code)'
432
468
  }
433
469
  },
434
470
  required: ["useCase"]
@@ -465,7 +501,7 @@ var MCP_TOOL_DEFINITIONS = [
465
501
  },
466
502
  variant: {
467
503
  type: "string",
468
- description: "Variant name for compare mode"
504
+ description: "Variant name to render (uses the variant's render function from the fragment definition). Works in both render and compare modes."
469
505
  },
470
506
  threshold: {
471
507
  type: "number",
@@ -496,7 +532,7 @@ var MCP_TOOL_DEFINITIONS = [
496
532
  },
497
533
  {
498
534
  key: "graph",
499
- description: 'Query the component relationship graph. Understand dependencies, impact analysis, composition trees, alternatives, and design system health. Use "health" for an overview, "dependencies"/"dependents" for direct relationships, "impact" for change analysis, "composition" for compound component trees.',
535
+ description: 'Query the component relationship graph. Understand dependencies, impact analysis, composition trees, alternatives, and design system health. Use "health" for an overview, "dependencies"/"dependents" for composition and declared relationships (not code-level imports), "impact" for change analysis, "composition" for compound component trees.',
500
536
  params: {
501
537
  mode: {
502
538
  type: "string",
@@ -737,9 +773,8 @@ function findFragmentsJson(startDir) {
737
773
  return found;
738
774
  }
739
775
 
740
- // src/search.ts
741
- var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
742
- var CONVEX_TIMEOUT_MS = 3e3;
776
+ // src/orama-index.ts
777
+ import { create, insertMultiple, search } from "@orama/orama";
743
778
  var SYNONYM_MAP = {
744
779
  "form": ["input", "field", "submit", "validation"],
745
780
  "input": ["form", "field", "text", "entry"],
@@ -759,8 +794,259 @@ var SYNONYM_MAP = {
759
794
  "chat": ["message", "conversation", "ai"],
760
795
  "table": ["data", "grid", "list", "rows"],
761
796
  "textarea": ["text", "input", "multiline", "area", "comment"],
762
- "area": ["textarea", "multiline", "text"]
797
+ "area": ["textarea", "multiline", "text"],
798
+ "landing": ["page", "hero", "marketing", "section", "layout"],
799
+ "hero": ["landing", "marketing", "banner", "headline", "section"],
800
+ "marketing": ["landing", "hero", "pricing", "testimonial", "cta"],
801
+ "cta": ["marketing", "banner", "action", "button"],
802
+ "testimonial": ["marketing", "review", "quote", "feedback"],
803
+ "layout": ["stack", "grid", "box", "container", "page"],
804
+ "page": ["layout", "landing", "section", "container"],
805
+ "section": ["hero", "feature", "testimonial", "cta", "faq"],
806
+ "pricing": ["card", "plan", "tier", "marketing"],
807
+ "plan": ["pricing", "card", "tier", "subscription"],
808
+ "dashboard": ["metrics", "stats", "chart", "card", "grid"],
809
+ "metrics": ["dashboard", "stats", "progress", "number"],
810
+ "stats": ["metrics", "dashboard", "progress", "badge"],
811
+ "chart": ["dashboard", "metrics", "data", "graph"]
812
+ };
813
+ function expandQuery(query) {
814
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
815
+ const expanded = new Set(terms);
816
+ for (const term of terms) {
817
+ const synonyms = SYNONYM_MAP[term];
818
+ if (synonyms) {
819
+ for (const syn of synonyms) expanded.add(syn);
820
+ }
821
+ }
822
+ return Array.from(expanded).join(" ");
823
+ }
824
+ function twoPassSearch(config) {
825
+ const { index, query, properties, boost, limit, kind } = config;
826
+ const baseConfig = {
827
+ mode: "fulltext",
828
+ properties,
829
+ boost,
830
+ limit
831
+ };
832
+ const originalTermsQuery = query.toLowerCase().split(/\s+/).filter(Boolean).join(" ");
833
+ const expandedQuery = expandQuery(query);
834
+ const originalResults = search(index, { term: originalTermsQuery, ...baseConfig, threshold: 0.8 });
835
+ const expandedResults = search(index, { term: expandedQuery, ...baseConfig, threshold: 1 });
836
+ const origHits = originalResults.hits;
837
+ const expHits = expandedResults.hits;
838
+ const scoreMap = /* @__PURE__ */ new Map();
839
+ for (const hit of origHits) {
840
+ scoreMap.set(hit.document.name, (hit.score || 0) * 2);
841
+ }
842
+ for (const hit of expHits) {
843
+ const name = hit.document.name;
844
+ const existing = scoreMap.get(name) ?? 0;
845
+ scoreMap.set(name, existing + (hit.score || 0));
846
+ }
847
+ const scored = [];
848
+ for (const [name, score] of scoreMap) {
849
+ if (score > 0) {
850
+ scored.push({ name, kind, rank: scored.length, score });
851
+ }
852
+ }
853
+ scored.sort((a, b) => b.score - a.score);
854
+ scored.forEach((s, i) => {
855
+ s.rank = i;
856
+ });
857
+ return scored;
858
+ }
859
+ var componentSchema = {
860
+ name: "string",
861
+ description: "string",
862
+ category: "string",
863
+ tags: "string",
864
+ whenUsed: "string",
865
+ variants: "string",
866
+ status: "string"
867
+ };
868
+ function buildComponentIndex(fragments) {
869
+ const db = create({ schema: componentSchema, language: "english" });
870
+ const docs = fragments.map((f) => ({
871
+ name: f.meta.name,
872
+ description: f.meta.description ?? "",
873
+ category: f.meta.category ?? "",
874
+ tags: (f.meta.tags ?? []).join(" "),
875
+ whenUsed: (f.usage?.when ?? []).join(" "),
876
+ variants: f.variants.map((v) => `${v.name} ${v.description || ""}`).join(" "),
877
+ status: f.meta.status ?? "stable"
878
+ }));
879
+ insertMultiple(db, docs);
880
+ return db;
881
+ }
882
+ function searchComponents(query, index, fragments, limit = 50) {
883
+ const boostConfig = {
884
+ mode: "fulltext",
885
+ properties: ["name", "whenUsed", "description", "category", "tags", "variants"],
886
+ boost: {
887
+ name: 3,
888
+ whenUsed: 2.5,
889
+ description: 2,
890
+ category: 1.5,
891
+ tags: 1.5,
892
+ variants: 1
893
+ },
894
+ limit
895
+ };
896
+ const originalTermsList = query.toLowerCase().split(/\s+/).filter(Boolean);
897
+ const originalTermsQuery = originalTermsList.join(" ");
898
+ const expandedQuery = expandQuery(query);
899
+ const originalResults = search(index, { term: originalTermsQuery, ...boostConfig, threshold: 0.8 });
900
+ const expandedResults = search(index, { term: expandedQuery, ...boostConfig, threshold: 1 });
901
+ const origHits = originalResults.hits;
902
+ const expHits = expandedResults.hits;
903
+ const scoreMap = /* @__PURE__ */ new Map();
904
+ for (const hit of origHits) {
905
+ scoreMap.set(hit.document.name, (hit.score || 0) * 2);
906
+ }
907
+ for (const hit of expHits) {
908
+ const name = hit.document.name;
909
+ const existing = scoreMap.get(name) ?? 0;
910
+ scoreMap.set(name, existing + (hit.score || 0));
911
+ }
912
+ const fragmentMap = /* @__PURE__ */ new Map();
913
+ for (const f of fragments) {
914
+ fragmentMap.set(f.meta.name.toLowerCase(), f);
915
+ }
916
+ const originalTermsSet = new Set(originalTermsList);
917
+ const scored = [];
918
+ for (const [name, rawScore] of scoreMap) {
919
+ let score = rawScore;
920
+ const nameLower = name.toLowerCase();
921
+ const fragment = fragmentMap.get(nameLower);
922
+ if (originalTermsSet.has(nameLower)) {
923
+ score += 25;
924
+ }
925
+ if (fragment) {
926
+ if (fragment.meta.status === "stable") score += 5;
927
+ else if (fragment.meta.status === "beta") score += 2;
928
+ if (fragment.meta.status === "deprecated") score -= 25;
929
+ }
930
+ if (score > 0) {
931
+ scored.push({ name, kind: "component", rank: scored.length, score });
932
+ }
933
+ }
934
+ scored.sort((a, b) => b.score - a.score);
935
+ scored.forEach((s, i) => {
936
+ s.rank = i;
937
+ });
938
+ return scored;
939
+ }
940
+ var blockSchema = {
941
+ name: "string",
942
+ description: "string",
943
+ category: "string",
944
+ tags: "string",
945
+ components: "string"
946
+ };
947
+ function buildBlockIndex(blocks) {
948
+ const db = create({ schema: blockSchema, language: "english" });
949
+ const docs = blocks.map((b) => ({
950
+ name: b.name,
951
+ description: b.description ?? "",
952
+ category: b.category ?? "",
953
+ tags: (b.tags ?? []).join(" "),
954
+ components: b.components.join(" ")
955
+ }));
956
+ insertMultiple(db, docs);
957
+ return db;
958
+ }
959
+ function searchBlocks(query, index, limit = 50) {
960
+ return twoPassSearch({
961
+ index,
962
+ query,
963
+ properties: ["name", "description", "components", "tags", "category"],
964
+ boost: {
965
+ name: 3,
966
+ description: 2,
967
+ components: 1.5,
968
+ tags: 1.5,
969
+ category: 1.5
970
+ },
971
+ limit,
972
+ kind: "block"
973
+ });
974
+ }
975
+ var tokenSchema = {
976
+ name: "string",
977
+ category: "string",
978
+ description: "string"
763
979
  };
980
+ function buildTokenIndex(tokenData) {
981
+ const db = create({ schema: tokenSchema, language: "english" });
982
+ const docs = [];
983
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
984
+ for (const token of tokens) {
985
+ docs.push({
986
+ name: token.name,
987
+ category: cat,
988
+ description: token.description ?? ""
989
+ });
990
+ }
991
+ }
992
+ insertMultiple(db, docs);
993
+ return db;
994
+ }
995
+ function searchTokens(query, index, limit = 50) {
996
+ return twoPassSearch({
997
+ index,
998
+ query,
999
+ properties: ["name", "category", "description"],
1000
+ boost: {
1001
+ name: 2.5,
1002
+ category: 2,
1003
+ description: 1.5
1004
+ },
1005
+ limit,
1006
+ kind: "token"
1007
+ });
1008
+ }
1009
+ var USE_CASE_TOKEN_CATEGORIES = {
1010
+ "table": ["spacing", "borders", "surfaces", "text"],
1011
+ "data": ["spacing", "borders", "surfaces"],
1012
+ "grid": ["spacing", "layout"],
1013
+ "form": ["spacing", "borders", "radius", "focus"],
1014
+ "input": ["spacing", "borders", "radius", "focus"],
1015
+ "card": ["surfaces", "shadows", "radius", "borders", "spacing"],
1016
+ "button": ["colors", "radius", "spacing", "focus"],
1017
+ "layout": ["spacing", "layout", "surfaces"],
1018
+ "dashboard": ["spacing", "surfaces", "borders", "shadows"],
1019
+ "chat": ["spacing", "surfaces", "radius", "shadows"],
1020
+ "modal": ["shadows", "surfaces", "radius", "spacing"],
1021
+ "dialog": ["shadows", "surfaces", "radius", "spacing"],
1022
+ "navigation": ["spacing", "surfaces", "borders"],
1023
+ "sidebar": ["spacing", "surfaces", "borders"],
1024
+ "hero": ["spacing", "typography", "colors"],
1025
+ "landing": ["spacing", "typography", "colors"],
1026
+ "pricing": ["spacing", "surfaces", "borders", "radius"],
1027
+ "auth": ["spacing", "borders", "radius", "focus"],
1028
+ "login": ["spacing", "borders", "radius", "focus"],
1029
+ "dark": ["colors", "surfaces"],
1030
+ "theme": ["colors", "surfaces", "text"]
1031
+ };
1032
+ function extractTokenCategories(query) {
1033
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
1034
+ const categories = /* @__PURE__ */ new Set();
1035
+ for (const term of terms) {
1036
+ const cats = USE_CASE_TOKEN_CATEGORIES[term];
1037
+ if (cats) {
1038
+ for (const cat of cats) categories.add(cat);
1039
+ }
1040
+ }
1041
+ if (categories.size === 0) {
1042
+ return ["spacing", "colors", "surfaces"];
1043
+ }
1044
+ return Array.from(categories);
1045
+ }
1046
+
1047
+ // src/search.ts
1048
+ var CONVEX_SEARCH_URL = "https://combative-jay-834.convex.site/search";
1049
+ var CONVEX_TIMEOUT_MS = 3e3;
764
1050
  async function searchConvex(query, apiKey, limit = 10, kind) {
765
1051
  try {
766
1052
  const controller = new AbortController();
@@ -789,94 +1075,17 @@ async function searchConvex(query, apiKey, limit = 10, kind) {
789
1075
  return [];
790
1076
  }
791
1077
  }
792
- function keywordScoreComponents(query, fragments) {
793
- const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
794
- const expandedTerms = new Set(searchTerms);
795
- for (const term of searchTerms) {
796
- const synonyms = SYNONYM_MAP[term];
797
- if (synonyms) {
798
- for (const syn of synonyms) expandedTerms.add(syn);
799
- }
800
- }
801
- const scored = fragments.map((s) => {
802
- let score = 0;
803
- const nameLower = s.meta.name.toLowerCase();
804
- if (searchTerms.some((term) => nameLower.includes(term))) {
805
- score += 15;
806
- } else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
807
- score += 8;
808
- }
809
- const desc = s.meta.description?.toLowerCase() ?? "";
810
- score += searchTerms.filter((term) => desc.includes(term)).length * 6;
811
- const tags = s.meta.tags?.map((t) => t.toLowerCase()) ?? [];
812
- score += searchTerms.filter((term) => tags.some((tag) => tag.includes(term))).length * 4;
813
- const whenUsed = s.usage?.when?.join(" ").toLowerCase() ?? "";
814
- score += searchTerms.filter((term) => whenUsed.includes(term)).length * 10;
815
- score += Array.from(expandedTerms).filter((term) => !searchTerms.includes(term) && whenUsed.includes(term)).length * 5;
816
- const cat = s.meta.category?.toLowerCase() ?? "";
817
- if (searchTerms.some((term) => cat.includes(term))) {
818
- score += 8;
819
- }
820
- const variantText = s.variants.map((v) => `${v.name} ${v.description || ""}`.toLowerCase()).join(" ");
821
- score += searchTerms.filter((term) => variantText.includes(term)).length * 3;
822
- if (s.meta.status === "stable") score += 5;
823
- else if (s.meta.status === "beta") score += 2;
824
- if (s.meta.status === "deprecated") score -= 25;
825
- return { name: s.meta.name, kind: "component", score };
826
- });
827
- return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
1078
+ function keywordScoreComponents(query, fragments, componentIndex) {
1079
+ const index = componentIndex ?? buildComponentIndex(fragments);
1080
+ return searchComponents(query, index, fragments);
828
1081
  }
829
- function keywordScoreBlocks(query, blocks) {
830
- const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
831
- const expandedTerms = new Set(searchTerms);
832
- for (const term of searchTerms) {
833
- const synonyms = SYNONYM_MAP[term];
834
- if (synonyms) {
835
- for (const syn of synonyms) expandedTerms.add(syn);
836
- }
837
- }
838
- const scored = blocks.map((b) => {
839
- let score = 0;
840
- const nameLower = b.name.toLowerCase();
841
- if (searchTerms.some((term) => nameLower.includes(term))) {
842
- score += 15;
843
- } else if (Array.from(expandedTerms).some((term) => nameLower.includes(term))) {
844
- score += 8;
845
- }
846
- const desc = b.description.toLowerCase();
847
- score += searchTerms.filter((term) => desc.includes(term)).length * 6;
848
- const tags = b.tags?.map((t) => t.toLowerCase()) ?? [];
849
- score += searchTerms.filter((term) => tags.some((tag) => tag.includes(term))).length * 4;
850
- const componentText = b.components.join(" ").toLowerCase();
851
- score += searchTerms.filter((term) => componentText.includes(term)).length * 5;
852
- const cat = b.category.toLowerCase();
853
- if (searchTerms.some((term) => cat.includes(term))) {
854
- score += 8;
855
- }
856
- return { name: b.name, kind: "block", score };
857
- });
858
- return scored.filter((s) => s.score > 0).sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
1082
+ function keywordScoreBlocks(query, blocks, blockIndex) {
1083
+ const index = blockIndex ?? buildBlockIndex(blocks);
1084
+ return searchBlocks(query, index);
859
1085
  }
860
- function keywordScoreTokens(query, tokenData) {
861
- const searchTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
862
- const scored = [];
863
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
864
- const catLower = cat.toLowerCase();
865
- const catBonus = searchTerms.some((term) => catLower.includes(term)) ? 8 : 0;
866
- for (const token of tokens) {
867
- let score = catBonus;
868
- const nameLower = token.name.toLowerCase();
869
- score += searchTerms.filter((term) => nameLower.includes(term)).length * 10;
870
- if (token.description) {
871
- const descLower = token.description.toLowerCase();
872
- score += searchTerms.filter((term) => descLower.includes(term)).length * 6;
873
- }
874
- if (score > 0) {
875
- scored.push({ name: token.name, kind: "token", score });
876
- }
877
- }
878
- }
879
- return scored.sort((a, b) => b.score - a.score).map((s, i) => ({ ...s, rank: i }));
1086
+ function keywordScoreTokens(query, tokenData, tokenIndex) {
1087
+ const index = tokenIndex ?? buildTokenIndex(tokenData);
1088
+ return searchTokens(query, index);
880
1089
  }
881
1090
  function reciprocalRankFusion(resultSets, k = 60) {
882
1091
  const scoreMap = /* @__PURE__ */ new Map();
@@ -906,13 +1115,13 @@ function reciprocalRankFusion(resultSets, k = 60) {
906
1115
  async function hybridSearch(query, data, limit = 10, kind, apiKey) {
907
1116
  const keywordResults = [];
908
1117
  if (!kind || kind === "component") {
909
- keywordResults.push(...keywordScoreComponents(query, data.fragments));
1118
+ keywordResults.push(...keywordScoreComponents(query, data.fragments, data.componentIndex));
910
1119
  }
911
1120
  if ((!kind || kind === "block") && data.blocks) {
912
- keywordResults.push(...keywordScoreBlocks(query, data.blocks));
1121
+ keywordResults.push(...keywordScoreBlocks(query, data.blocks, data.blockIndex));
913
1122
  }
914
1123
  if ((!kind || kind === "token") && data.tokenData) {
915
- keywordResults.push(...keywordScoreTokens(query, data.tokenData));
1124
+ keywordResults.push(...keywordScoreTokens(query, data.tokenData, data.tokenIndex));
916
1125
  }
917
1126
  keywordResults.sort((a, b) => b.score - a.score);
918
1127
  keywordResults.forEach((r, i) => {
@@ -962,6 +1171,78 @@ async function hybridSearch(query, data, limit = 10, kind, apiKey) {
962
1171
  return fused.slice(0, limit);
963
1172
  }
964
1173
 
1174
+ // src/scoring.ts
1175
+ var MINIMUM_SCORE_THRESHOLD = 5;
1176
+ function assignConfidence(score, maxScore) {
1177
+ if (maxScore <= 0) return "low";
1178
+ const ratio = score / maxScore;
1179
+ if (ratio >= 0.7) return "high";
1180
+ if (ratio >= 0.4) return "medium";
1181
+ return "low";
1182
+ }
1183
+ function meetsMinimumThreshold(maxScore) {
1184
+ return maxScore >= MINIMUM_SCORE_THRESHOLD;
1185
+ }
1186
+ function levenshtein(a, b) {
1187
+ const la = a.length;
1188
+ const lb = b.length;
1189
+ const dp = Array.from({ length: lb + 1 }, (_, i) => i);
1190
+ for (let i = 1; i <= la; i++) {
1191
+ let prev = i - 1;
1192
+ dp[0] = i;
1193
+ for (let j = 1; j <= lb; j++) {
1194
+ const temp = dp[j];
1195
+ dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
1196
+ prev = temp;
1197
+ }
1198
+ }
1199
+ return dp[lb];
1200
+ }
1201
+ function findClosestMatch(input, candidates, maxDistance = 3) {
1202
+ const inputLower = input.toLowerCase();
1203
+ let bestMatch = null;
1204
+ let bestDist = maxDistance + 1;
1205
+ for (const candidate of candidates) {
1206
+ const candidateLower = candidate.toLowerCase();
1207
+ const dist = levenshtein(inputLower, candidateLower);
1208
+ if (dist < bestDist) {
1209
+ bestDist = dist;
1210
+ bestMatch = candidate;
1211
+ } else if (dist === bestDist && bestMatch) {
1212
+ const currentLenDiff = Math.abs(bestMatch.length - input.length);
1213
+ const newLenDiff = Math.abs(candidate.length - input.length);
1214
+ if (newLenDiff < currentLenDiff) {
1215
+ bestMatch = candidate;
1216
+ }
1217
+ }
1218
+ }
1219
+ return bestDist <= maxDistance ? bestMatch : null;
1220
+ }
1221
+ var BLOCK_BOOST_PER_OCCURRENCE = 5;
1222
+ function buildBlockComponentFrequency(blocks) {
1223
+ const freq = /* @__PURE__ */ new Map();
1224
+ for (const block of blocks) {
1225
+ for (const comp of block.components) {
1226
+ const key = comp.toLowerCase();
1227
+ freq.set(key, (freq.get(key) ?? 0) + 1);
1228
+ }
1229
+ }
1230
+ return freq;
1231
+ }
1232
+ function boostByBlockFrequency(results, freq) {
1233
+ for (const result of results) {
1234
+ const count = freq.get(result.name.toLowerCase()) ?? 0;
1235
+ if (count > 0) {
1236
+ result.score += count * BLOCK_BOOST_PER_OCCURRENCE;
1237
+ }
1238
+ }
1239
+ results.sort((a, b) => b.score - a.score);
1240
+ results.forEach((r, i) => {
1241
+ r.rank = i;
1242
+ });
1243
+ return results;
1244
+ }
1245
+
965
1246
  // src/service.ts
966
1247
  async function renderComponent(viewerUrl, request) {
967
1248
  const renderUrl = `${viewerUrl}/fragments/render`;
@@ -971,6 +1252,7 @@ async function renderComponent(viewerUrl, request) {
971
1252
  body: JSON.stringify({
972
1253
  component: request.component,
973
1254
  props: request.props ?? {},
1255
+ variant: request.variant,
974
1256
  viewport: request.viewport ?? { width: 800, height: 600 }
975
1257
  })
976
1258
  });
@@ -1001,7 +1283,9 @@ async function auditComponent(viewerUrl, request) {
1001
1283
  headers: { "Content-Type": "application/json" },
1002
1284
  body: JSON.stringify({
1003
1285
  component: request.component,
1004
- variant: request.variant
1286
+ variant: request.variant,
1287
+ standard: request.standard,
1288
+ includeFixPatches: request.includeFixPatches
1005
1289
  })
1006
1290
  });
1007
1291
  const raw = await response.json();
@@ -1031,18 +1315,31 @@ async function auditComponent(viewerUrl, request) {
1031
1315
  }
1032
1316
  const deductions = totalCritical * 10 + totalSerious * 5 + totalModerate * 2 + totalMinor * 1;
1033
1317
  const score = Math.max(0, 100 - deductions);
1318
+ const variantCount = results.length;
1319
+ const aaPassCount = results.filter((r) => {
1320
+ const critical = r.summary.critical;
1321
+ const serious = r.summary.serious;
1322
+ return critical === 0 && serious === 0;
1323
+ }).length;
1324
+ const aaaPassCount = results.filter((r) => {
1325
+ const total = r.summary.critical + r.summary.serious + r.summary.moderate + r.summary.minor;
1326
+ return total === 0;
1327
+ }).length;
1328
+ const totalPasses = results.reduce((sum, r) => sum + r.passes, 0);
1034
1329
  const totalViolations = totalCritical + totalSerious + totalModerate + totalMinor;
1035
- const aaPass = totalCritical === 0 && totalSerious === 0;
1036
- const aaaPass = totalViolations === 0;
1037
- const aaPercent = aaPass ? 100 : 0;
1038
- const aaaPercent = aaaPass ? 100 : 0;
1330
+ const emptyAudit = results.length > 0 && totalPasses === 0 && totalViolations === 0;
1331
+ const aaPercent = variantCount > 0 ? Math.round(aaPassCount / variantCount * 100) : 100;
1332
+ const aaaPercent = variantCount > 0 ? Math.round(aaaPassCount / variantCount * 100) : 100;
1333
+ const aaPass = !emptyAudit && totalCritical === 0 && totalSerious === 0;
1334
+ const aaaPass = !emptyAudit && totalViolations === 0;
1039
1335
  const passed = standard === "AAA" ? aaaPass : aaPass;
1040
1336
  return {
1041
1337
  component: request.component,
1042
1338
  results,
1043
- score,
1044
- aaPercent,
1045
- aaaPercent,
1339
+ score: emptyAudit ? 0 : score,
1340
+ aaPercent: emptyAudit ? 0 : aaPercent,
1341
+ aaaPercent: emptyAudit ? 0 : aaaPercent,
1342
+ ...emptyAudit && { emptyAudit },
1046
1343
  passed,
1047
1344
  standard
1048
1345
  };
@@ -1081,7 +1378,7 @@ function projectFields(obj, fields) {
1081
1378
  }
1082
1379
 
1083
1380
  // src/graph-handler.ts
1084
- function handleGraphTool(args, serializedGraph, blocks) {
1381
+ function handleGraphTool(args, serializedGraph, blocks, componentNames) {
1085
1382
  if (!serializedGraph) {
1086
1383
  return {
1087
1384
  text: JSON.stringify({
@@ -1109,7 +1406,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1109
1406
  compositionNote: "No composition blocks defined yet \u2014 compositionCoverage will increase as blocks are added"
1110
1407
  },
1111
1408
  summary: `${health.nodeCount} components, ${health.edgeCount} edges, ${health.connectedComponents.length} island(s), ${health.orphans.length} orphan(s), ${health.compositionCoverage}% in blocks`
1112
- }, null, 2)
1409
+ })
1113
1410
  };
1114
1411
  }
1115
1412
  case "dependencies": {
@@ -1117,7 +1414,9 @@ function handleGraphTool(args, serializedGraph, blocks) {
1117
1414
  return { text: JSON.stringify({ error: "component is required for dependencies mode" }), isError: true };
1118
1415
  }
1119
1416
  if (!engine.hasNode(args.component)) {
1120
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1417
+ const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
1418
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1419
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
1121
1420
  }
1122
1421
  const deps = engine.dependencies(args.component, edgeTypes);
1123
1422
  return {
@@ -1132,7 +1431,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1132
1431
  note: e.note,
1133
1432
  provenance: e.provenance
1134
1433
  }))
1135
- }, null, 2)
1434
+ })
1136
1435
  };
1137
1436
  }
1138
1437
  case "dependents": {
@@ -1140,7 +1439,9 @@ function handleGraphTool(args, serializedGraph, blocks) {
1140
1439
  return { text: JSON.stringify({ error: "component is required for dependents mode" }), isError: true };
1141
1440
  }
1142
1441
  if (!engine.hasNode(args.component)) {
1143
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1442
+ const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
1443
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1444
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
1144
1445
  }
1145
1446
  const deps = engine.dependents(args.component, edgeTypes);
1146
1447
  return {
@@ -1155,7 +1456,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1155
1456
  note: e.note,
1156
1457
  provenance: e.provenance
1157
1458
  }))
1158
- }, null, 2)
1459
+ })
1159
1460
  };
1160
1461
  }
1161
1462
  case "impact": {
@@ -1163,7 +1464,9 @@ function handleGraphTool(args, serializedGraph, blocks) {
1163
1464
  return { text: JSON.stringify({ error: "component is required for impact mode" }), isError: true };
1164
1465
  }
1165
1466
  if (!engine.hasNode(args.component)) {
1166
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1467
+ const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
1468
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1469
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
1167
1470
  }
1168
1471
  const result = engine.impact(args.component, args.maxDepth ?? 3);
1169
1472
  return {
@@ -1171,7 +1474,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1171
1474
  mode: "impact",
1172
1475
  ...result,
1173
1476
  summary: `Changing ${args.component} affects ${result.totalAffected} component(s) and ${result.affectedBlocks.length} block(s)`
1174
- }, null, 2)
1477
+ })
1175
1478
  };
1176
1479
  }
1177
1480
  case "path": {
@@ -1190,7 +1493,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1190
1493
  target: e.target,
1191
1494
  type: e.type
1192
1495
  }))
1193
- }, null, 2)
1496
+ })
1194
1497
  };
1195
1498
  }
1196
1499
  case "composition": {
@@ -1198,14 +1501,16 @@ function handleGraphTool(args, serializedGraph, blocks) {
1198
1501
  return { text: JSON.stringify({ error: "component is required for composition mode" }), isError: true };
1199
1502
  }
1200
1503
  if (!engine.hasNode(args.component)) {
1201
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1504
+ const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
1505
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1506
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
1202
1507
  }
1203
1508
  const tree = engine.composition(args.component);
1204
1509
  return {
1205
1510
  text: JSON.stringify({
1206
1511
  mode: "composition",
1207
1512
  ...tree
1208
- }, null, 2)
1513
+ })
1209
1514
  };
1210
1515
  }
1211
1516
  case "alternatives": {
@@ -1213,7 +1518,9 @@ function handleGraphTool(args, serializedGraph, blocks) {
1213
1518
  return { text: JSON.stringify({ error: "component is required for alternatives mode" }), isError: true };
1214
1519
  }
1215
1520
  if (!engine.hasNode(args.component)) {
1216
- return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph` }), isError: true };
1521
+ const closest = componentNames ? findClosestMatch(args.component, componentNames) : null;
1522
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1523
+ return { text: JSON.stringify({ error: `Component "${args.component}" not found in graph.${suggestion}` }), isError: true };
1217
1524
  }
1218
1525
  const alts = engine.alternatives(args.component);
1219
1526
  return {
@@ -1222,7 +1529,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1222
1529
  component: args.component,
1223
1530
  count: alts.length,
1224
1531
  alternatives: alts
1225
- }, null, 2)
1532
+ })
1226
1533
  };
1227
1534
  }
1228
1535
  case "islands": {
@@ -1236,7 +1543,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1236
1543
  size: island.length,
1237
1544
  components: island
1238
1545
  }))
1239
- }, null, 2)
1546
+ })
1240
1547
  };
1241
1548
  }
1242
1549
  default:
@@ -1251,7 +1558,7 @@ function handleGraphTool(args, serializedGraph, blocks) {
1251
1558
 
1252
1559
  // src/server.ts
1253
1560
  var TOOL_NAMES = buildToolNames(BRAND.nameLower);
1254
- var NO_VIEWER_MSG = "This tool requires a running dev server. Pass --viewer-url to the MCP server, or use the fragments-dev MCP config which connects to your local dev server.";
1561
+ var NO_VIEWER_MSG = "This tool requires a running dev server. Add --viewer-url http://localhost:PORT to this server's config args. Start the viewer with 'fragments dev' in your component library directory.";
1255
1562
  var TOOLS = buildMcpTools(BRAND.nameLower);
1256
1563
  function createMcpServer(config) {
1257
1564
  const server = new Server(
@@ -1269,6 +1576,9 @@ function createMcpServer(config) {
1269
1576
  const fragmentPackageMap = /* @__PURE__ */ new Map();
1270
1577
  let defaultPackageName = null;
1271
1578
  let resolvedRoot = null;
1579
+ let componentIndex = null;
1580
+ let blockIndex = null;
1581
+ let tokenIndex = null;
1272
1582
  async function resolveProjectRoot() {
1273
1583
  if (resolvedRoot) return resolvedRoot;
1274
1584
  try {
@@ -1326,6 +1636,15 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1326
1636
  fragmentsData.blocks = { ...fragmentsData.blocks, ...extraBlocks };
1327
1637
  }
1328
1638
  }
1639
+ const allFragments = Object.values(fragmentsData.fragments);
1640
+ const allBlocks = Object.values(fragmentsData.blocks ?? fragmentsData.recipes ?? {});
1641
+ componentIndex = buildComponentIndex(allFragments);
1642
+ if (allBlocks.length > 0) {
1643
+ blockIndex = buildBlockIndex(allBlocks);
1644
+ }
1645
+ if (fragmentsData.tokens && fragmentsData.tokens.total > 0) {
1646
+ tokenIndex = buildTokenIndex(fragmentsData.tokens);
1647
+ }
1329
1648
  return fragmentsData;
1330
1649
  }
1331
1650
  async function getPackageName(fragmentName) {
@@ -1355,8 +1674,15 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1355
1674
  defaultPackageName = "your-component-library";
1356
1675
  return defaultPackageName;
1357
1676
  }
1677
+ const VIEWER_TOOL_KEYS = /* @__PURE__ */ new Set(["render", "fix", "a11y"]);
1358
1678
  server.setRequestHandler(ListToolsRequestSchema, async () => {
1359
- return { tools: TOOLS };
1679
+ if (config.viewerUrl) {
1680
+ return { tools: TOOLS };
1681
+ }
1682
+ const availableTools = TOOLS.filter(
1683
+ (t) => !VIEWER_TOOL_KEYS.has(t.name.replace(`${BRAND.nameLower}_`, ""))
1684
+ );
1685
+ return { tools: availableTools };
1360
1686
  });
1361
1687
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
1362
1688
  const { name, arguments: args } = request.params;
@@ -1370,15 +1696,28 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1370
1696
  const useCase = args?.useCase ?? void 0;
1371
1697
  const componentForAlts = args?.component ?? void 0;
1372
1698
  const category = args?.category ?? void 0;
1373
- const search = args?.search?.toLowerCase() ?? void 0;
1699
+ const search2 = args?.search?.toLowerCase() ?? void 0;
1374
1700
  const status = args?.status ?? void 0;
1375
1701
  const format = args?.format ?? "markdown";
1376
1702
  const compact = args?.compact ?? false;
1377
1703
  const includeCode = args?.includeCode ?? false;
1378
1704
  const includeRelations = args?.includeRelations ?? false;
1379
- if (compact || args?.format && !useCase && !componentForAlts && !category && !search && !status) {
1380
- const fragments2 = Object.values(data.fragments);
1705
+ const limit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
1706
+ const verbosity = args?.verbosity ?? "standard";
1707
+ if (compact || args?.format && !useCase && !componentForAlts) {
1708
+ let fragments2 = Object.values(data.fragments);
1381
1709
  const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1710
+ if (category) {
1711
+ fragments2 = fragments2.filter((f) => f.meta.category?.toLowerCase() === category);
1712
+ }
1713
+ if (search2) {
1714
+ fragments2 = fragments2.filter(
1715
+ (f) => f.meta.name.toLowerCase().includes(search2) || f.meta.description?.toLowerCase().includes(search2) || f.meta.tags?.some((t) => t.toLowerCase().includes(search2))
1716
+ );
1717
+ }
1718
+ if (status) {
1719
+ fragments2 = fragments2.filter((f) => f.meta.status === status);
1720
+ }
1382
1721
  const { content: ctxContent, tokenEstimate } = generateContext(fragments2, {
1383
1722
  format,
1384
1723
  compact,
@@ -1400,9 +1739,19 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1400
1739
  const localData = {
1401
1740
  fragments: allFragments,
1402
1741
  blocks: allBlocks,
1403
- tokenData: data.tokens
1742
+ tokenData: data.tokens,
1743
+ componentIndex: componentIndex ?? void 0,
1744
+ blockIndex: blockIndex ?? void 0,
1745
+ tokenIndex: tokenIndex ?? void 0
1404
1746
  };
1405
- const searchResults = await hybridSearch(fullQuery, localData, 10, "component", config.apiKey);
1747
+ const searchResults = await hybridSearch(fullQuery, localData, limit, "component", config.apiKey);
1748
+ const blockMatches = keywordScoreBlocks(fullQuery, allBlocks, blockIndex ?? void 0).slice(0, 5);
1749
+ if (blockMatches.length > 0) {
1750
+ const matchedBlocks = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean);
1751
+ const blockFreq = buildBlockComponentFrequency(matchedBlocks);
1752
+ boostByBlockFrequency(searchResults, blockFreq);
1753
+ }
1754
+ const maxScore = searchResults.length > 0 ? searchResults[0].score : 0;
1406
1755
  const scored = searchResults.map((result) => {
1407
1756
  const fragment = allFragments.find(
1408
1757
  (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
@@ -1410,15 +1759,11 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1410
1759
  if (!fragment) return null;
1411
1760
  const filteredWhen = filterPlaceholders(fragment.usage?.when).slice(0, 3);
1412
1761
  const filteredWhenNot = filterPlaceholders(fragment.usage?.whenNot).slice(0, 2);
1413
- let confidence;
1414
- if (result.score >= 0.025) confidence = "high";
1415
- else if (result.score >= 0.015) confidence = "medium";
1416
- else confidence = "low";
1417
1762
  return {
1418
1763
  component: fragment.meta.name,
1419
1764
  category: fragment.meta.category,
1420
1765
  description: fragment.meta.description,
1421
- confidence,
1766
+ confidence: assignConfidence(result.score, maxScore),
1422
1767
  reasons: [`Matched via hybrid search (score: ${result.score.toFixed(4)})`],
1423
1768
  usage: { when: filteredWhen, whenNot: filteredWhenNot },
1424
1769
  variantCount: fragment.variants.length,
@@ -1434,7 +1779,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1434
1779
  if (count < 2 || suggestions.length < 3) {
1435
1780
  suggestions.push(item);
1436
1781
  categoryCount[cat] = count + 1;
1437
- if (suggestions.length >= 5) break;
1782
+ if (suggestions.length >= limit) break;
1438
1783
  }
1439
1784
  }
1440
1785
  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;
@@ -1442,7 +1787,8 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1442
1787
  const STYLE_KEYWORDS = ["color", "spacing", "padding", "margin", "font", "border", "radius", "shadow", "variable", "token", "css", "theme", "dark mode", "background", "hover"];
1443
1788
  const isStyleQuery = STYLE_KEYWORDS.some((kw) => useCaseLower.includes(kw));
1444
1789
  const noMatch = suggestions.length === 0;
1445
- const weakMatch = !noMatch && suggestions.every((s) => s.confidence === "low");
1790
+ const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
1791
+ const weakMatch = !noMatch && (belowThreshold || suggestions.every((s) => s.confidence === "low"));
1446
1792
  let recommendation;
1447
1793
  let nextStep;
1448
1794
  if (noMatch) {
@@ -1455,19 +1801,33 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1455
1801
  recommendation = `Best match: ${suggestions[0].component} (${suggestions[0].confidence} confidence) - ${suggestions[0].description}`;
1456
1802
  nextStep = `Use ${TOOL_NAMES.inspect}("${suggestions[0].component}") for full details.`;
1457
1803
  }
1804
+ const tokenHint = isStyleQuery && !noMatch ? `Your query includes styling terms. For CSS custom properties, also try ${TOOL_NAMES.tokens}(search: "${useCaseLower.split(/\s+/)[0]}").` : void 0;
1805
+ const blockNames = blockMatches.map((bm) => allBlocks.find((b) => b.name.toLowerCase() === bm.name.toLowerCase())).filter(Boolean).slice(0, 3).map((b) => b.name);
1806
+ const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${TOOL_NAMES.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
1807
+ const suggestResponse = verbosity === "compact" ? {
1808
+ useCase,
1809
+ suggestions: suggestions.map((s) => ({
1810
+ component: s.component,
1811
+ description: s.description,
1812
+ confidence: s.confidence
1813
+ })),
1814
+ recommendation
1815
+ } : {
1816
+ useCase,
1817
+ context: context || void 0,
1818
+ suggestions,
1819
+ noMatch,
1820
+ weakMatch,
1821
+ recommendation,
1822
+ compositionHint,
1823
+ ...tokenHint && { tokenHint },
1824
+ ...blockHint && { blockHint },
1825
+ nextStep
1826
+ };
1458
1827
  return {
1459
1828
  content: [{
1460
1829
  type: "text",
1461
- text: JSON.stringify({
1462
- useCase,
1463
- context: context || void 0,
1464
- suggestions,
1465
- noMatch,
1466
- weakMatch,
1467
- recommendation,
1468
- compositionHint,
1469
- nextStep
1470
- }, null, 2)
1830
+ text: JSON.stringify(suggestResponse)
1471
1831
  }]
1472
1832
  };
1473
1833
  }
@@ -1476,7 +1836,10 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1476
1836
  (s) => s.meta.name.toLowerCase() === componentForAlts.toLowerCase()
1477
1837
  );
1478
1838
  if (!fragment) {
1479
- throw new Error(`Component "${componentForAlts}" not found. Use fragments_discover to see available components.`);
1839
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
1840
+ const closest = findClosestMatch(componentForAlts, allNames);
1841
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
1842
+ throw new Error(`Component "${componentForAlts}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1480
1843
  }
1481
1844
  const relations = fragment.relations ?? [];
1482
1845
  const referencedBy = Object.values(data.fragments).filter(
@@ -1506,31 +1869,36 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1506
1869
  referencedBy,
1507
1870
  sameCategory,
1508
1871
  suggestion: relations.find((r) => r.relationship === "alternative") ? `Consider ${relations.find((r) => r.relationship === "alternative")?.component}: ${relations.find((r) => r.relationship === "alternative")?.note}` : void 0
1509
- }, null, 2)
1872
+ })
1510
1873
  }]
1511
1874
  };
1512
1875
  }
1513
1876
  const fragments = Object.values(data.fragments).filter((s) => {
1514
1877
  if (category && s.meta.category !== category) return false;
1515
1878
  if (status && (s.meta.status ?? "stable") !== status) return false;
1516
- if (search) {
1517
- const nameMatch = s.meta.name.toLowerCase().includes(search);
1518
- const descMatch = s.meta.description?.toLowerCase().includes(search);
1519
- const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search));
1879
+ if (search2) {
1880
+ const nameMatch = s.meta.name.toLowerCase().includes(search2);
1881
+ const descMatch = s.meta.description?.toLowerCase().includes(search2);
1882
+ const tagMatch = s.meta.tags?.some((t) => t.toLowerCase().includes(search2));
1520
1883
  if (!nameMatch && !descMatch && !tagMatch) return false;
1521
1884
  }
1522
1885
  return true;
1523
- }).map((s) => ({
1524
- name: s.meta.name,
1525
- category: s.meta.category,
1526
- description: s.meta.description,
1527
- status: s.meta.status ?? "stable",
1528
- variantCount: s.variants.length,
1529
- tags: s.meta.tags ?? [],
1530
- ...includeCode && s.variants[0]?.code && {
1531
- example: s.variants[0].code
1886
+ }).map((s) => {
1887
+ if (verbosity === "compact") {
1888
+ return { name: s.meta.name, category: s.meta.category };
1532
1889
  }
1533
- }));
1890
+ return {
1891
+ name: s.meta.name,
1892
+ category: s.meta.category,
1893
+ description: s.meta.description,
1894
+ status: s.meta.status ?? "stable",
1895
+ variantCount: s.variants.length,
1896
+ tags: s.meta.tags ?? [],
1897
+ ...(includeCode || verbosity === "full") && s.variants[0]?.code && {
1898
+ example: s.variants[0].code
1899
+ }
1900
+ };
1901
+ });
1534
1902
  return {
1535
1903
  content: [{
1536
1904
  type: "text",
@@ -1539,7 +1907,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1539
1907
  fragments,
1540
1908
  categories: [...new Set(fragments.map((s) => s.category))],
1541
1909
  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
1542
- }, null, 2)
1910
+ })
1543
1911
  }]
1544
1912
  };
1545
1913
  }
@@ -1553,6 +1921,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1553
1921
  const variantName = args?.variant ?? void 0;
1554
1922
  const maxExamples = args?.maxExamples;
1555
1923
  const maxLines = args?.maxLines;
1924
+ const verbosity = args?.verbosity ?? "standard";
1556
1925
  if (!componentName) {
1557
1926
  throw new Error("component is required");
1558
1927
  }
@@ -1560,7 +1929,10 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1560
1929
  (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1561
1930
  );
1562
1931
  if (!fragment) {
1563
- throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
1932
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
1933
+ const closest = findClosestMatch(componentName, allNames);
1934
+ const suggestion = closest ? ` Did you mean "${closest}"? Use ${TOOL_NAMES.inspect}("${closest}") to inspect it.` : "";
1935
+ throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1564
1936
  }
1565
1937
  const pkgName = await getPackageName(fragment.meta.name);
1566
1938
  let variants = fragment.variants;
@@ -1640,9 +2012,24 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1640
2012
  propsReference
1641
2013
  }
1642
2014
  };
1643
- const result = fields && fields.length > 0 ? projectFields(fullResult, fields) : fullResult;
2015
+ const aliasMap = { "usage": "guidelines" };
2016
+ const resolvedFields = fields?.map((f) => {
2017
+ const parts = f.split(".");
2018
+ if (aliasMap[parts[0]]) parts[0] = aliasMap[parts[0]];
2019
+ return parts.join(".");
2020
+ });
2021
+ let result;
2022
+ if (verbosity === "compact" && !resolvedFields?.length) {
2023
+ result = {
2024
+ meta: fullResult.meta,
2025
+ propNames: Object.keys(fragment.props ?? {}),
2026
+ variantNames: fragment.variants.map((v) => v.name)
2027
+ };
2028
+ } else {
2029
+ result = resolvedFields && resolvedFields.length > 0 ? projectFields(fullResult, resolvedFields) : fullResult;
2030
+ }
1644
2031
  return {
1645
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
2032
+ content: [{ type: "text", text: JSON.stringify(result) }]
1646
2033
  };
1647
2034
  }
1648
2035
  // ================================================================
@@ -1651,9 +2038,10 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1651
2038
  case TOOL_NAMES.blocks: {
1652
2039
  const data = await loadFragments();
1653
2040
  const blockName = args?.name;
1654
- const search = args?.search?.toLowerCase() ?? void 0;
2041
+ const search2 = args?.search?.toLowerCase() ?? void 0;
1655
2042
  const component = args?.component?.toLowerCase() ?? void 0;
1656
2043
  const category = args?.category?.toLowerCase() ?? void 0;
2044
+ const blocksLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 50) : search2 ? 10 : void 0;
1657
2045
  const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1658
2046
  if (allBlocks.length === 0) {
1659
2047
  return {
@@ -1663,7 +2051,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1663
2051
  total: 0,
1664
2052
  blocks: [],
1665
2053
  hint: `No composition blocks found. Blocks are reusable patterns showing how components wire together (e.g., "Login Form", "Settings Page"). Create .block.ts files and run \`${BRAND.cliCommand} build\`.`
1666
- }, null, 2)
2054
+ })
1667
2055
  }]
1668
2056
  };
1669
2057
  }
@@ -1672,18 +2060,35 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1672
2060
  filtered = filtered.filter(
1673
2061
  (b) => b.name.toLowerCase() === blockName.toLowerCase()
1674
2062
  );
2063
+ if (filtered.length === 0) {
2064
+ const allBlockNames = allBlocks.map((b) => b.name);
2065
+ const closest = findClosestMatch(blockName, allBlockNames);
2066
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2067
+ throw new Error(`Block "${blockName}" not found.${suggestion} Use ${TOOL_NAMES.blocks} to see available blocks.`);
2068
+ }
1675
2069
  }
1676
- if (search) {
1677
- filtered = filtered.filter((b) => {
1678
- const haystack = [
1679
- b.name,
1680
- b.description,
1681
- ...b.tags ?? [],
1682
- ...b.components,
1683
- b.category
1684
- ].join(" ").toLowerCase();
1685
- return haystack.includes(search);
1686
- });
2070
+ if (search2) {
2071
+ if (blockIndex) {
2072
+ const ranked = searchBlocks(search2, blockIndex, 50);
2073
+ const rankedNames = new Set(ranked.map((r) => r.name.toLowerCase()));
2074
+ filtered = filtered.filter((b) => rankedNames.has(b.name.toLowerCase()));
2075
+ filtered.sort((a, b) => {
2076
+ const aIdx = ranked.findIndex((r) => r.name.toLowerCase() === a.name.toLowerCase());
2077
+ const bIdx = ranked.findIndex((r) => r.name.toLowerCase() === b.name.toLowerCase());
2078
+ return (aIdx === -1 ? Infinity : aIdx) - (bIdx === -1 ? Infinity : bIdx);
2079
+ });
2080
+ } else {
2081
+ filtered = filtered.filter((b) => {
2082
+ const haystack = [
2083
+ b.name,
2084
+ b.description,
2085
+ ...b.tags ?? [],
2086
+ ...b.components,
2087
+ b.category
2088
+ ].join(" ").toLowerCase();
2089
+ return haystack.includes(search2);
2090
+ });
2091
+ }
1687
2092
  }
1688
2093
  if (component) {
1689
2094
  filtered = filtered.filter(
@@ -1695,16 +2100,41 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1695
2100
  (b) => b.category.toLowerCase() === category
1696
2101
  );
1697
2102
  }
2103
+ const blocksUseIcons = filtered.some(
2104
+ (b) => b.components.some((c) => c === "Icon") || b.code && /\bIcon\b/.test(b.code)
2105
+ );
2106
+ const iconHint = blocksUseIcons ? "Icon components in block code are from @phosphor-icons/react. Import them as: import { IconName } from '@phosphor-icons/react';" : void 0;
2107
+ if (blocksLimit !== void 0) {
2108
+ filtered = filtered.slice(0, blocksLimit);
2109
+ }
2110
+ const pkgName = fragmentPackageMap.values().next().value ?? "your-component-library";
2111
+ const verbosity = args?.verbosity ?? "standard";
2112
+ const blocksWithImports = filtered.map((b) => {
2113
+ const base = {
2114
+ name: b.name,
2115
+ description: b.description,
2116
+ category: b.category,
2117
+ components: b.components,
2118
+ tags: b.tags,
2119
+ import: `import { ${b.components.join(", ")} } from '${pkgName}';`
2120
+ };
2121
+ if (verbosity === "compact") return base;
2122
+ if (verbosity === "full") return { ...base, code: b.code };
2123
+ const codeLines = b.code.split("\n");
2124
+ const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : b.code;
2125
+ return { ...base, code };
2126
+ });
1698
2127
  return {
1699
2128
  content: [{
1700
2129
  type: "text",
1701
2130
  text: JSON.stringify({
1702
- total: filtered.length,
1703
- blocks: filtered,
1704
- ...filtered.length === 0 && allBlocks.length > 0 && {
2131
+ total: blocksWithImports.length,
2132
+ blocks: blocksWithImports,
2133
+ ...iconHint && { iconHint },
2134
+ ...blocksWithImports.length === 0 && allBlocks.length > 0 && {
1705
2135
  hint: "No blocks matching your query. Try broader search terms."
1706
2136
  }
1707
- }, null, 2)
2137
+ })
1708
2138
  }]
1709
2139
  };
1710
2140
  }
@@ -1714,7 +2144,8 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1714
2144
  case TOOL_NAMES.tokens: {
1715
2145
  const data = await loadFragments();
1716
2146
  const category = args?.category?.toLowerCase() ?? void 0;
1717
- const search = args?.search?.toLowerCase() ?? void 0;
2147
+ const search2 = args?.search?.toLowerCase() ?? void 0;
2148
+ const tokensLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 100) : search2 ? 25 : void 0;
1718
2149
  const tokenData = data.tokens;
1719
2150
  if (!tokenData || tokenData.total === 0) {
1720
2151
  return {
@@ -1724,30 +2155,53 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1724
2155
  total: 0,
1725
2156
  categories: {},
1726
2157
  hint: `No design tokens found. Add a tokens.include pattern to your ${BRAND.configFile} and run \`${BRAND.cliCommand} build\`.`
1727
- }, null, 2)
2158
+ })
1728
2159
  }]
1729
2160
  };
1730
2161
  }
1731
2162
  let filteredCategories = {};
1732
2163
  let filteredTotal = 0;
2164
+ const searchMatchesCategory = search2 ? Object.keys(tokenData.categories).find((cat) => cat.toLowerCase() === search2) : void 0;
1733
2165
  for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1734
2166
  if (category && cat !== category) continue;
1735
2167
  let filtered = tokens;
1736
- if (search) {
1737
- filtered = tokens.filter(
1738
- (t) => t.name.toLowerCase().includes(search) || t.description && t.description.toLowerCase().includes(search)
1739
- );
2168
+ if (search2) {
2169
+ if (searchMatchesCategory && cat.toLowerCase() === search2) {
2170
+ filtered = tokens;
2171
+ } else {
2172
+ filtered = tokens.filter(
2173
+ (t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || cat.toLowerCase().includes(search2)
2174
+ );
2175
+ }
1740
2176
  }
1741
2177
  if (filtered.length > 0) {
1742
2178
  filteredCategories[cat] = filtered;
1743
2179
  filteredTotal += filtered.length;
1744
2180
  }
1745
2181
  }
2182
+ if (tokensLimit !== void 0) {
2183
+ let remaining = tokensLimit;
2184
+ const limited = {};
2185
+ for (const [cat, tokens] of Object.entries(filteredCategories)) {
2186
+ if (remaining <= 0) break;
2187
+ limited[cat] = tokens.slice(0, remaining);
2188
+ remaining -= limited[cat].length;
2189
+ }
2190
+ filteredCategories = limited;
2191
+ filteredTotal = Math.min(filteredTotal, tokensLimit);
2192
+ }
1746
2193
  let hint;
1747
2194
  if (filteredTotal === 0) {
1748
2195
  const availableCategories = Object.keys(tokenData.categories);
1749
- hint = search ? `No tokens matching "${search}". Try: ${availableCategories.join(", ")}` : category ? `Category "${category}" not found. Available: ${availableCategories.join(", ")}` : void 0;
1750
- } else if (!category && !search) {
2196
+ if (category && search2) {
2197
+ const categoryTotal = tokenData.categories[category]?.length ?? 0;
2198
+ hint = categoryTotal > 0 ? `No tokens matching "${search2}" in category "${category}" (${categoryTotal} tokens in this category). Try a broader search or remove the search term.` : `Category "${category}" not found. Available: ${availableCategories.join(", ")}`;
2199
+ } else if (search2) {
2200
+ hint = `No tokens matching "${search2}". Available categories: ${availableCategories.join(", ")}`;
2201
+ } else if (category) {
2202
+ hint = `Category "${category}" not found. Available: ${availableCategories.join(", ")}`;
2203
+ }
2204
+ } else if (!category && !search2) {
1751
2205
  hint = `Use var(--token-name) in your CSS/styles. Filter by category or search to narrow results.`;
1752
2206
  }
1753
2207
  return {
@@ -1759,12 +2213,12 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1759
2213
  totalAvailable: tokenData.total,
1760
2214
  categories: filteredCategories,
1761
2215
  ...hint && { hint },
1762
- ...!category && !search && {
2216
+ ...!category && !search2 && {
1763
2217
  availableCategories: Object.entries(tokenData.categories).map(
1764
2218
  ([cat, tokens]) => ({ category: cat, count: tokens.length })
1765
2219
  )
1766
2220
  }
1767
- }, null, 2)
2221
+ })
1768
2222
  }]
1769
2223
  };
1770
2224
  }
@@ -1777,19 +2231,34 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1777
2231
  if (!useCase) {
1778
2232
  throw new Error("useCase is required");
1779
2233
  }
2234
+ const verbosity = args?.verbosity ?? "standard";
1780
2235
  const allFragments = Object.values(data.fragments);
1781
2236
  const allBlocks = Object.values(data.blocks ?? data.recipes ?? {});
1782
2237
  const tokenData = data.tokens;
2238
+ const implLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 15) : 5;
1783
2239
  const localData = {
1784
2240
  fragments: allFragments,
1785
2241
  blocks: allBlocks,
1786
- tokenData
2242
+ tokenData,
2243
+ componentIndex: componentIndex ?? void 0,
2244
+ blockIndex: blockIndex ?? void 0,
2245
+ tokenIndex: tokenIndex ?? void 0
1787
2246
  };
1788
- const searchResults = await hybridSearch(useCase, localData, 15, void 0, config.apiKey);
1789
- const componentResults = searchResults.filter((r) => r.kind === "component").slice(0, 3);
1790
- const blockResults = searchResults.filter((r) => r.kind === "block").slice(0, 2);
1791
- const tokenResults = searchResults.filter((r) => r.kind === "token").slice(0, 5);
1792
- const topMatches = componentResults.map((result) => {
2247
+ const [componentResults, blockResults, tokenResults] = await Promise.all([
2248
+ hybridSearch(useCase, localData, implLimit * 3, "component", config.apiKey),
2249
+ hybridSearch(useCase, localData, implLimit, "block", config.apiKey),
2250
+ hybridSearch(useCase, localData, implLimit, "token", config.apiKey)
2251
+ ]);
2252
+ const topBlockScore = blockResults.length > 0 ? blockResults[0].score : 0;
2253
+ const filteredBlockResults = blockResults.filter((r) => r.score >= topBlockScore * 0.3);
2254
+ if (filteredBlockResults.length > 0) {
2255
+ const matchedBlocks = filteredBlockResults.map((r) => allBlocks.find((b) => b.name.toLowerCase() === r.name.toLowerCase())).filter(Boolean);
2256
+ const blockFreq = buildBlockComponentFrequency(matchedBlocks);
2257
+ boostByBlockFrequency(componentResults, blockFreq);
2258
+ }
2259
+ const topComponentResults = componentResults.slice(0, implLimit);
2260
+ const maxCompScore = topComponentResults.length > 0 ? topComponentResults[0].score : 0;
2261
+ const topMatches = topComponentResults.map((result) => {
1793
2262
  const fragment = allFragments.find(
1794
2263
  (s) => s.meta.name.toLowerCase() === result.name.toLowerCase()
1795
2264
  );
@@ -1798,18 +2267,28 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1798
2267
  const components = await Promise.all(
1799
2268
  topMatches.map(async ({ fragment: s, score }) => {
1800
2269
  const pkgName = await getPackageName(s.meta.name);
1801
- const examples = s.variants.slice(0, 2).map((v) => ({
2270
+ if (verbosity === "compact") {
2271
+ return {
2272
+ name: s.meta.name,
2273
+ description: s.meta.description,
2274
+ confidence: assignConfidence(score, maxCompScore),
2275
+ import: `import { ${s.meta.name} } from '${pkgName}';`
2276
+ };
2277
+ }
2278
+ const exampleLimit = verbosity === "full" ? s.variants.length : 2;
2279
+ const propsLimit = verbosity === "full" ? Object.keys(s.props ?? {}).length : 5;
2280
+ const examples = s.variants.slice(0, exampleLimit).map((v) => ({
1802
2281
  variant: v.name,
1803
2282
  code: v.code ?? `<${s.meta.name} />`
1804
2283
  }));
1805
- const propsSummary = Object.entries(s.props ?? {}).slice(0, 10).map(
2284
+ const propsSummary = Object.entries(s.props ?? {}).slice(0, propsLimit).map(
1806
2285
  ([pName, p]) => `${pName}${p.required ? " (required)" : ""}: ${p.type}${p.values ? ` = ${p.values.join("|")}` : ""}`
1807
2286
  );
1808
2287
  return {
1809
2288
  name: s.meta.name,
1810
2289
  category: s.meta.category,
1811
2290
  description: s.meta.description,
1812
- confidence: score >= 0.025 ? "high" : score >= 0.015 ? "medium" : "low",
2291
+ confidence: assignConfidence(score, maxCompScore),
1813
2292
  import: `import { ${s.meta.name} } from '${pkgName}';`,
1814
2293
  props: propsSummary,
1815
2294
  examples,
@@ -1818,11 +2297,21 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1818
2297
  };
1819
2298
  })
1820
2299
  );
1821
- const matchingBlocks = blockResults.map((result) => {
2300
+ const matchingBlocks = filteredBlockResults.slice(0, 5).map((result) => {
1822
2301
  const block = allBlocks.find(
1823
2302
  (b) => b.name.toLowerCase() === result.name.toLowerCase()
1824
2303
  );
1825
- return block ? { name: block.name, description: block.description, components: block.components, code: block.code } : null;
2304
+ if (!block) return null;
2305
+ const pkgName = fragmentPackageMap.values().next().value ?? "your-component-library";
2306
+ const codeLines = block.code.split("\n");
2307
+ const code = codeLines.length > 30 ? codeLines.slice(0, 20).join("\n") + "\n// ... truncated (" + codeLines.length + " lines total)" : block.code;
2308
+ return {
2309
+ name: block.name,
2310
+ description: block.description,
2311
+ components: block.components,
2312
+ code,
2313
+ import: `import { ${block.components.join(", ")} } from '${pkgName}';`
2314
+ };
1826
2315
  }).filter(Boolean);
1827
2316
  let relevantTokens;
1828
2317
  if (tokenResults.length > 0 && tokenData) {
@@ -1838,17 +2327,28 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1838
2327
  }
1839
2328
  if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
1840
2329
  }
2330
+ if (!relevantTokens && tokenData) {
2331
+ const categories = extractTokenCategories(useCase);
2332
+ relevantTokens = {};
2333
+ for (const cat of categories) {
2334
+ const tokens = tokenData.categories[cat];
2335
+ if (tokens && tokens.length > 0) {
2336
+ relevantTokens[cat] = tokens.slice(0, 5).map((t) => t.name);
2337
+ }
2338
+ }
2339
+ if (Object.keys(relevantTokens).length === 0) relevantTokens = void 0;
2340
+ }
1841
2341
  return {
1842
2342
  content: [{
1843
2343
  type: "text",
1844
2344
  text: JSON.stringify({
1845
2345
  useCase,
1846
2346
  components,
1847
- blocks: matchingBlocks.length > 0 ? matchingBlocks : void 0,
1848
- tokens: relevantTokens,
2347
+ blocks: verbosity !== "compact" && matchingBlocks.length > 0 ? matchingBlocks : void 0,
2348
+ tokens: verbosity !== "compact" ? relevantTokens : void 0,
1849
2349
  noMatch: components.length === 0,
1850
2350
  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` : ""}.`
1851
- }, null, 2)
2351
+ })
1852
2352
  }]
1853
2353
  };
1854
2354
  }
@@ -1868,6 +2368,18 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
1868
2368
  isError: true
1869
2369
  };
1870
2370
  }
2371
+ {
2372
+ const data = await loadFragments();
2373
+ const fragment = Object.values(data.fragments).find(
2374
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
2375
+ );
2376
+ if (!fragment) {
2377
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2378
+ const closest = findClosestMatch(componentName, allNames);
2379
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2380
+ throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
2381
+ }
2382
+ }
1871
2383
  const viewerUrl = config.viewerUrl;
1872
2384
  if (!viewerUrl) {
1873
2385
  return {
@@ -1919,7 +2431,7 @@ Suggestion: ${result.suggestion}` : ""}`
1919
2431
  threshold: result.threshold,
1920
2432
  figmaUrl: result.figmaUrl,
1921
2433
  changedRegions: result.changedRegions
1922
- }, null, 2)
2434
+ })
1923
2435
  });
1924
2436
  return { content };
1925
2437
  } catch (error) {
@@ -1936,6 +2448,7 @@ Suggestion: ${result.suggestion}` : ""}`
1936
2448
  const result = await renderComponent(viewerUrl, {
1937
2449
  component: componentName,
1938
2450
  props,
2451
+ variant: variantName,
1939
2452
  viewport: viewport ?? { width: 800, height: 600 }
1940
2453
  });
1941
2454
  if (result.error) {
@@ -1953,7 +2466,7 @@ Suggestion: ${result.suggestion}` : ""}`
1953
2466
  },
1954
2467
  {
1955
2468
  type: "text",
1956
- text: `Successfully rendered ${componentName} with props: ${JSON.stringify(props)}`
2469
+ text: `Successfully rendered ${componentName}${variantName ? ` (variant: "${variantName}")` : ""}${Object.keys(props).length > 0 ? ` with props: ${JSON.stringify(props)}` : ""}`
1957
2470
  }
1958
2471
  ]
1959
2472
  };
@@ -1982,7 +2495,10 @@ Suggestion: ${result.suggestion}` : ""}`
1982
2495
  (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
1983
2496
  );
1984
2497
  if (!fragment) {
1985
- throw new Error(`Component "${componentName}" not found. Use fragments_discover to see available components.`);
2498
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2499
+ const closest = findClosestMatch(componentName, allNames);
2500
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2501
+ throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
1986
2502
  }
1987
2503
  const viewerUrl = config.viewerUrl;
1988
2504
  if (!viewerUrl) {
@@ -2020,7 +2536,7 @@ Suggestion: ${result.suggestion}` : ""}`
2020
2536
  summary: result.summary,
2021
2537
  patchCount: result.patches.length,
2022
2538
  nextStep: result.patches.length > 0 ? "Apply patches using your editor or `patch` command, then run fragments_render to confirm fixes." : void 0
2023
- }, null, 2)
2539
+ })
2024
2540
  }]
2025
2541
  };
2026
2542
  } catch (error) {
@@ -2044,6 +2560,18 @@ Suggestion: ${result.suggestion}` : ""}`
2044
2560
  if (!componentName) {
2045
2561
  throw new Error("component is required");
2046
2562
  }
2563
+ {
2564
+ const data = await loadFragments();
2565
+ const fragment = Object.values(data.fragments).find(
2566
+ (s) => s.meta.name.toLowerCase() === componentName.toLowerCase()
2567
+ );
2568
+ if (!fragment) {
2569
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2570
+ const closest = findClosestMatch(componentName, allNames);
2571
+ const suggestion = closest ? ` Did you mean "${closest}"?` : "";
2572
+ throw new Error(`Component "${componentName}" not found.${suggestion} Use ${TOOL_NAMES.discover} to see available components.`);
2573
+ }
2574
+ }
2047
2575
  const viewerUrl = config.viewerUrl;
2048
2576
  if (!viewerUrl) {
2049
2577
  return {
@@ -2070,6 +2598,14 @@ Suggestion: ${result.suggestion}` : ""}`
2070
2598
  isError: true
2071
2599
  };
2072
2600
  }
2601
+ let nextStep;
2602
+ if (result.emptyAudit) {
2603
+ nextStep = `No testable elements found for ${variantName ? `variant "${variantName}"` : componentName}. The variant may not exist or renders no accessible content. Check available variants with ${TOOL_NAMES.inspect}("${componentName}").`;
2604
+ } else if (result.passed) {
2605
+ nextStep = 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.';
2606
+ } else {
2607
+ nextStep = `Fix the violations above, then re-run ${TOOL_NAMES.a11y} to verify. Use ${TOOL_NAMES.fix} for automated fixes.`;
2608
+ }
2073
2609
  return {
2074
2610
  content: [{
2075
2611
  type: "text",
@@ -2077,13 +2613,14 @@ Suggestion: ${result.suggestion}` : ""}`
2077
2613
  component: componentName,
2078
2614
  variant: variantName ?? "all",
2079
2615
  standard,
2080
- score: result.score,
2081
- aaCompliance: `${result.aaPercent}%`,
2082
- aaaCompliance: `${result.aaaPercent}%`,
2616
+ totalViolations: result.results.reduce((sum, r) => sum + r.summary.total, 0),
2617
+ variantsPassingAA: `${result.aaPercent}%`,
2618
+ variantsPassingAAA: `${result.aaaPercent}%`,
2083
2619
  passed: result.passed,
2620
+ ...result.emptyAudit && { emptyAudit: true },
2084
2621
  results: result.results,
2085
- nextStep: result.passed ? 'All accessibility checks passed. Consider running with standard: "AAA" for enhanced compliance.' : `Fix the violations above, then re-run ${TOOL_NAMES.a11y} to verify. Use ${TOOL_NAMES.fix} for automated fixes.`
2086
- }, null, 2)
2622
+ nextStep
2623
+ })
2087
2624
  }]
2088
2625
  };
2089
2626
  } catch (error) {
@@ -2108,10 +2645,12 @@ Suggestion: ${result.suggestion}` : ""}`
2108
2645
  edgeTypes: args?.edgeTypes,
2109
2646
  maxDepth: args?.maxDepth
2110
2647
  };
2648
+ const allNames = Object.values(data.fragments).map((s) => s.meta.name);
2111
2649
  const result = handleGraphTool(
2112
2650
  graphArgs,
2113
2651
  data.graph,
2114
- data.blocks ?? data.recipes
2652
+ data.blocks ?? data.recipes,
2653
+ allNames
2115
2654
  );
2116
2655
  if (result.isError) {
2117
2656
  return {
@@ -2150,4 +2689,4 @@ export {
2150
2689
  createMcpServer,
2151
2690
  startMcpServer
2152
2691
  };
2153
- //# sourceMappingURL=chunk-WEHZRM4L.js.map
2692
+ //# sourceMappingURL=chunk-FEUUOMWK.js.map