webcake-storefront-mcp 1.33.0 → 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 +14 -14
- package/dist/tools/articles.js +3 -5
- package/dist/tools/catalog-write.js +9 -17
- 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,18 @@
|
|
|
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
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.33.1",
|
|
11
|
+
"d": "14/10/2026",
|
|
12
|
+
"type": "Changed",
|
|
13
|
+
"en": "create_product, create_product_category, create_blog_category, and create_article no longer accept a seo parameter; set SEO afterwards with…",
|
|
14
|
+
"vi": "create_product, create_product_category, create_blog_category và create_article không còn nhận tham số seo; hãy đặt SEO sau khi tạo bằng…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.33.0",
|
|
4
18
|
"d": "14/10/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
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…",
|
|
28
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) để…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.31.11",
|
|
32
|
-
"d": "11/09/2026",
|
|
33
|
-
"type": "Changed",
|
|
34
|
-
"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…",
|
|
35
|
-
"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ỉ…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.31.10",
|
|
39
|
-
"d": "08/09/2026",
|
|
40
|
-
"type": "Added",
|
|
41
|
-
"en": "list_articles accepts a term parameter to search articles by title or slug, and its response now includes page and limit alongside total.",
|
|
42
|
-
"vi": "list_articles nay nhận thêm tham số term để tìm bài viết theo tiêu đề hoặc slug, và kết quả trả về nay có thêm page và limit bên cạnh total."
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/tools/articles.js
CHANGED
|
@@ -144,14 +144,14 @@ that view is the public one, so hidden posts and posts scheduled in the future a
|
|
|
144
144
|
Built via the dashboard command pipeline: title + optional summary, HTML content, image URLs, and
|
|
145
145
|
category linkage. Pass category_ids from create_blog_category / list articles' categories so the
|
|
146
146
|
post shows up under those categories (it is also auto-filed under the default category). Image URLs
|
|
147
|
-
must be hosted (search_images / upload_images). The backend generates the id and slug
|
|
147
|
+
must be hosted (search_images / upload_images). The backend generates the id and slug.
|
|
148
|
+
SEO: set it afterwards with update_article({ id, seo }).`, {
|
|
148
149
|
name: z.string().describe("Article title"),
|
|
149
150
|
content: z.string().optional().describe("HTML content of the post"),
|
|
150
151
|
summary: z.string().optional().describe("Short summary / excerpt"),
|
|
151
152
|
images: z.array(z.string()).optional().describe("Hosted image URLs; the first is the cover image"),
|
|
152
153
|
category_ids: z.array(z.string()).optional().describe("Blog category IDs to file the post under (from create_blog_category)"),
|
|
153
|
-
|
|
154
|
-
}, ({ name, content, summary, images, category_ids, seo }) => handle(async () => {
|
|
154
|
+
}, ({ name, content, summary, images, category_ids }) => handle(async () => {
|
|
155
155
|
const id = randomUUID();
|
|
156
156
|
const commands = [{ name: "create_article", data: { id, name } }];
|
|
157
157
|
if (summary)
|
|
@@ -162,8 +162,6 @@ must be hosted (search_images / upload_images). The backend generates the id and
|
|
|
162
162
|
commands.push({ name: "content_article", data: { id, content } });
|
|
163
163
|
if (category_ids && category_ids.length)
|
|
164
164
|
commands.push({ name: "bulk_add_category_to_article", data: { id, ids: category_ids } });
|
|
165
|
-
if (seo)
|
|
166
|
-
commands.push({ name: "set_article_seo", data: { id, meta_tags: mergeSeoTags(seo) } });
|
|
167
165
|
await api.createBlogArticle(commands);
|
|
168
166
|
return {
|
|
169
167
|
success: true,
|
|
@@ -56,7 +56,8 @@ export function registerCatalogWriteTools(server, api, handle) {
|
|
|
56
56
|
server.tool("create_product", `Create a product so the storefront has real merchandise (grid-product / slider-product bindings need this).
|
|
57
57
|
Simple use: pass name + price (+ images, category_ids). One default variation with the price/stock is created for you.
|
|
58
58
|
Advanced use: pass attributes (e.g. Color/Size) + variations for a multi-SKU product.
|
|
59
|
-
Images must be HOSTED URLs — get them from search_images or upload_images first. The backend generates id, slug and publishes the product
|
|
59
|
+
Images must be HOSTED URLs — get them from search_images or upload_images first. The backend generates id, slug and publishes the product.
|
|
60
|
+
SEO: set it afterwards with update_product({ product_id, seo }).`, {
|
|
60
61
|
name: z.string().describe("Product name"),
|
|
61
62
|
price: z.number().optional().describe("Selling price (used when you don't pass variations). Required unless variations are given."),
|
|
62
63
|
original_price: z.number().optional().describe("List/compare-at price for the simple single-variation case"),
|
|
@@ -71,8 +72,7 @@ Images must be HOSTED URLs — get them from search_images or upload_images firs
|
|
|
71
72
|
.optional()
|
|
72
73
|
.describe("Variant axes, e.g. [{name:'Color',values:['Đen','Trắng']},{name:'Size',values:['S','M','L']}]"),
|
|
73
74
|
variations: z.array(variationSpec).optional().describe("Explicit per-SKU variations. Omit to auto-build one from price/stock/sku."),
|
|
74
|
-
|
|
75
|
-
}, ({ name, price, original_price, stock, sku, images, description, short_description, category_ids, attributes, variations, seo }) => handle(async () => {
|
|
75
|
+
}, ({ name, price, original_price, stock, sku, images, description, short_description, category_ids, attributes, variations }) => handle(async () => {
|
|
76
76
|
let vars = variations;
|
|
77
77
|
// Enrich attribute axes with the id + keyword shape the storefront variation selector
|
|
78
78
|
// needs (a raw {name,values} declares the axis but attaches it to no variation).
|
|
@@ -146,11 +146,7 @@ Images must be HOSTED URLs — get them from search_images or upload_images firs
|
|
|
146
146
|
// Success response is { product: {...} } at the top level (not under data).
|
|
147
147
|
const prod = res?.product || res?.data?.product || res?.data?.attributes || res?.data || {};
|
|
148
148
|
const productId = prod?.id || null;
|
|
149
|
-
const warning = seo && productId
|
|
150
|
-
? await applyProductSeo(api, productId, seo).then(() => undefined, (e) => `Product created, but SEO failed: ${e?.message ?? e}`)
|
|
151
|
-
: undefined;
|
|
152
149
|
return {
|
|
153
|
-
...(warning ? { warning } : {}),
|
|
154
150
|
success: true,
|
|
155
151
|
product_id: productId,
|
|
156
152
|
name,
|
|
@@ -161,13 +157,13 @@ Images must be HOSTED URLs — get them from search_images or upload_images firs
|
|
|
161
157
|
};
|
|
162
158
|
}));
|
|
163
159
|
server.tool("create_product_category", `Create a product category (so grid-category / a category page has something to show, and products can be filed under it).
|
|
164
|
-
Returns the new category id — pass it to create_product's category_ids. Image must be a hosted URL
|
|
160
|
+
Returns the new category id — pass it to create_product's category_ids. Image must be a hosted URL.
|
|
161
|
+
SEO: set it afterwards with update_product_category({ id, seo }).`, {
|
|
165
162
|
name: z.string().describe("Category name"),
|
|
166
163
|
image: z.string().optional().describe("Hosted image URL for the category card"),
|
|
167
164
|
description: z.string().optional().describe("Category description"),
|
|
168
165
|
parent_id: z.string().optional().describe("Parent category id for a sub-category"),
|
|
169
|
-
|
|
170
|
-
}, ({ name, image, description, parent_id, seo }) => handle(async () => {
|
|
166
|
+
}, ({ name, image, description, parent_id }) => handle(async () => {
|
|
171
167
|
const id = randomUUID();
|
|
172
168
|
const commands = [
|
|
173
169
|
{ name: "create_category", data: { id, name, ...(parent_id ? { parent_id } : {}) } },
|
|
@@ -180,25 +176,21 @@ Returns the new category id — pass it to create_product's category_ids. Image
|
|
|
180
176
|
data: { id, multi_description: [{ id: randomUUID(), title: name, description }] },
|
|
181
177
|
});
|
|
182
178
|
}
|
|
183
|
-
if (seo)
|
|
184
|
-
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo) } });
|
|
185
179
|
await api.createProductCategory(commands);
|
|
186
180
|
return { success: true, category_id: id, name, ...(parent_id ? { parent_id } : {}) };
|
|
187
181
|
}));
|
|
188
|
-
server.tool("create_blog_category", `Create a blog/article category. Returns the new category id — pass it to create_article's category_id so posts are grouped (post-list / blog pages bind to it). Image must be a hosted URL
|
|
182
|
+
server.tool("create_blog_category", `Create a blog/article category. Returns the new category id — pass it to create_article's category_id so posts are grouped (post-list / blog pages bind to it). Image must be a hosted URL.
|
|
183
|
+
SEO: set it afterwards with update_blog_category({ id, seo }).`, {
|
|
189
184
|
name: z.string().describe("Blog category name"),
|
|
190
185
|
image: z.string().optional().describe("Hosted image URL"),
|
|
191
186
|
description: z.string().optional().describe("Category description"),
|
|
192
|
-
|
|
193
|
-
}, ({ name, image, description, seo }) => handle(async () => {
|
|
187
|
+
}, ({ name, image, description }) => handle(async () => {
|
|
194
188
|
const id = randomUUID();
|
|
195
189
|
const commands = [{ name: "create_category", data: { id, name } }];
|
|
196
190
|
if (description)
|
|
197
191
|
commands.push({ name: "description_category", data: { id, description } });
|
|
198
192
|
if (image)
|
|
199
193
|
commands.push({ name: "image_category", data: { id, image } });
|
|
200
|
-
if (seo)
|
|
201
|
-
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo) } });
|
|
202
194
|
await api.createBlogCategory(commands);
|
|
203
195
|
return { success: true, category_id: id, name };
|
|
204
196
|
}));
|
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",
|