webcake-storefront-mcp 1.26.1 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -237,10 +237,13 @@ create the globals — they embed into each page's source. If you later overwrit
237
237
  later with update_global_section_element(s) and it updates on every page at once.
238
238
 
239
239
  ## Popups (newsletter / promo / age-gate)
240
- A popup is a GLOBAL SOURCE, not a page section. Build it, store it, then trigger it:
241
- 1. Build the popup body (new_section: heading + text + form/input + a close button), then wrap it:
242
- new_element("popup", { children:[<that section>], specials:{ /* trigger + overlay */ } }).
243
- Trigger/behaviour (auto-open after a delay, exit-intent, only-once, overlay) lives in specials.
240
+ A popup is a GLOBAL SOURCE, not a page section. FAST PATH: \`scaffold_popup({ brand, headline, offer })\`
241
+ builds + saves a designed newsletter popup (heading + text + email form + close button, centred)
242
+ and returns its id. To build one by hand:
243
+ 1. Build the popup node: new_element("popup", { children:[…heading,text,form,close button…], style:{ width:480, background:"#fff", borderRadius:"12px" }, config:{ popupHorizontalPosition:"center", popupVerticalPosition:"center" }, specials:{ effect:"fade-in", timeAnim:0.5 } }).
244
+ - SIZE + POSITION live in the popup's runtime style/config (width/height/background + popupHorizontalPosition/popupVerticalPosition), NOT in specials. createPopup seeds a centred-modal default.
245
+ - TRIGGER (auto-open) lives in SPECIALS: \`openPopupAction:"openPopupWithTime"\` + \`timeOpenPopup:<seconds>\` for a delay; \`page_ids:[…]\` to limit which pages it shows on; \`effect\`/\`timeAnim\` for the animation. (There is no exit-intent/only-once flag in the node — those are app settings.)
246
+ - OVERLAY is NOT on the popup node — it's a field on the open_popup EVENT (popup_overlay:true).
244
247
  2. Save it: create_global_source({ component:"popup", source:{ sections:[<popupNode>] } }) -> returns its id.
245
248
  3. Open/close from any element via events: a button { action:"open_popup", popup_id:"<id>", popup_overlay:true };
246
249
  a close button inside { action:"close_popup", popup_id:"<id>" }; a form can auto-close on its
@@ -192,7 +192,7 @@ export function buildSection(childSpecs = [], sectionOpts = {}) {
192
192
  };
193
193
  return section;
194
194
  }
195
- function buildFromSpec(spec) {
195
+ export function buildFromSpec(spec) {
196
196
  if (!spec || !spec.type) {
197
197
  throw new Error("Each element spec must have a 'type'.");
198
198
  }
@@ -13,9 +13,10 @@
13
13
  // Colours use the site theme CSS vars by default (var(--color_02) accent, var(--color_00)
14
14
  // text) so a generated site matches whatever palette the theme already defines; callers can
15
15
  // override any slot via a Palette object.
16
- import { buildSection, walk } from "./page.js";
16
+ import { buildSection, buildFromSpec, stackChildren, walk } from "./page.js";
17
17
  import { buildElement } from "./catalog.js";
18
18
  import { normalizeEvents } from "./events.js";
19
+ const X_ICON_SVG = "<svg xmlns='http://www.w3.org/2000/svg' fill='currentColor' width='100%' height='100%' viewBox='0 0 24 24'><path d='M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z'></path></svg>";
19
20
  // Slots map onto the site theme's 5×5 colour matrix, surfaced as CSS vars var(--color_RC)
20
21
  // (R=row, C=col). Row 0 is greyscale (00=white … 04=black); row 2 is the BRAND row. Using
21
22
  // var(--color_00) for text was a bug — that's WHITE (invisible on the white surface).
@@ -31,6 +32,40 @@ const DEFAULT_PALETTE = {
31
32
  export function resolvePalette(p = {}) {
32
33
  return { ...DEFAULT_PALETTE, ...Object.fromEntries(Object.entries(p).filter(([, v]) => v != null)) };
33
34
  }
35
+ /** Relative luminance of a #hex colour (0 dark … 1 light). */
36
+ function luminance(hex) {
37
+ const h = String(hex || "").replace("#", "").slice(0, 6);
38
+ if (h.length < 6)
39
+ return 0;
40
+ const r = parseInt(h.slice(0, 2), 16) / 255;
41
+ const g = parseInt(h.slice(2, 4), 16) / 255;
42
+ const b = parseInt(h.slice(4, 6), 16) / 255;
43
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
44
+ }
45
+ /**
46
+ * Read the site's active theme colour matrix and return a Palette override so generated pages
47
+ * stay BRAND-CONSISTENT and CONTRAST-SAFE. The default palette uses var(--color_20) (the brand
48
+ * seed) as the accent — but for a LIGHT brand seed (e.g. a beige store) a white button label on
49
+ * it is unreadable, so we switch the accent to var(--color_24) (the darkest brand shade). Best
50
+ * effort: returns {} on any error (the var(--color_NN) defaults still resolve per-site).
51
+ */
52
+ export async function contrastSafePalette(api) {
53
+ try {
54
+ const res = await api.listThemes();
55
+ const themes = (res && res.data) || res || [];
56
+ const arr = Array.isArray(themes) ? themes : (themes.themes || []);
57
+ const th = arr.find((t) => t.is_selected) || arr[0];
58
+ const m = th && th.colors;
59
+ if (Array.isArray(m) && Array.isArray(m[2]) && m[2][0]) {
60
+ if (luminance(m[2][0]) > 0.62)
61
+ return { accent: "var(--color_24)" };
62
+ }
63
+ }
64
+ catch {
65
+ /* keep the var()-based defaults */
66
+ }
67
+ return {};
68
+ }
34
69
  // ---------------------------------------------------------------------------
35
70
  // small spec helpers (return element specs for buildSection/buildRow children)
36
71
  // ---------------------------------------------------------------------------
@@ -358,6 +393,48 @@ export function footerSection(opts = {}) {
358
393
  });
359
394
  }
360
395
  // ---------------------------------------------------------------------------
396
+ // POPUP (newsletter / promo) — a global SOURCE, not a page section
397
+ // ---------------------------------------------------------------------------
398
+ /**
399
+ * A designed newsletter/promo popup, returned as a ready-to-save global-source source
400
+ * `{ sections: [ <popup node> ] }`. The popup carries an auto-open-after-delay trigger in
401
+ * specials and a centred-modal geometry (seeded by createPopup); the close button references
402
+ * the popup's own id. Save with create_global_source({ component:"popup", source }).
403
+ * Run finalizeForRender(source) BEFORE saving (the tool does this).
404
+ */
405
+ export function newsletterPopupSource(opts = {}) {
406
+ const p = resolvePalette(opts.palette);
407
+ const heading = {
408
+ type: "text",
409
+ opts: { text: opts.headline || "Nhận ưu đãi 10%", specials: { tag: "h2" }, style: { fontSize: "26px", fontWeight: "800", color: p.text, textAlign: "center", lineHeight: "1.3" } },
410
+ };
411
+ const sub = {
412
+ type: "text",
413
+ opts: { text: opts.subtext || "Đăng ký nhận tin để không bỏ lỡ khuyến mãi mới nhất.", style: { fontSize: "15px", color: p.muted, textAlign: "center", lineHeight: "1.6" } },
414
+ };
415
+ const form = {
416
+ type: "form",
417
+ opts: { specials: { type: "subscribe" }, config: { backgroundInput: p.surface, placeholderColor: p.muted, labelColor: p.text, textPadding: 14, rowGap: 12 } },
418
+ children: [
419
+ { type: "email", opts: { specials: { field_name: "email", placeholder: "Email của bạn", show_label: false }, config: { backgroundInput: p.surface, textPadding: 14 }, style: { borderColor: p.border, borderRadius: "8px", height: 46 } } },
420
+ { type: "submit-button", opts: { text: opts.ctaLabel || "Đăng ký ngay", style: { background: p.accent, color: p.onAccent, borderRadius: "8px", height: 46, fontWeight: "600", width: "100%", textAlign: "center" } } },
421
+ ],
422
+ };
423
+ // Close button: a small X in the top-right that closes this popup (wired to the popup id below).
424
+ const close = { type: "rectangle", opts: { config: { mask: X_ICON_SVG }, style: { width: 22, height: 22, background: p.muted } } };
425
+ const content = [heading, sub, form, close].map((s) => buildFromSpec(s));
426
+ const popup = buildElement("popup", {
427
+ specials: { effect: "fade-in", timeAnim: 0.5, openPopupAction: "openPopupWithTime", timeOpenPopup: opts.delaySeconds ?? 6 },
428
+ style: { width: 460, background: p.surface, borderRadius: "14px", paddingTop: 40, paddingBottom: 40, paddingLeft: 32, paddingRight: 32 },
429
+ config: { popupHorizontalPosition: "center", popupVerticalPosition: "center" },
430
+ });
431
+ stackChildren(popup, content, { rowGap: 18 });
432
+ // Wire the close button to dismiss THIS popup now that we know its id.
433
+ const closeNode = content[content.length - 1];
434
+ closeNode.events = normalizeEvents([{ action: "close_popup", popup_id: popup.id }], "rectangle");
435
+ return { sections: [popup] };
436
+ }
437
+ // ---------------------------------------------------------------------------
361
438
  // registry the scaffolder iterates
362
439
  // ---------------------------------------------------------------------------
363
440
  /**
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.28.0",
4
+ "d": "26/06/2026",
5
+ "type": "Added",
6
+ "en": "New scaffold_popup tool builds and saves a designed newsletter/promo popup (heading + subtext + email subscribe form + close button, centred modal)…",
7
+ "vi": "Tool mới scaffold_popup xây dựng và lưu một popup newsletter/promo được thiết kế sẵn (tiêu đề + phụ đề + form đăng ký email + nút đóng, dạng modal…"
8
+ },
9
+ {
10
+ "v": "1.27.0",
11
+ "d": "26/06/2026",
12
+ "type": "Added",
13
+ "en": "build_page now accepts an seo object (title, description, keyword, favicon, thumbnail) that is written to page.settings.seo with Open Graph…",
14
+ "vi": "build_page nay nhận thêm đối tượng seo (title, description, keyword, favicon, thumbnail) và ghi vào page.settings.seo kèm mirroring Open Graph; hỗ…"
15
+ },
2
16
  {
3
17
  "v": "1.26.1",
4
18
  "d": "26/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Changed",
27
41
  "en": "list_elements now documents tabIndex and text_animation_type as shared specials present on every element, covering tab-panel visibility binding and…",
28
42
  "vi": "list_elements nay tài liệu hóa tabIndex và text_animation_type là các specials dùng chung có trên mọi phần tử, bao gồm gắn kết hiển thị/ẩn cho…"
29
- },
30
- {
31
- "v": "1.23.0",
32
- "d": "25/06/2026",
33
- "type": "Added",
34
- "en": "get_build_guide now includes a category-page filter recipe explaining how sidebar filter widgets (checkbox-group, color-group, tags,…",
35
- "vi": "get_build_guide nay bổ sung công thức filter trang danh mục, giải thích cách các widget lọc trong sidebar (checkbox-group, color-group, tags,…"
36
- },
37
- {
38
- "v": "1.22.0",
39
- "d": "25/06/2026",
40
- "type": "Added",
41
- "en": "list_bindings now returns a meta_keys map documenting binding combine-keys mined from 34 production templates — name_style, attr_id, prefix_content,…",
42
- "vi": "list_bindings nay trả về map meta_keys tài liệu hóa các combine-key liên kết dữ liệu được khai thác từ 34 template production — name_style, attr_id,…"
43
43
  }
44
44
  ]
@@ -4,7 +4,7 @@ import { listElements, getElement, buildElement } from "../builder/catalog.js";
4
4
  import { describeEventsCatalog } from "../builder/events.js";
5
5
  import { describeBindingsCatalog } from "../builder/bindings.js";
6
6
  import { buildSection, buildRow, newPageSkeleton, validatePage, finalizeForRender, reassignIds, } from "../builder/page.js";
7
- import { STORE_PAGE_TEMPLATES, resolvePalette, wireNavigation } from "../builder/templates.js";
7
+ import { STORE_PAGE_TEMPLATES, resolvePalette, contrastSafePalette, wireNavigation } from "../builder/templates.js";
8
8
  // Recursive spec for new_section / build_page children.
9
9
  const elementSpec = z.object({
10
10
  type: z.string().describe("Element type (see list_elements)"),
@@ -28,14 +28,39 @@ function newPageId(res) {
28
28
  // builderx_spa); SPECIAL kinds also require a site-level data-source flag enabled on
29
29
  // site.settings, otherwise components that bind to store/customer/blog data render
30
30
  // with null bindings. build_page sets both for you.
31
- const PAGE_TYPE_NUM = {
31
+ // Page kind → numeric backend type. Exported so create_page maps the same way (the backend
32
+ // type is numeric 1–7, NOT a string).
33
+ export const PAGE_TYPE_NUM = {
32
34
  main: 1, store: 2, member: 3, blog: 4, custom: 5, error: 6, maintain: 7,
33
35
  };
34
36
  const PAGE_TYPE_FLAG = {
35
37
  store: "use_store", member: "use_member", blog: "use_blog",
36
38
  error: "use_error", maintain: "use_maintain",
37
39
  };
38
- const PAGE_KINDS = ["main", "store", "member", "blog", "custom", "error", "maintain"];
40
+ export const PAGE_KINDS = ["main", "store", "member", "blog", "custom", "error", "maintain"];
41
+ /** Build the page.settings.seo block from simple inputs (the real shape; tokens like
42
+ * {{name_page}} / {{name_site}} are resolved by the storefront). */
43
+ export function buildPageSeo(seo = {}) {
44
+ const out = {};
45
+ if (seo.title)
46
+ out.title = seo.title;
47
+ if (seo.description)
48
+ out.description = seo.description;
49
+ if (seo.keyword)
50
+ out.keyword = seo.keyword;
51
+ if (seo.favicon)
52
+ out.favicon = seo.favicon;
53
+ if (seo.thumbnail)
54
+ out.thumbnail = seo.thumbnail;
55
+ // Open Graph mirrors title/description/thumbnail when not given explicitly.
56
+ if (seo.title || seo.og_title)
57
+ out.og_title = seo.og_title || seo.title;
58
+ if (seo.description || seo.og_description)
59
+ out.og_description = seo.og_description || seo.description;
60
+ if (seo.thumbnail)
61
+ out.og_image = seo.thumbnail;
62
+ return out;
63
+ }
39
64
  export function registerBuilderTools(server, api, handle) {
40
65
  server.tool("get_build_guide", "Get the BuilderX page authoring guide: page shape, the grid layout model, styling, breakpoints, forms/data, and the build workflow. Read this before building or heavily editing a page.", {}, () => handle(async () => ({ guide: BUILD_GUIDE })));
41
66
  server.tool("list_elements", "List all BuilderX element/component types you can place on a page, grouped by category with a one-line summary and whether each is a container.", {}, () => handle(async () => listElements()));
@@ -90,8 +115,18 @@ The source must be { sections: [...] } — build sections with new_section. Vali
90
115
  .optional()
91
116
  .describe("Page kind. SPECIAL pages need a site data-source enabled — build_page does this automatically: store→use_store (product/cart bindings), member→use_member (customer/order bindings), blog→use_blog, error→use_error, maintain→use_maintain. 'main'/'custom' need nothing. Omit for a normal content page (defaults to 'main' for the homepage)."),
92
117
  is_homepage: z.boolean().default(false).describe("Set as the site homepage"),
118
+ seo: z
119
+ .object({
120
+ title: z.string().optional().describe("SEO/browser title. Tokens allowed: {{name_page}}, {{name_site}}, {{name_product}}, {{name_category}}."),
121
+ description: z.string().optional().describe("Meta description (~155 chars)."),
122
+ keyword: z.string().optional().describe("Comma-separated keywords."),
123
+ favicon: z.string().optional().describe("Favicon URL (hosted)."),
124
+ thumbnail: z.string().optional().describe("Social/OG share image URL (hosted)."),
125
+ })
126
+ .optional()
127
+ .describe("SEO for this page → settings.seo. Without it the page publishes with an EMPTY title/description. For a store page a good default title is '{{name_product}} | {{name_site}}' (product) or '{{name_category}} | {{name_site}}' (category)."),
93
128
  dry_run: z.boolean().default(true).describe("Preview+validate only (true) or create+save (false)"),
94
- }, ({ name, slug, source, type, is_homepage, dry_run }) => handle(async () => {
129
+ }, ({ name, slug, source, type, is_homepage, seo, dry_run }) => handle(async () => {
95
130
  const parsed = parseSource(source);
96
131
  const validation = validatePage(parsed);
97
132
  // Resolve numeric page type + the site data-source flag a special page needs.
@@ -132,10 +167,15 @@ The source must be { sections: [...] } — build sections with new_section. Vali
132
167
  if (!pageId) {
133
168
  return { error: "Page created but no id was returned.", created };
134
169
  }
135
- // slug / homepage are not applied at create — set them via update_page.
136
- if (slug || is_homepage) {
170
+ // slug / homepage / SEO are not applied at create — set them via update_page.
171
+ const seoBlock = seo ? buildPageSeo(seo) : null;
172
+ if (slug || is_homepage || (seoBlock && Object.keys(seoBlock).length)) {
137
173
  await api
138
- .updatePage(pageId, { ...(slug ? { slug } : {}), ...(is_homepage ? { is_homepage: true } : {}) })
174
+ .updatePage(pageId, {
175
+ ...(slug ? { slug } : {}),
176
+ ...(is_homepage ? { is_homepage: true } : {}),
177
+ ...(seoBlock && Object.keys(seoBlock).length ? { settings: { seo: seoBlock } } : {}),
178
+ })
139
179
  .catch(() => { });
140
180
  }
141
181
  return {
@@ -220,8 +260,10 @@ Pass style:"minimal" for the old bare stubs (heading + binding element only).`,
220
260
  const h1 = (text) => ({ type: "text", opts: { text, specials: { tag: "h1" }, style: { fontSize: "32px", fontWeight: "700" } } });
221
261
  const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_20)", color: "var(--color_00)", borderRadius: "8px", height: 48, fontWeight: "600" } } });
222
262
  // Rich (default) store pages come from the designed, palette-aware templates;
223
- // 'minimal' falls back to the original bare stubs.
224
- const pal = resolvePalette(palette || {});
263
+ // 'minimal' falls back to the original bare stubs. Derive a contrast-safe accent from
264
+ // the site's active theme (explicit palette overrides win).
265
+ const themePal = await contrastSafePalette(api);
266
+ const pal = resolvePalette({ ...themePal, ...(palette || {}) });
225
267
  const minimalStore = {
226
268
  collections: () => ({ sections: [buildSection([h1("Danh mục sản phẩm"), { type: "grid-product", opts: { config: { columns: 3, image_ratio: "1/1", gap_column: 24, gap_row: 32 } } }])] }),
227
269
  products: () => ({ sections: [buildSection([
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
  import { getConfirmMode } from "./context.js";
3
3
  import { normalizeEvents } from "../builder/events.js";
4
4
  import { normalizeBindings } from "../builder/bindings.js";
5
+ import { newsletterPopupSource, contrastSafePalette } from "../builder/templates.js";
6
+ import { finalizeForRender, validatePage } from "../builder/page.js";
5
7
  /**
6
8
  * Global Sources tools — manage site-wide components: cart, popup, overview, etc.
7
9
  *
@@ -575,6 +577,35 @@ Same merge rules: style/config/specials = shallow merge, events/bindings = repla
575
577
  invalidateGsCache();
576
578
  return res;
577
579
  }));
580
+ server.tool("scaffold_popup", `Build AND save a designed newsletter/promo popup in one call (a 'popup' global source).
581
+ Returns the popup id — open it from any element with an open_popup event { popup_id, popup_overlay:true },
582
+ or set it to auto-open after a delay (built in). Centred modal: heading + subtext + email subscribe
583
+ form + close button, palette-aware (accent derived from the site theme). Use dry_run to preview.`, {
584
+ headline: z.string().optional().describe("Popup heading (default: 'Nhận ưu đãi 10%')"),
585
+ subtext: z.string().optional().describe("Supporting line under the heading"),
586
+ cta_label: z.string().optional().describe("Submit button label (default: 'Đăng ký ngay')"),
587
+ delay_seconds: z.number().optional().describe("Auto-open after N seconds (default 6)"),
588
+ palette: z.record(z.any()).optional().describe("Colour overrides { accent, text, surface, ... }; defaults to the site theme."),
589
+ dry_run: z.boolean().default(true).describe("Preview the popup source (true) or create+save it (false)"),
590
+ }, ({ headline, subtext, cta_label, delay_seconds, palette, dry_run }) => handle(async () => {
591
+ const themePal = await contrastSafePalette(api);
592
+ const source = newsletterPopupSource({
593
+ headline, subtext, ctaLabel: cta_label, delaySeconds: delay_seconds,
594
+ palette: { ...themePal, ...(palette || {}) },
595
+ });
596
+ const validation = validatePage(source);
597
+ finalizeForRender(source);
598
+ const popupId = source.sections[0]?.id || null;
599
+ if (dry_run) {
600
+ return { dry_run: true, valid: validation.valid, popup_id: popupId, ...(validation.errors ? { errors: validation.errors } : {}), hint: "Centred newsletter popup. Call again with dry_run=false to save it, then open it via an open_popup event (popup_id above)." };
601
+ }
602
+ if (!validation.valid)
603
+ return { error: "Popup failed validation — not saving.", validation };
604
+ const res = await api.createGlobalSource({ component: "popup", source, type: "default", site_id: api.siteId });
605
+ invalidateGsCache();
606
+ const id = res?.data?.id || res?.id || popupId;
607
+ return { success: true, global_source_id: id, popup_id: popupId, next_step: "Open it from a button/header with events:[{ action:'open_popup', popup_id, popup_overlay:true }] (or it auto-opens after the delay). publish_site to take it live." };
608
+ }));
578
609
  server.tool("update_global_source", `Replace full source of a global source.
579
610
  IMPORTANT: Before calling this tool, you MUST:
580
611
  1. Read existing source with get_global_source_detail first
@@ -3,6 +3,7 @@ import { CUSTOM_CODE_GUIDE } from "../guides.js";
3
3
  import { getConfirmMode } from "./context.js";
4
4
  import { normalizeEvents } from "../builder/events.js";
5
5
  import { normalizeBindings } from "../builder/bindings.js";
6
+ import { PAGE_TYPE_NUM, PAGE_KINDS, buildPageSeo } from "./builder.js";
6
7
  /**
7
8
  * Page source utilities.
8
9
  *
@@ -293,15 +294,32 @@ Examples:
293
294
  const results = searchElements(source, filters);
294
295
  return { page_id, matched: results.length, elements: results };
295
296
  }));
296
- server.tool("create_page", "Create a new page", {
297
+ server.tool("create_page", "Create a new (empty) page. For a page with content use build_page instead. type is a KIND (main/store/member/blog/custom/error/maintain) mapped to the numeric backend type; pass seo so it doesn't publish with an empty title.", {
297
298
  name: z.string().describe("Page name"),
298
299
  slug: z.string().describe("URL slug (e.g. '/about')"),
299
- type: z.string().optional().describe("Page type"),
300
+ type: z.enum(PAGE_KINDS).optional().describe("Page kind (main/store/member/blog/custom/error/maintain). store/member/blog need their data-source flag enabled — prefer build_page which auto-enables it."),
300
301
  is_homepage: z.boolean().default(false).describe("Set as homepage"),
301
- }, ({ name, slug, type, is_homepage }) => handle(async () => {
302
- const res = await api.createPage({ name, slug, type, is_homepage });
302
+ seo: z
303
+ .object({ title: z.string().optional(), description: z.string().optional(), keyword: z.string().optional(), favicon: z.string().optional(), thumbnail: z.string().optional() })
304
+ .optional()
305
+ .describe("SEO → settings.seo (title/description/keyword/favicon/thumbnail). Tokens {{name_page}}/{{name_site}} allowed."),
306
+ }, ({ name, slug, type, is_homepage, seo }) => handle(async () => {
307
+ const typeNum = type ? PAGE_TYPE_NUM[type] : undefined;
308
+ const created = await api.createPage({ name, ...(typeNum != null ? { type: typeNum } : {}) });
303
309
  invalidatePageCache();
304
- return res;
310
+ const pageId = (created && (created.id || created.data?.id || created.page?.id)) || null;
311
+ const seoBlock = seo ? buildPageSeo(seo) : null;
312
+ if (pageId && (slug || is_homepage || (seoBlock && Object.keys(seoBlock).length))) {
313
+ await api
314
+ .updatePage(pageId, {
315
+ ...(slug ? { slug } : {}),
316
+ ...(is_homepage ? { is_homepage: true } : {}),
317
+ ...(seoBlock && Object.keys(seoBlock).length ? { settings: { seo: seoBlock } } : {}),
318
+ })
319
+ .catch(() => { });
320
+ invalidatePageCache();
321
+ }
322
+ return { success: true, page_id: pageId, name, slug, type: type ?? null, raw: pageId ? undefined : created };
305
323
  }));
306
324
  server.tool("update_page", "Update page properties (name, slug, settings, custom code)", {
307
325
  page_id: z.string().describe("Page ID"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.26.1",
3
+ "version": "1.28.0",
4
4
  "description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
5
5
  "mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
6
6
  "license": "MIT",