@fragments-sdk/mcp 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,12 +4,13 @@ import {
4
4
  } from "./chunk-4SVS3AA3.js";
5
5
  import {
6
6
  componentNames,
7
+ findComponent,
7
8
  findComponentByName,
8
9
  getGuidanceWhen,
9
10
  getGuidanceWhenNot,
10
11
  listBlocks,
11
12
  listComponents
12
- } from "./chunk-3IHXJEMM.js";
13
+ } from "./chunk-YSRGQDEB.js";
13
14
 
14
15
  // src/server.ts
15
16
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
@@ -19,7 +20,7 @@ import {
19
20
  ListToolsRequestSchema
20
21
  } from "@modelcontextprotocol/sdk/types.js";
21
22
  import { existsSync as existsSync9 } from "fs";
22
- import { readFileSync as readFileSync7 } from "fs";
23
+ import { readFileSync as readFileSync6 } from "fs";
23
24
  import { join as join8 } from "path";
24
25
  import { fileURLToPath } from "url";
25
26
 
@@ -653,17 +654,20 @@ function buildLocalSearchData(data, indexes) {
653
654
  }
654
655
  async function buildImportStatements(components, resolvePackageName) {
655
656
  const grouped = /* @__PURE__ */ new Map();
656
- for (const component of components) {
657
- if (!component) continue;
658
- const packageName = await resolvePackageName(component);
657
+ const uniqueComponents = [...new Set(components.filter(Boolean))];
658
+ const resolvedPackages = await Promise.all(
659
+ uniqueComponents.map(async (component) => ({
660
+ component,
661
+ packageName: await resolvePackageName(component)
662
+ }))
663
+ );
664
+ for (const { component, packageName } of resolvedPackages) {
659
665
  const existing = grouped.get(packageName);
660
666
  if (!existing) {
661
667
  grouped.set(packageName, [component]);
662
668
  continue;
663
669
  }
664
- if (!existing.includes(component)) {
665
- existing.push(component);
666
- }
670
+ existing.push(component);
667
671
  }
668
672
  return Array.from(grouped.entries()).map(
669
673
  ([packageName, componentNames2]) => `import { ${componentNames2.join(", ")} } from '${packageName}';`
@@ -687,6 +691,56 @@ function limitTokensPerCategory(categories, limit) {
687
691
  return { categories: limited, total };
688
692
  }
689
693
 
694
+ // src/search-helpers.ts
695
+ var STOP_WORDS = /* @__PURE__ */ new Set([
696
+ "a",
697
+ "an",
698
+ "and",
699
+ "bar",
700
+ "build",
701
+ "button",
702
+ "for",
703
+ "form",
704
+ "i",
705
+ "login",
706
+ "me",
707
+ "need",
708
+ "of",
709
+ "or",
710
+ "the",
711
+ "to",
712
+ "use",
713
+ "with"
714
+ ]);
715
+ function normalizeTerms(value) {
716
+ return value.toLowerCase().split(/[^a-z0-9]+/g).filter((term) => term.length > 1 && !STOP_WORDS.has(term));
717
+ }
718
+ function hasDirectQueryOverlap(query, component) {
719
+ const queryTerms = normalizeTerms(query);
720
+ if (queryTerms.length === 0) return true;
721
+ const haystack = new Set(
722
+ normalizeTerms(
723
+ [
724
+ component.name,
725
+ component.description,
726
+ component.category,
727
+ component.tags.join(" "),
728
+ getGuidanceWhen(component).join(" "),
729
+ getGuidanceWhenNot(component).join(" "),
730
+ component.propsSummary.join(" ")
731
+ ].join(" ")
732
+ )
733
+ );
734
+ return queryTerms.some((term) => haystack.has(term));
735
+ }
736
+ function getRankingBonus(component) {
737
+ let bonus = 0;
738
+ if (component.isCanonical) bonus += 30;
739
+ if (component.tier === "core") bonus += 20;
740
+ if (component.status === "stable") bonus += 5;
741
+ return bonus;
742
+ }
743
+
690
744
  // src/tools/discover.ts
691
745
  function renderContextMarkdown(args) {
692
746
  const lines = ["# Design System Context", ""];
@@ -769,11 +823,13 @@ var discoverHandler = async (args, ctx) => {
769
823
  const compact = args?.compact ?? false;
770
824
  const includeCode = args?.includeCode ?? false;
771
825
  const includeRelations = args?.includeRelations ?? false;
826
+ const depth = args?.depth ?? "quick";
772
827
  const suggestLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : 10;
773
828
  const listLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 25) : void 0;
774
829
  const verbosity = args?.verbosity ?? (compact ? "compact" : "standard");
775
- if (args?.format && !useCase && !componentForAlts) {
776
- let components2 = listComponents(data.snapshot);
830
+ const allSnapshotComponents = listComponents(data.snapshot);
831
+ if (!useCase && !componentForAlts && (args?.format || includeCode || includeRelations)) {
832
+ let components2 = allSnapshotComponents;
777
833
  const allBlocks = listBlocks(data.snapshot);
778
834
  if (category) {
779
835
  components2 = components2.filter(
@@ -781,12 +837,16 @@ var discoverHandler = async (args, ctx) => {
781
837
  );
782
838
  }
783
839
  if (search2) {
784
- components2 = components2.filter((component) => {
785
- const tagMatch = component.tags.some(
786
- (tag) => tag.toLowerCase().includes(search2)
787
- );
788
- return component.name.toLowerCase().includes(search2) || component.description.toLowerCase().includes(search2) || tagMatch;
789
- });
840
+ const scored = keywordScoreComponents(
841
+ search2,
842
+ components2,
843
+ ctx.indexes.componentIndex ?? void 0
844
+ );
845
+ const allowedNames = new Set(components2.map((component) => component.name));
846
+ const sortedNames = scored.filter((result) => allowedNames.has(result.name)).map((result) => result.name.toLowerCase());
847
+ components2 = components2.filter((component) => sortedNames.includes(component.name.toLowerCase())).sort(
848
+ (a, b) => sortedNames.indexOf(a.name.toLowerCase()) - sortedNames.indexOf(b.name.toLowerCase())
849
+ );
790
850
  }
791
851
  if (status) {
792
852
  components2 = components2.filter((component) => component.status === status);
@@ -838,6 +898,14 @@ var discoverHandler = async (args, ctx) => {
838
898
  "component",
839
899
  ctx.config.searchApiKey
840
900
  );
901
+ const filteredSearchResults = searchResults.filter((result) => {
902
+ const component = componentsByName.get(result.name.toLowerCase());
903
+ if (!component) return false;
904
+ if (category && !categoryMatches(component.category, category)) return false;
905
+ if (status && component.status !== status) return false;
906
+ result.score += getRankingBonus(component);
907
+ return true;
908
+ });
841
909
  const blockMatches = keywordScoreBlocks(
842
910
  fullQuery,
843
911
  allBlocks,
@@ -848,10 +916,10 @@ var discoverHandler = async (args, ctx) => {
848
916
  (match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
849
917
  ).filter(Boolean);
850
918
  const blockFreq = buildBlockComponentFrequency(matchedBlocks);
851
- boostByBlockFrequency(searchResults, blockFreq);
919
+ boostByBlockFrequency(filteredSearchResults, blockFreq);
852
920
  }
853
- const maxScore = searchResults.length > 0 ? searchResults[0].score : 0;
854
- const scored = searchResults.map((result) => {
921
+ const maxScore = filteredSearchResults.length > 0 ? filteredSearchResults[0].score : 0;
922
+ const scored = filteredSearchResults.map((result) => {
855
923
  const component = componentsByName.get(result.name.toLowerCase());
856
924
  if (!component) return null;
857
925
  return {
@@ -864,6 +932,11 @@ var discoverHandler = async (args, ctx) => {
864
932
  when: getGuidanceWhen(component).slice(0, 3),
865
933
  whenNot: getGuidanceWhenNot(component).slice(0, 2)
866
934
  },
935
+ publicRef: component.publicRef,
936
+ componentKey: component.id,
937
+ tier: component.tier,
938
+ isCanonical: component.isCanonical ?? false,
939
+ sourcePath: component.sourcePath,
867
940
  exampleCount: component.examples.length,
868
941
  status: component.status
869
942
  };
@@ -902,7 +975,10 @@ var discoverHandler = async (args, ctx) => {
902
975
  const isStyleQuery = styleKeywords.some(
903
976
  (keyword) => useCaseLower.includes(keyword)
904
977
  );
905
- const noMatch = suggestions.length === 0;
978
+ const noMatch = suggestions.length === 0 || !suggestions.some((item) => {
979
+ const component = componentsByName.get(item.component.toLowerCase());
980
+ return component ? hasDirectQueryOverlap(useCase, component) : false;
981
+ });
906
982
  const belowThreshold = !noMatch && maxScore > 1 && !meetsMinimumThreshold(maxScore);
907
983
  const weakMatch = !noMatch && (belowThreshold || suggestions.every((item) => item.confidence === "low"));
908
984
  let recommendation;
@@ -922,6 +998,80 @@ var discoverHandler = async (args, ctx) => {
922
998
  (match) => allBlocks.find((block) => block.name.toLowerCase() === match.name.toLowerCase())
923
999
  ).filter(Boolean).slice(0, 3).map((block) => block.name);
924
1000
  const blockHint = blockNames.length > 0 ? `Related blocks: ${blockNames.join(", ")}. Use ${ctx.toolNames.blocks}(search: "${useCase}") for ready-to-use patterns.` : void 0;
1001
+ let fullBlocks;
1002
+ let fullTokens;
1003
+ let fullImports;
1004
+ if (depth === "full" && !noMatch) {
1005
+ const tokenData = ctx.data.tokens;
1006
+ const [blockSearchResults, tokenSearchResults] = await Promise.all([
1007
+ hybridSearch(fullQuery, localData, 5, "block", ctx.config.searchApiKey),
1008
+ tokenData ? hybridSearch(fullQuery, localData, 10, "token", ctx.config.searchApiKey) : Promise.resolve([])
1009
+ ]);
1010
+ const topBlockScore = blockSearchResults.length > 0 ? blockSearchResults[0].score : 0;
1011
+ const relevantBlockResults = blockSearchResults.filter(
1012
+ (result) => result.score >= topBlockScore * 0.3
1013
+ );
1014
+ if (relevantBlockResults.length > 0) {
1015
+ fullBlocks = (await Promise.all(
1016
+ relevantBlockResults.slice(0, 5).map(async (result) => {
1017
+ const block = allBlocks.find(
1018
+ (entry) => entry.name.toLowerCase() === result.name.toLowerCase()
1019
+ );
1020
+ if (!block) return null;
1021
+ const imports = await buildImportStatements(
1022
+ block.components,
1023
+ async (componentName) => ctx.resolvePackageName(componentName)
1024
+ );
1025
+ const codeLines = block.code.split("\n");
1026
+ const code = codeLines.length > 30 ? `${codeLines.slice(0, 20).join("\n")}
1027
+ // ... truncated (${codeLines.length} lines total)` : block.code;
1028
+ return {
1029
+ name: block.name,
1030
+ description: block.description,
1031
+ components: block.components,
1032
+ code,
1033
+ imports
1034
+ };
1035
+ })
1036
+ )).filter(Boolean);
1037
+ }
1038
+ if (tokenSearchResults.length > 0 && tokenData) {
1039
+ fullTokens = {};
1040
+ const tokensByName = /* @__PURE__ */ new Map();
1041
+ for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1042
+ for (const token of tokens) {
1043
+ tokensByName.set(token.name, cat);
1044
+ }
1045
+ }
1046
+ for (const result of tokenSearchResults) {
1047
+ const cat = tokensByName.get(result.name);
1048
+ if (cat) {
1049
+ if (!fullTokens[cat]) fullTokens[cat] = [];
1050
+ fullTokens[cat].push(result.name);
1051
+ }
1052
+ }
1053
+ if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
1054
+ }
1055
+ if (!fullTokens && tokenData) {
1056
+ const categories = extractTokenCategories(fullQuery);
1057
+ fullTokens = {};
1058
+ for (const cat of categories) {
1059
+ const tokens = tokenData.categories[cat];
1060
+ if (tokens && tokens.length > 0) {
1061
+ fullTokens[cat] = tokens.slice(0, 5).map((token) => token.name);
1062
+ }
1063
+ }
1064
+ if (Object.keys(fullTokens).length === 0) fullTokens = void 0;
1065
+ }
1066
+ if (suggestions.length > 0) {
1067
+ fullImports = {};
1068
+ for (const item of suggestions) {
1069
+ if (!item) continue;
1070
+ const pkgName = ctx.resolvePackageName(item.component);
1071
+ fullImports[item.component] = `import { ${item.component} } from '${pkgName}';`;
1072
+ }
1073
+ }
1074
+ }
925
1075
  const suggestResponse = verbosity === "compact" ? {
926
1076
  useCase,
927
1077
  suggestions: suggestions.map((item) => ({
@@ -933,14 +1083,19 @@ var discoverHandler = async (args, ctx) => {
933
1083
  } : {
934
1084
  useCase,
935
1085
  context: context || void 0,
936
- suggestions,
1086
+ suggestions: depth === "full" ? suggestions.map((item) => ({
1087
+ ...item,
1088
+ import: fullImports?.[item.component]
1089
+ })) : suggestions,
937
1090
  noMatch,
938
1091
  weakMatch,
939
1092
  recommendation,
940
1093
  compositionHint,
941
1094
  ...tokenHint && { tokenHint },
942
1095
  ...blockHint && { blockHint },
943
- nextStep
1096
+ nextStep,
1097
+ ...fullBlocks && fullBlocks.length > 0 && { blocks: fullBlocks },
1098
+ ...fullTokens && { tokens: fullTokens }
944
1099
  };
945
1100
  return {
946
1101
  content: [
@@ -952,33 +1107,39 @@ var discoverHandler = async (args, ctx) => {
952
1107
  };
953
1108
  }
954
1109
  if (componentForAlts) {
955
- const component = findComponentByName(data.snapshot, componentForAlts);
1110
+ const component = findComponent(data.snapshot, componentForAlts);
956
1111
  if (!component) {
957
1112
  const closest = findClosestMatch(
958
1113
  componentForAlts,
959
1114
  componentNames(data.snapshot)
960
1115
  );
961
1116
  const suggestion = closest ? ` Did you mean "${closest}"?` : "";
962
- throw new Error(
963
- `Component "${componentForAlts}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
964
- );
1117
+ return {
1118
+ content: [
1119
+ {
1120
+ type: "text",
1121
+ text: JSON.stringify({
1122
+ error: `Component "${componentForAlts}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
1123
+ })
1124
+ }
1125
+ ],
1126
+ isError: true
1127
+ };
965
1128
  }
966
1129
  const relations = component.relations;
967
- const referencedBy = listComponents(data.snapshot).filter(
968
- (entry) => entry.relations.some(
969
- (relation) => relation.componentName.toLowerCase() === componentForAlts.toLowerCase()
970
- )
971
- ).map((entry) => ({
972
- component: entry.name,
973
- relationship: entry.relations.find(
974
- (relation) => relation.componentName.toLowerCase() === componentForAlts.toLowerCase()
975
- )?.relationship,
976
- note: entry.relations.find(
977
- (relation) => relation.componentName.toLowerCase() === componentForAlts.toLowerCase()
978
- )?.note
979
- }));
980
- const sameCategory = listComponents(data.snapshot).filter(
981
- (entry) => entry.category === component.category && entry.name.toLowerCase() !== componentForAlts.toLowerCase()
1130
+ const referencedBy = allSnapshotComponents.map((entry) => {
1131
+ const relation = entry.relations.find(
1132
+ (candidate) => candidate.componentName.toLowerCase() === component.name.toLowerCase()
1133
+ );
1134
+ if (!relation) return null;
1135
+ return {
1136
+ component: entry.name,
1137
+ relationship: relation.relationship,
1138
+ note: relation.note
1139
+ };
1140
+ }).filter(Boolean);
1141
+ const sameCategory = allSnapshotComponents.filter(
1142
+ (entry) => entry.category === component.category && entry.name.toLowerCase() !== component.name.toLowerCase()
982
1143
  ).map((entry) => ({
983
1144
  component: entry.name,
984
1145
  description: entry.description
@@ -1001,25 +1162,44 @@ var discoverHandler = async (args, ctx) => {
1001
1162
  ]
1002
1163
  };
1003
1164
  }
1004
- const filteredComponents = listComponents(data.snapshot).filter((component) => {
1165
+ let filteredComponents = allSnapshotComponents.filter((component) => {
1005
1166
  if (category && !categoryMatches(component.category, category)) return false;
1006
1167
  if (status && (component.status ?? "stable") !== status) return false;
1007
- if (search2) {
1008
- const nameMatch = component.name.toLowerCase().includes(search2);
1009
- const descMatch = component.description.toLowerCase().includes(search2);
1010
- const tagMatch = component.tags.some(
1011
- (tag) => tag.toLowerCase().includes(search2)
1012
- );
1013
- if (!nameMatch && !descMatch && !tagMatch) return false;
1014
- }
1015
1168
  return true;
1016
1169
  });
1170
+ if (search2) {
1171
+ const scored = keywordScoreComponents(
1172
+ search2,
1173
+ filteredComponents,
1174
+ ctx.indexes.componentIndex ?? void 0
1175
+ ).map((result) => {
1176
+ const component = filteredComponents.find(
1177
+ (entry) => entry.name.toLowerCase() === result.name.toLowerCase()
1178
+ );
1179
+ if (!component) return null;
1180
+ return {
1181
+ component,
1182
+ score: result.score + getRankingBonus(component)
1183
+ };
1184
+ }).filter(Boolean);
1185
+ filteredComponents = scored.sort((a, b) => b.score - a.score).map((entry) => entry.component);
1186
+ } else {
1187
+ filteredComponents = filteredComponents.sort((a, b) => {
1188
+ const bonusDiff = getRankingBonus(b) - getRankingBonus(a);
1189
+ if (bonusDiff !== 0) return bonusDiff;
1190
+ return a.name.localeCompare(b.name);
1191
+ });
1192
+ }
1017
1193
  const limitedComponents = listLimit === void 0 ? filteredComponents : filteredComponents.slice(0, listLimit);
1018
1194
  const components = limitedComponents.map((component) => {
1019
1195
  if (verbosity === "compact") {
1020
1196
  return {
1021
1197
  name: component.name,
1022
1198
  category: component.category,
1199
+ publicRef: component.publicRef,
1200
+ componentKey: component.id,
1201
+ tier: component.tier,
1202
+ isCanonical: component.isCanonical ?? false,
1023
1203
  ...component.propsSummary.length > 0 && {
1024
1204
  propsSummary: component.propsSummary
1025
1205
  }
@@ -1030,6 +1210,11 @@ var discoverHandler = async (args, ctx) => {
1030
1210
  category: component.category,
1031
1211
  description: component.description,
1032
1212
  status: component.status ?? "stable",
1213
+ publicRef: component.publicRef,
1214
+ componentKey: component.id,
1215
+ tier: component.tier,
1216
+ isCanonical: component.isCanonical ?? false,
1217
+ sourcePath: component.sourcePath,
1033
1218
  exampleCount: component.examples.length,
1034
1219
  tags: component.tags,
1035
1220
  ...(includeCode || verbosity === "full") && component.examples[0]?.code ? {
@@ -1054,7 +1239,8 @@ var discoverHandler = async (args, ctx) => {
1054
1239
  };
1055
1240
 
1056
1241
  // src/tools/inspect.ts
1057
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
1242
+ import { promises as fs } from "fs";
1243
+ import { existsSync as existsSync2 } from "fs";
1058
1244
  import { join as join2 } from "path";
1059
1245
 
1060
1246
  // src/utils.ts
@@ -1090,13 +1276,13 @@ function projectFields(obj, fields) {
1090
1276
  }
1091
1277
 
1092
1278
  // src/tools/inspect.ts
1093
- function getSourceCode(component, projectRoot) {
1279
+ async function getSourceCode(component, projectRoot) {
1094
1280
  const sourcePath = component.sourcePath;
1095
1281
  if (!sourcePath) return void 0;
1096
1282
  const fullPath = join2(projectRoot, sourcePath);
1097
1283
  if (!existsSync2(fullPath)) return { path: sourcePath, code: null };
1098
1284
  try {
1099
- const code = readFileSync3(fullPath, "utf-8");
1285
+ const code = await fs.readFile(fullPath, "utf-8");
1100
1286
  return { path: sourcePath, code };
1101
1287
  } catch {
1102
1288
  return { path: sourcePath, code: null };
@@ -1110,18 +1296,34 @@ var inspectHandler = async (args, ctx) => {
1110
1296
  const maxLines = args?.maxLines;
1111
1297
  const verbosity = args?.verbosity ?? "standard";
1112
1298
  if (!componentName) {
1113
- throw new Error("component is required");
1299
+ return {
1300
+ content: [
1301
+ {
1302
+ type: "text",
1303
+ text: JSON.stringify({ error: "component is required" })
1304
+ }
1305
+ ],
1306
+ isError: true
1307
+ };
1114
1308
  }
1115
- const component = findComponentByName(ctx.data.snapshot, componentName);
1309
+ const component = findComponent(ctx.data.snapshot, componentName);
1116
1310
  if (!component) {
1117
1311
  const closest = findClosestMatch(
1118
1312
  componentName,
1119
1313
  componentNames(ctx.data.snapshot)
1120
1314
  );
1121
1315
  const suggestion = closest ? ` Did you mean "${closest}"? Use ${ctx.toolNames.inspect}("${closest}") to inspect it.` : "";
1122
- throw new Error(
1123
- `Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
1124
- );
1316
+ return {
1317
+ content: [
1318
+ {
1319
+ type: "text",
1320
+ text: JSON.stringify({
1321
+ error: `Component "${componentName}" not found.${suggestion} Use ${ctx.toolNames.discover} to see available components.`
1322
+ })
1323
+ }
1324
+ ],
1325
+ isError: true
1326
+ };
1125
1327
  }
1126
1328
  const pkgName = ctx.resolvePackageName(component.name);
1127
1329
  let examples = component.examples;
@@ -1141,25 +1343,41 @@ var inspectHandler = async (args, ctx) => {
1141
1343
  if (filtered.length > 0) {
1142
1344
  examples = filtered;
1143
1345
  } else {
1144
- throw new Error(
1145
- `Example "${exampleName}" not found for ${componentName}. Available: ${component.examples.map((example) => example.name).join(", ")}`
1146
- );
1346
+ return {
1347
+ content: [
1348
+ {
1349
+ type: "text",
1350
+ text: JSON.stringify({
1351
+ error: `Example "${exampleName}" not found for ${componentName}. Available: ${component.examples.map((example) => example.name).join(", ")}`
1352
+ })
1353
+ }
1354
+ ],
1355
+ isError: true
1356
+ };
1147
1357
  }
1148
1358
  }
1149
1359
  if (maxExamples && maxExamples > 0) {
1150
1360
  examples = examples.slice(0, maxExamples);
1151
1361
  }
1152
1362
  const truncateCode = (code) => {
1153
- if (!maxLines || maxLines <= 0) return code;
1363
+ if (!maxLines || maxLines <= 0) {
1364
+ return { code, truncated: false, remainingLines: 0 };
1365
+ }
1154
1366
  const lines = code.split("\n");
1155
- if (lines.length <= maxLines) return code;
1156
- return `${lines.slice(0, maxLines).join("\n")}
1157
- // ... truncated`;
1367
+ if (lines.length <= maxLines) {
1368
+ return { code, truncated: false, remainingLines: 0 };
1369
+ }
1370
+ return {
1371
+ code: lines.slice(0, maxLines).join("\n"),
1372
+ truncated: true,
1373
+ remainingLines: lines.length - maxLines
1374
+ };
1158
1375
  };
1159
1376
  const renderedExamples = examples.map((example) => ({
1377
+ ...example.code ? truncateCode(example.code) : { truncated: false, remainingLines: 0 },
1160
1378
  variant: example.name,
1161
1379
  description: example.description,
1162
- code: example.code ? truncateCode(example.code) : `<${component.name} />`,
1380
+ code: example.code ? truncateCode(example.code).code : `<${component.name} />`,
1163
1381
  ...example.code ? {} : {
1164
1382
  note: "No code example provided. Refer to props for customization."
1165
1383
  }
@@ -1189,13 +1407,13 @@ var inspectHandler = async (args, ctx) => {
1189
1407
  status: component.status,
1190
1408
  publicRef: component.publicRef,
1191
1409
  publicSlug: component.publicSlug,
1410
+ isCanonical: component.isCanonical ?? false,
1192
1411
  tier: component.tier
1193
1412
  },
1194
- props: component.props,
1413
+ props: propsReference,
1195
1414
  examples: {
1196
1415
  import: `import { ${component.name} } from '${pkgName}';`,
1197
- code: renderedExamples,
1198
- propsReference
1416
+ code: renderedExamples
1199
1417
  },
1200
1418
  relations: component.relations,
1201
1419
  compoundChildren: component.compoundChildren,
@@ -1215,7 +1433,7 @@ var inspectHandler = async (args, ctx) => {
1215
1433
  })) ?? []
1216
1434
  },
1217
1435
  metadata: component.metadata,
1218
- source: getSourceCode(component, ctx.config.projectRoot)
1436
+ source: await getSourceCode(component, ctx.config.projectRoot)
1219
1437
  };
1220
1438
  const aliasMap = { usage: "guidance" };
1221
1439
  const resolvedFields = fields?.map((field) => {
@@ -1395,6 +1613,44 @@ function resolveCategoryKeys(categories, requestedCategory) {
1395
1613
  }
1396
1614
  return keys.filter((key) => (normalizeCategoryValue(key) ?? "").includes(normalized));
1397
1615
  }
1616
+ function canonicalizeCategory(category, tokens) {
1617
+ const normalizedCategory = normalizeCategoryValue(category);
1618
+ const candidates = [
1619
+ normalizedCategory,
1620
+ ...tokens.flatMap((token) => [
1621
+ normalizeCategoryValue(token.category),
1622
+ normalizeCategoryValue(token.path?.[0]),
1623
+ normalizeCategoryValue(token.name.split(/[.:/-]/)[0])
1624
+ ])
1625
+ ].filter(Boolean);
1626
+ for (const candidate of candidates) {
1627
+ for (const [canonical, aliases] of Object.entries(TOKEN_CATEGORY_ALIASES)) {
1628
+ if (candidate === canonical || aliases.some(
1629
+ (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
1630
+ )) {
1631
+ return canonical;
1632
+ }
1633
+ }
1634
+ }
1635
+ return normalizedCategory || "other";
1636
+ }
1637
+ function normalizeTokenCategories(categories) {
1638
+ const normalized = {};
1639
+ for (const [category, tokens] of Object.entries(categories)) {
1640
+ const canonical = canonicalizeCategory(category, tokens);
1641
+ if (!normalized[canonical]) normalized[canonical] = [];
1642
+ normalized[canonical].push(
1643
+ ...tokens.map((token) => ({
1644
+ ...token,
1645
+ category: canonical
1646
+ }))
1647
+ );
1648
+ }
1649
+ for (const tokens of Object.values(normalized)) {
1650
+ tokens.sort((a, b) => a.name.localeCompare(b.name));
1651
+ }
1652
+ return normalized;
1653
+ }
1398
1654
  var tokensHandler = async (args, ctx) => {
1399
1655
  const category = args?.category;
1400
1656
  const search2 = args?.search?.toLowerCase() ?? void 0;
@@ -1412,12 +1668,13 @@ var tokensHandler = async (args, ctx) => {
1412
1668
  }]
1413
1669
  };
1414
1670
  }
1671
+ const normalizedCategories = normalizeTokenCategories(tokenData.categories);
1415
1672
  let filteredCategories = {};
1416
1673
  let filteredTotal = 0;
1417
- const resolvedCategoryKeys = resolveCategoryKeys(tokenData.categories, category);
1674
+ const resolvedCategoryKeys = resolveCategoryKeys(normalizedCategories, category);
1418
1675
  const friendlyCategories = Object.keys(TOKEN_CATEGORY_ALIASES);
1419
- const searchMatchesCategory = search2 ? resolveCategoryKeys(tokenData.categories, search2) : [];
1420
- for (const [cat, tokens] of Object.entries(tokenData.categories)) {
1676
+ const searchMatchesCategory = search2 ? resolveCategoryKeys(normalizedCategories, search2) : [];
1677
+ for (const [cat, tokens] of Object.entries(normalizedCategories)) {
1421
1678
  if (category && !resolvedCategoryKeys.includes(cat)) continue;
1422
1679
  let filtered = tokens;
1423
1680
  if (search2) {
@@ -1425,7 +1682,7 @@ var tokensHandler = async (args, ctx) => {
1425
1682
  filtered = tokens;
1426
1683
  } else {
1427
1684
  filtered = tokens.filter(
1428
- (t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || (normalizeCategoryValue(cat) ?? "").includes(search2)
1685
+ (t) => t.name.toLowerCase().includes(search2) || t.description && t.description.toLowerCase().includes(search2) || (normalizeCategoryValue(cat) ?? "").includes(search2) || t.value && t.value.toLowerCase().includes(search2) || t.path && t.path.join(" ").toLowerCase().includes(search2)
1429
1686
  );
1430
1687
  }
1431
1688
  }
@@ -1443,7 +1700,7 @@ var tokensHandler = async (args, ctx) => {
1443
1700
  if (filteredTotal === 0) {
1444
1701
  if (category && search2) {
1445
1702
  const categoryTotal = resolvedCategoryKeys.reduce(
1446
- (sum, key) => sum + (tokenData.categories[key]?.length ?? 0),
1703
+ (sum, key) => sum + (normalizedCategories[key]?.length ?? 0),
1447
1704
  0
1448
1705
  );
1449
1706
  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. Try categories like: ${friendlyCategories.join(", ")}.`;
@@ -1465,7 +1722,7 @@ var tokensHandler = async (args, ctx) => {
1465
1722
  categories: filteredCategories,
1466
1723
  ...hint && { hint },
1467
1724
  ...!category && !search2 && {
1468
- availableCategories: Object.entries(tokenData.categories).map(([cat, tokens]) => ({
1725
+ availableCategories: Object.entries(normalizedCategories).map(([cat, tokens]) => ({
1469
1726
  category: cat,
1470
1727
  count: tokens.length
1471
1728
  }))
@@ -1475,177 +1732,6 @@ var tokensHandler = async (args, ctx) => {
1475
1732
  };
1476
1733
  };
1477
1734
 
1478
- // src/tools/implement.ts
1479
- var implementHandler = async (args, ctx) => {
1480
- const useCase = args?.useCase;
1481
- if (!useCase) {
1482
- throw new Error("useCase is required");
1483
- }
1484
- const verbosity = args?.verbosity ?? "standard";
1485
- const snapshotComponents = listComponents(ctx.data.snapshot);
1486
- const componentsByName = new Map(
1487
- snapshotComponents.map((component) => [
1488
- component.name.toLowerCase(),
1489
- component
1490
- ])
1491
- );
1492
- const { allBlocks, localData } = buildLocalSearchData(
1493
- {
1494
- components: ctx.data.components,
1495
- blocks: ctx.data.blocks,
1496
- tokens: ctx.data.tokens,
1497
- graph: ctx.data.graph
1498
- },
1499
- {
1500
- componentIndex: ctx.indexes.componentIndex,
1501
- blockIndex: ctx.indexes.blockIndex,
1502
- tokenIndex: ctx.indexes.tokenIndex
1503
- }
1504
- );
1505
- const tokenData = ctx.data.tokens;
1506
- const implLimit = typeof args?.limit === "number" ? Math.min(Math.max(args.limit, 1), 15) : 5;
1507
- const [componentResults, blockResults, tokenResults] = await Promise.all([
1508
- hybridSearch(
1509
- useCase,
1510
- localData,
1511
- implLimit * 3,
1512
- "component",
1513
- ctx.config.searchApiKey
1514
- ),
1515
- hybridSearch(
1516
- useCase,
1517
- localData,
1518
- implLimit,
1519
- "block",
1520
- ctx.config.searchApiKey
1521
- ),
1522
- hybridSearch(
1523
- useCase,
1524
- localData,
1525
- implLimit,
1526
- "token",
1527
- ctx.config.searchApiKey
1528
- )
1529
- ]);
1530
- const topBlockScore = blockResults.length > 0 ? blockResults[0].score : 0;
1531
- const filteredBlockResults = blockResults.filter(
1532
- (result) => result.score >= topBlockScore * 0.3
1533
- );
1534
- if (filteredBlockResults.length > 0) {
1535
- const matchedBlocks = filteredBlockResults.map(
1536
- (result) => allBlocks.find((block) => block.name.toLowerCase() === result.name.toLowerCase())
1537
- ).filter(Boolean);
1538
- const blockFreq = buildBlockComponentFrequency(matchedBlocks);
1539
- boostByBlockFrequency(componentResults, blockFreq);
1540
- }
1541
- const topComponentResults = componentResults.slice(0, implLimit);
1542
- const maxCompScore = topComponentResults.length > 0 ? topComponentResults[0].score : 0;
1543
- const topMatches = topComponentResults.map((result) => {
1544
- const component = componentsByName.get(result.name.toLowerCase());
1545
- return component ? { component, score: result.score } : null;
1546
- }).filter(Boolean);
1547
- const components = await Promise.all(
1548
- topMatches.map(async ({ component, score }) => {
1549
- const pkgName = ctx.resolvePackageName(component.name);
1550
- if (verbosity === "compact") {
1551
- return {
1552
- name: component.name,
1553
- description: component.description,
1554
- confidence: assignConfidence(score, maxCompScore),
1555
- import: `import { ${component.name} } from '${pkgName}';`
1556
- };
1557
- }
1558
- const exampleLimit = verbosity === "full" ? component.examples.length : 2;
1559
- const propsLimit = verbosity === "full" ? Object.keys(component.props ?? {}).length : 5;
1560
- const examples = component.examples.slice(0, exampleLimit).map((example) => ({
1561
- variant: example.name,
1562
- code: example.code ?? `<${component.name} />`
1563
- }));
1564
- const propsSummary = Object.entries(component.props ?? {}).slice(0, propsLimit).map(
1565
- ([propName, prop]) => `${propName}${prop.required ? " (required)" : ""}: ${prop.type}${prop.values ? ` = ${prop.values.join("|")}` : ""}`
1566
- );
1567
- return {
1568
- name: component.name,
1569
- category: component.category,
1570
- description: component.description,
1571
- confidence: assignConfidence(score, maxCompScore),
1572
- import: `import { ${component.name} } from '${pkgName}';`,
1573
- props: propsSummary,
1574
- examples,
1575
- guidelines: getGuidanceWhen(component).slice(0, 3),
1576
- accessibility: component.guidance.accessibility.slice(0, 2)
1577
- };
1578
- })
1579
- );
1580
- const matchingBlocks = (await Promise.all(
1581
- filteredBlockResults.slice(0, 5).map(async (result) => {
1582
- const block = allBlocks.find(
1583
- (entry) => entry.name.toLowerCase() === result.name.toLowerCase()
1584
- );
1585
- if (!block) return null;
1586
- const imports = await buildImportStatements(
1587
- block.components,
1588
- async (componentName) => ctx.resolvePackageName(componentName)
1589
- );
1590
- const codeLines = block.code.split("\n");
1591
- const code = codeLines.length > 30 ? `${codeLines.slice(0, 20).join("\n")}
1592
- // ... truncated (${codeLines.length} lines total)` : block.code;
1593
- return {
1594
- name: block.name,
1595
- description: block.description,
1596
- components: block.components,
1597
- code,
1598
- import: imports.join("\n"),
1599
- imports
1600
- };
1601
- })
1602
- )).filter(Boolean);
1603
- let relevantTokens;
1604
- if (tokenResults.length > 0 && tokenData) {
1605
- relevantTokens = {};
1606
- for (const result of tokenResults) {
1607
- for (const [category, tokens] of Object.entries(tokenData.categories)) {
1608
- if (tokens.some((token) => token.name === result.name)) {
1609
- if (!relevantTokens[category]) relevantTokens[category] = [];
1610
- relevantTokens[category].push(result.name);
1611
- break;
1612
- }
1613
- }
1614
- }
1615
- if (Object.keys(relevantTokens).length === 0) {
1616
- relevantTokens = void 0;
1617
- }
1618
- }
1619
- if (!relevantTokens && tokenData) {
1620
- const categories = extractTokenCategories(useCase);
1621
- relevantTokens = {};
1622
- for (const category of categories) {
1623
- const tokens = tokenData.categories[category];
1624
- if (tokens && tokens.length > 0) {
1625
- relevantTokens[category] = tokens.slice(0, 5).map((token) => token.name);
1626
- }
1627
- }
1628
- if (Object.keys(relevantTokens).length === 0) {
1629
- relevantTokens = void 0;
1630
- }
1631
- }
1632
- return {
1633
- content: [
1634
- {
1635
- type: "text",
1636
- text: JSON.stringify({
1637
- useCase,
1638
- components,
1639
- blocks: verbosity !== "compact" && matchingBlocks.length > 0 ? matchingBlocks : void 0,
1640
- tokens: verbosity !== "compact" ? relevantTokens : void 0,
1641
- noMatch: components.length === 0,
1642
- 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 ${ctx.toolNames.discover} with different terms${tokenData ? ` or ${ctx.toolNames.tokens} for tokens` : ""}.`
1643
- })
1644
- }
1645
- ]
1646
- };
1647
- };
1648
-
1649
1735
  // src/service.ts
1650
1736
  var DEFAULT_ENDPOINTS = {
1651
1737
  render: "/fragments/render",
@@ -2327,8 +2413,18 @@ var governHandler = async (args, ctx) => {
2327
2413
  }
2328
2414
  const policyOverrides = args?.policy;
2329
2415
  const format = args?.format ?? "json";
2416
+ const allowedComponents = Object.values(ctx.data.components).map(
2417
+ (component) => component.name
2418
+ );
2330
2419
  const tokenPrefix = ctx.data.tokens?.prefix;
2331
2420
  const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset().rules } : { rules: universal().rules };
2421
+ basePolicy.rules["components/allow"] = {
2422
+ enabled: true,
2423
+ severity: "serious",
2424
+ options: {
2425
+ components: allowedComponents
2426
+ }
2427
+ };
2332
2428
  const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
2333
2429
  const input = {
2334
2430
  spec,
@@ -2366,6 +2462,205 @@ var governHandler = async (args, ctx) => {
2366
2462
  }
2367
2463
  };
2368
2464
 
2465
+ // src/tools/validate-and-fix.ts
2466
+ import {
2467
+ formatVerdict as formatVerdict2,
2468
+ fragments as fragmentsPreset2,
2469
+ handleGovernTool as handleGovernTool2,
2470
+ universal as universal2
2471
+ } from "@fragments-sdk/govern";
2472
+ function classifyComponent(component) {
2473
+ if (component.status === "deprecated") return "discouraged";
2474
+ if (component.isCanonical && component.tier === "core") return "preferred";
2475
+ return "allowed";
2476
+ }
2477
+ function buildEffectiveComponents(ctx) {
2478
+ const selectionByKey = new Map(
2479
+ (ctx.data.validateFixContext?.components ?? []).map((component) => [
2480
+ component.componentKey,
2481
+ component.selection
2482
+ ])
2483
+ );
2484
+ return Object.entries(ctx.data.components).map(([componentKey, component]) => ({
2485
+ component,
2486
+ selection: selectionByKey.get(componentKey) ?? classifyComponent(component)
2487
+ }));
2488
+ }
2489
+ function cloneSpec(spec) {
2490
+ return structuredClone(spec);
2491
+ }
2492
+ function getSelectionNames(effectiveComponents, selection) {
2493
+ const selections = Array.isArray(selection) ? selection : [selection];
2494
+ return Array.from(
2495
+ new Set(
2496
+ effectiveComponents.filter((entry) => selections.includes(entry.selection)).map((entry) => entry.component.name)
2497
+ )
2498
+ );
2499
+ }
2500
+ function buildBasePolicy(ctx) {
2501
+ const tokenPrefix = ctx.data.tokens?.prefix;
2502
+ const basePolicy = tokenPrefix === "fui-" ? { rules: fragmentsPreset2().rules } : { rules: universal2().rules };
2503
+ const allowedComponents = buildEffectiveComponents(ctx).filter(({ selection }) => selection === "preferred" || selection === "allowed").map(({ component }) => component.name);
2504
+ basePolicy.rules["components/allow"] = {
2505
+ enabled: true,
2506
+ severity: "serious",
2507
+ options: {
2508
+ components: allowedComponents
2509
+ }
2510
+ };
2511
+ return basePolicy;
2512
+ }
2513
+ async function runGovern(spec, ctx, policyOverrides) {
2514
+ const input = {
2515
+ spec,
2516
+ policy: policyOverrides,
2517
+ format: "json"
2518
+ };
2519
+ const engineOptions = ctx.data.tokens ? { tokenData: ctx.data.tokens } : void 0;
2520
+ return handleGovernTool2(input, buildBasePolicy(ctx), engineOptions);
2521
+ }
2522
+ function applyDeterministicReplacements(spec, ctx) {
2523
+ const effectiveComponents = buildEffectiveComponents(ctx);
2524
+ const byName = /* @__PURE__ */ new Map();
2525
+ const preferredByCategory = /* @__PURE__ */ new Map();
2526
+ for (const entry of effectiveComponents) {
2527
+ const list = byName.get(entry.component.name) ?? [];
2528
+ list.push(entry);
2529
+ byName.set(entry.component.name, list);
2530
+ if (entry.selection !== "preferred") continue;
2531
+ const category = entry.component.category ?? "uncategorized";
2532
+ const names = preferredByCategory.get(category) ?? [];
2533
+ if (!names.includes(entry.component.name)) {
2534
+ names.push(entry.component.name);
2535
+ }
2536
+ preferredByCategory.set(category, names);
2537
+ }
2538
+ const fixedSpec = cloneSpec(spec);
2539
+ const nodes = Array.isArray(fixedSpec.nodes) ? fixedSpec.nodes : [];
2540
+ const replacements = [];
2541
+ for (const node of nodes) {
2542
+ if (!node.type) continue;
2543
+ const componentEntries = byName.get(node.type) ?? [];
2544
+ const component = componentEntries[0];
2545
+ if (!component || component.selection !== "discouraged" && component.selection !== "forbidden") {
2546
+ continue;
2547
+ }
2548
+ const candidates = (preferredByCategory.get(component.component.category ?? "uncategorized") ?? []).filter((candidate) => candidate !== component.component.name);
2549
+ if (candidates.length !== 1) continue;
2550
+ const replacement = candidates[0];
2551
+ replacements.push({
2552
+ nodeId: node.id ?? node.type,
2553
+ from: component.component.name,
2554
+ to: replacement,
2555
+ reason: "deprecated_component"
2556
+ });
2557
+ node.type = replacement;
2558
+ }
2559
+ return {
2560
+ fixedSpec,
2561
+ replacements
2562
+ };
2563
+ }
2564
+ function buildSummary(args) {
2565
+ const lines = [
2566
+ `status: ${args.status}`,
2567
+ `original: ${formatVerdict2(args.originalVerdict, "summary")}`
2568
+ ];
2569
+ if (args.replacements.length > 0) {
2570
+ lines.push(
2571
+ `replacements: ${args.replacements.map((item) => `${item.from} -> ${item.to}`).join(", ")}`
2572
+ );
2573
+ lines.push(`final: ${formatVerdict2(args.finalVerdict, "summary")}`);
2574
+ }
2575
+ return lines.join("\n");
2576
+ }
2577
+ var validateAndFixHandler = async (args, ctx) => {
2578
+ const spec = args?.spec;
2579
+ if (!spec || typeof spec !== "object") {
2580
+ return {
2581
+ content: [
2582
+ {
2583
+ type: "text",
2584
+ text: JSON.stringify({
2585
+ error: "spec is required and must be an object with { nodes: [{ id, type, props, children }] }"
2586
+ })
2587
+ }
2588
+ ],
2589
+ isError: true
2590
+ };
2591
+ }
2592
+ const policyOverrides = args?.policy;
2593
+ const applyFixes = args?.applyFixes !== false;
2594
+ const format = args?.format ?? "json";
2595
+ try {
2596
+ const effectiveComponents = buildEffectiveComponents(ctx);
2597
+ const originalVerdict = await runGovern(spec, ctx, policyOverrides);
2598
+ let finalVerdict = originalVerdict;
2599
+ let fixedSpec;
2600
+ let replacements = [];
2601
+ if (!originalVerdict.passed && applyFixes) {
2602
+ const result = applyDeterministicReplacements(
2603
+ spec,
2604
+ ctx
2605
+ );
2606
+ replacements = result.replacements;
2607
+ fixedSpec = result.fixedSpec;
2608
+ if (replacements.length > 0) {
2609
+ finalVerdict = await runGovern(fixedSpec, ctx, policyOverrides);
2610
+ }
2611
+ }
2612
+ const status = originalVerdict.passed ? "pass" : replacements.length > 0 ? "fixed" : "fail";
2613
+ const payload = {
2614
+ status,
2615
+ applyFixes,
2616
+ replacements,
2617
+ originalVerdict,
2618
+ finalVerdict,
2619
+ ...fixedSpec ? { fixedSpec } : {},
2620
+ preferredComponents: getSelectionNames(
2621
+ effectiveComponents,
2622
+ "preferred"
2623
+ ),
2624
+ discouragedComponents: getSelectionNames(effectiveComponents, [
2625
+ "discouraged",
2626
+ "forbidden"
2627
+ ])
2628
+ };
2629
+ return {
2630
+ content: [
2631
+ {
2632
+ type: "text",
2633
+ text: format === "summary" ? buildSummary({
2634
+ status,
2635
+ originalVerdict,
2636
+ finalVerdict,
2637
+ replacements
2638
+ }) : JSON.stringify(payload)
2639
+ }
2640
+ ],
2641
+ _meta: {
2642
+ status,
2643
+ replacementCount: replacements.length,
2644
+ passed: finalVerdict.passed
2645
+ }
2646
+ };
2647
+ } catch (error) {
2648
+ const message = error instanceof Error ? error.message : String(error);
2649
+ const isSpecError = message.includes("Expected") || message.includes("Required");
2650
+ return {
2651
+ content: [
2652
+ {
2653
+ type: "text",
2654
+ text: JSON.stringify({
2655
+ error: isSpecError ? `Invalid spec format: ${message}. Expected: { nodes: [{ id: string, type: string, props: object, children?: string[] }] }` : message
2656
+ })
2657
+ }
2658
+ ],
2659
+ isError: true
2660
+ };
2661
+ }
2662
+ };
2663
+
2369
2664
  // src/tools/generate-ui.ts
2370
2665
  var generateUiHandler = async (args, ctx) => {
2371
2666
  const prompt = args?.prompt;
@@ -2395,16 +2690,154 @@ var generateUiHandler = async (args, ctx) => {
2395
2690
  };
2396
2691
  };
2397
2692
 
2693
+ // src/findings-service.ts
2694
+ var DEFAULT_CLOUD_URL = "https://app.usefragments.com";
2695
+ function normalizeCloudUrl(url) {
2696
+ if (!url) return DEFAULT_CLOUD_URL;
2697
+ return url.replace(/\/+$/, "");
2698
+ }
2699
+ async function fetchFindings(apiKey, params, cloudUrl) {
2700
+ const base = normalizeCloudUrl(cloudUrl);
2701
+ const url = new URL(`${base}/api/findings`);
2702
+ if (params.status) url.searchParams.set("status", params.status);
2703
+ if (params.severity) url.searchParams.set("severity", params.severity);
2704
+ if (params.category) url.searchParams.set("category", params.category);
2705
+ if (params.ruleId) url.searchParams.set("ruleId", params.ruleId);
2706
+ if (params.filePath) url.searchParams.set("filePath", params.filePath);
2707
+ if (params.limit != null) url.searchParams.set("limit", String(params.limit));
2708
+ const response = await fetch(url.toString(), {
2709
+ headers: { "X-API-Key": apiKey }
2710
+ });
2711
+ if (!response.ok) {
2712
+ const body = await response.text();
2713
+ let message;
2714
+ try {
2715
+ const parsed = JSON.parse(body);
2716
+ message = parsed.error ?? body;
2717
+ } catch {
2718
+ message = body;
2719
+ }
2720
+ throw new Error(
2721
+ `Cloud findings API error (${response.status}): ${message}`
2722
+ );
2723
+ }
2724
+ return await response.json();
2725
+ }
2726
+ async function fetchFindingsForFile(apiKey, filePath, cloudUrl) {
2727
+ return fetchFindings(
2728
+ apiKey,
2729
+ { status: "open", filePath, limit: 200 },
2730
+ cloudUrl
2731
+ );
2732
+ }
2733
+
2734
+ // src/tools/findings.ts
2735
+ function resolveCloudApiKey(ctx) {
2736
+ return ctx.config.cloudApiKey ?? ctx.config.fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
2737
+ }
2738
+ function resolveCloudUrl(ctx) {
2739
+ return ctx.config.fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
2740
+ }
2741
+ function missingKeyError() {
2742
+ return {
2743
+ content: [
2744
+ {
2745
+ type: "text",
2746
+ text: JSON.stringify({
2747
+ error: "Cloud API key required. Set FRAGMENTS_API_KEY env var, pass --cloud-api-key, or configure cloud.apiKey in ds-mcp.config.json."
2748
+ })
2749
+ }
2750
+ ],
2751
+ isError: true
2752
+ };
2753
+ }
2754
+ var findingsListHandler = async (args, ctx) => {
2755
+ const apiKey = resolveCloudApiKey(ctx);
2756
+ if (!apiKey) return missingKeyError();
2757
+ const cloudUrl = resolveCloudUrl(ctx);
2758
+ const params = {};
2759
+ if (args.status) params.status = args.status;
2760
+ if (args.severity)
2761
+ params.severity = args.severity;
2762
+ if (args.category) params.category = String(args.category);
2763
+ if (args.ruleId) params.ruleId = String(args.ruleId);
2764
+ if (args.filePath) params.filePath = String(args.filePath);
2765
+ if (args.limit != null) params.limit = Number(args.limit);
2766
+ try {
2767
+ const result = await fetchFindings(apiKey, params, cloudUrl);
2768
+ return {
2769
+ content: [{ type: "text", text: JSON.stringify(result) }],
2770
+ _meta: { count: result.findings.length }
2771
+ };
2772
+ } catch (error) {
2773
+ return {
2774
+ content: [
2775
+ {
2776
+ type: "text",
2777
+ text: JSON.stringify({
2778
+ error: error instanceof Error ? error.message : String(error)
2779
+ })
2780
+ }
2781
+ ],
2782
+ isError: true
2783
+ };
2784
+ }
2785
+ };
2786
+ var findingsForFileHandler = async (args, ctx) => {
2787
+ const apiKey = resolveCloudApiKey(ctx);
2788
+ if (!apiKey) return missingKeyError();
2789
+ const filePath = args.filePath;
2790
+ if (!filePath || typeof filePath !== "string") {
2791
+ return {
2792
+ content: [
2793
+ {
2794
+ type: "text",
2795
+ text: JSON.stringify({ error: "filePath is required." })
2796
+ }
2797
+ ],
2798
+ isError: true
2799
+ };
2800
+ }
2801
+ const cloudUrl = resolveCloudUrl(ctx);
2802
+ try {
2803
+ const result = await fetchFindingsForFile(apiKey, filePath, cloudUrl);
2804
+ const findings = result.findings.filter((f) => f.filePath === filePath);
2805
+ return {
2806
+ content: [
2807
+ {
2808
+ type: "text",
2809
+ text: JSON.stringify({ findings, filePath })
2810
+ }
2811
+ ],
2812
+ _meta: { count: findings.length, filePath }
2813
+ };
2814
+ } catch (error) {
2815
+ return {
2816
+ content: [
2817
+ {
2818
+ type: "text",
2819
+ text: JSON.stringify({
2820
+ error: error instanceof Error ? error.message : String(error)
2821
+ })
2822
+ }
2823
+ ],
2824
+ isError: true
2825
+ };
2826
+ }
2827
+ };
2828
+
2398
2829
  // src/tools/index.ts
2399
2830
  var CORE_TOOLS = {
2400
2831
  discover: discoverHandler,
2401
2832
  inspect: inspectHandler,
2402
2833
  blocks: blocksHandler,
2403
2834
  tokens: tokensHandler,
2404
- implement: implementHandler,
2405
2835
  graph: graphHandler,
2406
2836
  perf: perfHandler,
2407
- govern: governHandler
2837
+ govern: governHandler,
2838
+ validate_and_fix: validateAndFixHandler,
2839
+ findings_list: findingsListHandler,
2840
+ findings_for_file: findingsForFileHandler
2408
2841
  };
2409
2842
  var VIEWER_TOOLS = {
2410
2843
  render: renderHandler,
@@ -2424,9 +2857,9 @@ var TOOL_CAPABILITIES = {
2424
2857
  inspect: ["components"],
2425
2858
  blocks: ["blocks"],
2426
2859
  tokens: ["tokens"],
2427
- implement: ["components"],
2428
2860
  graph: ["graph"],
2429
2861
  perf: ["performance"],
2862
+ validate_and_fix: ["components"],
2430
2863
  render: ["components"],
2431
2864
  fix: ["components"],
2432
2865
  a11y: ["components"]
@@ -2576,7 +3009,7 @@ import { join as join7 } from "path";
2576
3009
  import { readFile } from "fs/promises";
2577
3010
 
2578
3011
  // src/discovery.ts
2579
- import { existsSync as existsSync3, readFileSync as readFileSync4, readdirSync } from "fs";
3012
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync } from "fs";
2580
3013
  import { join as join3, dirname, resolve } from "path";
2581
3014
  import { createRequire } from "module";
2582
3015
  function resolveWorkspaceGlob(baseDir, pattern) {
@@ -2609,7 +3042,7 @@ function getWorkspaceDirs(rootDir) {
2609
3042
  const rootPkgPath = join3(rootDir, "package.json");
2610
3043
  if (existsSync3(rootPkgPath)) {
2611
3044
  try {
2612
- const rootPkg = JSON.parse(readFileSync4(rootPkgPath, "utf-8"));
3045
+ const rootPkg = JSON.parse(readFileSync3(rootPkgPath, "utf-8"));
2613
3046
  const workspaces = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages;
2614
3047
  if (Array.isArray(workspaces)) {
2615
3048
  for (const pattern of workspaces) {
@@ -2623,7 +3056,7 @@ function getWorkspaceDirs(rootDir) {
2623
3056
  const pnpmWsPath = join3(rootDir, "pnpm-workspace.yaml");
2624
3057
  if (existsSync3(pnpmWsPath)) {
2625
3058
  try {
2626
- const content = readFileSync4(pnpmWsPath, "utf-8");
3059
+ const content = readFileSync3(pnpmWsPath, "utf-8");
2627
3060
  const lines = content.split("\n");
2628
3061
  let inPackages = false;
2629
3062
  for (const line of lines) {
@@ -2656,7 +3089,7 @@ function resolveDepPackageJson(localRequire, depName) {
2656
3089
  while (true) {
2657
3090
  const candidate = join3(dir, "package.json");
2658
3091
  if (existsSync3(candidate)) {
2659
- const pkg = JSON.parse(readFileSync4(candidate, "utf-8"));
3092
+ const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
2660
3093
  if (pkg.name === depName) return candidate;
2661
3094
  }
2662
3095
  const parent = dirname(dir);
@@ -2671,7 +3104,7 @@ function findFragmentsInDeps(dir, found, depField) {
2671
3104
  const pkgJsonPath = join3(dir, "package.json");
2672
3105
  if (!existsSync3(pkgJsonPath)) return;
2673
3106
  try {
2674
- const pkgJson = JSON.parse(readFileSync4(pkgJsonPath, "utf-8"));
3107
+ const pkgJson = JSON.parse(readFileSync3(pkgJsonPath, "utf-8"));
2675
3108
  const allDeps = {
2676
3109
  ...pkgJson.dependencies,
2677
3110
  ...pkgJson.devDependencies
@@ -2681,7 +3114,7 @@ function findFragmentsInDeps(dir, found, depField) {
2681
3114
  try {
2682
3115
  const depPkgPath = resolveDepPackageJson(localRequire, depName);
2683
3116
  if (!depPkgPath) continue;
2684
- const depPkg = JSON.parse(readFileSync4(depPkgPath, "utf-8"));
3117
+ const depPkg = JSON.parse(readFileSync3(depPkgPath, "utf-8"));
2685
3118
  if (depPkg[depField]) {
2686
3119
  const fragmentsPath = join3(dirname(depPkgPath), depPkg[depField]);
2687
3120
  if (existsSync3(fragmentsPath) && !found.includes(fragmentsPath)) {
@@ -2757,6 +3190,26 @@ function valueToString(value) {
2757
3190
  return void 0;
2758
3191
  }
2759
3192
  }
3193
+ function buildCompoundChildren(contract, ai) {
3194
+ const contractChildren = Object.entries(contract?.compoundChildren ?? {});
3195
+ if (contractChildren.length > 0) {
3196
+ return contractChildren.map(([childName, child]) => ({
3197
+ name: childName,
3198
+ description: child.description,
3199
+ required: child.required,
3200
+ accepts: child.accepts,
3201
+ visibility: "public"
3202
+ }));
3203
+ }
3204
+ const subs = ai?.subComponents;
3205
+ if (!subs || subs.length === 0) return [];
3206
+ const requiredSet = new Set(ai?.requiredChildren ?? []);
3207
+ return subs.map((name) => ({
3208
+ name,
3209
+ required: requiredSet.has(name) || void 0,
3210
+ visibility: "public"
3211
+ }));
3212
+ }
2760
3213
  function componentFromCompiledFragment(args) {
2761
3214
  const { fragment, sourceType } = args;
2762
3215
  const name = fragment.meta.name;
@@ -2806,15 +3259,7 @@ function componentFromCompiledFragment(args) {
2806
3259
  relationship: relation.relationship,
2807
3260
  note: relation.note
2808
3261
  })),
2809
- compoundChildren: Object.entries(contract?.compoundChildren ?? {}).map(
2810
- ([childName, child]) => ({
2811
- name: childName,
2812
- description: child.description,
2813
- required: child.required,
2814
- accepts: child.accepts,
2815
- visibility: "public"
2816
- })
2817
- ),
3262
+ compoundChildren: buildCompoundChildren(contract, ai),
2818
3263
  guidance: {
2819
3264
  when: usage.when ?? [],
2820
3265
  whenNot: usage.whenNot ?? [],
@@ -3010,7 +3455,7 @@ If you're a library author, run \`${BRAND.cliCommand} build\` first.`
3010
3455
  };
3011
3456
 
3012
3457
  // src/adapters/auto-extract.ts
3013
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
3458
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
3014
3459
  import { join as join6, relative, sep } from "path";
3015
3460
 
3016
3461
  // src/adapters/discover-components.ts
@@ -3098,7 +3543,7 @@ function inferComponentName(fileName, dirPath) {
3098
3543
  }
3099
3544
 
3100
3545
  // src/adapters/scan-tokens.ts
3101
- import { readdirSync as readdirSync3, readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
3546
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5 } from "fs";
3102
3547
  import { join as join5, extname as extname2 } from "path";
3103
3548
  function scanTokens(projectRoot) {
3104
3549
  const cssFiles = discoverCssFiles(projectRoot);
@@ -3107,7 +3552,7 @@ function scanTokens(projectRoot) {
3107
3552
  let prefix = "";
3108
3553
  for (const filePath of cssFiles) {
3109
3554
  try {
3110
- const content = readFileSync5(filePath, "utf-8");
3555
+ const content = readFileSync4(filePath, "utf-8");
3111
3556
  const tokens = extractCustomProperties(content);
3112
3557
  allTokens.push(...tokens);
3113
3558
  } catch {
@@ -3274,7 +3719,7 @@ var AutoExtractionAdapter = class {
3274
3719
  async load(projectRoot) {
3275
3720
  let extractMod;
3276
3721
  try {
3277
- extractMod = await import("./dist-V7D67NXS.js");
3722
+ extractMod = await loadExtractorModule();
3278
3723
  } catch (e) {
3279
3724
  throw new Error(
3280
3725
  "Auto-extraction requires @fragments-sdk/extract and TypeScript.\n\nIf you see this error, the MCP server may not be installed correctly.\nAlternative: pre-build your design system with `npx fragments build`"
@@ -3390,6 +3835,13 @@ Check that your tsconfig.json includes the component directories.`
3390
3835
  }
3391
3836
  }
3392
3837
  };
3838
+ var extractorModulePromise = null;
3839
+ async function loadExtractorModule() {
3840
+ if (!extractorModulePromise) {
3841
+ extractorModulePromise = import("./dist-V7D67NXS.js");
3842
+ }
3843
+ return extractorModulePromise;
3844
+ }
3393
3845
  var UNIVERSAL_INHERITED = /* @__PURE__ */ new Set(["children", "className", "id", "disabled"]);
3394
3846
  var FORM_INPUT_INHERITED = /* @__PURE__ */ new Set([
3395
3847
  "placeholder",
@@ -3661,7 +4113,7 @@ function readPackageName(projectRoot) {
3661
4113
  try {
3662
4114
  const pkgPath = join6(projectRoot, "package.json");
3663
4115
  if (!existsSync6(pkgPath)) return void 0;
3664
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf-8"));
4116
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
3665
4117
  return pkg.name;
3666
4118
  } catch {
3667
4119
  return void 0;
@@ -3669,16 +4121,53 @@ function readPackageName(projectRoot) {
3669
4121
  }
3670
4122
 
3671
4123
  // src/adapters/cloud-catalog.ts
3672
- var DEFAULT_CLOUD_URL = "https://app.usefragments.com/api/catalog";
4124
+ var DEFAULT_CLOUD_URL2 = "https://app.usefragments.com/api/catalog";
4125
+ var TOKEN_CATEGORY_ALIASES2 = {
4126
+ color: ["color", "colors", "accent", "background", "foreground", "danger", "brand"],
4127
+ spacing: ["spacing", "space", "padding", "margin", "gap", "inset"],
4128
+ typography: ["typography", "font", "text", "copy", "line-height", "letter"],
4129
+ border: ["border", "borders", "stroke", "outline"],
4130
+ radius: ["radius", "radii", "corner", "corners", "rounded"],
4131
+ shadow: ["shadow", "shadows", "elevation"],
4132
+ layout: ["layout", "grid", "container", "breakpoint"],
4133
+ focus: ["focus", "ring", "focus-ring"],
4134
+ surface: ["surface", "surfaces", "canvas", "card", "background"]
4135
+ };
3673
4136
  function normalizeCatalogUrl(url) {
3674
- if (!url) return DEFAULT_CLOUD_URL;
4137
+ if (!url) return DEFAULT_CLOUD_URL2;
3675
4138
  if (url.endsWith("/api/catalog")) return url;
3676
4139
  return `${url.replace(/\/+$/, "")}/api/catalog`;
3677
4140
  }
4141
+ function normalizeValidateFixUrl(url) {
4142
+ const base = normalizeCatalogUrl(url).replace(/\/api\/catalog$/, "");
4143
+ return `${base}/api/context?target=validate-fix`;
4144
+ }
4145
+ function normalizeValue(value) {
4146
+ return value?.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim() ?? "";
4147
+ }
4148
+ function canonicalizeTokenCategory(token) {
4149
+ const candidates = [
4150
+ token.category,
4151
+ token.path?.[0],
4152
+ token.name.split(/[.:/-]/)[0]
4153
+ ].map(normalizeValue).filter(Boolean);
4154
+ for (const candidate of candidates) {
4155
+ for (const [canonical, aliases] of Object.entries(
4156
+ TOKEN_CATEGORY_ALIASES2
4157
+ )) {
4158
+ if (candidate === canonical || aliases.some(
4159
+ (alias) => candidate === alias || candidate.includes(alias) || alias.includes(candidate)
4160
+ )) {
4161
+ return canonical;
4162
+ }
4163
+ }
4164
+ }
4165
+ return candidates[0] || "other";
4166
+ }
3678
4167
  function groupTokens(flat) {
3679
4168
  const categories = {};
3680
4169
  const normalizedFlat = (flat ?? []).map((token) => {
3681
- const category = token.category?.trim() || "other";
4170
+ const category = canonicalizeTokenCategory(token);
3682
4171
  const normalized = {
3683
4172
  name: token.name,
3684
4173
  category,
@@ -3761,6 +4250,7 @@ function mapComponent(component, designSystem) {
3761
4250
  importPath: designSystem?.importPath ?? designSystem?.packageName ?? void 0,
3762
4251
  publicRef: component.publicRef,
3763
4252
  publicSlug: component.publicSlug ?? null,
4253
+ isCanonical: component.isCanonical ?? false,
3764
4254
  tier: component.tier,
3765
4255
  parentComponentId: component.parentComponentKey,
3766
4256
  parentComponentName: component.parentComponentName,
@@ -3770,17 +4260,54 @@ function mapComponent(component, designSystem) {
3770
4260
  }
3771
4261
  };
3772
4262
  }
4263
+ function normalizeValidateFixContext(raw) {
4264
+ const content = raw.content;
4265
+ if (!content || !Array.isArray(content.components)) {
4266
+ return void 0;
4267
+ }
4268
+ const components = content.components.filter(
4269
+ (component) => Boolean(component?.componentKey) && Boolean(component?.publicRef) && Boolean(component?.name) && Boolean(component?.selection)
4270
+ ).map((component) => ({
4271
+ componentKey: component.componentKey,
4272
+ publicRef: component.publicRef,
4273
+ name: component.name,
4274
+ category: component.category ?? "uncategorized",
4275
+ status: component.status ?? "stable",
4276
+ tier: component.tier ?? "composition",
4277
+ isCanonical: component.isCanonical ?? false,
4278
+ isActive: component.isActive ?? true,
4279
+ selection: component.selection ?? "allowed",
4280
+ reasons: component.reasons ?? [],
4281
+ usageGuidance: component.usageGuidance,
4282
+ dos: component.dos ?? [],
4283
+ donts: component.donts ?? []
4284
+ }));
4285
+ return {
4286
+ version: 1,
4287
+ catalogRevision: content.catalogRevision,
4288
+ updatedAt: content.updatedAt,
4289
+ policy: {
4290
+ mode: "cloud",
4291
+ endpoint: content.policy?.endpoint ?? "/api/govern/policy"
4292
+ },
4293
+ components
4294
+ };
4295
+ }
3773
4296
  var CloudCatalogAdapter = class {
3774
4297
  constructor(options) {
3775
4298
  this.options = options;
3776
4299
  }
3777
4300
  name = "cloud";
3778
4301
  async load(_projectRoot) {
3779
- const response = await fetch(normalizeCatalogUrl(this.options.url), {
3780
- headers: {
3781
- Authorization: `Bearer ${this.options.apiKey}`
3782
- }
3783
- });
4302
+ const headers = {
4303
+ "X-API-Key": this.options.apiKey
4304
+ };
4305
+ const [response, validateFixResponse] = await Promise.all([
4306
+ fetch(normalizeCatalogUrl(this.options.url), { headers }),
4307
+ fetch(normalizeValidateFixUrl(this.options.url), { headers }).catch(
4308
+ () => null
4309
+ )
4310
+ ]);
3784
4311
  if (!response.ok) {
3785
4312
  throw new Error(
3786
4313
  `Failed to load Cloud catalog (${response.status} ${response.statusText}).`
@@ -3822,15 +4349,31 @@ var CloudCatalogAdapter = class {
3822
4349
  packageMap,
3823
4350
  defaultPackageName: packageName
3824
4351
  });
4352
+ const validateFixContext = validateFixResponse && validateFixResponse.ok ? normalizeValidateFixContext(
4353
+ await validateFixResponse.json()
4354
+ ) : void 0;
4355
+ const hydratedComponents = Object.fromEntries(
4356
+ Object.entries(snapshot.components).map(([componentId, component]) => [
4357
+ componentId,
4358
+ {
4359
+ ...component,
4360
+ isCanonical: components[componentId]?.isCanonical ?? false
4361
+ }
4362
+ ])
4363
+ );
3825
4364
  return {
3826
- snapshot,
3827
- components: snapshot.components,
4365
+ snapshot: {
4366
+ ...snapshot,
4367
+ components: hydratedComponents
4368
+ },
4369
+ components: hydratedComponents,
3828
4370
  blocks: snapshot.blocks,
3829
4371
  tokens: snapshot.tokens,
3830
4372
  graph: void 0,
3831
4373
  performanceSummary: void 0,
3832
4374
  packageMap: snapshot.packageMap,
3833
4375
  defaultPackageName: snapshot.defaultPackageName,
4376
+ validateFixContext,
3834
4377
  capabilities: new Set(snapshot.capabilities)
3835
4378
  };
3836
4379
  }
@@ -3908,6 +4451,7 @@ function normalizeRelations(relations, fallback) {
3908
4451
  function buildComponent(manifest, entry, shard) {
3909
4452
  const sourcePackage = manifest.designSystem.importPath ?? manifest.designSystem.packageName ?? void 0;
3910
4453
  const component = shard.component;
4454
+ const componentRecord = component;
3911
4455
  const props = normalizeProps(component.props);
3912
4456
  return {
3913
4457
  id: shard.componentId,
@@ -3944,6 +4488,7 @@ function buildComponent(manifest, entry, shard) {
3944
4488
  importPath: sourcePackage,
3945
4489
  publicRef: component.publicRef,
3946
4490
  publicSlug: component.publicSlug,
4491
+ isCanonical: componentRecord.isCanonical ?? component.tier === "core",
3947
4492
  tier: component.tier,
3948
4493
  parentComponentName: component.parentComponentName,
3949
4494
  metadata: {
@@ -4036,9 +4581,21 @@ var BundleAdapter = class {
4036
4581
  packageMap,
4037
4582
  defaultPackageName: packageName
4038
4583
  });
4584
+ const hydratedComponents = Object.fromEntries(
4585
+ Object.entries(snapshot.components).map(([componentId, component]) => [
4586
+ componentId,
4587
+ {
4588
+ ...component,
4589
+ isCanonical: components[componentId]?.isCanonical ?? component.tier === "core"
4590
+ }
4591
+ ])
4592
+ );
4039
4593
  return {
4040
- snapshot,
4041
- components: snapshot.components,
4594
+ snapshot: {
4595
+ ...snapshot,
4596
+ components: hydratedComponents
4597
+ },
4598
+ components: hydratedComponents,
4042
4599
  blocks: snapshot.blocks,
4043
4600
  tokens: snapshot.tokens,
4044
4601
  graph: snapshot.graph,
@@ -4051,10 +4608,10 @@ var BundleAdapter = class {
4051
4608
  };
4052
4609
 
4053
4610
  // src/source-selection.ts
4054
- function resolveCloudApiKey(config, fileConfig) {
4611
+ function resolveCloudApiKey2(config, fileConfig) {
4055
4612
  return config.cloudApiKey ?? fileConfig?.cloud?.apiKey ?? process.env.FRAGMENTS_API_KEY;
4056
4613
  }
4057
- function resolveCloudUrl(fileConfig) {
4614
+ function resolveCloudUrl2(fileConfig) {
4058
4615
  return fileConfig?.cloud?.url ?? process.env.FRAGMENTS_CLOUD_URL;
4059
4616
  }
4060
4617
  function hasTsProject(projectRoot) {
@@ -4064,8 +4621,8 @@ function resolveDataAdapter(config, fileConfig) {
4064
4621
  const source = config.source ?? fileConfig?.source ?? "auto";
4065
4622
  const fragmentsJsonPaths = findFragmentsJson(config.projectRoot);
4066
4623
  const bundleManifestPaths = findBundleManifest(config.projectRoot);
4067
- const cloudApiKey = resolveCloudApiKey(config, fileConfig);
4068
- const cloudUrl = resolveCloudUrl(fileConfig);
4624
+ const cloudApiKey = resolveCloudApiKey2(config, fileConfig);
4625
+ const cloudUrl = resolveCloudUrl2(fileConfig);
4069
4626
  switch (source) {
4070
4627
  case "fragments-json":
4071
4628
  return { adapter: new FragmentsJsonAdapter(), mode: "fragments-json" };
@@ -4122,6 +4679,9 @@ function resolveSearchApiKey(config, fileConfig) {
4122
4679
  // src/server.ts
4123
4680
  var TOOL_NAMES = buildToolNames();
4124
4681
  var TOOLS = buildMcpTools();
4682
+ var TOOL_DEFINITION_BY_KEY = new Map(
4683
+ MCP_TOOL_DEFINITIONS.map((definition) => [definition.key, definition])
4684
+ );
4125
4685
  function createMcpServer(config) {
4126
4686
  const server = new Server(
4127
4687
  {
@@ -4157,38 +4717,53 @@ function createMcpServer(config) {
4157
4717
  }
4158
4718
  }
4159
4719
  let cachedData = null;
4720
+ let loadDataPromise = null;
4160
4721
  let resolvedRoot = null;
4722
+ let resolveProjectRootPromise = null;
4161
4723
  let componentIndex = null;
4162
4724
  let blockIndex = null;
4163
4725
  let tokenIndex = null;
4164
4726
  async function resolveProjectRoot() {
4165
4727
  if (resolvedRoot) return resolvedRoot;
4166
- try {
4167
- const result = await server.listRoots();
4168
- if (result.roots?.length > 0) {
4169
- const rootUri = result.roots[0].uri;
4170
- resolvedRoot = fileURLToPath(rootUri);
4171
- return resolvedRoot;
4728
+ if (resolveProjectRootPromise) return resolveProjectRootPromise;
4729
+ resolveProjectRootPromise = (async () => {
4730
+ try {
4731
+ const result = await server.listRoots();
4732
+ if (result.roots?.length > 0) {
4733
+ const rootUri = result.roots[0].uri;
4734
+ resolvedRoot = fileURLToPath(rootUri);
4735
+ return resolvedRoot;
4736
+ }
4737
+ } catch {
4172
4738
  }
4173
- } catch {
4739
+ resolvedRoot = config.projectRoot;
4740
+ return resolvedRoot;
4741
+ })();
4742
+ try {
4743
+ return await resolveProjectRootPromise;
4744
+ } finally {
4745
+ resolveProjectRootPromise = null;
4174
4746
  }
4175
- resolvedRoot = config.projectRoot;
4176
- return resolvedRoot;
4177
4747
  }
4178
4748
  async function loadData() {
4179
4749
  if (cachedData) return cachedData;
4180
- const projectRoot = await resolveProjectRoot();
4181
- cachedData = await adapter.load(projectRoot);
4182
- const allFragments = Object.values(cachedData.components);
4183
- const allBlocks = Object.values(cachedData.blocks ?? {});
4184
- componentIndex = buildComponentIndex(allFragments);
4185
- if (allBlocks.length > 0) {
4186
- blockIndex = buildBlockIndex(allBlocks);
4187
- }
4188
- if (cachedData.tokens && cachedData.tokens.total > 0) {
4189
- tokenIndex = buildTokenIndex(cachedData.tokens);
4750
+ if (loadDataPromise) return loadDataPromise;
4751
+ loadDataPromise = (async () => {
4752
+ const projectRoot = await resolveProjectRoot();
4753
+ const loaded = await adapter.load(projectRoot);
4754
+ const allFragments = Object.values(loaded.components);
4755
+ const allBlocks = Object.values(loaded.blocks ?? {});
4756
+ componentIndex = buildComponentIndex(allFragments);
4757
+ blockIndex = allBlocks.length > 0 ? buildBlockIndex(allBlocks) : null;
4758
+ tokenIndex = loaded.tokens && loaded.tokens.total > 0 ? buildTokenIndex(loaded.tokens) : null;
4759
+ cachedData = loaded;
4760
+ return loaded;
4761
+ })();
4762
+ try {
4763
+ return await loadDataPromise;
4764
+ } finally {
4765
+ loadDataPromise = null;
4190
4766
  }
4191
- return cachedData;
4192
4767
  }
4193
4768
  server.setRequestHandler(ListToolsRequestSchema, async () => {
4194
4769
  const data = await loadData();
@@ -4220,7 +4795,7 @@ function createMcpServer(config) {
4220
4795
  const packageJsonPath = join8(root, "package.json");
4221
4796
  if (existsSync9(packageJsonPath)) {
4222
4797
  try {
4223
- const content = readFileSync7(packageJsonPath, "utf-8");
4798
+ const content = readFileSync6(packageJsonPath, "utf-8");
4224
4799
  const pkg = JSON.parse(content);
4225
4800
  if (pkg.name) {
4226
4801
  return pkg.name;
@@ -4234,6 +4809,23 @@ function createMcpServer(config) {
4234
4809
  };
4235
4810
  try {
4236
4811
  const toolKey = registry.resolveKey(name);
4812
+ const definition = TOOL_DEFINITION_BY_KEY.get(toolKey);
4813
+ const argumentKeys = Object.keys(args ?? {});
4814
+ const allowedKeys = new Set(Object.keys(definition?.params ?? {}));
4815
+ const unknownKeys = definition ? argumentKeys.filter((key) => !allowedKeys.has(key)) : [];
4816
+ if (unknownKeys.length > 0) {
4817
+ return {
4818
+ content: [
4819
+ {
4820
+ type: "text",
4821
+ text: JSON.stringify({
4822
+ error: `Unknown argument(s) for ${toolKey}: ${unknownKeys.join(", ")}`
4823
+ })
4824
+ }
4825
+ ],
4826
+ isError: true
4827
+ };
4828
+ }
4237
4829
  const mCtx = {
4238
4830
  toolName: name,
4239
4831
  toolKey,
@@ -4290,4 +4882,4 @@ export {
4290
4882
  startMcpServer,
4291
4883
  createSandboxServer
4292
4884
  };
4293
- //# sourceMappingURL=chunk-LZW5KUXV.js.map
4885
+ //# sourceMappingURL=chunk-6JMX4AMO.js.map