webcake-storefront-mcp 1.26.1 → 1.27.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.
- package/dist/builder/templates.js +34 -0
- package/dist/changelog.json +7 -7
- package/dist/tools/builder.js +51 -9
- package/dist/tools/pages.js +23 -5
- package/package.json +1 -1
|
@@ -31,6 +31,40 @@ const DEFAULT_PALETTE = {
|
|
|
31
31
|
export function resolvePalette(p = {}) {
|
|
32
32
|
return { ...DEFAULT_PALETTE, ...Object.fromEntries(Object.entries(p).filter(([, v]) => v != null)) };
|
|
33
33
|
}
|
|
34
|
+
/** Relative luminance of a #hex colour (0 dark … 1 light). */
|
|
35
|
+
function luminance(hex) {
|
|
36
|
+
const h = String(hex || "").replace("#", "").slice(0, 6);
|
|
37
|
+
if (h.length < 6)
|
|
38
|
+
return 0;
|
|
39
|
+
const r = parseInt(h.slice(0, 2), 16) / 255;
|
|
40
|
+
const g = parseInt(h.slice(2, 4), 16) / 255;
|
|
41
|
+
const b = parseInt(h.slice(4, 6), 16) / 255;
|
|
42
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read the site's active theme colour matrix and return a Palette override so generated pages
|
|
46
|
+
* stay BRAND-CONSISTENT and CONTRAST-SAFE. The default palette uses var(--color_20) (the brand
|
|
47
|
+
* seed) as the accent — but for a LIGHT brand seed (e.g. a beige store) a white button label on
|
|
48
|
+
* it is unreadable, so we switch the accent to var(--color_24) (the darkest brand shade). Best
|
|
49
|
+
* effort: returns {} on any error (the var(--color_NN) defaults still resolve per-site).
|
|
50
|
+
*/
|
|
51
|
+
export async function contrastSafePalette(api) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await api.listThemes();
|
|
54
|
+
const themes = (res && res.data) || res || [];
|
|
55
|
+
const arr = Array.isArray(themes) ? themes : (themes.themes || []);
|
|
56
|
+
const th = arr.find((t) => t.is_selected) || arr[0];
|
|
57
|
+
const m = th && th.colors;
|
|
58
|
+
if (Array.isArray(m) && Array.isArray(m[2]) && m[2][0]) {
|
|
59
|
+
if (luminance(m[2][0]) > 0.62)
|
|
60
|
+
return { accent: "var(--color_24)" };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* keep the var()-based defaults */
|
|
65
|
+
}
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
34
68
|
// ---------------------------------------------------------------------------
|
|
35
69
|
// small spec helpers (return element specs for buildSection/buildRow children)
|
|
36
70
|
// ---------------------------------------------------------------------------
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.27.0",
|
|
4
|
+
"d": "26/06/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "build_page now accepts an seo object (title, description, keyword, favicon, thumbnail) that is written to page.settings.seo with Open Graph…",
|
|
7
|
+
"vi": "build_page nay nhận thêm đối tượng seo (title, description, keyword, favicon, thumbnail) và ghi vào page.settings.seo kèm mirroring Open Graph; hỗ…"
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.26.1",
|
|
4
11
|
"d": "26/06/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "get_build_guide now includes a category-page filter recipe explaining how sidebar filter widgets (checkbox-group, color-group, tags,…",
|
|
35
42
|
"vi": "get_build_guide nay bổ sung công thức filter trang danh mục, giải thích cách các widget lọc trong sidebar (checkbox-group, color-group, tags,…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.22.0",
|
|
39
|
-
"d": "25/06/2026",
|
|
40
|
-
"type": "Added",
|
|
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,…",
|
|
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,…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/tools/builder.js
CHANGED
|
@@ -4,7 +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
|
+
import { STORE_PAGE_TEMPLATES, resolvePalette, contrastSafePalette, wireNavigation } from "../builder/templates.js";
|
|
8
8
|
// Recursive spec for new_section / build_page children.
|
|
9
9
|
const elementSpec = z.object({
|
|
10
10
|
type: z.string().describe("Element type (see list_elements)"),
|
|
@@ -28,14 +28,39 @@ function newPageId(res) {
|
|
|
28
28
|
// builderx_spa); SPECIAL kinds also require a site-level data-source flag enabled on
|
|
29
29
|
// site.settings, otherwise components that bind to store/customer/blog data render
|
|
30
30
|
// with null bindings. build_page sets both for you.
|
|
31
|
-
|
|
31
|
+
// Page kind → numeric backend type. Exported so create_page maps the same way (the backend
|
|
32
|
+
// type is numeric 1–7, NOT a string).
|
|
33
|
+
export const PAGE_TYPE_NUM = {
|
|
32
34
|
main: 1, store: 2, member: 3, blog: 4, custom: 5, error: 6, maintain: 7,
|
|
33
35
|
};
|
|
34
36
|
const PAGE_TYPE_FLAG = {
|
|
35
37
|
store: "use_store", member: "use_member", blog: "use_blog",
|
|
36
38
|
error: "use_error", maintain: "use_maintain",
|
|
37
39
|
};
|
|
38
|
-
const PAGE_KINDS = ["main", "store", "member", "blog", "custom", "error", "maintain"];
|
|
40
|
+
export const PAGE_KINDS = ["main", "store", "member", "blog", "custom", "error", "maintain"];
|
|
41
|
+
/** Build the page.settings.seo block from simple inputs (the real shape; tokens like
|
|
42
|
+
* {{name_page}} / {{name_site}} are resolved by the storefront). */
|
|
43
|
+
export function buildPageSeo(seo = {}) {
|
|
44
|
+
const out = {};
|
|
45
|
+
if (seo.title)
|
|
46
|
+
out.title = seo.title;
|
|
47
|
+
if (seo.description)
|
|
48
|
+
out.description = seo.description;
|
|
49
|
+
if (seo.keyword)
|
|
50
|
+
out.keyword = seo.keyword;
|
|
51
|
+
if (seo.favicon)
|
|
52
|
+
out.favicon = seo.favicon;
|
|
53
|
+
if (seo.thumbnail)
|
|
54
|
+
out.thumbnail = seo.thumbnail;
|
|
55
|
+
// Open Graph mirrors title/description/thumbnail when not given explicitly.
|
|
56
|
+
if (seo.title || seo.og_title)
|
|
57
|
+
out.og_title = seo.og_title || seo.title;
|
|
58
|
+
if (seo.description || seo.og_description)
|
|
59
|
+
out.og_description = seo.og_description || seo.description;
|
|
60
|
+
if (seo.thumbnail)
|
|
61
|
+
out.og_image = seo.thumbnail;
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
39
64
|
export function registerBuilderTools(server, api, handle) {
|
|
40
65
|
server.tool("get_build_guide", "Get the BuilderX page authoring guide: page shape, the grid layout model, styling, breakpoints, forms/data, and the build workflow. Read this before building or heavily editing a page.", {}, () => handle(async () => ({ guide: BUILD_GUIDE })));
|
|
41
66
|
server.tool("list_elements", "List all BuilderX element/component types you can place on a page, grouped by category with a one-line summary and whether each is a container.", {}, () => handle(async () => listElements()));
|
|
@@ -90,8 +115,18 @@ The source must be { sections: [...] } — build sections with new_section. Vali
|
|
|
90
115
|
.optional()
|
|
91
116
|
.describe("Page kind. SPECIAL pages need a site data-source enabled — build_page does this automatically: store→use_store (product/cart bindings), member→use_member (customer/order bindings), blog→use_blog, error→use_error, maintain→use_maintain. 'main'/'custom' need nothing. Omit for a normal content page (defaults to 'main' for the homepage)."),
|
|
92
117
|
is_homepage: z.boolean().default(false).describe("Set as the site homepage"),
|
|
118
|
+
seo: z
|
|
119
|
+
.object({
|
|
120
|
+
title: z.string().optional().describe("SEO/browser title. Tokens allowed: {{name_page}}, {{name_site}}, {{name_product}}, {{name_category}}."),
|
|
121
|
+
description: z.string().optional().describe("Meta description (~155 chars)."),
|
|
122
|
+
keyword: z.string().optional().describe("Comma-separated keywords."),
|
|
123
|
+
favicon: z.string().optional().describe("Favicon URL (hosted)."),
|
|
124
|
+
thumbnail: z.string().optional().describe("Social/OG share image URL (hosted)."),
|
|
125
|
+
})
|
|
126
|
+
.optional()
|
|
127
|
+
.describe("SEO for this page → settings.seo. Without it the page publishes with an EMPTY title/description. For a store page a good default title is '{{name_product}} | {{name_site}}' (product) or '{{name_category}} | {{name_site}}' (category)."),
|
|
93
128
|
dry_run: z.boolean().default(true).describe("Preview+validate only (true) or create+save (false)"),
|
|
94
|
-
}, ({ name, slug, source, type, is_homepage, dry_run }) => handle(async () => {
|
|
129
|
+
}, ({ name, slug, source, type, is_homepage, seo, dry_run }) => handle(async () => {
|
|
95
130
|
const parsed = parseSource(source);
|
|
96
131
|
const validation = validatePage(parsed);
|
|
97
132
|
// Resolve numeric page type + the site data-source flag a special page needs.
|
|
@@ -132,10 +167,15 @@ The source must be { sections: [...] } — build sections with new_section. Vali
|
|
|
132
167
|
if (!pageId) {
|
|
133
168
|
return { error: "Page created but no id was returned.", created };
|
|
134
169
|
}
|
|
135
|
-
// slug / homepage are not applied at create — set them via update_page.
|
|
136
|
-
|
|
170
|
+
// slug / homepage / SEO are not applied at create — set them via update_page.
|
|
171
|
+
const seoBlock = seo ? buildPageSeo(seo) : null;
|
|
172
|
+
if (slug || is_homepage || (seoBlock && Object.keys(seoBlock).length)) {
|
|
137
173
|
await api
|
|
138
|
-
.updatePage(pageId, {
|
|
174
|
+
.updatePage(pageId, {
|
|
175
|
+
...(slug ? { slug } : {}),
|
|
176
|
+
...(is_homepage ? { is_homepage: true } : {}),
|
|
177
|
+
...(seoBlock && Object.keys(seoBlock).length ? { settings: { seo: seoBlock } } : {}),
|
|
178
|
+
})
|
|
139
179
|
.catch(() => { });
|
|
140
180
|
}
|
|
141
181
|
return {
|
|
@@ -220,8 +260,10 @@ Pass style:"minimal" for the old bare stubs (heading + binding element only).`,
|
|
|
220
260
|
const h1 = (text) => ({ type: "text", opts: { text, specials: { tag: "h1" }, style: { fontSize: "32px", fontWeight: "700" } } });
|
|
221
261
|
const accentBtn = (text, type = "button") => ({ type, opts: { text, style: { background: "var(--color_20)", color: "var(--color_00)", borderRadius: "8px", height: 48, fontWeight: "600" } } });
|
|
222
262
|
// Rich (default) store pages come from the designed, palette-aware templates;
|
|
223
|
-
// 'minimal' falls back to the original bare stubs.
|
|
224
|
-
|
|
263
|
+
// 'minimal' falls back to the original bare stubs. Derive a contrast-safe accent from
|
|
264
|
+
// the site's active theme (explicit palette overrides win).
|
|
265
|
+
const themePal = await contrastSafePalette(api);
|
|
266
|
+
const pal = resolvePalette({ ...themePal, ...(palette || {}) });
|
|
225
267
|
const minimalStore = {
|
|
226
268
|
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
269
|
products: () => ({ sections: [buildSection([
|
package/dist/tools/pages.js
CHANGED
|
@@ -3,6 +3,7 @@ import { CUSTOM_CODE_GUIDE } from "../guides.js";
|
|
|
3
3
|
import { getConfirmMode } from "./context.js";
|
|
4
4
|
import { normalizeEvents } from "../builder/events.js";
|
|
5
5
|
import { normalizeBindings } from "../builder/bindings.js";
|
|
6
|
+
import { PAGE_TYPE_NUM, PAGE_KINDS, buildPageSeo } from "./builder.js";
|
|
6
7
|
/**
|
|
7
8
|
* Page source utilities.
|
|
8
9
|
*
|
|
@@ -293,15 +294,32 @@ Examples:
|
|
|
293
294
|
const results = searchElements(source, filters);
|
|
294
295
|
return { page_id, matched: results.length, elements: results };
|
|
295
296
|
}));
|
|
296
|
-
server.tool("create_page", "Create a new page", {
|
|
297
|
+
server.tool("create_page", "Create a new (empty) page. For a page with content use build_page instead. type is a KIND (main/store/member/blog/custom/error/maintain) mapped to the numeric backend type; pass seo so it doesn't publish with an empty title.", {
|
|
297
298
|
name: z.string().describe("Page name"),
|
|
298
299
|
slug: z.string().describe("URL slug (e.g. '/about')"),
|
|
299
|
-
type: z.
|
|
300
|
+
type: z.enum(PAGE_KINDS).optional().describe("Page kind (main/store/member/blog/custom/error/maintain). store/member/blog need their data-source flag enabled — prefer build_page which auto-enables it."),
|
|
300
301
|
is_homepage: z.boolean().default(false).describe("Set as homepage"),
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
seo: z
|
|
303
|
+
.object({ title: z.string().optional(), description: z.string().optional(), keyword: z.string().optional(), favicon: z.string().optional(), thumbnail: z.string().optional() })
|
|
304
|
+
.optional()
|
|
305
|
+
.describe("SEO → settings.seo (title/description/keyword/favicon/thumbnail). Tokens {{name_page}}/{{name_site}} allowed."),
|
|
306
|
+
}, ({ name, slug, type, is_homepage, seo }) => handle(async () => {
|
|
307
|
+
const typeNum = type ? PAGE_TYPE_NUM[type] : undefined;
|
|
308
|
+
const created = await api.createPage({ name, ...(typeNum != null ? { type: typeNum } : {}) });
|
|
303
309
|
invalidatePageCache();
|
|
304
|
-
|
|
310
|
+
const pageId = (created && (created.id || created.data?.id || created.page?.id)) || null;
|
|
311
|
+
const seoBlock = seo ? buildPageSeo(seo) : null;
|
|
312
|
+
if (pageId && (slug || is_homepage || (seoBlock && Object.keys(seoBlock).length))) {
|
|
313
|
+
await api
|
|
314
|
+
.updatePage(pageId, {
|
|
315
|
+
...(slug ? { slug } : {}),
|
|
316
|
+
...(is_homepage ? { is_homepage: true } : {}),
|
|
317
|
+
...(seoBlock && Object.keys(seoBlock).length ? { settings: { seo: seoBlock } } : {}),
|
|
318
|
+
})
|
|
319
|
+
.catch(() => { });
|
|
320
|
+
invalidatePageCache();
|
|
321
|
+
}
|
|
322
|
+
return { success: true, page_id: pageId, name, slug, type: type ?? null, raw: pageId ? undefined : created };
|
|
305
323
|
}));
|
|
306
324
|
server.tool("update_page", "Update page properties (name, slug, settings, custom code)", {
|
|
307
325
|
page_id: z.string().describe("Page ID"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.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",
|