webcake-storefront-mcp 1.33.1 → 1.33.2
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 +25 -13
- package/dist/builder/attributes.js +2 -2
- package/dist/builder/factory.js +3 -0
- package/dist/builder/guide.js +11 -1
- package/dist/changelog.json +7 -7
- package/dist/tools/orders.js +9 -1
- package/dist/tools/pages.js +26 -18
- package/dist/tools/sale-channels.js +2 -5
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -186,8 +186,15 @@ export class WebcakeCmsApi {
|
|
|
186
186
|
listPageContents(query) {
|
|
187
187
|
return this.request("GET", `/api/v1/site/${this.siteId}/page_contents`, { query });
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
/** One page's translation in one language: { content: { [elementId]: {...} } } ({} when none yet). Read-only. */
|
|
190
|
+
getPageContent(page_id, lang) {
|
|
191
|
+
return this.request("POST", `/api/v1/site/${this.siteId}/page_contents/page_content`, { body: { page_id, lang } });
|
|
192
|
+
}
|
|
193
|
+
/** Upsert one page translation (matched by page_id + language_code) — the editor's multilingual save. */
|
|
194
|
+
updatePageContent({ page_id, language_code, content, meta_tags }) {
|
|
195
|
+
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/multilingual/update_page_contents`, {
|
|
196
|
+
body: { pages: [{ contents: [{ page_id, language_code, content, meta_tags, site_id: this.siteId }] }] },
|
|
197
|
+
});
|
|
191
198
|
}
|
|
192
199
|
listGlobalSections() {
|
|
193
200
|
return this.request("GET", `/api/v1/site/${this.siteId}/global_sections`);
|
|
@@ -647,14 +654,25 @@ export class WebcakeCmsApi {
|
|
|
647
654
|
listCustomers(query) {
|
|
648
655
|
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/customer/all`, { query });
|
|
649
656
|
}
|
|
657
|
+
/** /api/v1/cms_function/:site_id/* sits behind builderx_api's CmsFunctionAuth plug, which only
|
|
658
|
+
* accepts the site's CMS admin token (db_collections/token) as the Bearer — the session JWT 401s. */
|
|
659
|
+
async cmsFunction(method, path, body) {
|
|
660
|
+
await this.fetchCmsTokens();
|
|
661
|
+
if (!this._adminToken)
|
|
662
|
+
throw new Error("This site has no CMS admin token, so cms_function routes are unavailable.");
|
|
663
|
+
return this.request(method, `/api/v1/cms_function/${this.siteId}${path}`, {
|
|
664
|
+
body,
|
|
665
|
+
headers: { Authorization: `Bearer ${this._adminToken}` },
|
|
666
|
+
});
|
|
667
|
+
}
|
|
650
668
|
findCustomerById(id) {
|
|
651
|
-
return this.
|
|
669
|
+
return this.cmsFunction("GET", `/customer/identity/${encodeURIComponent(id)}`);
|
|
652
670
|
}
|
|
653
671
|
findCustomerByPhone(phone) {
|
|
654
|
-
return this.
|
|
672
|
+
return this.cmsFunction("GET", `/customer/phone/${encodeURIComponent(phone)}`);
|
|
655
673
|
}
|
|
656
674
|
findCustomerByEmail(email) {
|
|
657
|
-
return this.
|
|
675
|
+
return this.cmsFunction("GET", `/customer/email/${encodeURIComponent(email)}`);
|
|
658
676
|
}
|
|
659
677
|
// ── Global Sources (cart, popup, overview, etc.) ──
|
|
660
678
|
getSourceCart() {
|
|
@@ -704,7 +722,7 @@ export class WebcakeCmsApi {
|
|
|
704
722
|
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/automations/delete`, { body: { ids } });
|
|
705
723
|
}
|
|
706
724
|
sendMail(params) {
|
|
707
|
-
return this.
|
|
725
|
+
return this.cmsFunction("POST", "/application/automation/send_mail", params);
|
|
708
726
|
}
|
|
709
727
|
// ── App lifecycle (applications/subcriptions) ──
|
|
710
728
|
/** Uninstall an app by its subscription id (the `id` from list_apps / get_app). */
|
|
@@ -1143,13 +1161,7 @@ export class WebcakeCmsApi {
|
|
|
1143
1161
|
getCourseMembers(query) {
|
|
1144
1162
|
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/course-app/members`, { query });
|
|
1145
1163
|
}
|
|
1146
|
-
// ── Sale channels:
|
|
1147
|
-
sitemapSync(kind) {
|
|
1148
|
-
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/sale_channels/sitemap/sync_${kind}`, { body: {} });
|
|
1149
|
-
}
|
|
1150
|
-
sitemapRebuild(type) {
|
|
1151
|
-
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/sale_channels/sitemap/rebuild`, { body: { type } });
|
|
1152
|
-
}
|
|
1164
|
+
// ── Sale channels: partner feeds + catalogs ──
|
|
1153
1165
|
listPartnerFeeds(query) {
|
|
1154
1166
|
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/sale_channels/partner_feeds/all`, { query });
|
|
1155
1167
|
}
|
|
@@ -174,7 +174,7 @@ export const ELEMENT_ATTRS = {
|
|
|
174
174
|
// ── layout ──
|
|
175
175
|
section: {
|
|
176
176
|
specials: { global: "Mark as a global/shared section (reused across pages).", custom_class: "CSS class.", layout: "Nested layout payload (advanced)." },
|
|
177
|
-
config: {
|
|
177
|
+
config: { position: "Scroll behaviour: default | sticky (sticks while scrolling — sticky header) | pinned (fixed to the viewport). See get_build_guide → Sticky & pinned.", stickyPosition: "With position:'sticky': autotop (default) | autobottom | default (both edges).", stickyTop: "Sticky offset from top (px, default 0).", stickyBottom: "Sticky offset from bottom (px, default 0).", showShadowWhenSticky: "Keep the section's boxShadow only while stuck (1, default) or always (0).", video_background: "Section video background {background_display,…}.", pattern_overlay: "Overlay pattern image url.", apply_pattern_overlay: "Apply the overlay pattern (boolean).", nameLineClamp: "Clamp the section title to N lines." },
|
|
178
178
|
events: "none",
|
|
179
179
|
notes: "Top-level band; centred 3-column grid. Children go in the centre column (new_section handles this). Make a header section sticky with config.stickyPosition; add a video/pattern background via config.",
|
|
180
180
|
},
|
|
@@ -322,7 +322,7 @@ export const ELEMENT_ATTRS = {
|
|
|
322
322
|
"next-lesson-droppable": { events: "none", notes: "Next-lesson panel (course app)." },
|
|
323
323
|
"list-lesson-droppable": { events: "none", notes: "Lessons droppable list (course app)." },
|
|
324
324
|
// ── marketing / popups / misc widgets (rarely in page sources; built via apps/global_sources) ──
|
|
325
|
-
popup: { specials: { effect: "Open animation (fade-in|slide|…).", timeAnim: "Animation duration (s).",
|
|
325
|
+
popup: { specials: { effect: "Open animation (fade-in|slide|…).", timeAnim: "Animation duration (s).", openPopupAction: "Auto-open: none | openPopupWithTime (after timeOpenPopup s) | openPopupWhenScroll (when the element in `target` scrolls into view) | openWhenNonLogin (after timeOpenPopup s, only for logged-out visitors).", timeOpenPopup: "Delay in seconds for openPopupWithTime / openWhenNonLogin (default 0).", target: "Element id for openPopupWhenScroll.", page_ids: "Pages it auto-opens on (default ['all']).", countOpenPopup: "Max auto-opens per browser session (0/unset = unlimited)." }, config: { popupHorizontalPosition: "left | center | right.", popupVerticalPosition: "top | center | bottom." }, events: "hide — fires when the popup closes.", notes: "Modal/overlay. NOT a page section — wrap a section in a popup node and save it as a global_source (component:'popup'). Open/close from any element via events open_popup/close_popup {popup_id}. See get_build_guide → Popups." },
|
|
326
326
|
notify: { specials: { type: "Toast/banner kind.", autoClose: "Auto-dismiss (boolean).", duration: "Visible time (s)." }, events: "none", notes: "Notification / toast banner (e.g. add-to-cart confirmation). Often referenced by add_to_cart's activeNotify." },
|
|
327
327
|
"random-number": { specials: { min: "Lowest value.", max: "Highest value." }, events: "none", notes: "Displays a random number (minigame / fake-stock urgency widget)." },
|
|
328
328
|
table: { specials: { datagrid_id: "Linked data-grid id (the table's data source).", tableItems: "Column definitions [{name,key}]." }, config: { titleBackground: "Header background.", titleColor: "Header text colour.", oddBackground: "Odd-row background.", evenBackground: "Even-row background." }, events: "none", notes: "Data table bound to a data grid (specs / comparison)." },
|
package/dist/builder/factory.js
CHANGED
|
@@ -507,6 +507,9 @@ export const createCartIcon = ({ svg, itemCount, config, style, children } = {})
|
|
|
507
507
|
cart.specials.itemCount = itemCount ?? 0;
|
|
508
508
|
if (children)
|
|
509
509
|
cart.children = children || [];
|
|
510
|
+
// The renderer's cart-icon has no click handler of its own — opening the cart IS this event
|
|
511
|
+
// (SPA factory.js 9877f2c99). Without it a built cart icon does nothing when clicked.
|
|
512
|
+
cart.events = [{ id: 'EVENT-' + randomString(6), eventName: 'click', action: 'open_cart' }];
|
|
510
513
|
return cart;
|
|
511
514
|
};
|
|
512
515
|
export const createWishlist = ({ svg, itemCount, config, children, style } = {}) => {
|
package/dist/builder/guide.js
CHANGED
|
@@ -347,6 +347,16 @@ create the globals — they embed into each page's source. If you later overwrit
|
|
|
347
347
|
(delete_global_section by the section NODE id, then create once) to re-embed cleanly. Edit a global
|
|
348
348
|
later with update_global_section_element(s) and it updates on every page at once.
|
|
349
349
|
|
|
350
|
+
STICKY & PINNED (runtime.config, on a section or any element):
|
|
351
|
+
position:"sticky" sticks while scrolling — the usual sticky header: the header section gets
|
|
352
|
+
{ position:"sticky", stickyPosition:"autotop", stickyTop:0 }. stickyPosition:
|
|
353
|
+
autotop (default) | autobottom | default (both); offsets stickyTop / stickyBottom (px).
|
|
354
|
+
position:"pinned" fixed to the viewport — floating hotline/Zalo button, back-to-top. ALSO set
|
|
355
|
+
runtime.style.position:"fixed". fixedPosition: top_left | top_center | top_right |
|
|
356
|
+
bottom_left | bottom_center | bottom_right | center_left | center_right; offsets
|
|
357
|
+
fixedTop / fixedLeft / fixedRight / fixedBottom (px, default 20).
|
|
358
|
+
Anything else (or unset) = normal flow. stickyPosition alone does nothing without position:"sticky".
|
|
359
|
+
|
|
350
360
|
### HEADER must be RESPONSIVE — add a MOBILE MENUBAR, don't just let nav stack
|
|
351
361
|
A header built as one horizontal row of nav links looks fine on desktop but on a phone the links
|
|
352
362
|
squash or collapse into an ugly vertical pile. Build a header that carries TWO navs and swap them
|
|
@@ -373,7 +383,7 @@ A popup is a GLOBAL SOURCE, not a page section. Compose it from elements (there
|
|
|
373
383
|
shortcut), then save it as a "popup" global source:
|
|
374
384
|
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 } }).
|
|
375
385
|
- SIZE + POSITION live in the popup's runtime style/config (width/height/background + popupHorizontalPosition/popupVerticalPosition), NOT in specials. createPopup seeds a centred-modal default.
|
|
376
|
-
- 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. (
|
|
386
|
+
- 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. Other triggers: \`openPopupWhenScroll\` + \`target:"<elementId>"\` (opens when that element scrolls into view), \`openWhenNonLogin\` (logged-out visitors only, after \`timeOpenPopup\`). \`countOpenPopup:<n>\` caps auto-opens per browser session (unset = every page view). There is no exit-intent trigger.
|
|
377
387
|
- OVERLAY is NOT on the popup node — it's a field on the open_popup EVENT (popup_overlay:true).
|
|
378
388
|
2. Save it: create_global_source({ component:"popup", source:{ sections:[<popupNode>] } }) -> returns its id.
|
|
379
389
|
3. Open/close from any element via events: a button { action:"open_popup", popup_id:"<id>", popup_overlay:true };
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.33.2",
|
|
4
|
+
"d": "14/10/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "list_orders responses now include a status_name field alongside the numeric status.",
|
|
7
|
+
"vi": "Kết quả trả về của list_orders nay có thêm trường status_name bên cạnh status dạng số."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.33.1",
|
|
4
11
|
"d": "14/10/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "create_article and update_article accept a new seo object (page_title, meta_description, page_keywords, og_title, og_description, og_image) that…",
|
|
35
42
|
"vi": "create_article và update_article nay nhận thêm đối tượng seo (page_title, meta_description, page_keywords, og_title, og_description, og_image) để…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.31.11",
|
|
39
|
-
"d": "11/09/2026",
|
|
40
|
-
"type": "Changed",
|
|
41
|
-
"en": "create_site and create_site_from_template now stamp the new site with by_ai: \"mcp\" (persisted to the backend sites.by_ai column), extending the…",
|
|
42
|
-
"vi": "create_site và create_site_from_template nay đánh dấu site mới với by_ai: \"mcp\" (lưu vào cột sites.by_ai ở backend), mở rộng dấu hiệu AI-tạo vốn chỉ…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/tools/orders.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
/** builderx_api Enum.OrderStatus (lib/builderx_api/enum.ex). */
|
|
3
|
+
const ORDER_STATUS = {
|
|
4
|
+
0: "new", 1: "submitted", 2: "shipped", 3: "delivered", 4: "returning", 5: "returned", 6: "canceled",
|
|
5
|
+
7: "removed", 8: "packing", 9: "pending", 11: "waiting", 12: "wait_print", 13: "printed",
|
|
6
|
+
15: "part_returned", 16: "received_money", 17: "wait_submit", 20: "ordered", 22: "draft", 50: "blocked",
|
|
7
|
+
};
|
|
8
|
+
const STATUS_HELP = Object.entries(ORDER_STATUS).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
2
9
|
export function registerOrderTools(server, api, handle) {
|
|
3
10
|
server.tool("list_orders", "List orders of the site (metadata only). Use get_order for full details including items", {
|
|
4
11
|
page: z.number().optional().describe("Page number"),
|
|
5
12
|
limit: z.number().optional().describe("Items per page"),
|
|
6
|
-
status: z.number().optional().describe(
|
|
13
|
+
status: z.number().optional().describe(`Filter by status: ${STATUS_HELP}. Omit or -1 = all (except removed/blocked).`),
|
|
7
14
|
}, ({ page, limit, status }) => handle(async () => {
|
|
8
15
|
const res = await api.listOrders({ page, limit, status });
|
|
9
16
|
// Real shape: { orders: { data:[…], total_entries, count_status } }.
|
|
@@ -16,6 +23,7 @@ export function registerOrderTools(server, api, handle) {
|
|
|
16
23
|
bill_full_name: o.bill_full_name,
|
|
17
24
|
bill_phone_number: o.bill_phone_number,
|
|
18
25
|
status: o.status,
|
|
26
|
+
status_name: ORDER_STATUS[o.status] ?? null,
|
|
19
27
|
payment_status: o.payment_status,
|
|
20
28
|
payment_method: o.payment_method,
|
|
21
29
|
invoice_value: o.invoice_value || o.total,
|
package/dist/tools/pages.js
CHANGED
|
@@ -453,31 +453,39 @@ For full rewrites, use update_site_custom_code instead.`, {
|
|
|
453
453
|
server.tool("get_page_versions", "View version history of a page", {
|
|
454
454
|
page_id: z.string().describe("Page ID"),
|
|
455
455
|
}, ({ page_id }) => handle(() => api.getPageVersions(page_id)));
|
|
456
|
-
server.tool("list_page_contents", "List multi-language contents of
|
|
457
|
-
page_id: z.string().optional().describe("
|
|
458
|
-
|
|
456
|
+
server.tool("list_page_contents", "List multi-language contents (translations) of the site's pages and global sections. Pass page_id + language_code to read one page's translation.", {
|
|
457
|
+
page_id: z.string().optional().describe("Page ID — narrows the result to that page"),
|
|
458
|
+
language_code: z.string().optional().describe("With page_id: return just that language's content map"),
|
|
459
|
+
}, ({ page_id, language_code }) => handle(async () => {
|
|
460
|
+
if (page_id && language_code) {
|
|
461
|
+
const res = await api.getPageContent(page_id, language_code);
|
|
462
|
+
return res?.data ?? res;
|
|
463
|
+
}
|
|
464
|
+
const res = await api.listPageContents(); // backend ignores page_id here, so filter client-side
|
|
465
|
+
const data = res?.data ?? res;
|
|
466
|
+
return page_id ? { pages: (data?.pages || []).filter((p) => p.id === page_id) } : data;
|
|
467
|
+
}));
|
|
459
468
|
server.tool("update_page_content", `Create/update page content for a specific language.
|
|
460
469
|
IMPORTANT: Before calling, you MUST read existing content with list_page_contents first, then show the user what will change and get explicit confirmation. NEVER update without user approval.`, {
|
|
461
470
|
page_id: z.string().describe("Page ID"),
|
|
462
471
|
language_code: z.string().describe("Language code (e.g. 'en', 'vi')"),
|
|
463
|
-
content: z
|
|
472
|
+
content: z
|
|
473
|
+
.record(z.any())
|
|
474
|
+
.describe('Translations keyed by element id → field, e.g. { "<elementId>": { "text": "Hello" } } (also "placeholder", or "options": [{ id, name }]). Replaces this language\'s whole map — merge into the existing one.'),
|
|
464
475
|
meta_tags: z.array(z.record(z.any())).optional().describe("SEO meta tags"),
|
|
465
476
|
}, ({ page_id, language_code, content, meta_tags }) => handle(async () => {
|
|
466
477
|
// Safeguard: read existing content and block if new content is suspiciously smaller
|
|
467
|
-
const existingRes = await api.
|
|
468
|
-
const
|
|
469
|
-
if (
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
new_length: newStr.length,
|
|
479
|
-
};
|
|
480
|
-
}
|
|
478
|
+
const existingRes = await api.getPageContent(page_id, language_code);
|
|
479
|
+
const existing = existingRes?.data ?? existingRes;
|
|
480
|
+
if (existing && existing.content) {
|
|
481
|
+
const existingStr = JSON.stringify(existing.content);
|
|
482
|
+
const newStr = JSON.stringify(content);
|
|
483
|
+
if (existingStr.length > 200 && newStr.length < existingStr.length * 0.5) {
|
|
484
|
+
return {
|
|
485
|
+
error: `BLOCKED: New content (${newStr.length} chars) is much smaller than existing (${existingStr.length} chars) — ${Math.round((newStr.length / existingStr.length) * 100)}% of original. This may indicate fabricated data or data loss. Read it with list_page_contents(page_id, language_code) first, then merge your changes.`,
|
|
486
|
+
existing_length: existingStr.length,
|
|
487
|
+
new_length: newStr.length,
|
|
488
|
+
};
|
|
481
489
|
}
|
|
482
490
|
}
|
|
483
491
|
return api.updatePageContent({ page_id, language_code, content, meta_tags });
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
// Sale-channels app:
|
|
3
|
-
//
|
|
2
|
+
// Sale-channels app: partner product feeds (Google/Facebook/etc.) and catalog syncs.
|
|
3
|
+
// Sitemaps need no tool: the backend builds them at render time (<domain>/root_sitemaps.xml). Google Ads / OAuth-connect flows are intentionally omitted —
|
|
4
4
|
// they require an externally-authorized Google/Facebook partner.
|
|
5
5
|
export function registerSaleChannelTools(server, api, handle) {
|
|
6
6
|
const unwrap = (res, key) => (key && (res?.data?.[key] ?? res?.[key])) ?? res?.data ?? res;
|
|
7
|
-
// ── Sitemap ──
|
|
8
|
-
server.tool("sync_sitemap", "Sync a resource type into the site's sitemap. Requires sitemap enabled in site settings.", { kind: z.enum(["products", "categories", "articles", "pages"]).describe("What to sync into the sitemap") }, ({ kind }) => handle(() => api.sitemapSync(kind)));
|
|
9
|
-
server.tool("rebuild_sitemap", "Rebuild the whole sitemap (async background job).", { type: z.string().optional().describe("Optional rebuild scope hint") }, ({ type }) => handle(() => api.sitemapRebuild(type)));
|
|
10
7
|
// ── Partner feeds ──
|
|
11
8
|
server.tool("list_partner_feeds", "List product feeds for sale channels (Google Shopping, Facebook, etc.).", {
|
|
12
9
|
page: z.number().optional().describe("Page number"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.33.
|
|
3
|
+
"version": "1.33.2",
|
|
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",
|