webcake-storefront-mcp 1.18.0 → 1.19.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.
@@ -158,6 +158,53 @@ Rule of thumb: if the page shows products, a cart, customer/order data, or blog
158
158
  set \`type\` accordingly so the binding source is turned on. A binding target like
159
159
  \`product::product_price\` REQUIRES its page to be the matching type.
160
160
 
161
+ ## Build the WHOLE storefront — every page to the SAME standard (NOT just the home page)
162
+ A shop is multi-page. Build EACH page to a real e-commerce standard with the same palette,
163
+ spacing and header/footer — never leave the home page rich and the rest as bare stubs.
164
+ \`scaffold_store_pages\` now builds FULLY-DESIGNED, palette-aware store pages by DEFAULT
165
+ (style:"rich") — banner+breadcrumb+styled grid (Category), 2-col gallery|info with price/
166
+ quantity/add-to-cart+trust badges+related (Product), 2-col cart|summary, 2-col checkout
167
+ form|summary, centred thank-you — and auto-wires navigation between them. Then add chrome with
168
+ \`scaffold_global_sections({ brand, contact })\` for a designed Header+Footer in one call. Pass
169
+ style:"minimal" only if you want bare stubs to hand-build. The per-page recipe each rich page
170
+ follows (so you can match it when editing or building extra pages):
171
+ - Category (collections, type store): banner + heading + grid-product (+ optional intro/CTA).
172
+ - Product detail (products, type store): 2-col [product-gallery | info: text-dataset
173
+ product::product_name + product_price/original_price + short_description, quantity-input,
174
+ "Thêm vào giỏ" (add_to_cart) + "Mua ngay" (buy_now), trust badges] then a description/feature
175
+ band then a related grid-product ("Có thể bạn cũng thích").
176
+ - Cart (cart, type store): heading + 2-col [cart-items | order-summary card with a
177
+ "Tiến hành thanh toán" button -> { action:"open_page", open_page_id:<checkout> }] + continue link.
178
+ - Checkout (checkout, type store): heading + 2-col [form{type:form_order} with input/
179
+ phone-number/email/address + submit-button "Đặt hàng" | order summary (cart-items)].
180
+ - Thank-you (complete, type store): centred confirmation + order-items + continue-shopping.
181
+ - Optional: About / Contact (custom), Blog (type blog: post-list) + Post.
182
+ Reuse the SAME section helpers, palette and card styling as the home page so the whole site
183
+ feels like ONE design.
184
+
185
+ ## Global Header & Footer — create them for EVERY site (don't inline per page)
186
+ A header (logo/nav/cart) and footer (links/contact/copyright) belong on EVERY page, so make them
187
+ GLOBAL, not copied into each page. Build a header section + a footer section (new_section, same as
188
+ any section), then:
189
+ create_global_section({ type:"header", name:"Header", section:<headerSection> }) // top of every page
190
+ create_global_section({ type:"footer", name:"Footer", section:<footerSection> }) // bottom of every page
191
+ (omit page_ids to apply to ALL pages). ORDER MATTERS: build/save the page CONTENT first, THEN
192
+ create the globals — they embed into each page's source. If you later overwrite a page's source
193
+ (update_page_source/build_page) you WIPE its embedded header/footer, so re-create the globals
194
+ (delete_global_section by the section NODE id, then create once) to re-embed cleanly. Edit a global
195
+ later with update_global_section_element(s) and it updates on every page at once.
196
+
197
+ ## Popups (newsletter / promo / age-gate)
198
+ A popup is a GLOBAL SOURCE, not a page section. Build it, store it, then trigger it:
199
+ 1. Build the popup body (new_section: heading + text + form/input + a close button), then wrap it:
200
+ new_element("popup", { children:[<that section>], specials:{ /* trigger + overlay */ } }).
201
+ Trigger/behaviour (auto-open after a delay, exit-intent, only-once, overlay) lives in specials.
202
+ 2. Save it: create_global_source({ component:"popup", source:{ sections:[<popupNode>] } }) -> returns its id.
203
+ 3. Open/close from any element via events: a button { action:"open_popup", popup_id:"<id>", popup_overlay:true };
204
+ a close button inside { action:"close_popup", popup_id:"<id>" }; a form can auto-close on its
205
+ success trigger ({ eventName:"success", action:"close_popup", popup_id:"<id>" }).
206
+ List existing popups with list_global_sources({component:"popup"}); edit via update_global_source(_element).
207
+
161
208
  ## Workflow (do this every time)
162
209
  1. Intake: confirm goal, brand, colours, sections wanted (ask 3-5 questions if unclear).
163
210
  2. list_elements / get_element to pick the right component types.
@@ -0,0 +1,399 @@
1
+ // High-level, palette-aware PAGE & CHROME templates.
2
+ //
3
+ // The low-level builders (new_section/new_element/new_row -> buildSection/buildRow/
4
+ // buildElement) are flexible but leave the AI to hand-compose every node, so generated
5
+ // store pages end up bare. These templates encode the per-page recipes from the BUILD_GUIDE
6
+ // as ready-to-save `{ sections: [...] }` sources that already look like a real shop:
7
+ // styled headings, breadcrumbs, 2-column product detail, styled product-grid cards, trust
8
+ // badges, an order summary, and a designed header/footer.
9
+ //
10
+ // Everything is built through buildSection/buildRow so the output is the same runtime shape
11
+ // the builder emits; finalizeForRender() (called by the tools) expands it to bp1..bp4.
12
+ //
13
+ // Colours use the site theme CSS vars by default (var(--color_02) accent, var(--color_00)
14
+ // text) so a generated site matches whatever palette the theme already defines; callers can
15
+ // override any slot via a Palette object.
16
+ import { buildSection, walk } from "./page.js";
17
+ import { normalizeEvents } from "./events.js";
18
+ const DEFAULT_PALETTE = {
19
+ accent: "var(--color_02)",
20
+ onAccent: "#ffffff",
21
+ text: "var(--color_00)",
22
+ muted: "#6b7280",
23
+ surface: "#ffffff",
24
+ surfaceAlt: "#f7f5f2",
25
+ border: "#e8e3dc",
26
+ };
27
+ export function resolvePalette(p = {}) {
28
+ return { ...DEFAULT_PALETTE, ...Object.fromEntries(Object.entries(p).filter(([, v]) => v != null)) };
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // small spec helpers (return element specs for buildSection/buildRow children)
32
+ // ---------------------------------------------------------------------------
33
+ const heading = (text, opts = {}) => ({
34
+ type: "text",
35
+ opts: {
36
+ text,
37
+ specials: { tag: opts.tag || "h2" },
38
+ style: {
39
+ fontSize: `${opts.size ?? 30}px`,
40
+ fontWeight: String(opts.weight ?? 700),
41
+ color: opts.color,
42
+ textAlign: opts.align,
43
+ lineHeight: "1.25",
44
+ },
45
+ },
46
+ });
47
+ const paragraph = (text, p, opts = {}) => ({
48
+ type: "text",
49
+ opts: {
50
+ text,
51
+ style: {
52
+ fontSize: `${opts.size ?? 16}px`,
53
+ fontWeight: "400",
54
+ color: opts.color || p.muted,
55
+ textAlign: opts.align,
56
+ lineHeight: "1.6",
57
+ },
58
+ },
59
+ });
60
+ /** Accent CTA button. `navTo` is a slug sentinel the scaffolder resolves to a real
61
+ * open_page event once all pages exist (see wireNavigation). The sentinel rides INSIDE an
62
+ * events entry because the button factory keeps opts.events but drops arbitrary specials. */
63
+ const cta = (text, p, opts = {}) => ({
64
+ type: "button",
65
+ opts: {
66
+ text,
67
+ ...(opts.events || opts.navTo ? { events: [...(opts.events || []), ...(opts.navTo ? [{ action: "open_page", _navTo: opts.navTo }] : [])] } : {}),
68
+ style: {
69
+ background: p.accent,
70
+ color: p.onAccent,
71
+ borderRadius: "10px",
72
+ height: opts.height ?? 50,
73
+ fontWeight: "600",
74
+ fontSize: "16px",
75
+ paddingLeft: "28px",
76
+ paddingRight: "28px",
77
+ ...(opts.full ? { width: "100%" } : {}),
78
+ textAlign: "center",
79
+ },
80
+ },
81
+ });
82
+ const outlineBtn = (text, p, opts = {}) => ({
83
+ type: "button",
84
+ opts: {
85
+ text,
86
+ ...(opts.navTo ? { events: [{ action: "open_page", _navTo: opts.navTo }] } : {}),
87
+ style: {
88
+ background: "transparent",
89
+ color: p.accent,
90
+ border: `1px solid ${p.accent}`,
91
+ borderRadius: "10px",
92
+ height: opts.height ?? 48,
93
+ fontWeight: "600",
94
+ fontSize: "15px",
95
+ paddingLeft: "24px",
96
+ paddingRight: "24px",
97
+ textAlign: "center",
98
+ },
99
+ },
100
+ });
101
+ /** A small "icon + label" trust badge (free shipping, authentic, support…). */
102
+ const trustBadge = (icon, label, p) => ({
103
+ type: "container",
104
+ layout: "row",
105
+ columnGap: 10,
106
+ colWidths: [{ unit: "px", absValue: 30 }, { unit: "fr", value: 1 }],
107
+ collapse: { bp4: 2 },
108
+ children: [
109
+ { type: "text", opts: { text: icon, style: { fontSize: "22px", lineHeight: "1.2" } } },
110
+ { type: "text", opts: { text: label, style: { fontSize: "13px", fontWeight: "600", color: p.text, lineHeight: "1.3" } } },
111
+ ],
112
+ });
113
+ /** A styled product grid card config that looks like a real shop, not a bare list. */
114
+ const productGrid = (p, columns = 4) => ({
115
+ type: "grid-product",
116
+ opts: {
117
+ config: {
118
+ columns,
119
+ image_ratio: "1/1",
120
+ img_object_fit: "cover",
121
+ gap_column: 24,
122
+ gap_row: 32,
123
+ cardBorderRadius: 14,
124
+ cardBoxShadow: "0 6px 22px rgba(0,0,0,0.06)",
125
+ cardPadding: 12,
126
+ cardBackground: p.surface,
127
+ productNameColor: p.text,
128
+ productPriceColor: p.accent,
129
+ showAddToCart: true,
130
+ },
131
+ },
132
+ });
133
+ /** Dataset text bound to a product field (name/price/description). */
134
+ const productField = (field, style) => ({
135
+ type: "text-dataset",
136
+ opts: {
137
+ bindings: [{ id: "BINDING" + Math.random().toString(36).slice(2, 8), name: "product", target: `product::${field}` }],
138
+ style,
139
+ },
140
+ });
141
+ const sectionStyle = (p, bg, padY = 64) => ({
142
+ style: { background: bg, paddingTop: padY, paddingBottom: padY, paddingLeft: 20, paddingRight: 20 },
143
+ });
144
+ // ---------------------------------------------------------------------------
145
+ // STORE PAGE templates -> { sections: [...] }
146
+ // ---------------------------------------------------------------------------
147
+ export function categoryPageSource(p, opts = {}) {
148
+ const banner = buildSection([
149
+ { type: "breadcrumb", opts: { style: { fontSize: "13px", color: p.muted } } },
150
+ heading(opts.title || "Tất cả sản phẩm", { tag: "h1", size: 40, color: p.text, align: "center" }),
151
+ paragraph(opts.subtitle || "Khám phá bộ sưu tập của chúng tôi", p, { align: "center", size: 17 }),
152
+ ], { ...sectionStyle(p, p.surfaceAlt, 56), rowGap: 12 });
153
+ const grid = buildSection([productGrid(p, 4)], sectionStyle(p, p.surface, 56));
154
+ return { sections: [banner, grid] };
155
+ }
156
+ export function productPageSource(p) {
157
+ const breadcrumbRow = buildSection([{ type: "breadcrumb", opts: { style: { fontSize: "13px", color: p.muted } } }], { ...sectionStyle(p, p.surface, 24) });
158
+ const infoColumn = {
159
+ type: "container",
160
+ children: [
161
+ productField("product_name", { fontSize: "30px", fontWeight: "700", color: p.text, lineHeight: "1.25" }),
162
+ productField("product_price", { fontSize: "26px", fontWeight: "700", color: p.accent }),
163
+ productField("product_description", { fontSize: "15px", fontWeight: "400", color: p.muted, lineHeight: "1.6" }),
164
+ { type: "quantity-input", opts: { style: { borderColor: p.border, borderRadius: "8px" } } },
165
+ cta("Thêm vào giỏ", p, { full: true, height: 52, events: [{ action: "add_to_cart", open_page: "cart" }] }),
166
+ {
167
+ type: "container",
168
+ layout: "row",
169
+ columnGap: 16,
170
+ collapse: { bp4: 1 },
171
+ children: [
172
+ trustBadge("🚚", "Miễn phí giao hàng", p),
173
+ trustBadge("✅", "Hàng chính hãng", p),
174
+ trustBadge("↩️", "Đổi trả 7 ngày", p),
175
+ ],
176
+ },
177
+ ],
178
+ };
179
+ const detail = buildSection([
180
+ {
181
+ type: "container",
182
+ layout: "row",
183
+ columnGap: 48,
184
+ colWidths: [{ unit: "fr", value: 1 }, { unit: "fr", value: 1 }],
185
+ collapse: { bp3: 1, bp4: 1 },
186
+ children: [{ type: "product-gallery", opts: { style: { borderRadius: "14px" } } }, infoColumn],
187
+ },
188
+ ], sectionStyle(p, p.surface, 32));
189
+ const related = buildSection([heading("Sản phẩm liên quan", { tag: "h2", size: 26, color: p.text, align: "center" }), productGrid(p, 4)], { ...sectionStyle(p, p.surfaceAlt, 56), rowGap: 24 });
190
+ return { sections: [breadcrumbRow, detail, related] };
191
+ }
192
+ export function cartPageSource(p) {
193
+ const summary = {
194
+ type: "container",
195
+ opts: { style: { background: p.surfaceAlt, borderRadius: "14px", padding: "24px", border: `1px solid ${p.border}` } },
196
+ children: [
197
+ heading("Tóm tắt đơn hàng", { tag: "h3", size: 20, color: p.text }),
198
+ paragraph("Phí vận chuyển và mã giảm giá sẽ được tính ở bước thanh toán.", p, { size: 14 }),
199
+ cta("Tiến hành thanh toán", p, { full: true, height: 52, navTo: "checkout" }),
200
+ outlineBtn("Tiếp tục mua sắm", p, { navTo: "home" }),
201
+ ],
202
+ };
203
+ const body = buildSection([
204
+ heading("Giỏ hàng của bạn", { tag: "h1", size: 34, color: p.text }),
205
+ {
206
+ type: "container",
207
+ layout: "row",
208
+ columnGap: 40,
209
+ colWidths: [{ unit: "fr", value: 2 }, { unit: "fr", value: 1 }],
210
+ collapse: { bp3: 1, bp4: 1 },
211
+ children: [{ type: "cart-items", opts: {} }, summary],
212
+ },
213
+ ], { ...sectionStyle(p, p.surface, 48), rowGap: 24 });
214
+ return { sections: [body] };
215
+ }
216
+ export function checkoutPageSource(p) {
217
+ const form = {
218
+ type: "form",
219
+ opts: { specials: { type: "form_order" }, style: { background: p.surface } },
220
+ children: [
221
+ { type: "input", opts: { specials: { field_name: "full_name", label: "Họ và tên", placeholder: "Nguyễn Văn A", required: true, show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
222
+ { type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
223
+ { type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ nhận hàng", show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
224
+ { type: "text-area", opts: { specials: { field_name: "note", label: "Ghi chú", show_label: true }, style: { borderColor: p.border, borderRadius: "8px" } } },
225
+ { type: "submit-button", opts: { text: "Đặt hàng", style: { background: p.accent, color: p.onAccent, borderRadius: "10px", height: 52, fontWeight: "600", width: "100%", textAlign: "center" } } },
226
+ ],
227
+ };
228
+ const summary = {
229
+ type: "container",
230
+ opts: { style: { background: p.surfaceAlt, borderRadius: "14px", padding: "24px", border: `1px solid ${p.border}` } },
231
+ children: [heading("Đơn hàng của bạn", { tag: "h3", size: 20, color: p.text }), { type: "cart-items", opts: {} }],
232
+ };
233
+ const body = buildSection([
234
+ heading("Thanh toán", { tag: "h1", size: 34, color: p.text }),
235
+ {
236
+ type: "container",
237
+ layout: "row",
238
+ columnGap: 40,
239
+ colWidths: [{ unit: "fr", value: 3 }, { unit: "fr", value: 2 }],
240
+ collapse: { bp3: 1, bp4: 1 },
241
+ children: [form, summary],
242
+ },
243
+ ], { ...sectionStyle(p, p.surface, 48), rowGap: 24 });
244
+ return { sections: [body] };
245
+ }
246
+ export function thankYouPageSource(p) {
247
+ const body = buildSection([
248
+ heading("🎉", { tag: "h2", size: 56, align: "center" }),
249
+ heading("Cảm ơn bạn đã đặt hàng!", { tag: "h1", size: 34, color: p.text, align: "center" }),
250
+ paragraph("Chúng tôi đã nhận được đơn hàng của bạn và sẽ liên hệ xác nhận trong thời gian sớm nhất.", p, { align: "center", size: 17 }),
251
+ { type: "order-items", opts: {} },
252
+ cta("Tiếp tục mua sắm", p, { navTo: "home" }),
253
+ ], { ...sectionStyle(p, p.surface, 64), rowGap: 18 });
254
+ return { sections: [body] };
255
+ }
256
+ export function headerSection(opts = {}) {
257
+ const p = resolvePalette(opts.palette);
258
+ const links = opts.links && opts.links.length ? opts.links : [
259
+ { label: "Trang chủ", navTo: "home" },
260
+ { label: "Sản phẩm", navTo: "collections" },
261
+ { label: "Giỏ hàng", navTo: "cart" },
262
+ ];
263
+ const logo = { type: "text", opts: { text: opts.brand || "Shop", specials: { tag: "h2" }, style: { fontSize: "24px", fontWeight: "800", color: p.text } } };
264
+ const nav = {
265
+ type: "container",
266
+ layout: "row",
267
+ columnGap: 28,
268
+ collapse: { bp4: 3 },
269
+ children: links.map((l) => ({
270
+ type: "text",
271
+ opts: {
272
+ text: l.label,
273
+ ...(l.navTo || l.url ? { specials: { ...(l.navTo ? { _navTo: l.navTo } : {}), ...(l.url ? { _navUrl: l.url } : {}) } } : {}),
274
+ style: { fontSize: "15px", fontWeight: "600", color: p.text, cursor: "pointer" },
275
+ },
276
+ })),
277
+ };
278
+ const actions = {
279
+ type: "container",
280
+ layout: "row",
281
+ columnGap: 16,
282
+ colWidths: [{ unit: "px", absValue: 32 }, { unit: "max-c" }],
283
+ collapse: { bp4: 2 },
284
+ children: [
285
+ { type: "cart-icon", opts: { config: { color: p.text }, style: { width: 26, height: 26 } } },
286
+ cta(opts.cta || "Đặt mua ngay", p, { navTo: "collections", height: 44 }),
287
+ ],
288
+ };
289
+ return buildSection([
290
+ {
291
+ type: "container",
292
+ layout: "row",
293
+ columnGap: 24,
294
+ colWidths: [{ unit: "max-c" }, { unit: "fr", value: 1 }, { unit: "max-c" }],
295
+ collapse: { bp4: 1 },
296
+ children: [logo, nav, actions],
297
+ },
298
+ ], { style: { background: p.surface, paddingTop: 16, paddingBottom: 16, paddingLeft: 20, paddingRight: 20, borderBottom: `1px solid ${p.border}` } });
299
+ }
300
+ export function footerSection(opts = {}) {
301
+ const p = resolvePalette(opts.palette);
302
+ const brandCol = {
303
+ type: "container",
304
+ children: [
305
+ { type: "text", opts: { text: opts.brand || "Shop", specials: { tag: "h3" }, style: { fontSize: "20px", fontWeight: "800", color: "#fff" } } },
306
+ { type: "text", opts: { text: opts.tagline || "Cảm ơn bạn đã ghé thăm cửa hàng.", style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "1.6" } } },
307
+ ],
308
+ };
309
+ const linkCols = (opts.columns && opts.columns.length ? opts.columns : [
310
+ { title: "Cửa hàng", links: ["Trang chủ", "Sản phẩm", "Giỏ hàng"] },
311
+ { title: "Hỗ trợ", links: ["Chính sách đổi trả", "Giao hàng", "Liên hệ"] },
312
+ ]).map((col) => ({
313
+ type: "container",
314
+ children: [
315
+ { type: "text", opts: { text: col.title, style: { fontSize: "15px", fontWeight: "700", color: "#fff" } } },
316
+ ...col.links.map((t) => ({ type: "text", opts: { text: t, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } })),
317
+ ],
318
+ }));
319
+ const c = opts.contact || {};
320
+ const contactCol = {
321
+ type: "container",
322
+ children: [
323
+ { type: "text", opts: { text: "Liên hệ", style: { fontSize: "15px", fontWeight: "700", color: "#fff" } } },
324
+ ...(c.phone ? [{ type: "text", opts: { text: `📞 ${c.phone}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } }] : []),
325
+ ...(c.email ? [{ type: "text", opts: { text: `✉️ ${c.email}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "2" } } }] : []),
326
+ ...(c.address ? [{ type: "text", opts: { text: `📍 ${c.address}`, style: { fontSize: "14px", color: "rgba(255,255,255,0.7)", lineHeight: "1.6" } } }] : []),
327
+ ],
328
+ };
329
+ const topRow = {
330
+ type: "container",
331
+ layout: "row",
332
+ columnGap: 40,
333
+ colWidths: [{ unit: "fr", value: 2 }, { unit: "fr", value: 1 }, { unit: "fr", value: 1 }, { unit: "fr", value: 1 }],
334
+ collapse: { bp3: 2, bp4: 1 },
335
+ children: [brandCol, ...linkCols, contactCol],
336
+ };
337
+ const copyright = { type: "text", opts: { text: `© ${opts.brand || "Shop"}. All rights reserved.`, style: { fontSize: "13px", color: "rgba(255,255,255,0.5)", textAlign: "center" } } };
338
+ return buildSection([topRow, copyright], {
339
+ style: { background: "#1c1917", paddingTop: 56, paddingBottom: 32, paddingLeft: 20, paddingRight: 20 },
340
+ rowGap: 32,
341
+ });
342
+ }
343
+ // ---------------------------------------------------------------------------
344
+ // registry the scaffolder iterates
345
+ // ---------------------------------------------------------------------------
346
+ /**
347
+ * Resolve the `_navTo` / `_navUrl` sentinels the templates leave on buttons/links into real
348
+ * open_page / open_link events, now that every page exists and we know its id. `slugToId`
349
+ * maps a page slug (e.g. "checkout", "cart", "collections") to its page id; the special
350
+ * slug "home" maps to the homepage id. Returns the number of nodes wired so the caller can
351
+ * skip re-saving an unchanged page. Run on the raw source BEFORE finalizeForRender.
352
+ */
353
+ export function wireNavigation(source, slugToId) {
354
+ let wired = 0;
355
+ const resolve = (slug) => slugToId[slug] || slugToId[slug.replace(/^\//, "")];
356
+ walk(source, (node) => {
357
+ // (a) text links carry the sentinel in specials (the text factory keeps it).
358
+ const sp = node && node.specials;
359
+ if (sp && sp._navTo) {
360
+ const id = resolve(sp._navTo);
361
+ if (id) {
362
+ node.events = normalizeEvents([{ action: "open_page", open_page_id: id }], node.type);
363
+ wired++;
364
+ }
365
+ delete sp._navTo;
366
+ }
367
+ if (sp && sp._navUrl) {
368
+ node.events = normalizeEvents([{ action: "open_link", link_target: sp._navUrl, link_target_url: sp._navUrl }], node.type);
369
+ delete sp._navUrl;
370
+ wired++;
371
+ }
372
+ // (b) buttons carry the sentinel inside an events entry (the button factory keeps events
373
+ // but drops arbitrary specials). Resolve _navTo -> open_page_id in place; drop the
374
+ // event entirely if its target page doesn't exist so we never emit a dead open_page.
375
+ if (Array.isArray(node && node.events) && node.events.length) {
376
+ node.events = node.events
377
+ .map((e) => {
378
+ if (e && e._navTo) {
379
+ const id = resolve(e._navTo);
380
+ const { _navTo, ...rest } = e;
381
+ if (!id)
382
+ return null;
383
+ wired++;
384
+ return { ...rest, open_page_id: id };
385
+ }
386
+ return e;
387
+ })
388
+ .filter(Boolean);
389
+ }
390
+ });
391
+ return wired;
392
+ }
393
+ export const STORE_PAGE_TEMPLATES = [
394
+ { name: "Category Page", slug: "collections", build: categoryPageSource },
395
+ { name: "Product Page", slug: "products", build: productPageSource },
396
+ { name: "Cart Page", slug: "cart", build: cartPageSource },
397
+ { name: "Checkout Page", slug: "checkout", build: checkoutPageSource },
398
+ { name: "Thank You Page", slug: "complete", build: thankYouPageSource },
399
+ ];
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.19.0",
4
+ "d": "25/06/2026",
5
+ "type": "Added",
6
+ "en": "New scaffold_global_sections tool generates a fully-designed global Header and Footer in one call — logo, nav links, cart icon, CTA button, and a…",
7
+ "vi": "Tool mới scaffold_global_sections tạo Header và Footer toàn cục đã thiết kế sẵn trong một lần gọi — logo, liên kết điều hướng, icon giỏ hàng, nút…"
8
+ },
9
+ {
10
+ "v": "1.18.1",
11
+ "d": "25/06/2026",
12
+ "type": "Changed",
13
+ "en": "get_build_guide now includes three new sections: \"Build the WHOLE storefront\" with per-page recipes for enriching category, product detail, cart,…",
14
+ "vi": "get_build_guide nay bổ sung ba mục mới: \"Build the WHOLE storefront\" — công thức từng trang để nâng cấp danh mục, trang chi tiết sản phẩm, giỏ hàng,…"
15
+ },
2
16
  {
3
17
  "v": "1.18.0",
4
18
  "d": "25/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Added",
27
41
  "en": "New uninstall_app, update_app, and update_app_review tools complete the app lifecycle: uninstall an installed app by subscription id, patch an app's…",
28
42
  "vi": "Tool mới uninstall_app, update_app và update_app_review hoàn thiện vòng đời ứng dụng: gỡ cài đặt theo subscription id, cập nhật config/status của…"
29
- },
30
- {
31
- "v": "1.15.1",
32
- "d": "24/06/2026",
33
- "type": "Fixed",
34
- "en": "update_page_element, update_page_elements, update_global_source_element, and update_global_source_elements now normalize events and bindings arrays…",
35
- "vi": "update_page_element, update_page_elements, update_global_source_element và update_global_source_elements nay chuẩn hóa mảng events và bindings khi…"
36
- },
37
- {
38
- "v": "1.15.0",
39
- "d": "24/06/2026",
40
- "type": "Added",
41
- "en": "New get_global_section tool returns a compact tree representation of a single global section (Header/Footer/block), listing each element's id, type,…",
42
- "vi": "Tool mới get_global_section trả về cây phần tử dạng compact của một global section (Header/Footer/block), liệt kê id, type, text, class, binding và…"
43
43
  }
44
44
  ]
package/dist/server.js CHANGED
@@ -8,6 +8,8 @@ Tool discovery: the common tools (pages, builder, products, orders, blog, custom
8
8
 
9
9
  You can also BUILD pages: use get_build_guide, list_elements, get_element to learn the BuilderX component model, new_section/new_element to compose, validate_page to check, then build_page (dry_run first) to create. Publishing is site-level via publish_site.
10
10
 
11
+ Build the WHOLE site, not just the home page: enrich EVERY page (category, product detail, cart, checkout, thank-you, …) to the same e-commerce standard with the same palette/header/footer — scaffold_store_pages makes MINIMAL starter pages, so design them properly afterwards. Make the Header and Footer GLOBAL (create_global_section type:"header"/"footer") so they're consistent on every page — don't inline them per page. For newsletter/promo offers, build a popup (a "popup" global_source) and open it with an open_popup event. See get_build_guide for the per-page recipes, the global-section order rules, and the popup workflow.
12
+
11
13
  To make a generated site look real, also CREATE DATA so dataset bindings resolve: create_product_category + create_product (storefront), create_blog_category + create_article (blog). Get image URLs from search_images / upload_images first, then reference them. A good flow for a fresh site: create_site → create a few categories → create products in them → build_page (home + store/blog pages) → publish_site.
12
14
 
13
15
  INTAKE FIRST — when the user asks to BUILD/CREATE a site, store, or page (a fresh build, not a small edit or a data question), do NOT jump straight to create_site/build_page on the same turn. Act as a professional shop-website designer doing client work: first ask ONE short batch of questions (call get_intake_guide for the exact list), restate the plan you'll build, get a "yes", THEN build. The shop owner is an ORDINARY person, not a designer — ask in plain words and visual outcomes ("tông nâu ấm, ảnh sản phẩm to"), never jargon. Ask the essentials (with sensible defaults so they answer fast): what they sell + a few products & prices, brand/shop name, primary color/style, which pages they need, real contact info (hotline/Zalo, address, email, hours) + the main call-to-action, and any promotion. NEVER invent or silently placeholder real data (shop name, products, prices, phone, address) — ask for it; placeholder only what the user explicitly skips, and tell them what to fill in. Skip intake only when the user already gave the brief, says "just do it / tự quyết", or it's a tiny edit.
@@ -4,6 +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
8
  // Recursive spec for new_section / build_page children.
8
9
  const elementSpec = z.object({
9
10
  type: z.string().describe("Element type (see list_elements)"),
@@ -197,11 +198,20 @@ product/category/cart from any page lands on a real page instead of a 404.
197
198
  Creates only the ones MISSING (matched by slug): Category (collections), Product (products),
198
199
  Cart (cart), Checkout (checkout), Thank-you (complete) — all type 'store' (enables use_store),
199
200
  plus optional member (login/register/profile) and blog (blog/post) pages.
200
- Run this right after you create products/categories. dry_run=true (default) previews.`, {
201
+ Run this right after you create products/categories. dry_run=true (default) previews.
202
+ By default (style:"rich") each store page is a FULLY-DESIGNED, palette-aware page — banner +
203
+ breadcrumb + styled product-grid cards (Category), 2-column gallery|info with price/quantity/
204
+ add-to-cart + trust badges + related products (Product), 2-col cart|summary, 2-col checkout
205
+ form|summary, and a centred thank-you — and navigation between them (cart→checkout, add-to-cart
206
+ →cart, thank-you→home) is auto-wired. Colours follow the site theme vars unless you pass a palette.
207
+ Still add a GLOBAL header/footer with scaffold_global_sections (or create_global_section).
208
+ Pass style:"minimal" for the old bare stubs (heading + binding element only).`, {
209
+ style: z.enum(["rich", "minimal"]).default("rich").describe("'rich' = fully-designed, palette-aware store pages (default). 'minimal' = bare starter stubs you must enrich yourself."),
210
+ palette: z.record(z.any()).optional().describe("Optional colour overrides for rich pages: { accent, onAccent, text, muted, surface, surfaceAlt, border }. Defaults to the site theme CSS vars (var(--color_02) accent, var(--color_00) text)."),
201
211
  include_member: z.boolean().default(false).describe("Also create login/register/profile (type member, use_member)"),
202
212
  include_blog: z.boolean().default(false).describe("Also create blog list + post pages (type blog, use_blog)"),
203
213
  dry_run: z.boolean().default(true).describe("Preview (true) or actually create the missing pages (false)"),
204
- }, ({ include_member, include_blog, dry_run }) => handle(async () => {
214
+ }, ({ style, palette, include_member, include_blog, dry_run }) => handle(async () => {
205
215
  const bind = (name, field) => ({
206
216
  id: "BINDING" + Math.random().toString(36).slice(2, 8),
207
217
  name,
@@ -209,38 +219,38 @@ Run this right after you create products/categories. dry_run=true (default) prev
209
219
  });
210
220
  const h1 = (text) => ({ type: "text", opts: { text, specials: { tag: "h1" }, style: { fontSize: "32px", fontWeight: "700" } } });
211
221
  const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_02)", color: "#fff", borderRadius: "8px", height: 48, fontWeight: "600" } } });
222
+ // 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 || {});
225
+ const minimalStore = {
226
+ 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
+ products: () => ({ sections: [buildSection([
228
+ { type: "product-gallery", opts: {} },
229
+ { type: "text-dataset", opts: { bindings: [bind("product", "product_name")], style: { fontSize: "28px", fontWeight: "700" } } },
230
+ { type: "text-dataset", opts: { bindings: [bind("product", "product_price")], style: { fontSize: "22px", fontWeight: "700", color: "var(--color_02)" } } },
231
+ { type: "quantity-input", opts: {} },
232
+ accentBtn("Thêm vào giỏ"),
233
+ ])] }),
234
+ cart: () => ({ sections: [buildSection([h1("Giỏ hàng"), { type: "cart-items", opts: {} }, accentBtn("Tiến hành thanh toán")])] }),
235
+ checkout: () => ({ sections: [buildSection([
236
+ h1("Thanh toán"),
237
+ { type: "form", opts: { specials: { type: "form_order" } }, children: [
238
+ { type: "input", opts: { specials: { field_name: "full_name", label: "Họ tên", placeholder: "Họ tên", required: true, show_label: true } } },
239
+ { type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true } } },
240
+ { type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ", show_label: true } } },
241
+ accentBtn("Đặt hàng", "submit-button"),
242
+ ] },
243
+ ])] }),
244
+ complete: () => ({ sections: [buildSection([h1("Cảm ơn bạn đã đặt hàng!"), { type: "order-items", opts: {} }])] }),
245
+ };
212
246
  // Each spec: { name, slug, kind, build() -> { sections } }
213
- const SPECS = [
214
- { name: "Category Page", slug: "collections", kind: "store", flag: "use_store", build: () => ({ sections: [
215
- 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 } } }]),
216
- ] }) },
217
- { name: "Product Page", slug: "products", kind: "store", flag: "use_store", build: () => ({ sections: [
218
- buildSection([
219
- { type: "product-gallery", opts: {} },
220
- { type: "text-dataset", opts: { bindings: [bind("product", "product_name")], style: { fontSize: "28px", fontWeight: "700" } } },
221
- { type: "text-dataset", opts: { bindings: [bind("product", "product_price")], style: { fontSize: "22px", fontWeight: "700", color: "var(--color_02)" } } },
222
- { type: "quantity-input", opts: {} },
223
- accentBtn("Thêm vào giỏ"),
224
- ]),
225
- ] }) },
226
- { name: "Cart Page", slug: "cart", kind: "store", flag: "use_store", build: () => ({ sections: [
227
- buildSection([h1("Giỏ hàng"), { type: "cart-items", opts: {} }, accentBtn("Tiến hành thanh toán")]),
228
- ] }) },
229
- { name: "Checkout Page", slug: "checkout", kind: "store", flag: "use_store", build: () => ({ sections: [
230
- buildSection([
231
- h1("Thanh toán"),
232
- { type: "form", opts: { specials: { type: "form_order" } }, children: [
233
- { type: "input", opts: { specials: { field_name: "full_name", label: "Họ tên", placeholder: "Họ tên", required: true, show_label: true } } },
234
- { type: "phone-number", opts: { specials: { field_name: "phone_number", label: "Số điện thoại", required: true, show_label: true } } },
235
- { type: "address", opts: { specials: { field_name: "address", label: "Địa chỉ", show_label: true } } },
236
- accentBtn("Đặt hàng", "submit-button"),
237
- ] },
238
- ]),
239
- ] }) },
240
- { name: "Thank You Page", slug: "complete", kind: "store", flag: "use_store", build: () => ({ sections: [
241
- buildSection([h1("Cảm ơn bạn đã đặt hàng!"), { type: "order-items", opts: {} }]),
242
- ] }) },
243
- ];
247
+ const SPECS = STORE_PAGE_TEMPLATES.map((t) => ({
248
+ name: t.name,
249
+ slug: t.slug,
250
+ kind: "store",
251
+ flag: "use_store",
252
+ build: () => (style === "minimal" ? minimalStore[t.slug]() : t.build(pal)),
253
+ }));
244
254
  if (include_member) {
245
255
  SPECS.push({ name: "Login Page", slug: "login", kind: "member", flag: "use_member", build: () => ({ sections: [
246
256
  buildSection([h1("Đăng nhập"), { type: "form", opts: { specials: { type: "form_login" } }, children: [
@@ -279,6 +289,18 @@ Run this right after you create products/categories. dry_run=true (default) prev
279
289
  const created = [];
280
290
  const errors = [];
281
291
  const flagsEnabled = new Set();
292
+ // slug -> page id, seeded with the pages that already exist (so cross-page nav
293
+ // resolves even when only some store pages are newly created).
294
+ const slugToId = {};
295
+ for (const pg of Array.isArray(pages) ? pages : []) {
296
+ const sl = (pg.slug || "").replace(/^\//, "");
297
+ if (sl)
298
+ slugToId[sl] = pg.id;
299
+ if (pg.is_homepage)
300
+ slugToId["home"] = pg.id;
301
+ }
302
+ // Keep each newly-built source so we can wire navigation once every id is known.
303
+ const built = [];
282
304
  for (const spec of missing) {
283
305
  try {
284
306
  if (!flagsEnabled.has(spec.flag)) {
@@ -294,21 +316,40 @@ Run this right after you create products/categories. dry_run=true (default) prev
294
316
  finalizeForRender(source);
295
317
  const res = await api.createPage({ name: spec.name, source, type: PAGE_TYPE_NUM[spec.kind] });
296
318
  const pid = newPageId(res);
297
- if (pid)
319
+ if (pid) {
298
320
  await api.updatePage(pid, { slug: spec.slug }).catch(() => { });
321
+ slugToId[spec.slug] = pid;
322
+ built.push({ spec, pid, source });
323
+ }
299
324
  created.push({ name: spec.name, slug: spec.slug, type: spec.kind, page_id: pid });
300
325
  }
301
326
  catch (e) {
302
327
  errors.push({ slug: spec.slug, error: e?.message ?? String(e) });
303
328
  }
304
329
  }
330
+ // Second pass: resolve the _navTo sentinels the templates left on buttons/links into
331
+ // real open_page events (cart→checkout, thank-you→home, …), now that every page id is
332
+ // known. Only re-save the pages that actually changed.
333
+ let nav_wired = 0;
334
+ for (const { pid, source } of built) {
335
+ try {
336
+ const n = wireNavigation(source, slugToId);
337
+ if (n > 0) {
338
+ nav_wired += n;
339
+ await api.updatePageSource(pid, { source }).catch(() => { });
340
+ }
341
+ }
342
+ catch { /* nav wiring is best-effort */ }
343
+ }
305
344
  return {
306
345
  success: true,
346
+ style,
307
347
  created,
308
348
  already_exist: skipped,
309
349
  ...(errors.length ? { errors } : {}),
310
350
  data_sources_enabled: [...flagsEnabled],
311
- note: "Publish the site (publish_site) to take the new pages live.",
351
+ nav_links_wired: nav_wired,
352
+ note: "Add a header/footer with scaffold_global_sections, then publish_site to take the new pages live.",
312
353
  };
313
354
  }));
314
355
  }
@@ -34,8 +34,8 @@ export function registerContextTools(server, api, handle) {
34
34
  "create_site (tên + slug) → tự chuyển sang site mới",
35
35
  "create_product_category + create_product cho từng sản phẩm (ảnh từ search_images/upload_images trước)",
36
36
  "build_page trang chủ (type:'main', is_homepage:true) với hero, lưới sản phẩm, câu chuyện, CTA, …",
37
- "scaffold_store_pages để tạo trang danh mục/chi tiết/giỏ/thanh toán/cảm ơn",
38
- "Tách Header/Footer thành global section (create_global_section) để dùng chung mọi trang",
37
+ "scaffold_store_pages (style:'rich' mặc định) → trang danh mục/chi tiết/giỏ/thanh toán/cảm ơn ĐÃ thiết kế sẵn, tự nối điều hướng",
38
+ "scaffold_global_sections({ brand, contact }) → Header + Footer thiết kế sẵn, dùng chung mọi trang (tự bỏ qua slot đã có)",
39
39
  "publish_site (cũng rebuild CSS storefront)",
40
40
  ],
41
41
  notes: "Sau khi build xong, QA trên builder editor (app_base/editor/:site_id) hoặc storefront đã publish; publish_site sẽ rebuild CSS để hết tình trạng trang thiếu style.",
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { validatePage, finalizeForRender } from "../builder/page.js";
3
+ import { headerSection, footerSection, wireNavigation } from "../builder/templates.js";
3
4
  /**
4
5
  * Write tools for global SECTIONS (Header / Footer / reusable blocks).
5
6
  *
@@ -124,6 +125,96 @@ Two-step safety: dry_run=true (default) previews which pages change; dry_run=fal
124
125
  raw: ok ? undefined : res,
125
126
  };
126
127
  }));
128
+ server.tool("scaffold_global_sections", `Generate a DESIGNED global Header and Footer in one call and embed them on every page —
129
+ the fast path to consistent site chrome. The header has logo + nav links + cart icon + CTA;
130
+ the footer has brand blurb + link columns + contact + copyright. Navigation is auto-wired to
131
+ real pages (Home/Products/Cart). Colours follow the site theme vars unless you pass a palette.
132
+ SKIPS a slot that already has a global section (won't create a second header/footer) unless
133
+ force=true. Two-step safety: dry_run=true (default) previews; dry_run=false performs the atomic save.`, {
134
+ brand: z.string().describe("Shop / brand name shown as the logo and in the footer."),
135
+ links: z.array(z.object({ label: z.string(), navTo: z.string().optional(), url: z.string().optional() })).optional().describe("Header nav links. navTo = a page slug ('home','collections','cart') auto-wired to open_page; url = external link. Defaults to Home/Products/Cart."),
136
+ contact: z.object({ phone: z.string().optional(), email: z.string().optional(), address: z.string().optional() }).optional().describe("Real contact info for the footer (don't invent — omit what you don't have)."),
137
+ cta: z.string().optional().describe("Header call-to-action button label (default 'Đặt mua ngay')."),
138
+ palette: z.record(z.any()).optional().describe("Optional colour overrides { accent, onAccent, text, muted, surface, surfaceAlt, border }."),
139
+ include: z.enum(["both", "header", "footer"]).default("both").describe("Which chrome to generate."),
140
+ force: z.boolean().default(false).describe("Create even if a header/footer global already exists (otherwise that slot is skipped)."),
141
+ dry_run: z.boolean().default(true).describe("Preview (true) or perform the atomic save (false)."),
142
+ }, ({ brand, links, contact, cta, palette, include, force, dry_run }) => handle(async () => {
143
+ // Which slots already have a global section? (dedupe by type unless force)
144
+ const existingRes = await api.listGlobalSections().catch(() => null);
145
+ const existing = (existingRes && (existingRes.data || existingRes.global_sections || existingRes)) || [];
146
+ const slotTaken = (slot) => Array.isArray(existing) && existing.some((g) => (g.slot || "").toLowerCase() === slot || TYPE_NUM[slot] === g.type);
147
+ const wantHeader = include !== "footer";
148
+ const wantFooter = include !== "header";
149
+ const skipped = [];
150
+ // Resolve slug -> page id for nav wiring + page embedding.
151
+ const allPages = await loadPages(api);
152
+ const slugToId = {};
153
+ const pagesRes = await api.listPages();
154
+ for (const pg of (pagesRes && pagesRes.data) || pagesRes || []) {
155
+ const sl = (pg.slug || "").replace(/^\//, "");
156
+ if (sl)
157
+ slugToId[sl] = pg.id;
158
+ if (pg.is_homepage)
159
+ slugToId["home"] = pg.id;
160
+ }
161
+ // Build the finalized, nav-wired global nodes for each requested slot.
162
+ const toCreate = [];
163
+ const buildNode = (type, raw) => {
164
+ raw.specials = { ...(raw.specials || {}), global: type };
165
+ const wrap = { sections: [raw] };
166
+ const validation = validatePage(wrap);
167
+ if (!validation.valid)
168
+ throw new Error(`${type} failed validation: ${JSON.stringify(validation.errors)}`);
169
+ finalizeForRender(wrap);
170
+ wireNavigation(wrap, slugToId); // resolve _navTo on header links/CTA
171
+ return wrap.sections[0];
172
+ };
173
+ if (wantHeader) {
174
+ if (!force && slotTaken("header"))
175
+ skipped.push("header");
176
+ else
177
+ toCreate.push({ type: "header", name: "Header", node: buildNode("header", headerSection({ brand, links: links, cta, palette })) });
178
+ }
179
+ if (wantFooter) {
180
+ if (!force && slotTaken("footer"))
181
+ skipped.push("footer");
182
+ else
183
+ toCreate.push({ type: "footer", name: "Footer", node: buildNode("footer", footerSection({ brand, contact, palette })) });
184
+ }
185
+ if (!toCreate.length) {
186
+ return { success: true, created: [], skipped, note: skipped.length ? "Those slots already have a global section (pass force=true to add anyway)." : "Nothing to create." };
187
+ }
188
+ if (dry_run) {
189
+ return {
190
+ dry_run: true,
191
+ will_create: toCreate.map((t) => ({ type: t.type, section_id: t.node.id })),
192
+ skipped,
193
+ embeds_into_pages: allPages.length,
194
+ hint: "Call again with dry_run=false to create + embed the header/footer, then publish_site.",
195
+ };
196
+ }
197
+ // Inject every node into every page, then one atomic /save with all globals + pages.
198
+ for (const t of toCreate)
199
+ for (const p of allPages)
200
+ injectNode(p.source, t.node, t.type);
201
+ const changedPages = allPages.map((p) => ({ id: p.id, source: JSON.stringify(p.source) }));
202
+ const changes = allPages.reduce((o, p) => { o[p.id] = 1; return o; }, {});
203
+ const global_sections = toCreate.map((t) => ({
204
+ section_id: t.node.id, name: t.name, type: TYPE_NUM[t.type],
205
+ pages: allPages.map((p) => p.id), section: t.node, status: "new", contents: [],
206
+ }));
207
+ const res = await api.saveGlobalSections({ global_sections, pages: changedPages, changes });
208
+ const ok = !!(res && (res.success || res.data));
209
+ return {
210
+ success: ok,
211
+ created: toCreate.map((t) => ({ type: t.type, section_id: t.node.id })),
212
+ skipped,
213
+ embedded_pages: changedPages.length,
214
+ note: "Publish the site (publish_site) to take the header/footer live.",
215
+ raw: ok ? undefined : res,
216
+ };
217
+ }));
127
218
  server.tool("delete_global_section", `Delete a global section (Header/Footer/block) and remove its node from every page source.
128
219
  Two-step safety: dry_run=true (default) shows which pages would change; dry_run=false performs the atomic save.`, {
129
220
  section_id: z.string().describe("The global section's section_id (the section node id) — from list_global_sections."),
@@ -5,7 +5,12 @@ export function registerProductTools(server, api, handle) {
5
5
  limit: z.number().optional().describe("Items per page"),
6
6
  term: z.string().optional().describe("Search by product name"),
7
7
  }, ({ page, limit, term }) => handle(async () => {
8
- const res = await api.listProducts({ page, limit, term });
8
+ // Build a clean query the /products/all endpoint 400s on undefined/blank keys,
9
+ // so default page/limit and drop term unless it's a real search string.
10
+ const query = { page: page ?? 1, limit: limit ?? 50 };
11
+ if (term && term.trim())
12
+ query.term = term.trim();
13
+ const res = await api.listProducts(query);
9
14
  const products = (res && res.data) || res || [];
10
15
  if (!Array.isArray(products))
11
16
  return res;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.18.0",
3
+ "version": "1.19.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",