@ikas/code-components-mcp 1.4.0-beta.15 → 1.4.0-beta.17

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,9 +98,16 @@ function loadStorefrontTypes() {
98
98
  }
99
99
  return null;
100
100
  }
101
- const SUBTREE_KINDS = ["children", "components", "sub-components"];
101
+ const SUBTREE_KINDS = [
102
+ "children",
103
+ "components",
104
+ "sub-components",
105
+ ];
102
106
  function normalizeName(value) {
103
- return value.trim().replace(/^`+|`+$/g, "").trim();
107
+ return value
108
+ .trim()
109
+ .replace(/^`+|`+$/g, "")
110
+ .trim();
104
111
  }
105
112
  const storefrontData = loadStorefrontData();
106
113
  const frameworkData = loadJsonFile("../data/framework.json");
@@ -261,7 +268,11 @@ function searchMigrationTopics(query) {
261
268
  const descScore = matchScore(topic.description, query) * 2;
262
269
  const contentScore = matchScore(topic.content, query);
263
270
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
264
- return { key, topic, score: titleScore + descScore + contentScore + tagScore };
271
+ return {
272
+ key,
273
+ topic,
274
+ score: titleScore + descScore + contentScore + tagScore,
275
+ };
265
276
  })
266
277
  .filter((item) => item.score > 0)
267
278
  .sort((a, b) => b.score - a.score);
@@ -288,7 +299,7 @@ function analyzeOldTheme(themeJson) {
288
299
  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`);
289
300
  parts.push(`## Summary Statistics\n`);
290
301
  parts.push(`- **Components:** ${components.length}`);
291
- parts.push(`- **Custom Data Definitions:** ${customData.filter(cd => cd.isRoot).length}`);
302
+ parts.push(`- **Custom Data Definitions:** ${customData.filter((cd) => cd.isRoot).length}`);
292
303
  parts.push(`- **Prop Groups:** ${groups.length}`);
293
304
  // Component analysis
294
305
  parts.push(`\n## Components (${components.length})\n`);
@@ -327,7 +338,11 @@ function analyzeOldTheme(themeJson) {
327
338
  const typesSummary = Object.entries(propTypeCounts)
328
339
  .map(([t, c]) => `${t}×${c}`)
329
340
  .join(", ");
330
- const headerFooter = comp.isHeader ? " [HEADER]" : comp.isFooter ? " [FOOTER]" : "";
341
+ const headerFooter = comp.isHeader
342
+ ? " [HEADER]"
343
+ : comp.isFooter
344
+ ? " [FOOTER]"
345
+ : "";
331
346
  parts.push(`### ${comp.displayName || comp.dir || comp.id}${headerFooter}`);
332
347
  parts.push(`- **Dir:** \`${comp.dir || "?"}\` | **Props:** ${props.length} (${typesSummary})`);
333
348
  parts.push(`- **Recommended new type:** section`);
@@ -342,7 +357,7 @@ function analyzeOldTheme(themeJson) {
342
357
  parts.push("");
343
358
  }
344
359
  // Custom data analysis
345
- const rootCustomData = customData.filter(cd => cd.isRoot);
360
+ const rootCustomData = customData.filter((cd) => cd.isRoot);
346
361
  if (rootCustomData.length > 0) {
347
362
  parts.push(`\n## Custom Data Definitions (${rootCustomData.length})\n`);
348
363
  for (const cd of rootCustomData) {
@@ -352,7 +367,9 @@ function analyzeOldTheme(themeJson) {
352
367
  const describeNested = (items, indent) => {
353
368
  const lines = [];
354
369
  for (const item of items) {
355
- const key = item.key ? `\`${item.key}\`` : item.typescriptName || item.name || "unnamed";
370
+ const key = item.key
371
+ ? `\`${item.key}\``
372
+ : item.typescriptName || item.name || "unnamed";
356
373
  lines.push(`${indent}- ${key}: ${item.type}${item.isRequired ? " (required)" : ""}`);
357
374
  if (item.nestedData && item.nestedData.length > 0) {
358
375
  lines.push(...describeNested(item.nestedData, indent + " "));
@@ -364,7 +381,7 @@ function analyzeOldTheme(themeJson) {
364
381
  parts.push(...describeNested(cd.nestedData, " "));
365
382
  }
366
383
  if (cd.enumOptions && cd.enumOptions.length > 0) {
367
- parts.push(`- **Enum options:** ${cd.enumOptions.map(o => `"${o.value}"`).join(", ")}`);
384
+ parts.push(`- **Enum options:** ${cd.enumOptions.map((o) => `"${o.value}"`).join(", ")}`);
368
385
  }
369
386
  // Find which components reference this customData
370
387
  const referencingComponents = [];
@@ -446,7 +463,9 @@ function scanSharedSubcomponents(sourceDir) {
446
463
  const walk = (dir) => {
447
464
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
448
465
  if (entry.isDirectory()) {
449
- if (entry.name === "node_modules" || entry.name === "__generated__" || entry.name.startsWith("."))
466
+ if (entry.name === "node_modules" ||
467
+ entry.name === "__generated__" ||
468
+ entry.name.startsWith("."))
450
469
  continue;
451
470
  walk(path.join(dir, entry.name));
452
471
  }
@@ -481,10 +500,14 @@ function scanSharedSubcomponents(sourceDir) {
481
500
  if (!importPath.startsWith("."))
482
501
  continue;
483
502
  // Skip imports of generated types, utils, hooks
484
- if (importPath.includes("__generated__") || importPath.includes("/utils") || importPath.includes("/hooks"))
503
+ if (importPath.includes("__generated__") ||
504
+ importPath.includes("/utils") ||
505
+ importPath.includes("/hooks"))
485
506
  continue;
486
507
  // Extract base name from path
487
- const pathSegments = importPath.split("/").filter(s => s && s !== "." && s !== "..");
508
+ const pathSegments = importPath
509
+ .split("/")
510
+ .filter((s) => s && s !== "." && s !== "..");
488
511
  if (pathSegments.length === 0)
489
512
  continue;
490
513
  const lastSegment = pathSegments[pathSegments.length - 1];
@@ -495,7 +518,10 @@ function scanSharedSubcomponents(sourceDir) {
495
518
  continue;
496
519
  seenInFile.add(key);
497
520
  if (!importUsage.has(key)) {
498
- importUsage.set(key, { usingComponents: new Set(), rawImportPath: importPath });
521
+ importUsage.set(key, {
522
+ usingComponents: new Set(),
523
+ rawImportPath: importPath,
524
+ });
499
525
  }
500
526
  importUsage.get(key).usingComponents.add(componentDir);
501
527
  }
@@ -504,7 +530,7 @@ function scanSharedSubcomponents(sourceDir) {
504
530
  const shared = [];
505
531
  for (const [name, { usingComponents, rawImportPath }] of importUsage) {
506
532
  // Don't flag the component itself (e.g., Navbar imports from ../Navbar/something)
507
- const users = [...usingComponents].filter(c => c !== name);
533
+ const users = [...usingComponents].filter((c) => c !== name);
508
534
  if (users.length >= 3) {
509
535
  shared.push({ name, usedBy: users.sort(), importPaths: [rawImportPath] });
510
536
  }
@@ -521,7 +547,7 @@ function toKebabCase(s) {
521
547
  }
522
548
  function classifyComplexity(comp, customDataMap) {
523
549
  const props = comp.props || [];
524
- const customCount = props.filter(p => p.type === "CUSTOM").length;
550
+ const customCount = props.filter((p) => p.type === "CUSTOM").length;
525
551
  if (customCount === 0 && props.length < 10)
526
552
  return "simple";
527
553
  // Check for deeply nested CUSTOM (customData referencing another customData)
@@ -532,7 +558,9 @@ function classifyComplexity(comp, customDataMap) {
532
558
  if (cd?.nestedData) {
533
559
  const hasNested = (items) => {
534
560
  for (const item of items) {
535
- if (item.type === "DYNAMIC_LIST" || item.type === "STATIC_LIST" || item.customDataId)
561
+ if (item.type === "DYNAMIC_LIST" ||
562
+ item.type === "STATIC_LIST" ||
563
+ item.customDataId)
536
564
  return true;
537
565
  if (item.nestedData && hasNested(item.nestedData))
538
566
  return true;
@@ -564,7 +592,7 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
564
592
  parts.push(`# Theme Migration Plan — \`${projectName}\``);
565
593
  parts.push("");
566
594
  parts.push(`**Generated:** ${new Date().toISOString().slice(0, 10)}`);
567
- parts.push(`**Source:** ${components.length} old components, ${customData.filter(cd => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
595
+ parts.push(`**Source:** ${components.length} old components, ${customData.filter((cd) => cd.isRoot).length} custom data types, ${(theme.pages || []).length} pages`);
568
596
  parts.push("");
569
597
  parts.push(`> ## READ THIS FIRST`);
570
598
  parts.push(`>`);
@@ -654,16 +682,22 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
654
682
  const cdType = cd.type || "?";
655
683
  let shape = "";
656
684
  if (cd.type === "ENUM") {
657
- const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
685
+ const opts = (cd.enumOptions || [])
686
+ .map((o) => o.value || o.displayName)
687
+ .filter(Boolean);
658
688
  shape = ` — shape: \`enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}\``;
659
689
  }
660
690
  else if (cd.nestedData && cd.nestedData.length > 0) {
661
691
  const first = cd.nestedData[0];
662
- const fields = (first?.nestedData || cd.nestedData || []).map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`).slice(0, 8);
692
+ const fields = (first?.nestedData || cd.nestedData || [])
693
+ .map((f) => `${f.key || f.name || "?"}: ${f.type || "?"}`)
694
+ .slice(0, 8);
663
695
  shape = ` — shape: \`{${fields.join(", ")}}\``;
664
696
  }
665
697
  const usedBy = cd.id ? usageByCustomDataId.get(cd.id) || [] : [];
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)_`;
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)_`;
667
701
  parts.push(`- \`${cdName}\` (${cdType})${shape}${usedByStr}`);
668
702
  }
669
703
  parts.push("");
@@ -697,7 +731,11 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
697
731
  const oldName = comp.displayName || comp.dir || comp.id || "Unknown";
698
732
  const kebabName = toKebabCase(comp.dir || comp.displayName || comp.id || "unknown");
699
733
  const newId = `${projectName}-${kebabName}`;
700
- const headerFooter = comp.isHeader ? " **[HEADER]**" : comp.isFooter ? " **[FOOTER]**" : "";
734
+ const headerFooter = comp.isHeader
735
+ ? " **[HEADER]**"
736
+ : comp.isFooter
737
+ ? " **[FOOTER]**"
738
+ : "";
701
739
  const propCount = (comp.props || []).length;
702
740
  // Detect children from CUSTOM DYNAMIC_LIST props
703
741
  const children = [];
@@ -707,8 +745,13 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
707
745
  if (cd && (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST")) {
708
746
  const itemObj = cd.nestedData?.[0];
709
747
  if (itemObj) {
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);
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);
712
755
  children.push({ propName: p.name || "?", childName, fields });
713
756
  }
714
757
  }
@@ -767,11 +810,25 @@ function generateMigrationPlan(theme, projectName, oldSourceDir) {
767
810
  }
768
811
  // Known libraries we detect in old themes and want to flag for replacement
769
812
  const KNOWN_LIBRARIES = [
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",
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",
775
832
  ];
776
833
  // Heuristic: member-access patterns on old storefront stores/singletons that likely need new-system equivalents
777
834
  const OLD_STOREFRONT_CALL_REGEX = /\b(customerStore|cartStore|productStore|categoryStore|orderStore|searchStore|favoritesStore|i18nStore|Router|useStore)\.\w+/g;
@@ -790,7 +847,8 @@ function scanSectionSource(componentDir, propNames) {
790
847
  // Collect all .tsx/.ts files in the component dir
791
848
  try {
792
849
  for (const entry of fs.readdirSync(componentDir, { withFileTypes: true })) {
793
- if (entry.isFile() && (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
850
+ if (entry.isFile() &&
851
+ (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))) {
794
852
  result.sourceFiles.push(path.join(componentDir, entry.name));
795
853
  }
796
854
  }
@@ -807,7 +865,18 @@ function scanSectionSource(componentDir, propNames) {
807
865
  const subCompSet = new Map();
808
866
  const callSet = new Set();
809
867
  // Packages we treat as "framework" and don't flag for replacement (but do note in reactPackageUsage)
810
- const REACT_PACKAGES = new Set(["react", "react-dom", "next", "next/link", "next/image", "next/router", "next/head", "next/script", "mobx-react-lite", "mobx"]);
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
+ ]);
811
880
  for (const file of result.sourceFiles) {
812
881
  let content;
813
882
  try {
@@ -821,7 +890,7 @@ function scanSectionSource(componentDir, propNames) {
821
890
  while ((m = importRegex.exec(content)) !== null) {
822
891
  const p = m[1];
823
892
  if (p.startsWith(".")) {
824
- const segs = p.split("/").filter(s => s && s !== "." && s !== "..");
893
+ const segs = p.split("/").filter((s) => s && s !== "." && s !== "..");
825
894
  const last = segs[segs.length - 1];
826
895
  if (last && /^[A-Z]/.test(last) && !last.includes("__generated__")) {
827
896
  subCompSet.set(last, p);
@@ -829,7 +898,9 @@ function scanSectionSource(componentDir, propNames) {
829
898
  }
830
899
  else if (!p.startsWith("@ikas/")) {
831
900
  // Classify: known library, react-family, or unknown-external
832
- const base = p.startsWith("@") ? p.split("/").slice(0, 2).join("/") : p.split("/")[0];
901
+ const base = p.startsWith("@")
902
+ ? p.split("/").slice(0, 2).join("/")
903
+ : p.split("/")[0];
833
904
  if (REACT_PACKAGES.has(p) || REACT_PACKAGES.has(base)) {
834
905
  reactSet.add(base);
835
906
  }
@@ -892,7 +963,10 @@ function scanSectionSource(componentDir, propNames) {
892
963
  }
893
964
  }
894
965
  }
895
- result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({ name, path: p }));
966
+ result.importedSubComponents = [...subCompSet.entries()].map(([name, p]) => ({
967
+ name,
968
+ path: p,
969
+ }));
896
970
  result.importedLibraries = [...libSet].sort();
897
971
  result.importedUnknownLibraries = [...unknownLibSet].sort();
898
972
  result.reactPackageUsage = [...reactSet].sort();
@@ -908,27 +982,38 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
908
982
  customDataMap.set(cd.id, cd);
909
983
  }
910
984
  // Find the component — try match by dir, displayName, id, or new-id
911
- const target = components.find(c => {
985
+ const target = components.find((c) => {
912
986
  if (!c)
913
987
  return false;
914
- if (c.dir === sectionName || c.displayName === sectionName || c.id === sectionName)
988
+ if (c.dir === sectionName ||
989
+ c.displayName === sectionName ||
990
+ c.id === sectionName)
915
991
  return true;
916
992
  const kebab = toKebabCase(c.dir || c.displayName || c.id || "");
917
993
  const newId = `${projectName}-${kebab}`;
918
994
  return newId === sectionName;
919
995
  });
920
996
  if (!target) {
921
- const available = components.map(c => c.dir || c.displayName || c.id).filter(Boolean).join(", ");
997
+ const available = components
998
+ .map((c) => c.dir || c.displayName || c.id)
999
+ .filter(Boolean)
1000
+ .join(", ");
922
1001
  return `Section "${sectionName}" not found in theme. Available: ${available}`;
923
1002
  }
924
1003
  const parts = [];
925
1004
  const oldName = target.displayName || target.dir || target.id || "Unknown";
926
1005
  const kebabName = toKebabCase(target.dir || target.displayName || target.id || "unknown");
927
1006
  const sectionId = `${projectName}-${kebabName}`;
928
- const sectionPascal = (target.dir || target.displayName || "").replace(/[^a-zA-Z0-9]/g, "") || kebabName.split("-").map(s => s[0]?.toUpperCase() + s.slice(1)).join("");
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("");
929
1012
  // Scan the old source for imports, libraries, field usage
930
- const propNames = (target.props || []).map(p => p.name || "").filter(Boolean);
931
- const sourceScan = (oldSourceDir && target.dir)
1013
+ const propNames = (target.props || [])
1014
+ .map((p) => p.name || "")
1015
+ .filter(Boolean);
1016
+ const sourceScan = oldSourceDir && target.dir
932
1017
  ? scanSectionSource(path.join(oldSourceDir, target.dir), propNames)
933
1018
  : null;
934
1019
  parts.push(`# Section Migration Plan: ${oldName}`);
@@ -974,7 +1059,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
974
1059
  }
975
1060
  if (sourceScan.reactPackageUsage.length > 0) {
976
1061
  parts.push("");
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).`);
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).`);
978
1063
  }
979
1064
  if (sourceScan.oldStorefrontCalls.length > 0) {
980
1065
  parts.push("");
@@ -1020,7 +1105,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1020
1105
  if (oldType === "SLIDER") {
1021
1106
  newType = "NUMBER";
1022
1107
  notes = `Was SLIDER(min=${p.sliderData?.min}, max=${p.sliderData?.max}) — replace \`.value\` access with direct number`;
1023
- const prop = { name: newName, displayName: p.displayName || newName, type: "NUMBER" };
1108
+ const prop = {
1109
+ name: newName,
1110
+ displayName: p.displayName || newName,
1111
+ type: "NUMBER",
1112
+ };
1024
1113
  if (p.isRequired)
1025
1114
  prop.required = true;
1026
1115
  parentPropsJson.push(prop);
@@ -1028,7 +1117,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1028
1117
  else if (oldType === "PRODUCT_DETAIL") {
1029
1118
  newType = "PRODUCT";
1030
1119
  notes = "Renamed — PRODUCT_DETAIL → PRODUCT";
1031
- const prop = { name: newName, displayName: p.displayName || newName, type: "PRODUCT" };
1120
+ const prop = {
1121
+ name: newName,
1122
+ displayName: p.displayName || newName,
1123
+ type: "PRODUCT",
1124
+ };
1032
1125
  if (p.isRequired)
1033
1126
  prop.required = true;
1034
1127
  parentPropsJson.push(prop);
@@ -1039,7 +1132,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1039
1132
  if (cd.type === "DYNAMIC_LIST" || cd.type === "STATIC_LIST") {
1040
1133
  // Child component needed
1041
1134
  const itemObj = cd.nestedData?.[0];
1042
- const childName = itemObj?.typescriptName || (itemObj?.name ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1135
+ const childName = itemObj?.typescriptName ||
1136
+ (itemObj?.name
1137
+ ? itemObj.name.replace(/[^a-zA-Z0-9]/g, "")
1138
+ : `${sectionPascal}Item`);
1043
1139
  const childProps = [];
1044
1140
  const nestedWarnings = [];
1045
1141
  for (const f of (itemObj?.nestedData || [])) {
@@ -1050,11 +1146,18 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1050
1146
  fType = "NUMBER";
1051
1147
  else if (fType === "PRODUCT_DETAIL")
1052
1148
  fType = "PRODUCT";
1053
- else if (fType === "CUSTOM" || fType === "DYNAMIC_LIST" || fType === "STATIC_LIST" || fType === "OBJECT") {
1149
+ else if (fType === "CUSTOM" ||
1150
+ fType === "DYNAMIC_LIST" ||
1151
+ fType === "STATIC_LIST" ||
1152
+ fType === "OBJECT") {
1054
1153
  nestedWarnings.push(`\`${f.key}\` (${fType})`);
1055
1154
  fType = "COMPONENT_LIST";
1056
1155
  }
1057
- const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1156
+ const prop = {
1157
+ name: f.key,
1158
+ displayName: f.name || f.key,
1159
+ type: fType,
1160
+ };
1058
1161
  if (f.isRequired)
1059
1162
  prop.required = true;
1060
1163
  childProps.push(prop);
@@ -1066,10 +1169,14 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1066
1169
  if (childProps.length === 0 && sourceScan?.propFieldUsage[oldName]) {
1067
1170
  const inferred = sourceScan.propFieldUsage[oldName];
1068
1171
  for (const fieldName of inferred) {
1069
- childProps.push({ name: fieldName, displayName: fieldName, type: "TEXT" });
1172
+ childProps.push({
1173
+ name: fieldName,
1174
+ displayName: fieldName,
1175
+ type: "TEXT",
1176
+ });
1070
1177
  }
1071
1178
  if (inferred.length > 0) {
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.`;
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.`;
1073
1180
  }
1074
1181
  }
1075
1182
  children.push({
@@ -1098,7 +1205,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1098
1205
  let fType = f.type;
1099
1206
  if (fType === "SLIDER")
1100
1207
  fType = "NUMBER";
1101
- const prop = { name: f.key, displayName: f.name || f.key, type: fType };
1208
+ const prop = {
1209
+ name: f.key,
1210
+ displayName: f.name || f.key,
1211
+ type: fType,
1212
+ };
1102
1213
  if (f.isRequired)
1103
1214
  prop.required = true;
1104
1215
  parentPropsJson.push(prop);
@@ -1113,7 +1224,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1113
1224
  }
1114
1225
  else if (cd.type === "ENUM") {
1115
1226
  newType = "ENUM";
1116
- const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1227
+ const enumName = cd.typescriptName ||
1228
+ (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1117
1229
  const options = (cd.enumOptions || []).reduce((acc, o) => {
1118
1230
  if (o.displayName && o.value)
1119
1231
  acc[o.displayName] = o.value;
@@ -1121,7 +1233,12 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1121
1233
  }, {});
1122
1234
  enumsNeeded.push({ name: enumName, options });
1123
1235
  notes = `Was CUSTOM (ENUM) — create enum \`${enumName}\` via \`config add-enum\` first, then reference its enumId here`;
1124
- const prop = { name: newName, displayName: p.displayName || newName, type: "ENUM", enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>` };
1236
+ const prop = {
1237
+ name: newName,
1238
+ displayName: p.displayName || newName,
1239
+ type: "ENUM",
1240
+ enumTypeId: `<ENUM_ID_FROM_add-enum_${enumName}>`,
1241
+ };
1125
1242
  if (p.isRequired)
1126
1243
  prop.required = true;
1127
1244
  parentPropsJson.push(prop);
@@ -1130,7 +1247,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1130
1247
  }
1131
1248
  else {
1132
1249
  // Direct mapping
1133
- const prop = { name: newName, displayName: p.displayName || newName, type: newType };
1250
+ const prop = {
1251
+ name: newName,
1252
+ displayName: p.displayName || newName,
1253
+ type: newType,
1254
+ };
1134
1255
  if (p.isRequired)
1135
1256
  prop.required = true;
1136
1257
  parentPropsJson.push(prop);
@@ -1139,7 +1260,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1139
1260
  }
1140
1261
  parts.push("");
1141
1262
  // Custom Data Decision Callouts — per prop referencing a customData type
1142
- const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" && p.customDataId && customDataMap.has(p.customDataId));
1263
+ const customDataPropsForCallouts = (target.props || []).filter((p) => p.type === "CUSTOM" &&
1264
+ p.customDataId &&
1265
+ customDataMap.has(p.customDataId));
1143
1266
  if (customDataPropsForCallouts.length > 0) {
1144
1267
  parts.push(`## Custom Data Decisions to Make`);
1145
1268
  parts.push("");
@@ -1154,7 +1277,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1154
1277
  let shape = "";
1155
1278
  let shapeKind = "unknown";
1156
1279
  if (cd.type === "ENUM") {
1157
- const opts = (cd.enumOptions || []).map((o) => o.value || o.displayName).filter(Boolean);
1280
+ const opts = (cd.enumOptions || [])
1281
+ .map((o) => o.value || o.displayName)
1282
+ .filter(Boolean);
1158
1283
  shape = `enum {${opts.slice(0, 6).join(", ")}${opts.length > 6 ? ", ..." : ""}}`;
1159
1284
  shapeKind = "enum";
1160
1285
  }
@@ -1187,7 +1312,8 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1187
1312
  if (shapeKind === "enum") {
1188
1313
  parts.push(`**Default: enum prop.** Flat scalar set; use \`config add-enum\`.`);
1189
1314
  parts.push("");
1190
- const enumName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1315
+ const enumName = cd.typescriptName ||
1316
+ (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : "Enum");
1191
1317
  const enumOptions = (cd.enumOptions || []).reduce((acc, o) => {
1192
1318
  if (o.displayName && o.value)
1193
1319
  acc[o.displayName] = o.value;
@@ -1222,7 +1348,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1222
1348
  parts.push(`> See \`get_migration_guide("component-composition-decision-guide")\` for when \`COMPONENT_LIST\` is overkill.`);
1223
1349
  parts.push("");
1224
1350
  }
1225
- const compName = cd.typescriptName || (cd.name ? cd.name.replace(/[^a-zA-Z0-9]/g, "") : `${sectionPascal}Item`);
1351
+ const compName = cd.typescriptName ||
1352
+ (cd.name
1353
+ ? cd.name.replace(/[^a-zA-Z0-9]/g, "")
1354
+ : `${sectionPascal}Item`);
1226
1355
  const compPropsForCli = [];
1227
1356
  for (const f of fieldSource) {
1228
1357
  if (!f.key)
@@ -1232,7 +1361,11 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1232
1361
  fType = "NUMBER";
1233
1362
  else if (fType === "PRODUCT_DETAIL")
1234
1363
  fType = "PRODUCT";
1235
- compPropsForCli.push({ name: f.key, displayName: f.name || f.key, type: fType });
1364
+ compPropsForCli.push({
1365
+ name: f.key,
1366
+ displayName: f.name || f.key,
1367
+ type: fType,
1368
+ });
1236
1369
  }
1237
1370
  parts.push("```bash");
1238
1371
  if (isMinimal) {
@@ -1271,7 +1404,10 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1271
1404
  existing.usedByProps.push(ch.propName);
1272
1405
  }
1273
1406
  else {
1274
- uniqueChildren.set(ch.childName, { child: ch, usedByProps: [ch.propName] });
1407
+ uniqueChildren.set(ch.childName, {
1408
+ child: ch,
1409
+ usedByProps: [ch.propName],
1410
+ });
1275
1411
  }
1276
1412
  }
1277
1413
  if (uniqueChildren.size > 0) {
@@ -1284,7 +1420,7 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1284
1420
  for (const { child: ch, usedByProps } of uniqueChildren.values()) {
1285
1421
  parts.push(`### \`${ch.childName}\``);
1286
1422
  const propsLabel = usedByProps.length > 1
1287
- ? `Used by parent props: ${usedByProps.map(p => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1423
+ ? `Used by parent props: ${usedByProps.map((p) => `\`${p}\``).join(", ")} (${usedByProps.length}×)`
1288
1424
  : `For parent prop: \`${usedByProps[0]}\``;
1289
1425
  parts.push(propsLabel);
1290
1426
  parts.push(`Old customData: "${ch.customDataName}"`);
@@ -1330,12 +1466,15 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1330
1466
  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.`);
1331
1467
  }
1332
1468
  // Check if the section itself has data-driven list props (PRODUCT_LIST, BLOG_LIST, CATEGORY_LIST)
1333
- const dataListProps = (target.props || []).filter(p => p.type === "PRODUCT_LIST" || p.type === "BLOG_LIST" || p.type === "CATEGORY_LIST" || p.type === "BRAND_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");
1334
1473
  if (dataListProps.length > 0) {
1335
1474
  parts.push("");
1336
1475
  parts.push(`### Data-Driven List Rendering`);
1337
1476
  parts.push("");
1338
- parts.push(`This section has data-driven list props: ${dataListProps.map(p => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1477
+ parts.push(`This section has data-driven list props: ${dataListProps.map((p) => `\`${p.name}\` (${p.type})`).join(", ")}.`);
1339
1478
  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:`);
1340
1479
  parts.push("");
1341
1480
  parts.push("```tsx");
@@ -1362,9 +1501,22 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1362
1501
  parts.push(`See \`get_migration_guide("custom-data-conversion")\` → "Two Ways to Render Lists" for the full pattern.`);
1363
1502
  }
1364
1503
  // Detect form-page sections (0 or few props, name suggests a form/auth page)
1365
- const formKeywords = ["login", "register", "forgot", "recover", "password", "account", "email", "verification", "activate", "contact", "checkout", "address"];
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
+ ];
1366
1518
  const lowerDir = (target.dir || "").toLowerCase();
1367
- const isLikelyFormPage = formKeywords.some(kw => lowerDir.includes(kw));
1519
+ const isLikelyFormPage = formKeywords.some((kw) => lowerDir.includes(kw));
1368
1520
  if (isLikelyFormPage) {
1369
1521
  parts.push("");
1370
1522
  parts.push(`### Form Page Pattern`);
@@ -1393,7 +1545,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1393
1545
  // Fallback heuristic only when source scan unavailable
1394
1546
  const heuristicLibs = [];
1395
1547
  const lowerName = oldName.toLowerCase();
1396
- if (lowerName.includes("slider") || lowerName.includes("carousel") || lowerName.includes("banner")) {
1548
+ if (lowerName.includes("slider") ||
1549
+ lowerName.includes("carousel") ||
1550
+ lowerName.includes("banner")) {
1397
1551
  heuristicLibs.push("swiper");
1398
1552
  }
1399
1553
  if (lowerName.includes("marquee"))
@@ -1402,7 +1556,9 @@ function generateSectionMigrationPlan(theme, sectionName, projectName, oldSource
1402
1556
  heuristicLibs.push("react-player");
1403
1557
  if (lowerName.includes("chart"))
1404
1558
  heuristicLibs.push("recharts");
1405
- if (lowerName.includes("star") || lowerName.includes("rating") || lowerName.includes("review"))
1559
+ if (lowerName.includes("star") ||
1560
+ lowerName.includes("rating") ||
1561
+ lowerName.includes("review"))
1406
1562
  heuristicLibs.push("react-simple-star-rating");
1407
1563
  if (heuristicLibs.length > 0) {
1408
1564
  parts.push(`### Likely Library Replacements (heuristic — source not scanned)`);
@@ -1474,8 +1630,7 @@ function levenshtein(a, b) {
1474
1630
  const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
1475
1631
  curr[j] = Math.min(curr[j - 1] + 1, // insertion
1476
1632
  prev[j] + 1, // deletion
1477
- prev[j - 1] + cost // substitution
1478
- );
1633
+ prev[j - 1] + cost);
1479
1634
  }
1480
1635
  for (let j = 0; j <= n; j++)
1481
1636
  prev[j] = curr[j];
@@ -1506,17 +1661,29 @@ function searchFunctions(query) {
1506
1661
  const scored = storefrontData.functions
1507
1662
  .map((fn) => {
1508
1663
  const nameScore = matchScore(fn.name, query) * 3;
1509
- const displayNameScore = fn.displayName ? matchScore(fn.displayName, query) * 3 : 0;
1664
+ const displayNameScore = fn.displayName
1665
+ ? matchScore(fn.displayName, query) * 3
1666
+ : 0;
1510
1667
  const descScore = matchScore(fn.description, query);
1511
- const catScore = fn.categories.some((c) => matchScore(c, query) > 0) ? 5 : 0;
1668
+ const catScore = fn.categories.some((c) => matchScore(c, query) > 0)
1669
+ ? 5
1670
+ : 0;
1512
1671
  const paramScore = fn.params.some((p) => matchScore(p.name, query) > 0 || matchScore(p.description, query) > 0)
1513
1672
  ? 2
1514
1673
  : 0;
1515
1674
  const sigScore = matchScore(fn.signature, query) * 2;
1516
- const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0) ? 8 : 0;
1675
+ const typeScore = fn.parameterTypes?.some((t) => matchScore(t, query) > 0)
1676
+ ? 8
1677
+ : 0;
1517
1678
  return {
1518
1679
  fn,
1519
- score: nameScore + displayNameScore + descScore + catScore + paramScore + sigScore + typeScore,
1680
+ score: nameScore +
1681
+ displayNameScore +
1682
+ descScore +
1683
+ catScore +
1684
+ paramScore +
1685
+ sigScore +
1686
+ typeScore,
1520
1687
  };
1521
1688
  })
1522
1689
  .filter((item) => item.score > 0)
@@ -1530,7 +1697,11 @@ function searchFrameworkTopics(query) {
1530
1697
  const descScore = matchScore(topic.description, query) * 2;
1531
1698
  const contentScore = matchScore(topic.content, query);
1532
1699
  const tagScore = topic.tags.some((t) => matchScore(t, query) > 0) ? 5 : 0;
1533
- return { key, topic, score: titleScore + descScore + contentScore + tagScore };
1700
+ return {
1701
+ key,
1702
+ topic,
1703
+ score: titleScore + descScore + contentScore + tagScore,
1704
+ };
1534
1705
  })
1535
1706
  .filter((item) => item.score > 0)
1536
1707
  .sort((a, b) => b.score - a.score);
@@ -1545,7 +1716,9 @@ function searchTypes(query) {
1545
1716
  const propScore = td.properties?.some((p) => matchScore(p.name, query) > 0 || matchScore(p.type, query) > 0)
1546
1717
  ? 4
1547
1718
  : 0;
1548
- const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0) ? 4 : 0;
1719
+ const enumScore = td.enumValues?.some((v) => matchScore(v, query) > 0)
1720
+ ? 4
1721
+ : 0;
1549
1722
  return { td, score: nameScore + domainScore + propScore + enumScore };
1550
1723
  })
1551
1724
  .filter((item) => item.score > 0)
@@ -1587,8 +1760,12 @@ function formatFunctionDoc(fn) {
1587
1760
  return lines.join("\n");
1588
1761
  }
1589
1762
  function formatFunctionSummary(fn) {
1590
- const desc = fn.description ? fn.description.split(".")[0] + "." : "No description.";
1591
- const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
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
+ : "";
1592
1769
  return `- \`${fn.name}\`${alias} - ${desc}`;
1593
1770
  }
1594
1771
  function formatTypeDefinition(td, opts = {}) {
@@ -1629,7 +1806,9 @@ function formatTypeDefinition(td, opts = {}) {
1629
1806
  const fn = storefrontData.functions.find((f) => f.name === fnName);
1630
1807
  if (fn) {
1631
1808
  const desc = fn.description ? fn.description.split(".")[0] + "." : "";
1632
- const alias = fn.displayName && fn.displayName !== fn.name ? ` (alias: ${fn.displayName})` : "";
1809
+ const alias = fn.displayName && fn.displayName !== fn.name
1810
+ ? ` (alias: ${fn.displayName})`
1811
+ : "";
1633
1812
  lines.push(`- **\`${fn.name}\`**${alias} — ${desc}`);
1634
1813
  lines.push(` \`${fn.signature}\``);
1635
1814
  }
@@ -1638,7 +1817,9 @@ function formatTypeDefinition(td, opts = {}) {
1638
1817
  }
1639
1818
  }
1640
1819
  lines.push("");
1641
- lines.push("Use `get_functions_for_type(\"" + td.name + "\")` for full documentation of these functions.");
1820
+ lines.push('Use `get_functions_for_type("' +
1821
+ td.name +
1822
+ '")` for full documentation of these functions.');
1642
1823
  }
1643
1824
  return lines.join("\n");
1644
1825
  }
@@ -1656,7 +1837,9 @@ const server = new McpServer({
1656
1837
  name: "ikas-code-components",
1657
1838
  version: "0.1.0",
1658
1839
  }, {
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.",
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.\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.",
1660
1843
  });
1661
1844
  // Tool: search_docs
1662
1845
  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 }) => {
@@ -1697,13 +1880,20 @@ server.tool("search_docs", "Search across all ikas storefront API docs, framewor
1697
1880
  parts.push("");
1698
1881
  parts.push("Use `get_migration_guide(topic)` to get full content for any migration topic.");
1699
1882
  }
1700
- if (functions.length === 0 && topics.length === 0 && types.length === 0 && migrationTopics.length === 0) {
1883
+ if (functions.length === 0 &&
1884
+ topics.length === 0 &&
1885
+ types.length === 0 &&
1886
+ migrationTopics.length === 0) {
1701
1887
  parts.push(`No results found for "${query}". Try different keywords or use \`list_functions()\` to see all available functions.`);
1702
1888
  }
1703
1889
  return { content: [{ type: "text", text: parts.join("\n") }] };
1704
1890
  });
1705
1891
  // Tool: get_function_doc
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 }) => {
1892
+ server.tool("get_function_doc", "Get full documentation for a specific storefront API function including signature, parameters, return type, and example.", {
1893
+ name: z
1894
+ .string()
1895
+ .describe("Function name (e.g. 'addItemToCart', 'Router.navigate')"),
1896
+ }, async ({ name }) => {
1707
1897
  const nameLower = name.toLowerCase();
1708
1898
  // Phase 1: canonical-name match wins. A real function name always outranks
1709
1899
  // any displayName alias so aliases can never shadow the function they're
@@ -1711,7 +1901,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1711
1901
  // [BP-DISPLAY-NAME: hasCustomer] alias).
1712
1902
  const byName = storefrontData.functions.find((f) => f.name.toLowerCase() === nameLower);
1713
1903
  if (byName) {
1714
- return { content: [{ type: "text", text: formatFunctionDoc(byName) }] };
1904
+ return {
1905
+ content: [{ type: "text", text: formatFunctionDoc(byName) }],
1906
+ };
1715
1907
  }
1716
1908
  // Phase 2: fall back to displayName aliases.
1717
1909
  const byAlias = storefrontData.functions.filter((f) => f.displayName && f.displayName.toLowerCase() === nameLower);
@@ -1719,7 +1911,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1719
1911
  const fn = byAlias[0];
1720
1912
  const note = `> Note: "${name}" is a display alias for \`${fn.name}\`.\n\n`;
1721
1913
  return {
1722
- content: [{ type: "text", text: note + formatFunctionDoc(fn) }],
1914
+ content: [
1915
+ { type: "text", text: note + formatFunctionDoc(fn) },
1916
+ ],
1723
1917
  };
1724
1918
  }
1725
1919
  if (byAlias.length > 1) {
@@ -1737,7 +1931,9 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1737
1931
  (f.displayName && f.displayName.toLowerCase().includes(nameLower)));
1738
1932
  if (matches.length > 0) {
1739
1933
  const suggestions = matches.slice(0, 5).map((f) => {
1740
- const alias = f.displayName && f.displayName !== f.name ? ` (alias: ${f.displayName})` : "";
1934
+ const alias = f.displayName && f.displayName !== f.name
1935
+ ? ` (alias: ${f.displayName})`
1936
+ : "";
1741
1937
  return ` - ${f.name}${alias}`;
1742
1938
  });
1743
1939
  return {
@@ -1750,7 +1946,12 @@ server.tool("get_function_doc", "Get full documentation for a specific storefron
1750
1946
  };
1751
1947
  }
1752
1948
  return {
1753
- content: [{ type: "text", text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.` }],
1949
+ content: [
1950
+ {
1951
+ type: "text",
1952
+ text: `Function "${name}" not found. Use \`list_functions()\` to see all available functions.`,
1953
+ },
1954
+ ],
1754
1955
  };
1755
1956
  });
1756
1957
  // Tool: list_functions
@@ -1800,7 +2001,7 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
1800
2001
  if (uncategorized > 0) {
1801
2002
  lines.push(`- \`Other\` (${uncategorized})`);
1802
2003
  }
1803
- lines.push("", "Call `list_functions(category: \"<name>\")` to see one-line summaries for a category.");
2004
+ lines.push("", 'Call `list_functions(category: "<name>")` to see one-line summaries for a category.');
1804
2005
  return { content: [{ type: "text", text: lines.join("\n") }] };
1805
2006
  }
1806
2007
  const catLower = category.toLowerCase();
@@ -1832,7 +2033,11 @@ server.tool("list_functions", "List storefront API functions. Without a `categor
1832
2033
  return { content: [{ type: "text", text: parts.join("\n") }] };
1833
2034
  });
1834
2035
  // Tool: get_code_example
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 }) => {
2036
+ 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.", {
2037
+ task: z
2038
+ .string()
2039
+ .describe("Task description or example ID (call `list_examples()` for the full list)"),
2040
+ }, async ({ task }) => {
1836
2041
  const taskLower = task.toLowerCase();
1837
2042
  // Try exact ID match first
1838
2043
  let example = storefrontData.codeExamples.find((e) => e.id === taskLower);
@@ -1852,7 +2057,9 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1852
2057
  }
1853
2058
  }
1854
2059
  if (!example) {
1855
- const available = storefrontData.codeExamples.map((e) => ` - \`${e.id}\` - ${e.title}`).join("\n");
2060
+ const available = storefrontData.codeExamples
2061
+ .map((e) => ` - \`${e.id}\` - ${e.title}`)
2062
+ .join("\n");
1856
2063
  return {
1857
2064
  content: [
1858
2065
  {
@@ -1875,14 +2082,24 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1875
2082
  if (example.files && example.files.length > 0) {
1876
2083
  for (const file of example.files) {
1877
2084
  const ext = file.filename.split(".").pop() || "text";
1878
- const lang = ext === "tsx" || ext === "ts" ? "typescript" : ext === "css" ? "css" : ext === "json" ? "json" : "text";
2085
+ const lang = ext === "tsx" || ext === "ts"
2086
+ ? "typescript"
2087
+ : ext === "css"
2088
+ ? "css"
2089
+ : ext === "json"
2090
+ ? "json"
2091
+ : "text";
1879
2092
  // Add inline originality comments to CSS and TSX files
1880
2093
  let content = file.content;
1881
2094
  if (ext === "css") {
1882
- content = "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" + content;
2095
+ content =
2096
+ "/* EXAMPLE STYLING — create your own original CSS with different class names and design */\n" +
2097
+ content;
1883
2098
  }
1884
2099
  else if (ext === "tsx") {
1885
- content = "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" + content;
2100
+ content =
2101
+ "// EXAMPLE COMPONENT — use the API patterns but create your own JSX structure and layout\n" +
2102
+ content;
1886
2103
  }
1887
2104
  parts.push(`### ${file.filename}`, "", `\`\`\`${lang}`, content, "```", "");
1888
2105
  }
@@ -1896,63 +2113,88 @@ server.tool("get_code_example", "Get an API usage reference for a specific task.
1896
2113
  return { content: [{ type: "text", text: parts.join("\n") }] };
1897
2114
  });
1898
2115
  // Tool: get_framework_guide
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 }) => {
2116
+ 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.", {
2117
+ topic: z
2118
+ .string()
2119
+ .describe("Topic key or keyword (call `list_topics()` for the full list)"),
2120
+ }, async ({ topic }) => {
1900
2121
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
1901
2122
  // Alias mapping for common alternative topic names
1902
2123
  const topicAliases = {
1903
2124
  "form-handling": "form-patterns",
1904
- "forms": "form-patterns",
2125
+ forms: "form-patterns",
1905
2126
  "data-fetching": "async-data-patterns",
1906
- "async": "async-data-patterns",
1907
- "loading": "async-data-patterns",
2127
+ async: "async-data-patterns",
2128
+ loading: "async-data-patterns",
1908
2129
  "sub-components": "sub-component-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",
2130
+ subcomponents: "sub-component-patterns",
2131
+ routing: "navigation-patterns",
2132
+ router: "navigation-patterns",
2133
+ observer: "component-structure",
2134
+ reactivity: "component-structure",
2135
+ pitfalls: "common-pitfalls",
2136
+ gotchas: "common-pitfalls",
2137
+ mistakes: "common-pitfalls",
2138
+ header: "header-footer-patterns",
2139
+ footer: "header-footer-patterns",
2140
+ blog: "blog-patterns",
2141
+ cart: "cart-patterns",
2142
+ account: "account-patterns",
1922
2143
  "product-detail": "product-detail-patterns",
1923
2144
  "product-list": "product-list-patterns",
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",
2145
+ filtering: "product-list-patterns",
2146
+ reviews: "review-patterns",
2147
+ slider: "slider-overlay-patterns",
2148
+ overlay: "slider-overlay-patterns",
2149
+ modal: "slider-overlay-patterns",
2150
+ architecture: "real-world-architecture",
2151
+ theme: "real-world-architecture",
1931
2152
  "global-styles": "global-css",
1932
- "global": "global-css",
2153
+ global: "global-css",
1933
2154
  "css-variables": "global-css",
1934
2155
  "custom-properties": "global-css",
1935
2156
  };
1936
2157
  const resolvedTopic = topicAliases[topicLower] || topicLower;
1937
2158
  // Topics that involve MobX store reads get a reminder about root reactivity
1938
2159
  const storeTopics = new Set([
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",
2160
+ "product-detail-patterns",
2161
+ "product-list-patterns",
2162
+ "cart-patterns",
2163
+ "account-patterns",
2164
+ "header-footer-patterns",
2165
+ "review-patterns",
2166
+ "blog-patterns",
2167
+ "form-handling",
2168
+ "async-data-patterns",
2169
+ "component-structure",
2170
+ "imports",
1943
2171
  ]);
1944
2172
  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";
1945
2173
  // Try exact key match (with alias resolution)
1946
2174
  if (frameworkData.topics[resolvedTopic]) {
1947
2175
  const t = frameworkData.topics[resolvedTopic];
1948
2176
  const prefix = storeTopics.has(resolvedTopic) ? observerReminder : "";
1949
- return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2177
+ return {
2178
+ content: [
2179
+ {
2180
+ type: "text",
2181
+ text: `## ${t.title}\n\n${prefix}${t.content}`,
2182
+ },
2183
+ ],
2184
+ };
1950
2185
  }
1951
2186
  // Try original topic key (without alias) in case it's a direct key
1952
2187
  if (resolvedTopic !== topicLower && frameworkData.topics[topicLower]) {
1953
2188
  const t = frameworkData.topics[topicLower];
1954
2189
  const prefix = storeTopics.has(topicLower) ? observerReminder : "";
1955
- return { content: [{ type: "text", text: `## ${t.title}\n\n${prefix}${t.content}` }] };
2190
+ return {
2191
+ content: [
2192
+ {
2193
+ type: "text",
2194
+ text: `## ${t.title}\n\n${prefix}${t.content}`,
2195
+ },
2196
+ ],
2197
+ };
1956
2198
  }
1957
2199
  // Try keyword search
1958
2200
  const matches = searchFrameworkTopics(topic);
@@ -1960,7 +2202,12 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
1960
2202
  const best = matches[0];
1961
2203
  const prefix = storeTopics.has(best.key) ? observerReminder : "";
1962
2204
  return {
1963
- content: [{ type: "text", text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}` }],
2205
+ content: [
2206
+ {
2207
+ type: "text",
2208
+ text: `## ${best.topic.title}\n\n${prefix}${best.topic.content}`,
2209
+ },
2210
+ ],
1964
2211
  };
1965
2212
  }
1966
2213
  const available = Object.entries(frameworkData.topics)
@@ -1976,16 +2223,27 @@ server.tool("get_framework_guide", "Get a framework guide on a specific topic (e
1976
2223
  };
1977
2224
  });
1978
2225
  // Tool: get_type_definition
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 }) => {
2226
+ 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.", {
2227
+ name: z
2228
+ .string()
2229
+ .describe("Type or enum name (e.g. 'IkasProduct', 'IkasOrderStatus')"),
2230
+ }, async ({ name }) => {
1980
2231
  if (!typesData) {
1981
2232
  return {
1982
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2233
+ content: [
2234
+ {
2235
+ type: "text",
2236
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2237
+ },
2238
+ ],
1983
2239
  };
1984
2240
  }
1985
2241
  const nameLower = name.toLowerCase();
1986
2242
  const td = typesData.types.find((t) => t.name.toLowerCase() === nameLower);
1987
2243
  if (td) {
1988
- return { content: [{ type: "text", text: formatTypeDefinition(td) }] };
2244
+ return {
2245
+ content: [{ type: "text", text: formatTypeDefinition(td) }],
2246
+ };
1989
2247
  }
1990
2248
  // Fuzzy match
1991
2249
  const matches = typesData.types.filter((t) => t.name.toLowerCase().includes(nameLower));
@@ -2001,14 +2259,28 @@ server.tool("get_type_definition", "Get the full definition of a storefront type
2001
2259
  };
2002
2260
  }
2003
2261
  return {
2004
- content: [{ type: "text", text: `Type "${name}" not found. Use \`list_types()\` to see all available types.` }],
2262
+ content: [
2263
+ {
2264
+ type: "text",
2265
+ text: `Type "${name}" not found. Use \`list_types()\` to see all available types.`,
2266
+ },
2267
+ ],
2005
2268
  };
2006
2269
  });
2007
2270
  // Tool: get_functions_for_type
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 }) => {
2271
+ 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.", {
2272
+ typeName: z
2273
+ .string()
2274
+ .describe("Type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2275
+ }, async ({ typeName }) => {
2009
2276
  if (!typesData) {
2010
2277
  return {
2011
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2278
+ content: [
2279
+ {
2280
+ type: "text",
2281
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2282
+ },
2283
+ ],
2012
2284
  };
2013
2285
  }
2014
2286
  const nameLower = typeName.toLowerCase();
@@ -2028,7 +2300,12 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2028
2300
  };
2029
2301
  }
2030
2302
  return {
2031
- content: [{ type: "text", text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.` }],
2303
+ content: [
2304
+ {
2305
+ type: "text",
2306
+ text: `Type "${typeName}" not found. Use \`list_types()\` to see all available types.`,
2307
+ },
2308
+ ],
2032
2309
  };
2033
2310
  }
2034
2311
  if (!td.relatedFunctions || td.relatedFunctions.length === 0) {
@@ -2059,7 +2336,10 @@ server.tool("get_functions_for_type", "Get full documentation for all utility fu
2059
2336
  });
2060
2337
  // Tool: get_model_guide
2061
2338
  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.", {
2062
- model: z.string().optional().describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2339
+ model: z
2340
+ .string()
2341
+ .optional()
2342
+ .describe("Model type name (e.g. 'IkasImage', 'IkasProduct', 'IkasOrder')"),
2063
2343
  name: z.string().optional().describe("Alias for 'model'"),
2064
2344
  mode: z
2065
2345
  .enum(["summary", "full"])
@@ -2070,12 +2350,22 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2070
2350
  const model = modelParam || nameParam;
2071
2351
  if (!model) {
2072
2352
  return {
2073
- content: [{ type: "text", text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter." }],
2353
+ content: [
2354
+ {
2355
+ type: "text",
2356
+ text: "Please provide a model name (e.g. 'IkasProduct', 'IkasOrder'). Use the 'model' or 'name' parameter.",
2357
+ },
2358
+ ],
2074
2359
  };
2075
2360
  }
2076
2361
  if (!typesData) {
2077
2362
  return {
2078
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2363
+ content: [
2364
+ {
2365
+ type: "text",
2366
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2367
+ },
2368
+ ],
2079
2369
  };
2080
2370
  }
2081
2371
  const modelLower = model.toLowerCase();
@@ -2097,7 +2387,12 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2097
2387
  };
2098
2388
  }
2099
2389
  return {
2100
- content: [{ type: "text", text: `Model "${model}" not found. Use \`list_types()\` to see all available types.` }],
2390
+ content: [
2391
+ {
2392
+ type: "text",
2393
+ text: `Model "${model}" not found. Use \`list_types()\` to see all available types.`,
2394
+ },
2395
+ ],
2101
2396
  };
2102
2397
  }
2103
2398
  const parts = [`# Model Guide: ${td.name}\n`];
@@ -2205,13 +2500,23 @@ server.tool("get_model_guide", "Get an overview of a storefront model type. By d
2205
2500
  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 }) => {
2206
2501
  if (!typesData) {
2207
2502
  return {
2208
- content: [{ type: "text", text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first." }],
2503
+ content: [
2504
+ {
2505
+ type: "text",
2506
+ text: "Type data not available. Run 'npm run docs:generate' in storefront-docs first.",
2507
+ },
2508
+ ],
2209
2509
  };
2210
2510
  }
2211
2511
  const results = searchTypes(query).slice(0, 15);
2212
2512
  if (results.length === 0) {
2213
2513
  return {
2214
- content: [{ type: "text", text: `No types found matching "${query}". Use \`list_types()\` to see all available types.` }],
2514
+ content: [
2515
+ {
2516
+ type: "text",
2517
+ text: `No types found matching "${query}". Use \`list_types()\` to see all available types.`,
2518
+ },
2519
+ ],
2215
2520
  };
2216
2521
  }
2217
2522
  const parts = [`## Type Search Results for "${query}"\n`];
@@ -2275,7 +2580,7 @@ server.tool("list_types", "List storefront types and enums. Use `domain` and/or
2275
2580
  for (const [d, count] of sorted) {
2276
2581
  lines.push(`- \`${d}\` (${count})`);
2277
2582
  }
2278
- lines.push("", "Call `list_types(domain: \"<name>\")` to see summaries for a domain.");
2583
+ lines.push("", 'Call `list_types(domain: "<name>")` to see summaries for a domain.');
2279
2584
  return { content: [{ type: "text", text: lines.join("\n") }] };
2280
2585
  }
2281
2586
  const domainLower = domain.toLowerCase();
@@ -2318,11 +2623,21 @@ server.tool("get_prop_types", "Get all available ikas.config.json prop types wit
2318
2623
  const propTypesTopic = frameworkData.topics["prop-types"];
2319
2624
  if (propTypesTopic) {
2320
2625
  return {
2321
- content: [{ type: "text", text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}` }],
2626
+ content: [
2627
+ {
2628
+ type: "text",
2629
+ text: `## ${propTypesTopic.title}\n\n${propTypesTopic.content}`,
2630
+ },
2631
+ ],
2322
2632
  };
2323
2633
  }
2324
2634
  return {
2325
- content: [{ type: "text", text: "Prop types documentation not available." }],
2635
+ content: [
2636
+ {
2637
+ type: "text",
2638
+ text: "Prop types documentation not available.",
2639
+ },
2640
+ ],
2326
2641
  };
2327
2642
  });
2328
2643
  // Tool: get_section_template
@@ -2419,7 +2734,8 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2419
2734
  // the end of the response in the existing recipe-builder block.
2420
2735
  {
2421
2736
  const snippetStrForBanner = bundle.rootFiles["ikas-config-snippet.json"];
2422
- if (snippetStrForBanner && /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2737
+ if (snippetStrForBanner &&
2738
+ /<id-of-[A-Za-z0-9_]+>/.test(snippetStrForBanner)) {
2423
2739
  const childMatches = Array.from(snippetStrForBanner.matchAll(/<id-of-([A-Za-z0-9_]+)>/g));
2424
2740
  const uniqueChildren = Array.from(new Set(childMatches.map((m) => m[1])));
2425
2741
  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.`, "");
@@ -2543,7 +2859,10 @@ server.tool("get_section_template", "Get the root files of a starter section tem
2543
2859
  try {
2544
2860
  const childSnippet = JSON.parse(fs.readFileSync(childSnippetPath, "utf-8"));
2545
2861
  const childProps = (childSnippet.props || []).map((p) => {
2546
- const out = { name: p.name, type: p.type };
2862
+ const out = {
2863
+ name: p.name,
2864
+ type: p.type,
2865
+ };
2547
2866
  if (p.displayName)
2548
2867
  out.displayName = p.displayName;
2549
2868
  if (p.required)
@@ -2616,10 +2935,7 @@ server.tool("get_section_child", "Fetch one item's files from a section's childr
2616
2935
  .string()
2617
2936
  .optional()
2618
2937
  .describe("The item name as listed in `get_section_template`'s response (Children/Components/Sub-components)"),
2619
- child: z
2620
- .string()
2621
- .optional()
2622
- .describe("Alias for `name`"),
2938
+ child: z.string().optional().describe("Alias for `name`"),
2623
2939
  kind: z
2624
2940
  .enum(["children", "components", "sub-components"])
2625
2941
  .optional()
@@ -2782,7 +3098,11 @@ server.tool("list_section_types", "List all available `get_section_template` sec
2782
3098
  });
2783
3099
  // --- Migration tools ---
2784
3100
  // Tool: analyze_old_theme
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 }) => {
3101
+ 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.", {
3102
+ theme_json: z
3103
+ .string()
3104
+ .describe("The raw JSON content of the old theme.json file"),
3105
+ }, async ({ theme_json }) => {
2786
3106
  try {
2787
3107
  const parsed = JSON.parse(theme_json);
2788
3108
  const analysis = analyzeOldTheme(parsed);
@@ -2790,102 +3110,162 @@ server.tool("analyze_old_theme", "Analyze an old ikas storefront theme.json and
2790
3110
  }
2791
3111
  catch (err) {
2792
3112
  return {
2793
- content: [{ type: "text", text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.` }],
3113
+ content: [
3114
+ {
3115
+ type: "text",
3116
+ text: `Error parsing theme.json: ${err instanceof Error ? err.message : String(err)}. Make sure you're passing valid JSON.`,
3117
+ },
3118
+ ],
2794
3119
  };
2795
3120
  }
2796
3121
  });
2797
3122
  // Tool: get_migration_guide
2798
3123
  const migrationTopicAliases = {
2799
- "overview": "migration-overview",
2800
- "migrate": "migration-overview",
2801
- "custom": "custom-data-conversion",
3124
+ overview: "migration-overview",
3125
+ migrate: "migration-overview",
3126
+ custom: "custom-data-conversion",
2802
3127
  "custom-data": "custom-data-conversion",
2803
- "customdata": "custom-data-conversion",
3128
+ customdata: "custom-data-conversion",
2804
3129
  "dynamic-list": "custom-data-conversion",
2805
3130
  "component-list": "custom-data-conversion",
2806
- "slider": "prop-type-mapping",
2807
- "props": "prop-type-mapping",
3131
+ slider: "prop-type-mapping",
3132
+ props: "prop-type-mapping",
2808
3133
  "prop-mapping": "prop-type-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",
3134
+ types: "prop-type-mapping",
3135
+ react: "react-to-preact",
3136
+ preact: "react-to-preact",
3137
+ observer: "react-to-preact",
3138
+ libraries: "library-replacements",
3139
+ swiper: "library-replacements",
3140
+ headlessui: "library-replacements",
3141
+ tailwind: "library-replacements",
3142
+ tailwindcss: "library-replacements",
3143
+ recharts: "library-replacements",
3144
+ marquee: "library-replacements",
3145
+ imports: "storefront-import-mapping",
3146
+ storefront: "storefront-import-mapping",
2822
3147
  "bp-storefront": "storefront-import-mapping",
2823
3148
  "theme-json": "theme-json-anatomy",
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",
3149
+ anatomy: "theme-json-anatomy",
3150
+ decompose: "component-decomposition-strategy",
3151
+ decomposition: "component-decomposition-strategy",
3152
+ strategy: "component-decomposition-strategy",
3153
+ project: "complete-project-generation",
3154
+ generate: "complete-project-generation",
3155
+ generation: "complete-project-generation",
3156
+ settings: "settings-conversion",
3157
+ colors: "settings-conversion",
3158
+ fonts: "settings-conversion",
3159
+ find: "finding-new-system-equivalents",
3160
+ search: "finding-new-system-equivalents",
3161
+ discover: "finding-new-system-equivalents",
3162
+ equivalent: "finding-new-system-equivalents",
3163
+ equivalents: "finding-new-system-equivalents",
3164
+ replacement: "finding-new-system-equivalents",
2840
3165
  };
2841
3166
  const migrationTopicKeys = migrationData
2842
3167
  ? Object.keys(migrationData.topics)
2843
3168
  : [];
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 }) => {
3169
+ 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.`, {
3170
+ topic: z
3171
+ .string()
3172
+ .describe("Migration topic key, alias, or 'list' to see all topics"),
3173
+ }, async ({ topic }) => {
2845
3174
  if (!migrationData) {
2846
- return { content: [{ type: "text", text: "Migration data not available. Ensure data/migration.json exists." }] };
3175
+ return {
3176
+ content: [
3177
+ {
3178
+ type: "text",
3179
+ text: "Migration data not available. Ensure data/migration.json exists.",
3180
+ },
3181
+ ],
3182
+ };
2847
3183
  }
2848
3184
  if (topic.toLowerCase() === "list") {
2849
3185
  const available = Object.entries(migrationData.topics)
2850
3186
  .map(([key, t]) => `- \`${key}\` — ${t.title}: ${t.description}`)
2851
3187
  .join("\n");
2852
- return { content: [{ type: "text", text: `## Available Migration Topics\n\n${available}` }] };
3188
+ return {
3189
+ content: [
3190
+ {
3191
+ type: "text",
3192
+ text: `## Available Migration Topics\n\n${available}`,
3193
+ },
3194
+ ],
3195
+ };
2853
3196
  }
2854
3197
  const topicLower = topic.toLowerCase().replace(/\s+/g, "-");
2855
3198
  const resolvedTopic = migrationTopicAliases[topicLower] || topicLower;
2856
3199
  if (migrationData.topics[resolvedTopic]) {
2857
3200
  const t = migrationData.topics[resolvedTopic];
2858
- return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3201
+ return {
3202
+ content: [
3203
+ { type: "text", text: `## ${t.title}\n\n${t.content}` },
3204
+ ],
3205
+ };
2859
3206
  }
2860
3207
  // Try original key
2861
3208
  if (resolvedTopic !== topicLower && migrationData.topics[topicLower]) {
2862
3209
  const t = migrationData.topics[topicLower];
2863
- return { content: [{ type: "text", text: `## ${t.title}\n\n${t.content}` }] };
3210
+ return {
3211
+ content: [
3212
+ { type: "text", text: `## ${t.title}\n\n${t.content}` },
3213
+ ],
3214
+ };
2864
3215
  }
2865
3216
  // Keyword search
2866
3217
  const matches = searchMigrationTopics(topic);
2867
3218
  if (matches.length > 0) {
2868
3219
  const best = matches[0];
2869
- return { content: [{ type: "text", text: `## ${best.topic.title}\n\n${best.topic.content}` }] };
3220
+ return {
3221
+ content: [
3222
+ {
3223
+ type: "text",
3224
+ text: `## ${best.topic.title}\n\n${best.topic.content}`,
3225
+ },
3226
+ ],
3227
+ };
2870
3228
  }
2871
3229
  const available = Object.entries(migrationData.topics)
2872
3230
  .map(([key, t]) => ` - \`${key}\` - ${t.title}`)
2873
3231
  .join("\n");
2874
3232
  return {
2875
- content: [{ type: "text", text: `Migration topic "${topic}" not found. Available topics:\n${available}` }],
3233
+ content: [
3234
+ {
3235
+ type: "text",
3236
+ text: `Migration topic "${topic}" not found. Available topics:\n${available}`,
3237
+ },
3238
+ ],
2876
3239
  };
2877
3240
  });
2878
3241
  // Tool: get_migration_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 }) => {
3242
+ 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.`, {
3243
+ example: z.string().describe("Example name or 'list' to see all examples"),
3244
+ }, async ({ example }) => {
2880
3245
  if (example.toLowerCase() === "list") {
2881
3246
  if (migrationExampleNames.length === 0) {
2882
- return { content: [{ type: "text", text: "No migration examples available." }] };
3247
+ return {
3248
+ content: [
3249
+ { type: "text", text: "No migration examples available." },
3250
+ ],
3251
+ };
2883
3252
  }
2884
- const list = migrationExampleNames.map((name) => {
3253
+ const list = migrationExampleNames
3254
+ .map((name) => {
2885
3255
  const ex = loadMigrationExample(name);
2886
- return ex ? `- \`${name}\` — ${ex.title}: ${ex.description}` : `- \`${name}\``;
2887
- }).join("\n");
2888
- return { content: [{ type: "text", text: `## Available Migration Examples\n\n${list}` }] };
3256
+ return ex
3257
+ ? `- \`${name}\` — ${ex.title}: ${ex.description}`
3258
+ : `- \`${name}\``;
3259
+ })
3260
+ .join("\n");
3261
+ return {
3262
+ content: [
3263
+ {
3264
+ type: "text",
3265
+ text: `## Available Migration Examples\n\n${list}`,
3266
+ },
3267
+ ],
3268
+ };
2889
3269
  }
2890
3270
  const exampleLower = example.toLowerCase();
2891
3271
  let exName = migrationExampleNames.find((n) => n === exampleLower);
@@ -2895,19 +3275,26 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
2895
3275
  if (!exName) {
2896
3276
  const available = migrationExampleNames.join(", ");
2897
3277
  return {
2898
- content: [{ type: "text", text: `Migration example "${example}" not found. Available: ${available}` }],
3278
+ content: [
3279
+ {
3280
+ type: "text",
3281
+ text: `Migration example "${example}" not found. Available: ${available}`,
3282
+ },
3283
+ ],
2899
3284
  };
2900
3285
  }
2901
3286
  const ex = loadMigrationExample(exName);
2902
3287
  if (!ex) {
2903
- return { content: [{ type: "text", text: `Failed to load migration example "${exName}".` }] };
3288
+ return {
3289
+ content: [
3290
+ {
3291
+ type: "text",
3292
+ text: `Failed to load migration example "${exName}".`,
3293
+ },
3294
+ ],
3295
+ };
2904
3296
  }
2905
- const parts = [
2906
- `## ${ex.title}`,
2907
- "",
2908
- ex.description,
2909
- "",
2910
- ];
3297
+ const parts = [`## ${ex.title}`, "", ex.description, ""];
2911
3298
  for (const [filename, content] of Object.entries(ex.files)) {
2912
3299
  const ext = filename.split(".").pop() || "text";
2913
3300
  const lang = ext === "tsx" || ext === "ts"
@@ -2926,13 +3313,31 @@ server.tool("get_migration_example", `Get a concrete before/after migration exam
2926
3313
  });
2927
3314
  // Tool: plan_migration
2928
3315
  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).", {
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 }) => {
3316
+ theme_json: z
3317
+ .string()
3318
+ .optional()
3319
+ .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."),
3320
+ theme_json_path: z
3321
+ .string()
3322
+ .optional()
3323
+ .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3324
+ project_name: z
3325
+ .string()
3326
+ .optional()
3327
+ .describe("Target new project name, used to prefix migration-tracking IDs (default: 'my-theme')"),
3328
+ old_source_dir: z
3329
+ .string()
3330
+ .optional()
3331
+ .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."),
3332
+ project_root: z
3333
+ .string()
3334
+ .optional()
3335
+ .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."),
3336
+ overwrite: z
3337
+ .boolean()
3338
+ .optional()
3339
+ .describe("If MIGRATION.md already exists at <project_root>/MIGRATION.md and is non-empty, refuse the write unless this is true. Default: false."),
3340
+ }, async ({ theme_json, theme_json_path, project_name, old_source_dir, project_root, overwrite, }) => {
2936
3341
  try {
2937
3342
  const parsed = resolveThemeJson(theme_json, theme_json_path);
2938
3343
  const projectName = project_name || "my-theme";
@@ -2993,18 +3398,37 @@ server.tool("plan_migration", "Generate the **initial** migration plan and (when
2993
3398
  }
2994
3399
  catch (err) {
2995
3400
  return {
2996
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3401
+ content: [
3402
+ {
3403
+ type: "text",
3404
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3405
+ },
3406
+ ],
2997
3407
  };
2998
3408
  }
2999
3409
  });
3000
3410
  // Tool: get_section_migration_plan
3001
3411
  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.", {
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 }) => {
3412
+ theme_json: z
3413
+ .string()
3414
+ .optional()
3415
+ .describe("Raw JSON content of the old theme.json. EITHER this OR theme_json_path is required (not both)."),
3416
+ theme_json_path: z
3417
+ .string()
3418
+ .optional()
3419
+ .describe("Absolute path to the old theme.json file on disk. Preferred for any real-world theme."),
3420
+ section_name: z
3421
+ .string()
3422
+ .describe("Old component name (e.g. 'Navbar', 'ProductGrid') or dir name, OR the new section ID (e.g. 'my-theme-navbar')"),
3423
+ project_name: z
3424
+ .string()
3425
+ .optional()
3426
+ .describe("Target new project name (must match what was used in plan_migration). Default: 'my-theme'"),
3427
+ old_source_dir: z
3428
+ .string()
3429
+ .optional()
3430
+ .describe("Absolute path to old src/ directory (used to output exact source file paths to read)"),
3431
+ }, async ({ theme_json, theme_json_path, section_name, project_name, old_source_dir, }) => {
3008
3432
  try {
3009
3433
  const parsed = resolveThemeJson(theme_json, theme_json_path);
3010
3434
  const projectName = project_name || "my-theme";
@@ -3013,7 +3437,12 @@ server.tool("get_section_migration_plan", "Returns concrete CLI commands and pro
3013
3437
  }
3014
3438
  catch (err) {
3015
3439
  return {
3016
- content: [{ type: "text", text: `Error: ${err instanceof Error ? err.message : String(err)}` }],
3440
+ content: [
3441
+ {
3442
+ type: "text",
3443
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
3444
+ },
3445
+ ],
3017
3446
  };
3018
3447
  }
3019
3448
  });
@@ -3049,7 +3478,11 @@ async function runIkasComponentCli(projectRoot, args) {
3049
3478
  : err
3050
3479
  ? 1
3051
3480
  : 0;
3052
- resolve({ stdout: stdout?.toString() ?? "", stderr: stderr?.toString() ?? "", exitCode });
3481
+ resolve({
3482
+ stdout: stdout?.toString() ?? "",
3483
+ stderr: stderr?.toString() ?? "",
3484
+ exitCode,
3485
+ });
3053
3486
  });
3054
3487
  });
3055
3488
  }
@@ -3059,7 +3492,11 @@ function parseCliJson(stdout) {
3059
3492
  return null;
3060
3493
  // CLI prints exactly one JSON object on stdout; if multiple lines appeared
3061
3494
  // (e.g., warnings on stderr leaked), take the last non-empty line.
3062
- const last = trimmed.split("\n").map(l => l.trim()).filter(Boolean).pop();
3495
+ const last = trimmed
3496
+ .split("\n")
3497
+ .map((l) => l.trim())
3498
+ .filter(Boolean)
3499
+ .pop();
3063
3500
  if (!last)
3064
3501
  return null;
3065
3502
  try {
@@ -3074,7 +3511,11 @@ async function callEditorAction(projectRoot, args) {
3074
3511
  const { stdout, stderr, exitCode } = await runIkasComponentCli(projectRoot, args);
3075
3512
  const parsed = parseCliJson(stdout);
3076
3513
  if (parsed) {
3077
- return { content: [{ type: "text", text: JSON.stringify(parsed, null, 2) }] };
3514
+ return {
3515
+ content: [
3516
+ { type: "text", text: JSON.stringify(parsed, null, 2) },
3517
+ ],
3518
+ };
3078
3519
  }
3079
3520
  return {
3080
3521
  content: [
@@ -3097,38 +3538,77 @@ async function callEditorAction(projectRoot, args) {
3097
3538
  };
3098
3539
  }
3099
3540
  }
3541
+ //
3100
3542
  // Tool: list_editor_pages
3101
3543
  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`.", {
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)."),
3544
+ project_root: z
3545
+ .string()
3546
+ .describe("Absolute path to the code-component project (where `node_modules/.bin/ikas-component` lives)."),
3547
+ port: z
3548
+ .number()
3549
+ .optional()
3550
+ .describe("Dev server WebSocket port (default 5201)."),
3104
3551
  }, async ({ project_root, port }) => {
3105
3552
  const args = ["list-pages", ...(port ? ["--port", String(port)] : [])];
3106
3553
  return callEditorAction(project_root, args);
3107
3554
  });
3108
3555
  // Tool: list_imported_sections
3109
3556
  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`.", {
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)."),
3557
+ project_root: z
3558
+ .string()
3559
+ .describe("Absolute path to the code-component project."),
3560
+ port: z
3561
+ .number()
3562
+ .optional()
3563
+ .describe("Dev server WebSocket port (default 5201)."),
3112
3564
  }, async ({ project_root, port }) => {
3113
- const args = ["list-imported", "--sections-only", ...(port ? ["--port", String(port)] : [])];
3565
+ const args = [
3566
+ "list-imported",
3567
+ "--sections-only",
3568
+ ...(port ? ["--port", String(port)] : []),
3569
+ ];
3114
3570
  return callEditorAction(project_root, args);
3115
3571
  });
3116
3572
  // Tool: import_section
3117
3573
  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`).", {
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)."),
3574
+ project_root: z
3575
+ .string()
3576
+ .describe("Absolute path to the code-component project."),
3577
+ component_id: z
3578
+ .string()
3579
+ .describe("Component id from `ikas.config.json` (strict — no name resolution)."),
3580
+ port: z
3581
+ .number()
3582
+ .optional()
3583
+ .describe("Dev server WebSocket port (default 5201)."),
3121
3584
  }, async ({ project_root, component_id, port }) => {
3122
- const args = ["import", "--id", component_id, ...(port ? ["--port", String(port)] : [])];
3585
+ const args = [
3586
+ "import",
3587
+ "--id",
3588
+ component_id,
3589
+ ...(port ? ["--port", String(port)] : []),
3590
+ ];
3123
3591
  return callEditorAction(project_root, args);
3124
3592
  });
3125
3593
  // Tool: add_section_to_page
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)."),
3594
+ 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).', {
3595
+ project_root: z
3596
+ .string()
3597
+ .describe("Absolute path to the code-component project."),
3598
+ component_id: z
3599
+ .string()
3600
+ .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)."),
3129
3601
  page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
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)."),
3602
+ index: z
3603
+ .number()
3604
+ .int()
3605
+ .nonnegative()
3606
+ .optional()
3607
+ .describe("Zero-based insertion index in the page; appends when omitted."),
3608
+ port: z
3609
+ .number()
3610
+ .optional()
3611
+ .describe("Dev server WebSocket port (default 5201)."),
3132
3612
  }, async ({ project_root, component_id, page_id, index, port }) => {
3133
3613
  const args = [
3134
3614
  "add-to-page",
@@ -3141,6 +3621,64 @@ server.tool("add_section_to_page", "Place an already-imported section-type code
3141
3621
  ];
3142
3622
  return callEditorAction(project_root, args);
3143
3623
  });
3624
+ // Tool: list_page_sections
3625
+ 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.", {
3626
+ project_root: z
3627
+ .string()
3628
+ .describe("Absolute path to the code-component project."),
3629
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3630
+ port: z
3631
+ .number()
3632
+ .optional()
3633
+ .describe("Dev server WebSocket port (default 5201)."),
3634
+ }, async ({ project_root, page_id, port }) => {
3635
+ const args = [
3636
+ "list-page-sections",
3637
+ "--page-id",
3638
+ page_id,
3639
+ ...(port ? ["--port", String(port)] : []),
3640
+ ];
3641
+ return callEditorAction(project_root, args);
3642
+ });
3643
+ // Tool: update_section_prop
3644
+ server.tool("update_section_prop", 'Change a single prop value of a section placed on a page. 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 prop types (image, link, list types) use their own object shape — inspect the existing entry in `list_page_sections` `propValues` to match it. The change is applied with undo support.', {
3645
+ project_root: z
3646
+ .string()
3647
+ .describe("Absolute path to the code-component project."),
3648
+ page_id: z.string().describe("Target page id (from `list_editor_pages`)."),
3649
+ element_id: z
3650
+ .string()
3651
+ .describe("Placed-section elementId identifying THIS placement on the page (from `list_page_sections`)."),
3652
+ prop_id: z
3653
+ .string()
3654
+ .optional()
3655
+ .describe("Blueprint prop id to update (from `list_page_sections`). Provide this or `prop_name`."),
3656
+ prop_name: z
3657
+ .string()
3658
+ .optional()
3659
+ .describe("Blueprint prop name to update (alternative to `prop_id`)."),
3660
+ value: z
3661
+ .any()
3662
+ .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.'),
3663
+ port: z
3664
+ .number()
3665
+ .optional()
3666
+ .describe("Dev server WebSocket port (default 5201)."),
3667
+ }, async ({ project_root, page_id, element_id, prop_id, prop_name, value, port }) => {
3668
+ const args = [
3669
+ "update-section-prop",
3670
+ "--page-id",
3671
+ page_id,
3672
+ "--element-id",
3673
+ element_id,
3674
+ ...(prop_id ? ["--prop-id", prop_id] : []),
3675
+ ...(prop_name ? ["--prop-name", prop_name] : []),
3676
+ "--value",
3677
+ JSON.stringify(value),
3678
+ ...(port ? ["--port", String(port)] : []),
3679
+ ];
3680
+ return callEditorAction(project_root, args);
3681
+ });
3144
3682
  // --- Start server ---
3145
3683
  async function main() {
3146
3684
  const transport = new StdioServerTransport();