@ikas/code-components-mcp 1.4.0-beta.30 → 1.4.0-beta.32

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