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 +4 -1
- package/dist/builder/factory.js +17 -0
- package/dist/builder/guide.js +10 -6
- package/dist/builder/templates.js +10 -7
- package/dist/changelog.json +7 -7
- package/dist/tools/builder.js +3 -3
- package/dist/tools/collections.js +4 -2
- package/dist/tools/orders.js +4 -2
- package/dist/tools/products.js +3 -2
- package/dist/tools/promotions.js +16 -4
- package/package.json +1 -1
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/
|
|
498
|
+
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/promotion_advance/all`, { query });
|
|
496
499
|
}
|
|
497
500
|
// ── Combos ──
|
|
498
501
|
listCombos(query) {
|
package/dist/builder/factory.js
CHANGED
|
@@ -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
|
};
|
package/dist/builder/guide.js
CHANGED
|
@@ -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:
|
|
73
|
-
(
|
|
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(--
|
|
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:
|
|
100
|
-
|
|
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(--
|
|
21
|
-
onAccent: "
|
|
22
|
-
text: "var(--
|
|
23
|
-
muted: "
|
|
24
|
-
surface: "
|
|
25
|
-
surfaceAlt: "
|
|
26
|
-
border: "
|
|
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)) };
|
package/dist/changelog.json
CHANGED
|
@@ -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
|
]
|
package/dist/tools/builder.js
CHANGED
|
@@ -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
|
|
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(--
|
|
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(--
|
|
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
|
-
|
|
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
|
|
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", {
|
package/dist/tools/orders.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.", {
|
package/dist/tools/products.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.", {
|
package/dist/tools/promotions.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
128
|
-
|
|
129
|
-
|
|
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.
|
|
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",
|