webcake-storefront-mcp 1.26.0 → 1.26.1

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/api.js CHANGED
@@ -491,8 +491,11 @@ export class WebcakeCmsApi {
491
491
  getActivePromotions() {
492
492
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/promotion_advance/get_promotions_actived`);
493
493
  }
494
+ /** Search/list promotions. The dedicated get_promotions_advance endpoint 404s on prod, so
495
+ * this uses the same /all list endpoint (which accepts a query); the tool filters the
496
+ * returned page client-side for type/status/keyword. */
494
497
  searchPromotions(query) {
495
- return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/promotion_advance/get_promotions_advance`, { query });
498
+ return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/promotion_advance/all`, { query });
496
499
  }
497
500
  // ── Combos ──
498
501
  listCombos(query) {
@@ -1080,11 +1080,28 @@ export const createCoupon = (opts = {}) => {
1080
1080
  }
1081
1081
  return coupon;
1082
1082
  };
1083
+ // Real popups (mined from templates) keep TRIGGER/animation in specials (effect, timeAnim,
1084
+ // openPopupAction, timeOpenPopup, page_ids) and GEOMETRY in the per-breakpoint keys: config
1085
+ // holds popupHorizontalPosition/popupVerticalPosition, style holds width/height/background.
1086
+ // Seed sensible runtime defaults (a centred modal) so finalizeForRender expands them into bpN —
1087
+ // otherwise the popup renders with no size/position/background.
1083
1088
  export const createPopup = (opts = {}) => {
1084
1089
  const popup = cloneDeep(SKELETON);
1085
1090
  popup.id = 'POPUP-' + randomString(8);
1086
1091
  popup.type = 'popup';
1087
1092
  popup.specials = opts.specials || {};
1093
+ popup.runtime.config = {
1094
+ popupHorizontalPosition: 'center',
1095
+ popupVerticalPosition: 'center',
1096
+ heightUnit: 'auto',
1097
+ ...(opts.config || {})
1098
+ };
1099
+ popup.runtime.style = {
1100
+ width: 480,
1101
+ background: '#ffffff',
1102
+ borderRadius: '12px',
1103
+ ...(opts.style || {})
1104
+ };
1088
1105
  popup.children = opts.children || [];
1089
1106
  return popup;
1090
1107
  };
@@ -69,8 +69,11 @@ already in this shape — see the \`responsive\` field on get_page_element/updat
69
69
  - \`runtime.style\` holds CSS-ish props: width/height (numbers = px), color, background,
70
70
  fontSize ("16px"), fontWeight, textAlign, border*, boxShadow, etc.
71
71
  - \`runtime.config.heightUnit\`: "auto" lets content set height (default for text/image).
72
- - Colours: prefer the site THEME variables \`var(--color_00)\`, \`var(--color_01)\`,
73
- (the published site themes them); plain hex or rgba() also work.
72
+ - Colours: use the site THEME matrix vars \`var(--color_RC)\` (R=row 0-4, C=col 0-4). Row 0 is
73
+ greyscale: \`var(--color_00)\`=WHITE \`var(--color_04)\`=BLACK. Row 2 is the BRAND row:
74
+ \`var(--color_20)\` is the brand primary. So TEXT = \`var(--color_04)\` (NOT color_00, that's white →
75
+ invisible), ACCENT/buttons/prices = \`var(--color_20)\` (darker shade \`var(--color_24)\` if you need
76
+ white-on-accent contrast), page background = \`var(--color_00)\`. Plain hex/rgba() also work.
74
77
 
75
78
  ## Make it look DESIGNED (not plain) — do this, every page
76
79
  A bare stack of default elements looks unfinished. Apply real styling:
@@ -82,8 +85,8 @@ A bare stack of default elements looks unfinished. Apply real styling:
82
85
  - TYPOGRAPHY hierarchy: h1 40–56px / fontWeight 700, h2 28–34px / 600, body 16–18px with
83
86
  lineHeight "1.6", muted color for sub-text. Center hero text (textAlign:"center").
84
87
  - BUTTONS HAVE NO DEFAULT COLOUR — you MUST style them or they look like plain text:
85
- \`{ type:"button", opts:{ text:"Mua ngay", style:{ background:"var(--color_02)", color:"#fff",
86
- borderRadius:"8px", fontWeight:"600", height:48 } } }\`.
88
+ \`{ type:"button", opts:{ text:"Mua ngay", style:{ background:"var(--color_20)", color:"var(--color_00)",
89
+ borderRadius:"8px", fontWeight:"600", height:48 } } }\` (brand background, white label).
87
90
  - HERO: prefer a section with a background image + an overlay heading/sub/button on top
88
91
  (set section_opts.style.background or a full-width image, then text centered), rather than a
89
92
  small image stacked above text.
@@ -96,8 +99,9 @@ A bare stack of default elements looks unfinished. Apply real styling:
96
99
  and \`opts.specials\`: { products_per_load:8-36, on_hover:"zoom"|"swap", show_rating, show_ribbon }.
97
100
  (There are NO cardBorderRadius/cardBoxShadow keys.) For variations use \`attr\` elements
98
101
  (attrName:"auto"); steppers are \`quantity-input\` (defaults spinner:"hide-spin").
99
- - BRAND COLOURS: reuse var(--color_00) (text), and an accent (var(--color_02)/var(--color_03))
100
- for buttons, prices, highlights — consistent accent = looks intentional.
102
+ - BRAND COLOURS: text = var(--color_04) (black), accent = var(--color_20) (brand) for buttons,
103
+ prices, highlights — consistent accent = looks intentional. (var(--color_00) is WHITE — only
104
+ for backgrounds / labels on the accent, never for text on a white surface.)
101
105
  - IMAGES: must be WebCake-CDN urls (search_images cdn_url / upload_images), else they won't show.
102
106
 
103
107
  ## Responsive breakpoints
@@ -16,14 +16,17 @@
16
16
  import { buildSection, walk } from "./page.js";
17
17
  import { buildElement } from "./catalog.js";
18
18
  import { normalizeEvents } from "./events.js";
19
+ // Slots map onto the site theme's 5×5 colour matrix, surfaced as CSS vars var(--color_RC)
20
+ // (R=row, C=col). Row 0 is greyscale (00=white … 04=black); row 2 is the BRAND row. Using
21
+ // var(--color_00) for text was a bug — that's WHITE (invisible on the white surface).
19
22
  const DEFAULT_PALETTE = {
20
- accent: "var(--color_02)",
21
- onAccent: "#ffffff",
22
- text: "var(--color_00)",
23
- muted: "#6b7280",
24
- surface: "#ffffff",
25
- surfaceAlt: "#f7f5f2",
26
- border: "#e8e3dc",
23
+ accent: "var(--color_20)", // brand primary (row 2)
24
+ onAccent: "var(--color_00)", // white — label on the accent button
25
+ text: "var(--color_04)", // black — headings + body
26
+ muted: "var(--color_03)", // dark grey — secondary text
27
+ surface: "var(--color_00)", // white — page/card background
28
+ surfaceAlt: "var(--color_01)", // light grey — alternating band
29
+ border: "var(--color_01)", // hairline borders
27
30
  };
28
31
  export function resolvePalette(p = {}) {
29
32
  return { ...DEFAULT_PALETTE, ...Object.fromEntries(Object.entries(p).filter(([, v]) => v != null)) };
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "v": "1.26.1",
4
+ "d": "26/06/2026",
5
+ "type": "Fixed",
6
+ "en": "list_products now correctly unwraps the { products, total_product } API response shape, returning a product array and accurate total instead of…",
7
+ "vi": "list_products nay giải nén đúng kiểu phản hồi { products, total_product } từ API, trả về mảng sản phẩm và tổng chính xác thay vì trả thẳng đối tượng…"
8
+ },
2
9
  {
3
10
  "v": "1.26.0",
4
11
  "d": "26/06/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Added",
34
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,…",
35
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,…"
36
- },
37
- {
38
- "v": "1.21.0",
39
- "d": "25/06/2026",
40
- "type": "Changed",
41
- "en": "The grid-product factory now ships cross-industry neutral defaults (image_ratio:\"1/1\", gap_column/gap_row 24, products_per_load 12) instead of the…",
42
- "vi": "Factory grid-product nay sử dụng các giá trị mặc định trung lập đa ngành (image_ratio:\"1/1\", gap_column/gap_row 24, products_per_load 12) thay vì…"
43
43
  }
44
44
  ]
@@ -207,7 +207,7 @@ form|summary, and a centred thank-you — and navigation between them (cart→ch
207
207
  Still add a GLOBAL header/footer with scaffold_global_sections (or create_global_section).
208
208
  Pass style:"minimal" for the old bare stubs (heading + binding element only).`, {
209
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)."),
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 matrix vars (var(--color_20) brand accent, var(--color_04) black text, var(--color_00) white surface)."),
211
211
  include_member: z.boolean().default(false).describe("Also create login/register/profile (type member, use_member)"),
212
212
  include_blog: z.boolean().default(false).describe("Also create blog list + post pages (type blog, use_blog)"),
213
213
  dry_run: z.boolean().default(true).describe("Preview (true) or actually create the missing pages (false)"),
@@ -218,7 +218,7 @@ Pass style:"minimal" for the old bare stubs (heading + binding element only).`,
218
218
  target: `${name}::${field}`,
219
219
  });
220
220
  const h1 = (text) => ({ type: "text", opts: { text, specials: { tag: "h1" }, style: { fontSize: "32px", fontWeight: "700" } } });
221
- const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_02)", color: "#fff", borderRadius: "8px", height: 48, fontWeight: "600" } } });
221
+ const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_20)", color: "var(--color_00)", borderRadius: "8px", height: 48, fontWeight: "600" } } });
222
222
  // Rich (default) store pages come from the designed, palette-aware templates;
223
223
  // 'minimal' falls back to the original bare stubs.
224
224
  const pal = resolvePalette(palette || {});
@@ -227,7 +227,7 @@ Pass style:"minimal" for the old bare stubs (heading + binding element only).`,
227
227
  products: () => ({ sections: [buildSection([
228
228
  { type: "product-gallery", opts: {} },
229
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)" } } },
230
+ { type: "text-dataset", opts: { bindings: [bind("product", "product_price")], style: { fontSize: "22px", fontWeight: "700", color: "var(--color_20)" } } },
231
231
  { type: "quantity-input", opts: {} },
232
232
  accentBtn("Thêm vào giỏ"),
233
233
  ])] }),
@@ -6,7 +6,9 @@ export function registerCollectionTools(server, api, handle) {
6
6
  term: z.string().optional().describe("Search by collection name"),
7
7
  }, ({ page, limit, term }) => handle(async () => {
8
8
  const res = await api.listCollections({ page, limit, term });
9
- const collections = (res && res.data) || res || [];
9
+ // Real shape: { data: { data:[…], total_entries } }. Accept a flat data[] too.
10
+ const d = res && res.data;
11
+ const collections = (d && (Array.isArray(d) ? d : d.data)) || res || [];
10
12
  if (!Array.isArray(collections))
11
13
  return res;
12
14
  return {
@@ -17,7 +19,7 @@ export function registerCollectionTools(server, api, handle) {
17
19
  fields_count: (c.schema || []).length,
18
20
  records_count: c.records_count || undefined,
19
21
  })),
20
- total: res.total || collections.length,
22
+ total: (d && !Array.isArray(d) && d.total_entries) ?? res.total ?? collections.length,
21
23
  };
22
24
  }));
23
25
  server.tool("get_collection", "Get a specific collection's details including full schema (field names, types, constraints, references) and records", {
@@ -6,7 +6,8 @@ export function registerOrderTools(server, api, handle) {
6
6
  status: z.number().optional().describe("Filter by status (0=pending, 50=confirmed, 100=shipping, 150=delivered, -1=cancelled)"),
7
7
  }, ({ page, limit, status }) => handle(async () => {
8
8
  const res = await api.listOrders({ page, limit, status });
9
- const orders = (res && res.data) || res || [];
9
+ // Real shape: { orders: { data:[…], total_entries, count_status } }.
10
+ const orders = (res && (res.orders?.data || res.data)) || res || [];
10
11
  if (!Array.isArray(orders))
11
12
  return res;
12
13
  return {
@@ -22,7 +23,8 @@ export function registerOrderTools(server, api, handle) {
22
23
  created_at: o.created_at,
23
24
  updated_at: o.updated_at,
24
25
  })),
25
- total: res.total || orders.length,
26
+ total: res.orders?.total_entries ?? res.total ?? orders.length,
27
+ ...(res.orders?.count_status ? { count_status: res.orders.count_status } : {}),
26
28
  };
27
29
  }));
28
30
  server.tool("get_order", "Get full order details by ID: customer info, items, payment, shipping, discounts, etc.", {
@@ -11,7 +11,8 @@ export function registerProductTools(server, api, handle) {
11
11
  if (term && term.trim())
12
12
  query.term = term.trim();
13
13
  const res = await api.listProducts(query);
14
- const products = (res && res.data) || res || [];
14
+ // Real shape: { products:[…], total_product }. Fall back to data/array for safety.
15
+ const products = (res && (res.products || res.data)) || res || [];
15
16
  if (!Array.isArray(products))
16
17
  return res;
17
18
  return {
@@ -28,7 +29,7 @@ export function registerProductTools(server, api, handle) {
28
29
  categories: p.categories || undefined,
29
30
  updated_at: p.updated_at,
30
31
  })),
31
- total: res.total || products.length,
32
+ total: res.total_product ?? res.total ?? products.length,
32
33
  };
33
34
  }));
34
35
  server.tool("get_product", "Get full product details by ID: name, description, price, variations, images, attributes, SEO, etc.", {
@@ -81,7 +81,8 @@ export function registerPromotionTools(server, api, handle) {
81
81
  }));
82
82
  server.tool("get_active_promotions", "Get all currently active promotions (is_activated=true and within start_time/end_time range)", {}, () => handle(async () => {
83
83
  const res = await api.getActivePromotions();
84
- const promotions = (res && res.data && res.data.promotions) || (res && res.data) || [];
84
+ // Real shape: { promotions:[…] } at the top level (not under data).
85
+ const promotions = (res && (res.promotions || (res.data && res.data.promotions) || res.data)) || [];
85
86
  return {
86
87
  data: Array.isArray(promotions)
87
88
  ? promotions.map((p) => ({
@@ -124,8 +125,19 @@ export function registerPromotionTools(server, api, handle) {
124
125
  query.page = page;
125
126
  if (limit)
126
127
  query.limit = limit;
127
- const res = await api.searchPromotions(query);
128
- const data = (res && res.data && res.data.result) || (res && res.data) || res;
129
- return data;
128
+ const res = await api.searchPromotions({ page: page ?? 1, limit: limit ?? 50 });
129
+ let list = (res && (res.data?.result || res.data)) || [];
130
+ if (!Array.isArray(list))
131
+ return res;
132
+ // The /all endpoint doesn't filter, so apply the requested filters client-side.
133
+ if (term)
134
+ list = list.filter((p) => (p.name || "").toLowerCase().includes(term.toLowerCase()));
135
+ if (type)
136
+ list = list.filter((p) => p.type === type);
137
+ if (status != null)
138
+ list = list.filter((p) => p.time_status === status || p.status === status);
139
+ if (is_activated != null)
140
+ list = list.filter((p) => Boolean(p.is_activated) === is_activated);
141
+ return { data: list, total: list.length, total_unfiltered: res.total_entries };
130
142
  }));
131
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.26.0",
3
+ "version": "1.26.1",
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",