@ikas/code-components-mcp 1.4.0-beta.22 → 1.4.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -98,16 +98,9 @@ function loadStorefrontTypes() {
98
98
  }
99
99
  return null;
100
100
  }
101
- const SUBTREE_KINDS = [
102
- "children",
103
- "components",
104
- "sub-components",
105
- ];
101
+ const SUBTREE_KINDS = ["children", "components", "sub-components"];
106
102
  function normalizeName(value) {
107
- return value
108
- .trim()
109
- .replace(/^`+|`+$/g, "")
110
- .trim();
103
+ return value.trim().replace(/^`+|`+$/g, "").trim();
111
104
  }
112
105
  const storefrontData = loadStorefrontData();
113
106
  const frameworkData = loadJsonFile("../data/framework.json");
@@ -268,11 +261,7 @@ function searchMigrationTopics(query) {
268
261
  const descScore = matchScore(topic.description, query) * 2;
269
262
  const contentScore = matchScore(topic.content, query);
270
263
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
271
- return {
272
- key,
273
- topic,
274
- score: titleScore + descScore + contentScore + tagScore,
275
- };
264
+ return { key, topic, score: titleScore + descScore + contentScore + tagScore };
276
265
  })
277
266
  .filter((item) => item.score > 0)
278
267
  .sort((a, b) => b.score - a.score);
@@ -299,7 +288,7 @@ function analyzeOldTheme(themeJson) {
299
288
  parts.push(`> **CRITICAL:** The old system (\`@ikas/storefront\`) and the new code-component system (\`@ikas/bp-storefront\`) are **entirely different packages**. Even when type names look the same (e.g., \`IkasImage\`, \`IkasProduct\`), they are different types with different properties. The prop type systems are also completely separate — old theme.json prop types and new ikas.config.json prop types have different semantics even when names match. **Never assume old-system knowledge applies to the new system.** Always use \`get_type_definition\`, \`get_model_guide\`, and \`get_function_doc\` to look up the correct new-system APIs.\n`);
300
289
  parts.push(`## Summary Statistics\n`);
301
290
  parts.push(`- **Components:** ${components.length}`);
302
- parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
291
+ parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
303
292
  parts.push(`- **Prop Groups:** ${groups.length}`);
304
293
  // Component analysis
305
294
  parts.push(`\n## Components (${components.length})\n`);
@@ -338,11 +327,7 @@ function analyzeOldTheme(themeJson) {
338
327
  const typesSummary = Object.entries(propTypeCounts)
339
328
  .map(([t, c]) => `${t}×${c}`)
340
329
  .join(", ");
341
- const headerFooter = comp.isHeader
342
- ? " [HEADER]"
343
- : comp.isFooter
344
- ? " [FOOTER]"
345
- : "";
330
+ const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
346
331
  parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
347
332
  parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
348
333
  parts.push(`- **Recommended new type:** section`);
@@ -357,7 +342,7 @@ function analyzeOldTheme(themeJson) {
357
342
  parts.push("");
358
343
  }
359
344
  // Custom data analysis
360
- const rootCustomData = customData.filter((cd) => cd.isRoot);
345
+ const rootCustomData = customData.filter(cd => cd.isRoot);
361
346
  if (rootCustomData.length > 0) {
362
347
  parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
363
348
  for (const cd of rootCustomData) {
@@ -367,9 +352,7 @@ function analyzeOldTheme(themeJson) {
367
352
  const describeNested = (items, indent) => {
368
353
  const lines = [];
369
354
  for (const item of items) {
370
- const key = item.key
371
- ? `\`${item.key}\``
372
- : item.typescriptName || item.name || "unnamed";
355
+ const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
373
356
  lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
374
357
  if (item.nestedData && item.nestedData.length > 0) {
375
358
  lines.push(...describeNested(item.nestedData, indent + " "));
@@ -381,7 +364,7 @@ function analyzeOldTheme(themeJson) {
381
364
  parts.push(...describeNested(cd.nestedData, " "));
382
365
  }
383
366
  if (cd.enumOptions && cd.enumOptions.length > 0) {
384
- parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
367
+ parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
385
368
  }
386
369
  // Find which components reference this customData
387
370
  const referencingComponents = [];
@@ -463,9 +446,7 @@ function scanSharedSubcomponents(sourceDir) {
463
446
  const walk = (dir) => {
464
447
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
465
448
  if (entry.isDirectory()) {
466
- if (entry.name === "node_modules" ||
467
- entry.name === "__generated__" ||
468
- entry.name.startsWith("."))
449
+ if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
469
450
  continue;
470
451
  walk(path.join(dir, entry.name));
471
452
  }
@@ -500,14 +481,10 @@ function scanSharedSubcomponents(sourceDir) {
500
481
  if (!importPath.startsWith("."))
501
482
  continue;
502
483
  // Skip imports of generated types, utils, hooks
503
- if (importPath.includes("__generated__") ||
504
- importPath.includes("/utils") ||
505
- importPath.includes("/hooks"))
484
+ if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
506
485
  continue;
507
486
  // Extract base name from path
508
- const pathSegments = importPath
509
- .split("/")
510
- .filter((s) => s && s !== "." && s !== "..");
487
+ const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
511
488
  if (pathSegments.length === 0)
512
489
  continue;
513
490
  const lastSegment = pathSegments[pathSegments.length - 1];
@@ -518,10 +495,7 @@ function scanSharedSubcomponents(sourceDir) {
518
495
  continue;
519
496
  seenInFile.add(key);
520
497
  if (!importUsage.has(key)) {
521
- importUsage.set(key, {
522
- usingComponents: new Set(),
523
- rawImportPath: importPath,
524
- });
498
+ importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
525
499
  }
526
500
  importUsage.get(key).usingComponents.add(componentDir);
527
501
  }
@@ -530,7 +504,7 @@ function scanSharedSubcomponents(sourceDir) {
530
504
  const shared = [];
531
505
  for (const [name, { usingComponents, rawImportPath }] of importUsage) {
532
506
  // Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
533
- const users = [...usingComponents].filter((c) => c !== name);
507
+ const users = [...usingComponents].filter(c => c !== name);
534
508
  if (users.length >= 3) {
535
509
  shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
536
510
  }
@@ -547,7 +521,7 @@ function toKebabCase(s) {
547
521
  }
548
522
  function classifyComplexity(comp, customDataMap) {
549
523
  const props = comp.props || [];
550
- const customCount = props.filter((p) => p.type === "CUSTOM").length;
524
+ const customCount = props.filter(p => p.type === "CUSTOM").length;
551
525
  if (customCount === 0 && props.length < 10)
552
526
  return "simple";
553
527
  // Check for deeply nested CUSTOM (customData referencing another customData)
@@ -558,9 +532,7 @@ function classifyComplexity(comp, customDataMap) {
558
532
  if (cd?.nestedData) {
559
533
  const hasNested = (items) => {
560
534
  for (const item of items) {
561
- if (item.type === "DYNAMIC_LIST" ||
562
- item.type === "STATIC_LIST" ||
563
- item.customDataId)
535
+ if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
564
536
  return true;
565
537
  if (item.nestedData && hasNested(item.nestedData))
566
538
  return true;
@@ -592,7 +564,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
592
564
  parts.push(`# Theme Migration Plan — \`${projectName}\``);
593
565
  parts.push("");
594
566
  parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
595
- parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
567
+ parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
596
568
  parts.push("");
597
569
  parts.push(`> ## READ THIS FIRST`);
598
570
  parts.push(`>`);
@@ -682,22 +654,16 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
682
654
  const cdType = cd.type || "?";
683
655
  let shape = "";
684
656
  if (cd.type === "ENUM") {
685
- const opts = (cd.enumOptions || [])
686
- .map((o) => o.value || o.displayName)
687
- .filter(Boolean);
657
+ const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
688
658
  shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
689
659
  }
690
660
  else if (cd.nestedData && cd.nestedData.length > 0) {
691
661
  const first = cd.nestedData[0];
692
- const fields = (first?.nestedData || cd.nestedData || [])
693
- .map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
694
- .slice(0, 8);
662
+ const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
695
663
  shape = ` — shape: \`{${fields.join(", ")}}\``;
696
664
  }
697
665
  const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
698
- const usedByStr = usedBy.length > 0
699
- ? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}`
700
- : ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
666
+ const usedByStr = usedBy.length > 0 ? ` — used by: ${usedBy.slice(0, 6).join(", ")}${usedBy.length > 6 ? `, +${usedBy.length - 6} more` : ""}` : ` — _not directly referenced by any section's props (may be nested inside another customData)_`;
701
667
  parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
702
668
  }
703
669
  parts.push("");
@@ -731,11 +697,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
731
697
  const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
732
698
  const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
733
699
  const newId = `${projectName}-${kebabName}`;
734
- const headerFooter = comp.isHeader
735
- ? " **[HEADER]**"
736
- : comp.isFooter
737
- ? " **[FOOTER]**"
738
- : "";
700
+ const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
739
701
  const propCount = (comp.props || []).length;
740
702
  // Detect children from CUSTOM DYNAMIC_LIST props
741
703
  const children = [];
@@ -745,13 +707,8 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
745
707
  if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
746
708
  const itemObj = cd.nestedData?.[0];
747
709
  if (itemObj) {
748
- const childName = itemObj.typescriptName ||
749
- (itemObj.name
750
- ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
751
- : `${oldName}Item`);
752
- const fields = (itemObj.nestedData || [])
753
- .map((f) => f.key || f.name || "?")
754
- .slice(0, 8);
710
+ const childName = itemObj.typescriptName || (itemObj.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${oldName}Item`);
711
+ const fields = (itemObj.nestedData || []).map((f) => f.key || f.name || "?").slice(0, 8);
755
712
  children.push({ propName: p.name || "?", childName, fields });
756
713
  }
757
714
  }
@@ -810,25 +767,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
810
767
  }
811
768
  // Known libraries we detect in old themes and want to flag for replacement
812
769
  const KNOWN_LIBRARIES = [
813
- "swiper",
814
- "@headlessui/react",
815
- "@heroicons/react",
816
- "recharts",
817
- "react-player",
818
- "react-simple-star-rating",
819
- "react-slider",
820
- "react-compound-slider",
821
- "react-zoom-pan-pinch",
822
- "react-hot-toast",
823
- "react-fast-marquee",
824
- "react-indiana-drag-scroll",
825
- "react-simple-typewriter",
826
- "react-timer-hook",
827
- "date-fns",
828
- "slugify",
829
- "classnames",
830
- "clsx",
831
- "@react-pdf/renderer",
770
+ "swiper", "@headlessui/react", "@heroicons/react", "recharts",
771
+ "react-player", "react-simple-star-rating", "react-slider", "react-compound-slider",
772
+ "react-zoom-pan-pinch", "react-hot-toast", "react-fast-marquee",
773
+ "react-indiana-drag-scroll", "react-simple-typewriter", "react-timer-hook",
774
+ "date-fns", "slugify", "classnames", "clsx", "@react-pdf/renderer",
832
775
  ];
833
776
  // Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
834
777
  const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
@@ -847,8 +790,7 @@ function scanSectionSource(componentDir, propNames) {
847
790
  // Collect all .tsx/.ts files in the component dir
848
791
  try {
849
792
  for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
850
- if (entry.isFile() &&
851
- (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
793
+ if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
852
794
  result.sourceFiles.push(path.join(componentDir, entry.name));
853
795
  }
854
796
  }
@@ -865,18 +807,7 @@ function scanSectionSource(componentDir, propNames) {
865
807
  const subCompSet = new Map();
866
808
  const callSet = new Set();
867
809
  // Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
868
- const REACT_PACKAGES = new Set([
869
- "react",
870
- "react-dom",
871
- "next",
872
- "next/link",
873
- "next/image",
874
- "next/router",
875
- "next/head",
876
- "next/script",
877
- "mobx-react-lite",
878
- "mobx",
879
- ]);
810
+ const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
880
811
  for (const file of result.sourceFiles) {
881
812
  let content;
882
813
  try {
@@ -890,7 +821,7 @@ function scanSectionSource(componentDir, propNames) {
890
821
  while ((m = importRegex.exec(content)) !== null) {
891
822
  const p = m[1];
892
823
  if (p.startsWith(".")) {
893
- const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
824
+ const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
894
825
  const last = segs[segs.length - 1];
895
826
  if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
896
827
  subCompSet.set(last, p);
@@ -898,9 +829,7 @@ function scanSectionSource(componentDir, propNames) {
898
829
  }
899
830
  else if (!p.startsWith("@ikas/")) {
900
831
  // Classify: known library, react-family, or unknown-external
901
- const base = p.startsWith("@")
902
- ? p.split("/").slice(0, 2).join("/")
903
- : p.split("/")[0];
832
+ const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
904
833
  if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
905
834
  reactSet.add(base);
906
835
  }
@@ -963,10 +892,7 @@ function scanSectionSource(componentDir, propNames) {
963
892
  }
964
893
  }
965
894
  }
966
- result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
967
- name,
968
- path: p,
969
- }));
895
+ result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
970
896
  result.importedLibraries = [...libSet].sort();
971
897
  result.importedUnknownLibraries = [...unknownLibSet].sort();
972
898
  result.reactPackageUsage = [...reactSet].sort();
@@ -982,38 +908,27 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
982
908
  customDataMap.set(cd.id, cd);
983
909
  }
984
910
  // Find the component — try match by dir, displayName, id, or new-id
985
- const target = components.find((c) => {
911
+ const target = components.find(c => {
986
912
  if (!c)
987
913
  return false;
988
- if (c.dir === sectionName ||
989
- c.displayName === sectionName ||
990
- c.id === sectionName)
914
+ if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
991
915
  return true;
992
916
  const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
993
917
  const newId = `${projectName}-${kebab}`;
994
918
  return newId === sectionName;
995
919
  });
996
920
  if (!target) {
997
- const available = components
998
- .map((c) => c.dir || c.displayName || c.id)
999
- .filter(Boolean)
1000
- .join(", ");
921
+ const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
1001
922
  return `Section "${sectionName}" not found in theme. Available: ${available}`;
1002
923
  }
1003
924
  const parts = [];
1004
925
  const oldName = target.displayName || target.dir || target.id || "Unknown";
1005
926
  const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
1006
927
  const sectionId = `${projectName}-${kebabName}`;
1007
- const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") ||
1008
- kebabName
1009
- .split("-")
1010
- .map((s) => s[0]?.toUpperCase() + s.slice(1))
1011
- .join("");
928
+ const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
1012
929
  // Scan the old source for imports, libraries, field usage
1013
- const propNames = (target.props || [])
1014
- .map((p) => p.name || "")
1015
- .filter(Boolean);
1016
- const sourceScan = oldSourceDir && target.dir
930
+ const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
931
+ const sourceScan = (oldSourceDir && target.dir)
1017
932
  ? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
1018
933
  : null;
1019
934
  parts.push(`# Section Migration Plan: ${oldName}`);
@@ -1059,7 +974,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1059
974
  }
1060
975
  if (sourceScan.reactPackageUsage.length > 0) {
1061
976
  parts.push("");
1062
- parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map((p) => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
977
+ parts.push(`**React/Next.js framework imports detected:** ${sourceScan.reactPackageUsage.map(p => `\`${p}\``).join(", ")}. These do NOT carry over — the new system is Preact. See \`get_migration_guide("react-to-preact")\` for conversion patterns (hooks, observer, event types, routing).`);
1063
978
  }
1064
979
  if (sourceScan.oldStorefrontCalls.length > 0) {
1065
980
  parts.push("");
@@ -1105,11 +1020,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1105
1020
  if (oldType === "SLIDER") {
1106
1021
  newType = "NUMBER";
1107
1022
  notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
1108
- const prop = {
1109
- name: newName,
1110
- displayName: p.displayName || newName,
1111
- type: "NUMBER",
1112
- };
1023
+ const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
1113
1024
  if (p.isRequired)
1114
1025
  prop.required = true;
1115
1026
  parentPropsJson.push(prop);
@@ -1117,11 +1028,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1117
1028
  else if (oldType === "PRODUCT_DETAIL") {
1118
1029
  newType = "PRODUCT";
1119
1030
  notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
1120
- const prop = {
1121
- name: newName,
1122
- displayName: p.displayName || newName,
1123
- type: "PRODUCT",
1124
- };
1031
+ const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
1125
1032
  if (p.isRequired)
1126
1033
  prop.required = true;
1127
1034
  parentPropsJson.push(prop);
@@ -1132,10 +1039,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1132
1039
  if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
1133
1040
  // Child component needed
1134
1041
  const itemObj = cd.nestedData?.[0];
1135
- const childName = itemObj?.typescriptName ||
1136
- (itemObj?.name
1137
- ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
1138
- : `${sectionPascal}Item`);
1042
+ const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1139
1043
  const childProps = [];
1140
1044
  const nestedWarnings = [];
1141
1045
  for (const f of (itemObj?.nestedData || [])) {
@@ -1146,18 +1050,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1146
1050
  fType = "NUMBER";
1147
1051
  else if (fType === "PRODUCT_DETAIL")
1148
1052
  fType = "PRODUCT";
1149
- else if (fType === "CUSTOM" ||
1150
- fType === "DYNAMIC_LIST" ||
1151
- fType === "STATIC_LIST" ||
1152
- fType === "OBJECT") {
1053
+ else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
1153
1054
  nestedWarnings.push(`\`${f.key}\` (${fType})`);
1154
1055
  fType = "COMPONENT_LIST";
1155
1056
  }
1156
- const prop = {
1157
- name: f.key,
1158
- displayName: f.name || f.key,
1159
- type: fType,
1160
- };
1057
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1161
1058
  if (f.isRequired)
1162
1059
  prop.required = true;
1163
1060
  childProps.push(prop);
@@ -1169,14 +1066,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1169
1066
  if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
1170
1067
  const inferred = sourceScan.propFieldUsage[oldName];
1171
1068
  for (const fieldName of inferred) {
1172
- childProps.push({
1173
- name: fieldName,
1174
- displayName: fieldName,
1175
- type: "TEXT",
1176
- });
1069
+ childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
1177
1070
  }
1178
1071
  if (inferred.length > 0) {
1179
- notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map((f) => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
1072
+ notes = `Was CUSTOM → ${cd.type} with empty customData. **Inferred props from source usage:** ${inferred.map(f => `\`${f}\``).join(", ")}. Verify types and required flags — default inferred type is TEXT.`;
1180
1073
  }
1181
1074
  }
1182
1075
  children.push({
@@ -1205,11 +1098,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1205
1098
  let fType = f.type;
1206
1099
  if (fType === "SLIDER")
1207
1100
  fType = "NUMBER";
1208
- const prop = {
1209
- name: f.key,
1210
- displayName: f.name || f.key,
1211
- type: fType,
1212
- };
1101
+ const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1213
1102
  if (f.isRequired)
1214
1103
  prop.required = true;
1215
1104
  parentPropsJson.push(prop);
@@ -1224,8 +1113,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1224
1113
  }
1225
1114
  else if (cd.type === "ENUM") {
1226
1115
  newType = "ENUM";
1227
- const enumName = cd.typescriptName ||
1228
- (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1116
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1229
1117
  const options = (cd.enumOptions || []).reduce((acc, o) => {
1230
1118
  if (o.displayName && o.value)
1231
1119
  acc[o.displayName] = o.value;
@@ -1233,12 +1121,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1233
1121
  }, {});
1234
1122
  enumsNeeded.push({ name: enumName, options });
1235
1123
  notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
1236
- const prop = {
1237
- name: newName,
1238
- displayName: p.displayName || newName,
1239
- type: "ENUM",
1240
- enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
1241
- };
1124
+ const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
1242
1125
  if (p.isRequired)
1243
1126
  prop.required = true;
1244
1127
  parentPropsJson.push(prop);
@@ -1247,11 +1130,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1247
1130
  }
1248
1131
  else {
1249
1132
  // Direct mapping
1250
- const prop = {
1251
- name: newName,
1252
- displayName: p.displayName || newName,
1253
- type: newType,
1254
- };
1133
+ const prop = { name: newName, displayName: p.displayName || newName, type: newType };
1255
1134
  if (p.isRequired)
1256
1135
  prop.required = true;
1257
1136
  parentPropsJson.push(prop);
@@ -1260,9 +1139,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1260
1139
  }
1261
1140
  parts.push("");
1262
1141
  // Custom Data Decision Callouts — per prop referencing a customData type
1263
- const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
1264
- p.customDataId &&
1265
- customDataMap.has(p.customDataId));
1142
+ const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" && p.customDataId && customDataMap.has(p.customDataId));
1266
1143
  if (customDataPropsForCallouts.length > 0) {
1267
1144
  parts.push(`## Custom Data Decisions to Make`);
1268
1145
  parts.push("");
@@ -1277,9 +1154,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1277
1154
  let shape = "";
1278
1155
  let shapeKind = "unknown";
1279
1156
  if (cd.type === "ENUM") {
1280
- const opts = (cd.enumOptions || [])
1281
- .map((o) => o.value || o.displayName)
1282
- .filter(Boolean);
1157
+ const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
1283
1158
  shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
1284
1159
  shapeKind = "enum";
1285
1160
  }
@@ -1312,8 +1187,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1312
1187
  if (shapeKind === "enum") {
1313
1188
  parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
1314
1189
  parts.push("");
1315
- const enumName = cd.typescriptName ||
1316
- (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1190
+ const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1317
1191
  const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
1318
1192
  if (o.displayName && o.value)
1319
1193
  acc[o.displayName] = o.value;
@@ -1348,10 +1222,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1348
1222
  parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
1349
1223
  parts.push("");
1350
1224
  }
1351
- const compName = cd.typescriptName ||
1352
- (cd.name
1353
- ? cd.name.replace(/[^a-zA-Z0-9]/g, "")
1354
- : `${sectionPascal}Item`);
1225
+ const compName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1355
1226
  const compPropsForCli = [];
1356
1227
  for (const f of fieldSource) {
1357
1228
  if (!f.key)
@@ -1361,11 +1232,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1361
1232
  fType = "NUMBER";
1362
1233
  else if (fType === "PRODUCT_DETAIL")
1363
1234
  fType = "PRODUCT";
1364
- compPropsForCli.push({
1365
- name: f.key,
1366
- displayName: f.name || f.key,
1367
- type: fType,
1368
- });
1235
+ compPropsForCli.push({ name: f.key, displayName: f.name || f.key, type: fType });
1369
1236
  }
1370
1237
  parts.push("```bash");
1371
1238
  if (isMinimal) {
@@ -1404,10 +1271,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1404
1271
  existing.usedByProps.push(ch.propName);
1405
1272
  }
1406
1273
  else {
1407
- uniqueChildren.set(ch.childName, {
1408
- child: ch,
1409
- usedByProps: [ch.propName],
1410
- });
1274
+ uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
1411
1275
  }
1412
1276
  }
1413
1277
  if (uniqueChildren.size > 0) {
@@ -1420,7 +1284,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1420
1284
  for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1421
1285
  parts.push(`### \`${ch.childName}\``);
1422
1286
  const propsLabel = usedByProps.length > 1
1423
- ? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1287
+ ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1424
1288
  : `For parent prop: \`${usedByProps[0]}\``;
1425
1289
  parts.push(propsLabel);
1426
1290
  parts.push(`Old customData: "${ch.customDataName}"`);
@@ -1466,15 +1330,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1466
1330
  parts.push(`- **Every COMPONENT_LIST slot needs a child component to render individual items.** A product list needs a ProductCard child, a blog list needs a BlogCard child, etc. Check if the child already exists (run \`config list\` to see all components and their opaque ids; reuse the existing id in \`filteredComponentIds\`). If not, create it as a registered component and use the \`componentId\` from the CLI's response.`);
1467
1331
  }
1468
1332
  // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1469
- const dataListProps = (target.props || []).filter((p) => p.type === "PRODUCT_LIST" ||
1470
- p.type === "BLOG_LIST" ||
1471
- p.type === "CATEGORY_LIST" ||
1472
- p.type === "BRAND_LIST");
1333
+ const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_LIST");
1473
1334
  if (dataListProps.length > 0) {
1474
1335
  parts.push("");
1475
1336
  parts.push(`### Data-Driven List Rendering`);
1476
1337
  parts.push("");
1477
- parts.push(`This section has data-driven list props: ${dataListProps.map((p) => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1338
+ parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1478
1339
  parts.push(`These are NOT COMPONENT_LIST — the data comes from dynamic queries (filters, categories, search), not hand-picked items. Render items **internally** by mapping over the data:`);
1479
1340
  parts.push("");
1480
1341
  parts.push("```tsx");
@@ -1501,22 +1362,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1501
1362
  parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
1502
1363
  }
1503
1364
  // Detect form-page sections (0 or few props, name suggests a form/auth page)
1504
- const formKeywords = [
1505
- "login",
1506
- "register",
1507
- "forgot",
1508
- "recover",
1509
- "password",
1510
- "account",
1511
- "email",
1512
- "verification",
1513
- "activate",
1514
- "contact",
1515
- "checkout",
1516
- "address",
1517
- ];
1365
+ const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
1518
1366
  const lowerDir = (target.dir || "").toLowerCase();
1519
- const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
1367
+ const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
1520
1368
  if (isLikelyFormPage) {
1521
1369
  parts.push("");
1522
1370
  parts.push(`### Form Page Pattern`);
@@ -1545,9 +1393,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1545
1393
  // Fallback heuristic only when source scan unavailable
1546
1394
  const heuristicLibs = [];
1547
1395
  const lowerName = oldName.toLowerCase();
1548
- if (lowerName.includes("slider") ||
1549
- lowerName.includes("carousel") ||
1550
- lowerName.includes("banner")) {
1396
+ if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
1551
1397
  heuristicLibs.push("swiper");
1552
1398
  }
1553
1399
  if (lowerName.includes("marquee"))
@@ -1556,9 +1402,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1556
1402
  heuristicLibs.push("react-player");
1557
1403
  if (lowerName.includes("chart"))
1558
1404
  heuristicLibs.push("recharts");
1559
- if (lowerName.includes("star") ||
1560
- lowerName.includes("rating") ||
1561
- lowerName.includes("review"))
1405
+ if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
1562
1406
  heuristicLibs.push("react-simple-star-rating");
1563
1407
  if (heuristicLibs.length > 0) {
1564
1408
  parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
@@ -1630,7 +1474,8 @@ function levenshtein(a, b) {
1630
1474
  const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
1631
1475
  curr[j] = Math.min(curr[j - 1] + 1, // insertion
1632
1476
  prev[j] + 1, // deletion
1633
- prev[j - 1] + cost);
1477
+ prev[j - 1] + cost // substitution
1478
+ );
1634
1479
  }
1635
1480
  for (let j = 0; j <= n; j++)
1636
1481
  prev[j] = curr[j];
@@ -1661,29 +1506,17 @@ function searchFunctions(query) {
1661
1506
  const scored = storefrontData.functions
1662
1507
  .map((fn) => {
1663
1508
  const nameScore = matchScore(fn.name, query) * 3;
1664
- const displayNameScore = fn.displayName
1665
- ? matchScore(fn.displayName, query) * 3
1666
- : 0;
1509
+ const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
1667
1510
  const descScore = matchScore(fn.description, query);
1668
- const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
1669
- ? 5
1670
- : 0;
1511
+ const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
1671
1512
  const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
1672
1513
  ? 2
1673
1514
  : 0;
1674
1515
  const sigScore = matchScore(fn.signature, query) * 2;
1675
- const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
1676
- ? 8
1677
- : 0;
1516
+ const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
1678
1517
  return {
1679
1518
  fn,
1680
- score: nameScore +
1681
- displayNameScore +
1682
- descScore +
1683
- catScore +
1684
- paramScore +
1685
- sigScore +
1686
- typeScore,
1519
+ score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
1687
1520
  };
1688
1521
  })
1689
1522
  .filter((item) => item.score > 0)
@@ -1697,11 +1530,7 @@ function searchFrameworkTopics(query) {
1697
1530
  const descScore = matchScore(topic.description, query) * 2;
1698
1531
  const contentScore = matchScore(topic.content, query);
1699
1532
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
1700
- return {
1701
- key,
1702
- topic,
1703
- score: titleScore + descScore + contentScore + tagScore,
1704
- };
1533
+ return { key, topic, score: titleScore + descScore + contentScore + tagScore };
1705
1534
  })
1706
1535
  .filter((item) => item.score > 0)
1707
1536
  .sort((a, b) => b.score - a.score);
@@ -1716,9 +1545,7 @@ function searchTypes(query) {
1716
1545
  const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
1717
1546
  ? 4
1718
1547
  : 0;
1719
- const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
1720
- ? 4
1721
- : 0;
1548
+ const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0) ? 4 : 0;
1722
1549
  return { td, score: nameScore + domainScore + propScore + enumScore };
1723
1550
  })
1724
1551
  .filter((item) => item.score > 0)
@@ -1760,12 +1587,8 @@ function formatFunctionDoc(fn) {
1760
1587
  return lines.join("\n");
1761
1588
  }
1762
1589
  function formatFunctionSummary(fn) {
1763
- const desc = fn.description
1764
- ? fn.description.split(".")[0] + "."
1765
- : "No description.";
1766
- const alias = fn.displayName && fn.displayName !== fn.name
1767
- ? ` (alias: ${fn.displayName})`
1768
- : "";
1590
+ const desc = fn.description ? fn.description.split(".")[0] + "." : "No description.";
1591
+ const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
1769
1592
  return `- \`${fn.name}\`${alias} - ${desc}`;
1770
1593
  }
1771
1594
  function formatTypeDefinition(td, opts = {}) {
@@ -1806,9 +1629,7 @@ function formatTypeDefinition(td, opts = {}) {
1806
1629
  const fn = storefrontData.functions.find((f) => f.name === fnName);
1807
1630
  if (fn) {
1808
1631
  const desc = fn.description ? fn.description.split(".")[0] + "." : "";
1809
- const alias = fn.displayName && fn.displayName !== fn.name
1810
- ? ` (alias: ${fn.displayName})`
1811
- : "";
1632
+ const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
1812
1633
  lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
1813
1634
  lines.push(` \`${fn.signature}\``);
1814
1635
  }
@@ -1817,9 +1638,7 @@ function formatTypeDefinition(td, opts = {}) {
1817
1638
  }
1818
1639
  }
1819
1640
  lines.push("");
1820
- lines.push('Use `get_functions_for_type("' +
1821
- td.name +
1822
- '")` for full documentation of these functions.');
1641
+ lines.push("Use `get_functions_for_type(\"" + td.name + "\")` for full documentation of these functions.");
1823
1642
  }
1824
1643
  return lines.join("\n");
1825
1644
  }
@@ -1837,13 +1656,7 @@ const server = new McpServer({
1837
1656
  name: "ikas-code-components",
1838
1657
  version: "0.1.0",
1839
1658
  }, {
1840
- instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.\n\n" +
1841
- "Live-editor actions (require `ikas-component dev` running with the editor connected): `list_editor_pages` → page ids; `list_imported_sections` → imported component ids; `import_section` → import a built component; `add_section_to_page` → place a section on a page; `list_page_sections` → the sections already placed on a page, each with its per-placement `elementId`, current `propValues`, and blueprint `props`; `update_section_prop` → change a single prop value of a placed section; `upload_image` → upload an image (file or URL) and get an image id to use in an IMAGE prop value.\n\n" +
1842
- "To CHANGE/EDIT a prop value (text, color, boolean, number, etc.) of a section that is already on a page, this IS supported: call `list_page_sections(page_id)` to get the placement `elementId` and the prop names/ids, then `update_section_prop(page_id, element_id, prop_name|prop_id, value)`. Do not assume prop editing is unavailable.\n\n" +
1843
- "TWO DISTINCT JOBS — do not confuse them, and do not jump to writing code for the second one:\n" +
1844
- "(A) DEFINING props / authoring a component = changing which props a component HAS. This edits the component source/config (types.ts via `ikas-component config add-prop`/`add-component`, JSX in index.tsx, etc.) and requires a rebuild. Use this ONLY when the needed prop does not exist yet, or the user explicitly asks to build/modify the component's code or prop schema.\n" +
1845
- "(B) SETTING prop VALUES / filling content = giving values to props that ALREADY exist on a section placed on a page. This is pure data entry via `list_page_sections` + `update_section_prop` (and `upload_image` for images). It writes NO code and needs NO rebuild.\n" +
1846
- "When the user says things like 'fill this section', 'set the heading/title/text/image/link', 'populate', 'change the content/value', or 'enter this data', that is JOB (B): use `update_section_prop`. First call `list_page_sections` and check the existing `props` — if the prop is already there (it usually is), just set its value. Only fall back to JOB (A) if the required prop genuinely does not exist in that section's `props`. Writing a new component or adding a prop to fill in a value the section already supports is the wrong move.",
1659
+ instructions: "Examples and section templates from this server are API reference only — reuse imports, function calls, and data-access patterns; create your own JSX structure, CSS class names, and visual design.",
1847
1660
  });
1848
1661
  // Tool: search_docs
1849
1662
  server.tool("search_docs", "Search across all ikas storefront API docs, framework guides, and migration guides. Returns matching functions, framework topics, and migration topics ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
@@ -1884,20 +1697,13 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1884
1697
  parts.push("");
1885
1698
  parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
1886
1699
  }
1887
- if (functions.length === 0 &&
1888
- topics.length === 0 &&
1889
- types.length === 0 &&
1890
- migrationTopics.length === 0) {
1700
+ if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
1891
1701
  parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
1892
1702
  }
1893
1703
  return { content: [{ type: "text", text: parts.join("\n") }] };
1894
1704
  });
1895
1705
  // Tool: get_function_doc
1896
- server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
1897
- name: z
1898
- .string()
1899
- .describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
1900
- }, async ({ name }) => {
1706
+ server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", { name: z.string().describe("Function name (e.g. 'addItemToCart', 'Router.navigate')") }, async ({ name }) => {
1901
1707
  const nameLower = name.toLowerCase();
1902
1708
  // Phase 1: canonical-name match wins. A real function name always outranks
1903
1709
  // any displayName alias so aliases can never shadow the function they're
@@ -1905,9 +1711,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1905
1711
  // [BP-DISPLAY-NAME: hasCustomer] alias).
1906
1712
  const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
1907
1713
  if (byName) {
1908
- return {
1909
- content: [{ type: "text", text: formatFunctionDoc(byName) }],
1910
- };
1714
+ return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
1911
1715
  }
1912
1716
  // Phase 2: fall back to displayName aliases.
1913
1717
  const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
@@ -1915,9 +1719,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1915
1719
  const fn = byAlias[0];
1916
1720
  const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
1917
1721
  return {
1918
- content: [
1919
- { type: "text", text: note + formatFunctionDoc(fn) },
1920
- ],
1722
+ content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
1921
1723
  };
1922
1724
  }
1923
1725
  if (byAlias.length > 1) {
@@ -1935,9 +1737,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1935
1737
  (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1936
1738
  if (matches.length > 0) {
1937
1739
  const suggestions = matches.slice(0, 5).map((f) => {
1938
- const alias = f.displayName && f.displayName !== f.name
1939
- ? ` (alias: ${f.displayName})`
1940
- : "";
1740
+ const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1941
1741
  return ` - ${f.name}${alias}`;
1942
1742
  });
1943
1743
  return {
@@ -1950,12 +1750,7 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1950
1750
  };
1951
1751
  }
1952
1752
  return {
1953
- content: [
1954
- {
1955
- type: "text",
1956
- text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
1957
- },
1958
- ],
1753
+ content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1959
1754
  };
1960
1755
  });
1961
1756
  // Tool: list_functions
@@ -2005,7 +1800,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
2005
1800
  if (uncategorized > 0) {
2006
1801
  lines.push(`- \`Other\` (${uncategorized})`);
2007
1802
  }
2008
- lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
1803
+ lines.push("", "Call `list_functions(category: \"<name>\")` to see one-line summaries for a category.");
2009
1804
  return { content: [{ type: "text", text: lines.join("\n") }] };
2010
1805
  }
2011
1806
  const catLower = category.toLowerCase();
@@ -2037,11 +1832,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
2037
1832
  return { content: [{ type: "text", text: parts.join("\n") }] };
2038
1833
  });
2039
1834
  // Tool: get_code_example
2040
- server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", {
2041
- task: z
2042
- .string()
2043
- .describe("Task description or example ID (call `list_examples()` for the full list)"),
2044
- }, async ({ task }) => {
1835
+ server.tool("get_code_example", "Get an API usage reference for a specific task. Shows correct function calls, imports, and data-handling patterns from a real production theme. The JSX layout and CSS are illustrative only — create your own original visual design. Call `list_examples()` to see available example IDs.", { task: z.string().describe("Task description or example ID (call `list_examples()` for the full list)") }, async ({ task }) => {
2045
1836
  const taskLower = task.toLowerCase();
2046
1837
  // Try exact ID match first
2047
1838
  let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
@@ -2061,9 +1852,7 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
2061
1852
  }
2062
1853
  }
2063
1854
  if (!example) {
2064
- const available = storefrontData.codeExamples
2065
- .map((e) => ` - \`${e.id}\` - ${e.title}`)
2066
- .join("\n");
1855
+ const available = storefrontData.codeExamples.map((e) => ` - \`${e.id}\` - ${e.title}`).join("\n");
2067
1856
  return {
2068
1857
  content: [
2069
1858
  {
@@ -2086,24 +1875,14 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
2086
1875
  if (example.files && example.files.length > 0) {
2087
1876
  for (const file of example.files) {
2088
1877
  const ext = file.filename.split(".").pop() || "text";
2089
- const lang = ext === "tsx" || ext === "ts"
2090
- ? "typescript"
2091
- : ext === "css"
2092
- ? "css"
2093
- : ext === "json"
2094
- ? "json"
2095
- : "text";
1878
+ const lang = ext === "tsx" || ext === "ts" ? "typescript" : ext === "css" ? "css" : ext === "json" ? "json" : "text";
2096
1879
  // Add inline originality comments to CSS and TSX files
2097
1880
  let content = file.content;
2098
1881
  if (ext === "css") {
2099
- content =
2100
- "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
2101
- content;
1882
+ content = "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" + content;
2102
1883
  }
2103
1884
  else if (ext === "tsx") {
2104
- content =
2105
- "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
2106
- content;
1885
+ content = "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" + content;
2107
1886
  }
2108
1887
  parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
2109
1888
  }
@@ -2117,88 +1896,63 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
2117
1896
  return { content: [{ type: "text", text: parts.join("\n") }] };
2118
1897
  });
2119
1898
  // Tool: get_framework_guide
2120
- server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", {
2121
- topic: z
2122
- .string()
2123
- .describe("Topic key or keyword (call `list_topics()` for the full list)"),
2124
- }, async ({ topic }) => {
1899
+ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e.g. 'ai-workflow', 'common-pitfalls', 'prop-types', 'css-scoping', 'form-handling'). Call `list_topics()` to see all available topic keys.", { topic: z.string().describe("Topic key or keyword (call `list_topics()` for the full list)") }, async ({ topic }) => {
2125
1900
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
2126
1901
  // Alias mapping for common alternative topic names
2127
1902
  const topicAliases = {
2128
1903
  "form-handling": "form-patterns",
2129
- forms: "form-patterns",
1904
+ "forms": "form-patterns",
2130
1905
  "data-fetching": "async-data-patterns",
2131
- async: "async-data-patterns",
2132
- loading: "async-data-patterns",
1906
+ "async": "async-data-patterns",
1907
+ "loading": "async-data-patterns",
2133
1908
  "sub-components": "sub-component-patterns",
2134
- subcomponents: "sub-component-patterns",
2135
- routing: "navigation-patterns",
2136
- router: "navigation-patterns",
2137
- observer: "component-structure",
2138
- reactivity: "component-structure",
2139
- pitfalls: "common-pitfalls",
2140
- gotchas: "common-pitfalls",
2141
- mistakes: "common-pitfalls",
2142
- header: "header-footer-patterns",
2143
- footer: "header-footer-patterns",
2144
- blog: "blog-patterns",
2145
- cart: "cart-patterns",
2146
- account: "account-patterns",
1909
+ "subcomponents": "sub-component-patterns",
1910
+ "routing": "navigation-patterns",
1911
+ "router": "navigation-patterns",
1912
+ "observer": "component-structure",
1913
+ "reactivity": "component-structure",
1914
+ "pitfalls": "common-pitfalls",
1915
+ "gotchas": "common-pitfalls",
1916
+ "mistakes": "common-pitfalls",
1917
+ "header": "header-footer-patterns",
1918
+ "footer": "header-footer-patterns",
1919
+ "blog": "blog-patterns",
1920
+ "cart": "cart-patterns",
1921
+ "account": "account-patterns",
2147
1922
  "product-detail": "product-detail-patterns",
2148
1923
  "product-list": "product-list-patterns",
2149
- filtering: "product-list-patterns",
2150
- reviews: "review-patterns",
2151
- slider: "slider-overlay-patterns",
2152
- overlay: "slider-overlay-patterns",
2153
- modal: "slider-overlay-patterns",
2154
- architecture: "real-world-architecture",
2155
- theme: "real-world-architecture",
1924
+ "filtering": "product-list-patterns",
1925
+ "reviews": "review-patterns",
1926
+ "slider": "slider-overlay-patterns",
1927
+ "overlay": "slider-overlay-patterns",
1928
+ "modal": "slider-overlay-patterns",
1929
+ "architecture": "real-world-architecture",
1930
+ "theme": "real-world-architecture",
2156
1931
  "global-styles": "global-css",
2157
- global: "global-css",
1932
+ "global": "global-css",
2158
1933
  "css-variables": "global-css",
2159
1934
  "custom-properties": "global-css",
2160
1935
  };
2161
1936
  const resolvedTopic = topicAliases[topicLower] || topicLower;
2162
1937
  // Topics that involve MobX store reads get a reminder about root reactivity
2163
1938
  const storeTopics = new Set([
2164
- "product-detail-patterns",
2165
- "product-list-patterns",
2166
- "cart-patterns",
2167
- "account-patterns",
2168
- "header-footer-patterns",
2169
- "review-patterns",
2170
- "blog-patterns",
2171
- "form-handling",
2172
- "async-data-patterns",
2173
- "component-structure",
2174
- "imports",
1939
+ "product-detail-patterns", "product-list-patterns", "cart-patterns",
1940
+ "account-patterns", "header-footer-patterns", "review-patterns",
1941
+ "blog-patterns", "form-handling", "async-data-patterns",
1942
+ "component-structure", "imports",
2175
1943
  ]);
2176
1944
  const observerReminder = "> **IMPORTANT: Do NOT use `observer()` on root component exports.** The ikas runtime wraps root renders in MobX `autorun()`, making them automatically reactive. All store reads (`cartStore`, `customerStore`, etc.) in root components are tracked automatically. Only use `observer()` on extracted sub-components.\n\n";
2177
1945
  // Try exact key match (with alias resolution)
2178
1946
  if (frameworkData.topics[resolvedTopic]) {
2179
1947
  const t = frameworkData.topics[resolvedTopic];
2180
1948
  const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
2181
- return {
2182
- content: [
2183
- {
2184
- type: "text",
2185
- text: `## ${t.title}\n\n${prefix}${t.content}`,
2186
- },
2187
- ],
2188
- };
1949
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2189
1950
  }
2190
1951
  // Try original topic key (without alias) in case it's a direct key
2191
1952
  if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
2192
1953
  const t = frameworkData.topics[topicLower];
2193
1954
  const prefix = storeTopics.has(topicLower) ? observerReminder : "";
2194
- return {
2195
- content: [
2196
- {
2197
- type: "text",
2198
- text: `## ${t.title}\n\n${prefix}${t.content}`,
2199
- },
2200
- ],
2201
- };
1955
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2202
1956
  }
2203
1957
  // Try keyword search
2204
1958
  const matches = searchFrameworkTopics(topic);
@@ -2206,12 +1960,7 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
2206
1960
  const best = matches[0];
2207
1961
  const prefix = storeTopics.has(best.key) ? observerReminder : "";
2208
1962
  return {
2209
- content: [
2210
- {
2211
- type: "text",
2212
- text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
2213
- },
2214
- ],
1963
+ content: [{ type: "text", text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}` }],
2215
1964
  };
2216
1965
  }
2217
1966
  const available = Object.entries(frameworkData.topics)
@@ -2227,27 +1976,16 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
2227
1976
  };
2228
1977
  });
2229
1978
  // Tool: get_type_definition
2230
- server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", {
2231
- name: z
2232
- .string()
2233
- .describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
2234
- }, async ({ name }) => {
1979
+ server.tool("get_type_definition", "Get the full definition of a storefront type or enum by name (e.g. 'IkasProduct', 'IkasOrderStatus'). Shows all properties with types, extends, or enum values.", { name: z.string().describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')") }, async ({ name }) => {
2235
1980
  if (!typesData) {
2236
1981
  return {
2237
- content: [
2238
- {
2239
- type: "text",
2240
- text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2241
- },
2242
- ],
1982
+ content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2243
1983
  };
2244
1984
  }
2245
1985
  const nameLower = name.toLowerCase();
2246
1986
  const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
2247
1987
  if (td) {
2248
- return {
2249
- content: [{ type: "text", text: formatTypeDefinition(td) }],
2250
- };
1988
+ return { content: [{ type: "text", text: formatTypeDefinition(td) }] };
2251
1989
  }
2252
1990
  // Fuzzy match
2253
1991
  const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
@@ -2263,28 +2001,14 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
2263
2001
  };
2264
2002
  }
2265
2003
  return {
2266
- content: [
2267
- {
2268
- type: "text",
2269
- text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
2270
- },
2271
- ],
2004
+ content: [{ type: "text", text: `Type "${name}" not found. Use \`list_types()\` to see all available types.` }],
2272
2005
  };
2273
2006
  });
2274
2007
  // Tool: get_functions_for_type
2275
- server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", {
2276
- typeName: z
2277
- .string()
2278
- .describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2279
- }, async ({ typeName }) => {
2008
+ server.tool("get_functions_for_type", "Get full documentation for all utility functions that operate on a given storefront type. For example, get_functions_for_type('IkasImage') returns getSrc, getDefaultSrc, getThumbnailSrc, createMediaSrcset with full signatures, descriptions, and examples.", { typeName: z.string().describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')") }, async ({ typeName }) => {
2280
2009
  if (!typesData) {
2281
2010
  return {
2282
- content: [
2283
- {
2284
- type: "text",
2285
- text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2286
- },
2287
- ],
2011
+ content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2288
2012
  };
2289
2013
  }
2290
2014
  const nameLower = typeName.toLowerCase();
@@ -2304,12 +2028,7 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2304
2028
  };
2305
2029
  }
2306
2030
  return {
2307
- content: [
2308
- {
2309
- type: "text",
2310
- text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
2311
- },
2312
- ],
2031
+ content: [{ type: "text", text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.` }],
2313
2032
  };
2314
2033
  }
2315
2034
  if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
@@ -2340,10 +2059,7 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2340
2059
  });
2341
2060
  // Tool: get_model_guide
2342
2061
  server.tool("get_model_guide", "Get an overview of a storefront model type. By default returns the type definition, related function names with one-line summaries, matching example titles/IDs, and related type summaries. Pass `mode: 'full'` to inline full function docs and example code.", {
2343
- model: z
2344
- .string()
2345
- .optional()
2346
- .describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2062
+ model: z.string().optional().describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2347
2063
  name: z.string().optional().describe("Alias for 'model'"),
2348
2064
  mode: z
2349
2065
  .enum(["summary", "full"])
@@ -2354,22 +2070,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2354
2070
  const model = modelParam || nameParam;
2355
2071
  if (!model) {
2356
2072
  return {
2357
- content: [
2358
- {
2359
- type: "text",
2360
- text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
2361
- },
2362
- ],
2073
+ content: [{ type: "text", text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter." }],
2363
2074
  };
2364
2075
  }
2365
2076
  if (!typesData) {
2366
2077
  return {
2367
- content: [
2368
- {
2369
- type: "text",
2370
- text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2371
- },
2372
- ],
2078
+ content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2373
2079
  };
2374
2080
  }
2375
2081
  const modelLower = model.toLowerCase();
@@ -2391,12 +2097,7 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2391
2097
  };
2392
2098
  }
2393
2099
  return {
2394
- content: [
2395
- {
2396
- type: "text",
2397
- text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
2398
- },
2399
- ],
2100
+ content: [{ type: "text", text: `Model "${model}" not found. Use \`list_types()\` to see all available types.` }],
2400
2101
  };
2401
2102
  }
2402
2103
  const parts = [`# Model Guide: ${td.name}\n`];
@@ -2504,23 +2205,13 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2504
2205
  server.tool("search_types", "Search storefront types and enums by keyword (e.g. 'price', 'address', 'status'). Returns top matches ranked by relevance.", { query: z.string().describe("Search keyword or phrase") }, async ({ query }) => {
2505
2206
  if (!typesData) {
2506
2207
  return {
2507
- content: [
2508
- {
2509
- type: "text",
2510
- text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2511
- },
2512
- ],
2208
+ content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2513
2209
  };
2514
2210
  }
2515
2211
  const results = searchTypes(query).slice(0, 15);
2516
2212
  if (results.length === 0) {
2517
2213
  return {
2518
- content: [
2519
- {
2520
- type: "text",
2521
- text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
2522
- },
2523
- ],
2214
+ content: [{ type: "text", text: `No types found matching "${query}". Use \`list_types()\` to see all available types.` }],
2524
2215
  };
2525
2216
  }
2526
2217
  const parts = [`## Type Search Results for "${query}"\n`];
@@ -2584,7 +2275,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2584
2275
  for (const [d, count] of sorted) {
2585
2276
  lines.push(`- \`${d}\` (${count})`);
2586
2277
  }
2587
- lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
2278
+ lines.push("", "Call `list_types(domain: \"<name>\")` to see summaries for a domain.");
2588
2279
  return { content: [{ type: "text", text: lines.join("\n") }] };
2589
2280
  }
2590
2281
  const domainLower = domain.toLowerCase();
@@ -2623,25 +2314,15 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2623
2314
  return { content: [{ type: "text", text: parts.join("\n") }] };
2624
2315
  });
2625
2316
  // Tool: get_prop_types
2626
- server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. NOTE: these are the TypeScript prop-definition types for authoring a component — they are NOT the runtime JSON value shapes you write via `update_section_prop` (e.g. a TEXT prop is typed `string` here but written as `{ \"value\": \"...\" }`, and an IMAGE is `IkasImage | null` here but written as `{ \"id\": \"...\" }`). For the value shapes to pass to `update_section_prop`, see that tool's description. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
2317
+ server.tool("get_prop_types", "Get all available ikas.config.json prop types with descriptions, TypeScript types, and examples. Tip: Use `npx ikas-component config add-component --props '[...]'` to create a component with all props in one command, or `add-prop` to add props incrementally. NEVER manually edit types.ts — it is auto-generated by the CLI.", {}, async () => {
2627
2318
  const propTypesTopic = frameworkData.topics["prop-types"];
2628
2319
  if (propTypesTopic) {
2629
2320
  return {
2630
- content: [
2631
- {
2632
- type: "text",
2633
- text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
2634
- },
2635
- ],
2321
+ content: [{ type: "text", text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}` }],
2636
2322
  };
2637
2323
  }
2638
2324
  return {
2639
- content: [
2640
- {
2641
- type: "text",
2642
- text: "Prop types documentation not available.",
2643
- },
2644
- ],
2325
+ content: [{ type: "text", text: "Prop types documentation not available." }],
2645
2326
  };
2646
2327
  });
2647
2328
  // Tool: get_section_template
@@ -2738,8 +2419,7 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2738
2419
  // the end of the response in the existing recipe-builder block.
2739
2420
  {
2740
2421
  const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
2741
- if (snippetStrForBanner &&
2742
- /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2422
+ if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2743
2423
  const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
2744
2424
  const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
2745
2425
  parts.push(`> 🔧 **CONTAINER SECTION — required wiring.** This template hosts ${uniqueChildren.length} child component${uniqueChildren.length === 1 ? "" : "s"} (${uniqueChildren.map((n) => `\`${n}\``).join(", ")}) via a \`COMPONENT_LIST\` slot. Creating the parent alone produces an empty, unusable section. **You MUST follow the full Setup Recipe below**: create each child → capture its \`componentId\` → wire them into the parent's \`filteredComponentIds\` with \`config update-prop\`. Component ids are opaque random strings (e.g. \`7ojrigep-Eml9n5sN3i\`) — they cannot be derived from names, and the CLI rejects unknown ids.`, "");
@@ -2863,10 +2543,7 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2863
2543
  try {
2864
2544
  const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
2865
2545
  const childProps = (childSnippet.props || []).map((p) => {
2866
- const out = {
2867
- name: p.name,
2868
- type: p.type,
2869
- };
2546
+ const out = { name: p.name, type: p.type };
2870
2547
  if (p.displayName)
2871
2548
  out.displayName = p.displayName;
2872
2549
  if (p.required)
@@ -2939,7 +2616,10 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
2939
2616
  .string()
2940
2617
  .optional()
2941
2618
  .describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
2942
- child: z.string().optional().describe("Alias for `name`"),
2619
+ child: z
2620
+ .string()
2621
+ .optional()
2622
+ .describe("Alias for `name`"),
2943
2623
  kind: z
2944
2624
  .enum(["children", "components", "sub-components"])
2945
2625
  .optional()
@@ -3102,11 +2782,7 @@ server.tool("list_section_types", "List all available `get_section_template` sec
3102
2782
  });
3103
2783
  // --- Migration tools ---
3104
2784
  // Tool: analyze_old_theme
3105
- server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", {
3106
- theme_json: z
3107
- .string()
3108
- .describe("The raw JSON content of the old theme.json file"),
3109
- }, async ({ theme_json }) => {
2785
+ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and produce a structured migration report. Shows all components, custom data definitions, prop type breakdown, and migration recommendations. Use this as the first step when converting an old theme.", { theme_json: z.string().describe("The raw JSON content of the old theme.json file") }, async ({ theme_json }) => {
3110
2786
  try {
3111
2787
  const parsed = JSON.parse(theme_json);
3112
2788
  const analysis = analyzeOldTheme(parsed);
@@ -3114,162 +2790,102 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
3114
2790
  }
3115
2791
  catch (err) {
3116
2792
  return {
3117
- content: [
3118
- {
3119
- type: "text",
3120
- text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
3121
- },
3122
- ],
2793
+ content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
3123
2794
  };
3124
2795
  }
3125
2796
  });
3126
2797
  // Tool: get_migration_guide
3127
2798
  const migrationTopicAliases = {
3128
- overview: "migration-overview",
3129
- migrate: "migration-overview",
3130
- custom: "custom-data-conversion",
2799
+ "overview": "migration-overview",
2800
+ "migrate": "migration-overview",
2801
+ "custom": "custom-data-conversion",
3131
2802
  "custom-data": "custom-data-conversion",
3132
- customdata: "custom-data-conversion",
2803
+ "customdata": "custom-data-conversion",
3133
2804
  "dynamic-list": "custom-data-conversion",
3134
2805
  "component-list": "custom-data-conversion",
3135
- slider: "prop-type-mapping",
3136
- props: "prop-type-mapping",
2806
+ "slider": "prop-type-mapping",
2807
+ "props": "prop-type-mapping",
3137
2808
  "prop-mapping": "prop-type-mapping",
3138
- types: "prop-type-mapping",
3139
- react: "react-to-preact",
3140
- preact: "react-to-preact",
3141
- observer: "react-to-preact",
3142
- libraries: "library-replacements",
3143
- swiper: "library-replacements",
3144
- headlessui: "library-replacements",
3145
- tailwind: "library-replacements",
3146
- tailwindcss: "library-replacements",
3147
- recharts: "library-replacements",
3148
- marquee: "library-replacements",
3149
- imports: "storefront-import-mapping",
3150
- storefront: "storefront-import-mapping",
2809
+ "types": "prop-type-mapping",
2810
+ "react": "react-to-preact",
2811
+ "preact": "react-to-preact",
2812
+ "observer": "react-to-preact",
2813
+ "libraries": "library-replacements",
2814
+ "swiper": "library-replacements",
2815
+ "headlessui": "library-replacements",
2816
+ "tailwind": "library-replacements",
2817
+ "tailwindcss": "library-replacements",
2818
+ "recharts": "library-replacements",
2819
+ "marquee": "library-replacements",
2820
+ "imports": "storefront-import-mapping",
2821
+ "storefront": "storefront-import-mapping",
3151
2822
  "bp-storefront": "storefront-import-mapping",
3152
2823
  "theme-json": "theme-json-anatomy",
3153
- anatomy: "theme-json-anatomy",
3154
- decompose: "component-decomposition-strategy",
3155
- decomposition: "component-decomposition-strategy",
3156
- strategy: "component-decomposition-strategy",
3157
- project: "complete-project-generation",
3158
- generate: "complete-project-generation",
3159
- generation: "complete-project-generation",
3160
- settings: "settings-conversion",
3161
- colors: "settings-conversion",
3162
- fonts: "settings-conversion",
3163
- find: "finding-new-system-equivalents",
3164
- search: "finding-new-system-equivalents",
3165
- discover: "finding-new-system-equivalents",
3166
- equivalent: "finding-new-system-equivalents",
3167
- equivalents: "finding-new-system-equivalents",
3168
- replacement: "finding-new-system-equivalents",
2824
+ "anatomy": "theme-json-anatomy",
2825
+ "decompose": "component-decomposition-strategy",
2826
+ "decomposition": "component-decomposition-strategy",
2827
+ "strategy": "component-decomposition-strategy",
2828
+ "project": "complete-project-generation",
2829
+ "generate": "complete-project-generation",
2830
+ "generation": "complete-project-generation",
2831
+ "settings": "settings-conversion",
2832
+ "colors": "settings-conversion",
2833
+ "fonts": "settings-conversion",
2834
+ "find": "finding-new-system-equivalents",
2835
+ "search": "finding-new-system-equivalents",
2836
+ "discover": "finding-new-system-equivalents",
2837
+ "equivalent": "finding-new-system-equivalents",
2838
+ "equivalents": "finding-new-system-equivalents",
2839
+ "replacement": "finding-new-system-equivalents",
3169
2840
  };
3170
2841
  const migrationTopicKeys = migrationData
3171
2842
  ? Object.keys(migrationData.topics)
3172
2843
  : [];
3173
- server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system. **Start with \`get_migration_guide("iterative-workflow")\` if you're new to this MCP** — it explains the MCP-vs-LLM responsibility split and the four phases.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, {
3174
- topic: z
3175
- .string()
3176
- .describe("Migration topic key, alias, or 'list' to see all topics"),
3177
- }, async ({ topic }) => {
2844
+ server.tool("get_migration_guide", `Get a migration guide for converting old ikas themes to the new code-component system. **Start with \`get_migration_guide("iterative-workflow")\` if you're new to this MCP** — it explains the MCP-vs-LLM responsibility split and the four phases.${migrationTopicKeys.length > 0 ? ` Available topics: ${migrationTopicKeys.join(", ")}. Also supports aliases like "custom", "slider", "react", "libraries", "imports", "settings".` : ""} Call with topic "list" to see all available topics.`, { topic: z.string().describe("Migration topic key, alias, or 'list' to see all topics") }, async ({ topic }) => {
3178
2845
  if (!migrationData) {
3179
- return {
3180
- content: [
3181
- {
3182
- type: "text",
3183
- text: "Migration data not available. Ensure data/migration.json exists.",
3184
- },
3185
- ],
3186
- };
2846
+ return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
3187
2847
  }
3188
2848
  if (topic.toLowerCase() === "list") {
3189
2849
  const available = Object.entries(migrationData.topics)
3190
2850
  .map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
3191
2851
  .join("\n");
3192
- return {
3193
- content: [
3194
- {
3195
- type: "text",
3196
- text: `## Available Migration Topics\n\n${available}`,
3197
- },
3198
- ],
3199
- };
2852
+ return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
3200
2853
  }
3201
2854
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
3202
2855
  const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
3203
2856
  if (migrationData.topics[resolvedTopic]) {
3204
2857
  const t = migrationData.topics[resolvedTopic];
3205
- return {
3206
- content: [
3207
- { type: "text", text: `## ${t.title}\n\n${t.content}` },
3208
- ],
3209
- };
2858
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3210
2859
  }
3211
2860
  // Try original key
3212
2861
  if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
3213
2862
  const t = migrationData.topics[topicLower];
3214
- return {
3215
- content: [
3216
- { type: "text", text: `## ${t.title}\n\n${t.content}` },
3217
- ],
3218
- };
2863
+ return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3219
2864
  }
3220
2865
  // Keyword search
3221
2866
  const matches = searchMigrationTopics(topic);
3222
2867
  if (matches.length > 0) {
3223
2868
  const best = matches[0];
3224
- return {
3225
- content: [
3226
- {
3227
- type: "text",
3228
- text: `## ${best.topic.title}\n\n${best.topic.content}`,
3229
- },
3230
- ],
3231
- };
2869
+ return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
3232
2870
  }
3233
2871
  const available = Object.entries(migrationData.topics)
3234
2872
  .map(([key, t]) => ` - \`${key}\` - ${t.title}`)
3235
2873
  .join("\n");
3236
2874
  return {
3237
- content: [
3238
- {
3239
- type: "text",
3240
- text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
3241
- },
3242
- ],
2875
+ content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
3243
2876
  };
3244
2877
  });
3245
2878
  // Tool: get_migration_example
3246
- server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, {
3247
- example: z.string().describe("Example name or 'list' to see all examples"),
3248
- }, async ({ example }) => {
2879
+ server.tool("get_migration_example", `Get a concrete before/after migration example showing how to convert an old theme component to the new code-component system.${migrationExampleNames.length > 0 ? ` Available examples: ${migrationExampleNames.join(", ")}.` : ""} Call with example "list" to see all examples.`, { example: z.string().describe("Example name or 'list' to see all examples") }, async ({ example }) => {
3249
2880
  if (example.toLowerCase() === "list") {
3250
2881
  if (migrationExampleNames.length === 0) {
3251
- return {
3252
- content: [
3253
- { type: "text", text: "No migration examples available." },
3254
- ],
3255
- };
2882
+ return { content: [{ type: "text", text: "No migration examples available." }] };
3256
2883
  }
3257
- const list = migrationExampleNames
3258
- .map((name) => {
2884
+ const list = migrationExampleNames.map((name) => {
3259
2885
  const ex = loadMigrationExample(name);
3260
- return ex
3261
- ? `- \`${name}\` — ${ex.title}: ${ex.description}`
3262
- : `- \`${name}\``;
3263
- })
3264
- .join("\n");
3265
- return {
3266
- content: [
3267
- {
3268
- type: "text",
3269
- text: `## Available Migration Examples\n\n${list}`,
3270
- },
3271
- ],
3272
- };
2886
+ return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
2887
+ }).join("\n");
2888
+ return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
3273
2889
  }
3274
2890
  const exampleLower = example.toLowerCase();
3275
2891
  let exName = migrationExampleNames.find((n) => n === exampleLower);
@@ -3279,26 +2895,19 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
3279
2895
  if (!exName) {
3280
2896
  const available = migrationExampleNames.join(", ");
3281
2897
  return {
3282
- content: [
3283
- {
3284
- type: "text",
3285
- text: `Migration example "${example}" not found. Available: ${available}`,
3286
- },
3287
- ],
2898
+ content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
3288
2899
  };
3289
2900
  }
3290
2901
  const ex = loadMigrationExample(exName);
3291
2902
  if (!ex) {
3292
- return {
3293
- content: [
3294
- {
3295
- type: "text",
3296
- text: `Failed to load migration example "${exName}".`,
3297
- },
3298
- ],
3299
- };
2903
+ return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
3300
2904
  }
3301
- const parts = [`## ${ex.title}`, "", ex.description, ""];
2905
+ const parts = [
2906
+ `## ${ex.title}`,
2907
+ "",
2908
+ ex.description,
2909
+ "",
2910
+ ];
3302
2911
  for (const [filename, content] of Object.entries(ex.files)) {
3303
2912
  const ext = filename.split(".").pop() || "text";
3304
2913
  const lang = ext === "tsx" || ext === "ts"
@@ -3317,31 +2926,13 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
3317
2926
  });
3318
2927
  // Tool: plan_migration
3319
2928
  server.tool("plan_migration", "Generate the **initial** migration plan and (when `project_root` is provided) write it to <project_root>/MIGRATION.md. **This is the only time the MCP writes that file.** From here, you own it: tick checkboxes as you finish work, log custom-data decisions, scan the old source for atomic components (Button, Input, Card, etc.) that theme.json doesn't see, and append them to MIGRATION.md yourself. theme.json is incomplete by design — the MCP can only describe what's listed there. Pass `theme_json_path` for large themes (raw `theme_json` string is supported for backward compat but fails on real-world sizes).", {
3320
- theme_json: z
3321
- .string()
3322
- .optional()
3323
- .describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both). For real themes use theme_json_path raw strings exceed tool/context limits at production sizes."),
3324
- theme_json_path: z
3325
- .string()
3326
- .optional()
3327
- .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3328
- project_name: z
3329
- .string()
3330
- .optional()
3331
- .describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
3332
- old_source_dir: z
3333
- .string()
3334
- .optional()
3335
- .describe("Absolute path to the old project's src/ directory. When provided, the tool scans .tsx files to detect shared sub-components used across 3+ components. This scan is partial — atomic components used by only 1-2 sections will be missed and must be added by the LLM."),
3336
- project_root: z
3337
- .string()
3338
- .optional()
3339
- .describe("Absolute path to the new project root. When provided, the MCP writes MIGRATION.md to <project_root>/MIGRATION.md and returns a short summary instead of the full markdown body."),
3340
- overwrite: z
3341
- .boolean()
3342
- .optional()
3343
- .describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
3344
- }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
2929
+ theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both). For real themes use theme_json_path — raw strings exceed tool/context limits at production sizes."),
2930
+ theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
2931
+ project_name: z.string().optional().describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
2932
+ old_source_dir: z.string().optional().describe("Absolute path to the old project's src/ directory. When provided, the tool scans .tsx files to detect shared sub-components used across 3+ components. This scan is partialatomic components used by only 1-2 sections will be missed and must be added by the LLM."),
2933
+ project_root: z.string().optional().describe("Absolute path to the new project root. When provided, the MCP writes MIGRATION.md to <project_root>/MIGRATION.md and returns a short summary instead of the full markdown body."),
2934
+ overwrite: z.boolean().optional().describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
2935
+ }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite }) => {
3345
2936
  try {
3346
2937
  const parsed = resolveThemeJson(theme_json, theme_json_path);
3347
2938
  const projectName = project_name || "my-theme";
@@ -3402,37 +2993,18 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
3402
2993
  }
3403
2994
  catch (err) {
3404
2995
  return {
3405
- content: [
3406
- {
3407
- type: "text",
3408
- text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3409
- },
3410
- ],
2996
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3411
2997
  };
3412
2998
  }
3413
2999
  });
3414
3000
  // Tool: get_section_migration_plan
3415
3001
  server.tool("get_section_migration_plan", "Returns concrete CLI commands and prop conversions for one section. For each prop that references a customData type, you'll see a 'Decide: enum or component?' callout — log your decision in MIGRATION.md under `## Custom Data Decisions`. Pass `theme_json_path` for large themes.", {
3416
- theme_json: z
3417
- .string()
3418
- .optional()
3419
- .describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3420
- theme_json_path: z
3421
- .string()
3422
- .optional()
3423
- .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3424
- section_name: z
3425
- .string()
3426
- .describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
3427
- project_name: z
3428
- .string()
3429
- .optional()
3430
- .describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3431
- old_source_dir: z
3432
- .string()
3433
- .optional()
3434
- .describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3435
- }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
3002
+ theme_json: z.string().optional().describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3003
+ theme_json_path: z.string().optional().describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3004
+ section_name: z.string().describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
3005
+ project_name: z.string().optional().describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3006
+ old_source_dir: z.string().optional().describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3007
+ }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir }) => {
3436
3008
  try {
3437
3009
  const parsed = resolveThemeJson(theme_json, theme_json_path);
3438
3010
  const projectName = project_name || "my-theme";
@@ -3441,12 +3013,7 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
3441
3013
  }
3442
3014
  catch (err) {
3443
3015
  return {
3444
- content: [
3445
- {
3446
- type: "text",
3447
- text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3448
- },
3449
- ],
3016
+ content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3450
3017
  };
3451
3018
  }
3452
3019
  });
@@ -3482,11 +3049,7 @@ async function runIkasComponentCli(projectRoot, args) {
3482
3049
  : err
3483
3050
  ? 1
3484
3051
  : 0;
3485
- resolve({
3486
- stdout: stdout?.toString() ?? "",
3487
- stderr: stderr?.toString() ?? "",
3488
- exitCode,
3489
- });
3052
+ resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", exitCode });
3490
3053
  });
3491
3054
  });
3492
3055
  }
@@ -3496,11 +3059,7 @@ function parseCliJson(stdout) {
3496
3059
  return null;
3497
3060
  // CLI prints exactly one JSON object on stdout; if multiple lines appeared
3498
3061
  // (e.g., warnings on stderr leaked), take the last non-empty line.
3499
- const last = trimmed
3500
- .split("\n")
3501
- .map((l) => l.trim())
3502
- .filter(Boolean)
3503
- .pop();
3062
+ const last = trimmed.split("\n").map(l => l.trim()).filter(Boolean).pop();
3504
3063
  if (!last)
3505
3064
  return null;
3506
3065
  try {
@@ -3515,11 +3074,7 @@ async function callEditorAction(projectRoot, args) {
3515
3074
  const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
3516
3075
  const parsed = parseCliJson(stdout);
3517
3076
  if (parsed) {
3518
- return {
3519
- content: [
3520
- { type: "text", text: JSON.stringify(parsed, null, 2) },
3521
- ],
3522
- };
3077
+ return { content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }] };
3523
3078
  }
3524
3079
  return {
3525
3080
  content: [
@@ -3542,77 +3097,38 @@ async function callEditorAction(projectRoot, args) {
3542
3097
  };
3543
3098
  }
3544
3099
  }
3545
- //
3546
3100
  // Tool: list_editor_pages
3547
3101
  server.tool("list_editor_pages", "List pages in the connected editor's project. Returns id, name, pageType, slug for each page. Requires `ikas-component dev` to be running with the editor connected. Use the returned `id`s as the `pageId` argument to `add_section_to_page`.", {
3548
- project_root: z
3549
- .string()
3550
- .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
3551
- port: z
3552
- .number()
3553
- .optional()
3554
- .describe("Dev server WebSocket port (default 5201)."),
3102
+ project_root: z.string().describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
3103
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3555
3104
  }, async ({ project_root, port }) => {
3556
3105
  const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
3557
3106
  return callEditorAction(project_root, args);
3558
3107
  });
3559
3108
  // Tool: list_imported_sections
3560
3109
  server.tool("list_imported_sections", "List section-type code components already imported into the editor's project (theme.codeComponents, filtered to type=section). Use to confirm a component is ready to be placed on a page. The component must be built (`ikas-component build`/`dev`) and imported (`import_section`) before it can be added to a page. The returned `id` is the editor's id for the imported component — normally identical to the id in `ikas.config.json`, but it can differ if the dev component was deleted and re-scaffolded after a previous import. Always use the `id` from this tool, not from `ikas.config.json`, when calling `add_section_to_page`.", {
3561
- project_root: z
3562
- .string()
3563
- .describe("Absolute path to the code-component project."),
3564
- port: z
3565
- .number()
3566
- .optional()
3567
- .describe("Dev server WebSocket port (default 5201)."),
3110
+ project_root: z.string().describe("Absolute path to the code-component project."),
3111
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3568
3112
  }, async ({ project_root, port }) => {
3569
- const args = [
3570
- "list-imported",
3571
- "--sections-only",
3572
- ...(port ? ["--port", String(port)] : []),
3573
- ];
3113
+ const args = ["list-imported", "--sections-only", ...(port ? ["--port", String(port)] : [])];
3574
3114
  return callEditorAction(project_root, args);
3575
3115
  });
3576
3116
  // Tool: import_section
3577
3117
  server.tool("import_section", "Import a built section-type code component into the editor's project. This copies the compiled JS/CSS/props into `theme.codeComponents` and auto-creates the wrapper section needed to place it on a page. Idempotent: re-running updates the existing entry in place. Required before `add_section_to_page`. The component must have been built (`ikas-component build`/`dev`).", {
3578
- project_root: z
3579
- .string()
3580
- .describe("Absolute path to the code-component project."),
3581
- component_id: z
3582
- .string()
3583
- .describe("Component id from `ikas.config.json` (strict — no name resolution)."),
3584
- port: z
3585
- .number()
3586
- .optional()
3587
- .describe("Dev server WebSocket port (default 5201)."),
3118
+ project_root: z.string().describe("Absolute path to the code-component project."),
3119
+ component_id: z.string().describe("Component id from `ikas.config.json` (strict — no name resolution)."),
3120
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3588
3121
  }, async ({ project_root, component_id, port }) => {
3589
- const args = [
3590
- "import",
3591
- "--id",
3592
- component_id,
3593
- ...(port ? ["--port", String(port)] : []),
3594
- ];
3122
+ const args = ["import", "--id", component_id, ...(port ? ["--port", String(port)] : [])];
3595
3123
  return callEditorAction(project_root, args);
3596
3124
  });
3597
3125
  // Tool: add_section_to_page
3598
- server.tool("add_section_to_page", 'Place an already-imported section-type code component on a page in the editor. Equivalent to right-clicking the section in the dev-components panel and choosing "Add to Page". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids. After placing, change the section\'s prop values with `update_section_prop` (use `list_page_sections` to get the placement\'s `elementId` and prop names).', {
3599
- project_root: z
3600
- .string()
3601
- .describe("Absolute path to the code-component project."),
3602
- component_id: z
3603
- .string()
3604
- .describe("Imported code component id from `list_imported_sections` — NOT the id in `ikas.config.json` (the two are usually the same but can diverge after a rescaffold)."),
3126
+ server.tool("add_section_to_page", "Place an already-imported section-type code component on a page in the editor. Equivalent to right-clicking the section in the dev-components panel and choosing \"Add to Page\". Errors if the component is not imported, is not section-type, or the page id is unknown. Use `list_editor_pages` to discover page ids and `list_imported_sections` to discover component ids.", {
3127
+ project_root: z.string().describe("Absolute path to the code-component project."),
3128
+ component_id: z.string().describe("Imported code component id from `list_imported_sections` — NOT the id in `ikas.config.json` (the two are usually the same but can diverge after a rescaffold)."),
3605
3129
  page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3606
- index: z
3607
- .number()
3608
- .int()
3609
- .nonnegative()
3610
- .optional()
3611
- .describe("Zero-based insertion index in the page; appends when omitted."),
3612
- port: z
3613
- .number()
3614
- .optional()
3615
- .describe("Dev server WebSocket port (default 5201)."),
3130
+ index: z.number().int().nonnegative().optional().describe("Zero-based insertion index in the page; appends when omitted."),
3131
+ port: z.number().optional().describe("Dev server WebSocket port (default 5201)."),
3616
3132
  }, async ({ project_root, component_id, page_id, index, port }) => {
3617
3133
  const args = [
3618
3134
  "add-to-page",
@@ -3625,98 +3141,6 @@ server.tool("add_section_to_page", 'Place an already-imported section-type code
3625
3141
  ];
3626
3142
  return callEditorAction(project_root, args);
3627
3143
  });
3628
- // Tool: list_page_sections
3629
- server.tool("list_page_sections", "List the sections placed on a page. Each entry includes the per-placement `elementId` (the identity of THIS placement — there can be multiple placements of the same section), `sectionId`, `componentId`, `name`, the current `propValues`, and the blueprint `props` (each with `id`, `name`, `type`). Use the returned `elementId` plus a prop `id`/`name` with `update_section_prop` to change a prop value of that specific placement. Use `list_editor_pages` to discover page ids.", {
3630
- project_root: z
3631
- .string()
3632
- .describe("Absolute path to the code-component project."),
3633
- page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3634
- port: z
3635
- .number()
3636
- .optional()
3637
- .describe("Dev server WebSocket port (default 5201)."),
3638
- }, async ({ project_root, page_id, port }) => {
3639
- const args = [
3640
- "list-page-sections",
3641
- "--page-id",
3642
- page_id,
3643
- ...(port ? ["--port", String(port)] : []),
3644
- ];
3645
- return callEditorAction(project_root, args);
3646
- });
3647
- // Tool: update_section_prop
3648
- server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. This is the tool for FILLING/SETTING content (heading, text, image, link, etc.) on an existing section — it is pure data entry: it writes NO component code and needs NO rebuild. Do not author a new prop or component to set a value the section already supports; first check the section\'s existing `props` via `list_page_sections`. Target the specific placement by its `element_id` (from `list_page_sections`) and the prop by `prop_id` or `prop_name` (also from `list_page_sections`). `value` is the prop value object as stored by the editor — for scalar props it is wrapped as `{ "value": <scalar> }` (e.g. `{ "value": "Hello" }` for TEXT, `{ "value": true }` for BOOLEAN, `{ "value": 12 }` for NUMBER, `{ "value": "#FF0000" }` for COLOR, `{ "value": "<enum-key>" }` for ENUM). Richer object prop types use their own object shape and are NOT `{ "value": ... }`-wrapped: IMAGE/VIDEO/SVG = `{ "id": "<asset-id>", "altText"?: "<alt>", "isVideo"?: false }` (the `id` MUST reference an already-uploaded asset — call `upload_image` with a `file_path`/`image_url` to get one, or reuse an existing `id` from another section\'s `propValues`); LINK = a single link object `{ "id": "<unique-short-id>", "linkType": "PAGE"|"EXTERNAL"|"FILE", "label": "...", "openInNewTab"?: false, "subLinks": [] }` plus the target for its type — for PAGE add `"pageId"` AND `"pageType"` (both from `list_editor_pages`, e.g. pageType "INDEX"; for dynamic page types like PRODUCT/CATEGORY/BLOG also add the target `"itemId"`), for EXTERNAL add `"externalLink": "https://..."`. Examples: PAGE → `{ "id": "k3p9x", "linkType": "PAGE", "label": "Home", "pageId": "<page-id>", "pageType": "INDEX", "openInNewTab": false, "subLinks": [] }`; EXTERNAL → `{ "id": "m7q2z", "linkType": "EXTERNAL", "label": "Docs", "externalLink": "https://example.com", "openInNewTab": true, "subLinks": [] }`. LIST_OF_LINK = `{ "links": [ <link>, ... ] }` (each item is a link object as above). PRODUCT = `{ "productId": "...", "variantId"?: "..." }`. This SAME unwrapped shape applies whether the prop sits at section level OR nested inside a COMPONENT_LIST entry\'s `propValues` — there is no section-vs-nested difference. IMPORTANT: pass `value` as the parsed JSON object/array itself, NEVER as a JSON string (a stringified value is double-encoded and stored as a useless string). Values are validated server-side (deeply, including nested COMPONENT_LIST children): wrong shapes AND wrong semantics are REJECTED with an explanatory error instead of being silently stored. This includes: a `{ "value": [...] }` wrapper for a COMPONENT_LIST; a `{ "value": ... }`-wrapped IMAGE; `props` instead of `propValues`; a missing/duplicate entry `id`; a child referenced by the wrong key (a code component MUST use `codeComponentId`, a theme component `componentId`) or by an id that does not exist (child ids are opaque — take them from `get_section_template` / `list_imported_sections`, never invent them); and any `propValues` key that is not a real prop of that child component (the error lists the valid prop names). Auto-import: a referenced child code component that has been BUILT but not yet imported into the theme is imported automatically before the value is set (the response lists any such ids under `autoImported`); only children that are not built at all fail with a "no imported code component" error — build them first (`ikas-component build`/`dev`). For any prop type you are unsure about, inspect the existing entry in `list_page_sections` `propValues` and match its exact shape. The change is applied with undo support.\n\n' +
3649
- 'COMPONENT_LIST / COMPONENT props: the value is NOT wrapped in `{ "value": ... }`. A COMPONENT_LIST value is `{ "components": [ <entry>, ... ] }`; a single COMPONENT value is one `<entry>`. Each entry = `{ "id": "<unique-id>", "codeComponentId": "<child-id>" | "componentId": "<child-id>", "propValues": { "<childPropName>": <wrapped-value>, ... } }`. Rules: (1) `id` is a unique short alphanumeric string identifying THIS entry — generate a fresh one per new entry and NEVER reuse an existing entry\'s id. (2) Use `codeComponentId` for code components, `componentId` for built-in theme components. (3) `propValues` is keyed by the CHILD component\'s prop NAMES, each using the same scalar wrappers as above (nesting is recursive — a child may itself hold a COMPONENT_LIST). (4) `update_section_prop` REPLACES the whole prop value — there is NO partial/deep merge. ALWAYS send the COMPLETE, fully-nested value for the prop: every existing entry (with its id), every nested `propValues`, and every nested COMPONENT_LIST at every depth. To change anything (even one deeply-nested scalar, or to add/remove/reorder a child), READ-MODIFY-WRITE: take the current `{ "components": [...] }` from `list_page_sections`, edit it in place (preserving all other entries, ids, and nested structures), then send the ENTIRE updated object back. Sending only the changed part — a single entry, or a child without its siblings — wipes everything you omitted. (5) You can only add child component types the prop permits; the allowed set is the blueprint prop\'s `filteredComponentIds`, which for container sections (Header, Footer, ProductDetail, …) is wired at build/config time per the `get_section_template` setup recipe (`config update-prop`), not here.\n\n' +
3650
- 'Example — a full COMPONENT_LIST `value` with two children, where the second child itself nests another COMPONENT_LIST (note: scalar leaves are `{ "value": ... }`-wrapped, COMPONENT_LIST values are `{ "components": [...] }` and are NOT wrapped, every entry has a unique `id`, and this is the COMPLETE value you would send even to change just one field): ' +
3651
- `{ "components": [ { "id": "a1b2c", "codeComponentId": "7ojrigep-Eml9n5sN3i", "propValues": { "heading": { "value": "Featured" }, "visible": { "value": true } } }, { "id": "d3e4f", "codeComponentId": "x2plk9zq-Qw8rt", "propValues": { "title": { "value": "Sub group" }, "cards": { "components": [ { "id": "g5h6i", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 1" } } }, { "id": "j7k8l", "codeComponentId": "card01ab-Zz12", "propValues": { "label": { "value": "Card 2" } } } ] } } } ] }`, {
3652
- project_root: z
3653
- .string()
3654
- .describe("Absolute path to the code-component project."),
3655
- page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3656
- element_id: z
3657
- .string()
3658
- .describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
3659
- prop_id: z
3660
- .string()
3661
- .optional()
3662
- .describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
3663
- prop_name: z
3664
- .string()
3665
- .optional()
3666
- .describe("Blueprint prop name to update (alternative to `prop_id`)."),
3667
- value: z
3668
- .any()
3669
- .describe('The prop value object to store, e.g. `{ "value": "Hello" }`. Match the shape of the existing entry in `list_page_sections` propValues for non-scalar prop types.'),
3670
- port: z
3671
- .number()
3672
- .optional()
3673
- .describe("Dev server WebSocket port (default 5201)."),
3674
- }, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
3675
- const args = [
3676
- "update-section-prop",
3677
- "--page-id",
3678
- page_id,
3679
- "--element-id",
3680
- element_id,
3681
- ...(prop_id ? ["--prop-id", prop_id] : []),
3682
- ...(prop_name ? ["--prop-name", prop_name] : []),
3683
- "--value",
3684
- // The CLI JSON.parses --value. If `value` is already a JSON string, pass
3685
- // it through verbatim — re-stringifying it would double-encode it and the
3686
- // editor would store a string instead of the object/array.
3687
- typeof value === "string" ? value : JSON.stringify(value),
3688
- ...(port ? ["--port", String(port)] : []),
3689
- ];
3690
- return callEditorAction(project_root, args);
3691
- });
3692
- // Tool: upload_image
3693
- server.tool("upload_image", "Upload an image to the connected editor's project and get back its image `id`. Provide the image via `file_path` (a local path) OR `image_url`. Use the returned `id` as the `id` of an IMAGE prop value passed to `update_section_prop` (e.g. `{ \"id\": \"<returned-id>\", \"altText\": \"...\", \"isVideo\": false }`). This is the way to set an image prop — `update_section_prop` does not upload, it only references an existing image id. Supported types: png, jpg, jpeg, webp, gif; max 10MB. Requires the editor to be embedded in the ikas admin panel (the admin panel performs the actual upload; a standalone editor cannot upload and will error/time out).", {
3694
- project_root: z
3695
- .string()
3696
- .describe("Absolute path to the code-component project."),
3697
- file_path: z
3698
- .string()
3699
- .optional()
3700
- .describe("Local image file path (.png, .jpg, .jpeg, .webp, .gif). Provide this or `image_url`."),
3701
- image_url: z
3702
- .string()
3703
- .optional()
3704
- .describe("Image URL to fetch and upload (alternative to `file_path`)."),
3705
- alt_text: z.string().optional().describe("Alt text to store with the image."),
3706
- port: z
3707
- .number()
3708
- .optional()
3709
- .describe("Dev server WebSocket port (default 5201)."),
3710
- }, async ({ project_root, file_path, image_url, alt_text, port }) => {
3711
- const args = [
3712
- "upload-image",
3713
- ...(file_path ? ["--file", file_path] : []),
3714
- ...(image_url ? ["--url", image_url] : []),
3715
- ...(alt_text ? ["--alt", alt_text] : []),
3716
- ...(port ? ["--port", String(port)] : []),
3717
- ];
3718
- return callEditorAction(project_root, args);
3719
- });
3720
3144
  // --- Start server ---
3721
3145
  async function main() {
3722
3146
  const transport = new StdioServerTransport();