webcake-storefront-mcp 1.0.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/README.md +1166 -0
- package/dist/api.js +346 -0
- package/dist/auth/login.js +87 -0
- package/dist/builder/catalog.js +186 -0
- package/dist/builder/factory.js +1677 -0
- package/dist/builder/guide.js +64 -0
- package/dist/builder/page.js +149 -0
- package/dist/config.js +97 -0
- package/dist/db.js +96 -0
- package/dist/guides.js +93 -0
- package/dist/http.js +120 -0
- package/dist/index.js +73 -0
- package/dist/install.js +140 -0
- package/dist/mongo.js +102 -0
- package/dist/server.js +63 -0
- package/dist/smoke.js +81 -0
- package/dist/tools/apps.js +7 -0
- package/dist/tools/articles.js +53 -0
- package/dist/tools/automation.js +8 -0
- package/dist/tools/builder-extras.js +165 -0
- package/dist/tools/builder.js +124 -0
- package/dist/tools/cms-files.js +255 -0
- package/dist/tools/collections.js +31 -0
- package/dist/tools/combos.js +72 -0
- package/dist/tools/context.js +158 -0
- package/dist/tools/customers.js +13 -0
- package/dist/tools/global-sources.js +662 -0
- package/dist/tools/images.js +875 -0
- package/dist/tools/orders.js +32 -0
- package/dist/tools/pages.js +621 -0
- package/dist/tools/products.js +38 -0
- package/dist/tools/promotions.js +131 -0
- package/dist/tools/site-style.js +157 -0
- package/package.json +38 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export function registerProductTools(server, api, handle) {
|
|
3
|
+
server.tool("list_products", "List products of the site (metadata only: id, name, slug, price, image, status). Use get_product for full details", {
|
|
4
|
+
page: z.number().optional().describe("Page number"),
|
|
5
|
+
limit: z.number().optional().describe("Items per page"),
|
|
6
|
+
term: z.string().optional().describe("Search by product name"),
|
|
7
|
+
}, ({ page, limit, term }) => handle(async () => {
|
|
8
|
+
const res = await api.listProducts({ page, limit, term });
|
|
9
|
+
const products = (res && res.data) || res || [];
|
|
10
|
+
if (!Array.isArray(products))
|
|
11
|
+
return res;
|
|
12
|
+
return {
|
|
13
|
+
data: products.map((p) => ({
|
|
14
|
+
id: p.id,
|
|
15
|
+
name: p.name,
|
|
16
|
+
slug: p.slug,
|
|
17
|
+
custom_id: p.custom_id || undefined,
|
|
18
|
+
image: p.image || undefined,
|
|
19
|
+
price: p.price || undefined,
|
|
20
|
+
is_published: p.is_published,
|
|
21
|
+
total_sold: p.total_sold || 0,
|
|
22
|
+
rating: p.rating || undefined,
|
|
23
|
+
categories: p.categories || undefined,
|
|
24
|
+
updated_at: p.updated_at,
|
|
25
|
+
})),
|
|
26
|
+
total: res.total || products.length,
|
|
27
|
+
};
|
|
28
|
+
}));
|
|
29
|
+
server.tool("get_product", "Get full product details by ID: name, description, price, variations, images, attributes, SEO, etc.", {
|
|
30
|
+
id: z.string().describe("Product ID"),
|
|
31
|
+
}, ({ id }) => handle(() => api.getProduct(id)));
|
|
32
|
+
server.tool("search_products", "Search products by keyword. Returns matching products with basic info", {
|
|
33
|
+
term: z.string().describe("Search keyword"),
|
|
34
|
+
page: z.number().optional().describe("Page number"),
|
|
35
|
+
limit: z.number().optional().describe("Items per page"),
|
|
36
|
+
}, ({ term, page, limit }) => handle(() => api.searchProducts({ term, page, limit })));
|
|
37
|
+
server.tool("list_categories", "List all product categories of the site", {}, () => handle(() => api.listCategories()));
|
|
38
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const PROMOTION_TYPES = `
|
|
3
|
+
## Promotion Types (type field)
|
|
4
|
+
- "normal" — Standard product discount (fixed price or level-based)
|
|
5
|
+
- "same_price" — Fixed price for all items (đồng giá)
|
|
6
|
+
- "coupon" — Coupon/voucher code discount
|
|
7
|
+
- "coupon_id_multiple_times" — Reusable coupon code
|
|
8
|
+
- "discount_by_coupon_id" — Discount by coupon ID
|
|
9
|
+
- "promotion_order" — Order-level discount (by total amount)
|
|
10
|
+
- "promotion_category" — Category-level discount
|
|
11
|
+
- "x_get_y_prod" — Buy X get Y products free
|
|
12
|
+
- "x_get_y_category" — Buy X get Y from category free
|
|
13
|
+
|
|
14
|
+
## Promotion Classify (promotion_classify)
|
|
15
|
+
- "product" — Apply to specific products/variations
|
|
16
|
+
- "order" — Apply to entire order
|
|
17
|
+
- "category" — Apply to product categories
|
|
18
|
+
- "shipping" — Free shipping promotion
|
|
19
|
+
|
|
20
|
+
## Key Fields
|
|
21
|
+
- is_activated: whether promotion is currently active
|
|
22
|
+
- start_time / end_time: promotion schedule (UTC+7)
|
|
23
|
+
- priority_level: higher = applied first when conflicts
|
|
24
|
+
- coupon_info: coupon settings (code, max_uses, min_order, etc.)
|
|
25
|
+
- promo_code_info: promo code settings
|
|
26
|
+
- is_free_shipping: whether promotion gives free shipping
|
|
27
|
+
- level_order_prices: tiered discounts by order amount
|
|
28
|
+
- arr_level_promotion: tiered discount levels
|
|
29
|
+
- arr_price_promotion: tiered price discounts
|
|
30
|
+
- payment_methods: restrict to specific payment methods
|
|
31
|
+
- warehouse_ids: restrict to specific warehouses
|
|
32
|
+
- customer_tags: restrict to specific customer tags
|
|
33
|
+
- is_detail_time: has specific day/time scheduling
|
|
34
|
+
- is_hidden: hidden from storefront but still active
|
|
35
|
+
`;
|
|
36
|
+
export function registerPromotionTools(server, api, handle) {
|
|
37
|
+
server.tool("list_promotions", "List all promotions/discounts of the site (metadata only). Use get_promotion for full details", {
|
|
38
|
+
page: z.number().optional().describe("Page number (default: 1)"),
|
|
39
|
+
limit: z.number().optional().describe("Items per page (default: 20)"),
|
|
40
|
+
include_guide: z.boolean().optional().describe("Include promotion type reference guide"),
|
|
41
|
+
}, ({ page, limit, include_guide }) => handle(async () => {
|
|
42
|
+
const res = await api.listPromotions({ page, limit });
|
|
43
|
+
const promotions = (res && res.data) || [];
|
|
44
|
+
const result = {
|
|
45
|
+
data: Array.isArray(promotions)
|
|
46
|
+
? promotions.map((p) => ({
|
|
47
|
+
id: p.id,
|
|
48
|
+
name: p.name,
|
|
49
|
+
type: p.type,
|
|
50
|
+
promotion_classify: p.promotion_classify || undefined,
|
|
51
|
+
is_activated: p.is_activated,
|
|
52
|
+
start_time: p.start_time || undefined,
|
|
53
|
+
end_time: p.end_time || undefined,
|
|
54
|
+
priority_level: p.priority_level,
|
|
55
|
+
is_free_shipping: p.is_free_shipping || undefined,
|
|
56
|
+
is_hidden: p.is_hidden || undefined,
|
|
57
|
+
coupon_info: p.coupon_info || undefined,
|
|
58
|
+
used_count: p.used_count || 0,
|
|
59
|
+
inserted_at: p.inserted_at,
|
|
60
|
+
}))
|
|
61
|
+
: promotions,
|
|
62
|
+
total: res.total_entries || res.total || (Array.isArray(promotions) ? promotions.length : 0),
|
|
63
|
+
};
|
|
64
|
+
if (include_guide)
|
|
65
|
+
result.guide = PROMOTION_TYPES.trim();
|
|
66
|
+
return result;
|
|
67
|
+
}));
|
|
68
|
+
server.tool("get_promotion", "Get full promotion details by ID: name, type, schedule, discount rules, coupon settings, items, bonus products, etc.", {
|
|
69
|
+
id: z.string().describe("Promotion ID"),
|
|
70
|
+
}, ({ id }) => handle(async () => {
|
|
71
|
+
const res = await api.getPromotion(id);
|
|
72
|
+
return (res && res.data && res.data.promotion) || (res && res.data) || res;
|
|
73
|
+
}));
|
|
74
|
+
server.tool("get_promotion_items", "Get products/variations/categories attached to a promotion. Returns items with discount details (fixed_prices, level_info, coupon_item_info)", {
|
|
75
|
+
id: z.string().describe("Promotion ID"),
|
|
76
|
+
page: z.number().optional().describe("Page number"),
|
|
77
|
+
limit: z.number().optional().describe("Items per page"),
|
|
78
|
+
}, ({ id, page, limit }) => handle(async () => {
|
|
79
|
+
const res = await api.getPromotionItems(id, { page, limit });
|
|
80
|
+
return (res && res.data && res.data.result) || (res && res.data) || res;
|
|
81
|
+
}));
|
|
82
|
+
server.tool("get_active_promotions", "Get all currently active promotions (is_activated=true and within start_time/end_time range)", {}, () => handle(async () => {
|
|
83
|
+
const res = await api.getActivePromotions();
|
|
84
|
+
const promotions = (res && res.data && res.data.promotions) || (res && res.data) || [];
|
|
85
|
+
return {
|
|
86
|
+
data: Array.isArray(promotions)
|
|
87
|
+
? promotions.map((p) => ({
|
|
88
|
+
id: p.id,
|
|
89
|
+
name: p.name,
|
|
90
|
+
type: p.type,
|
|
91
|
+
promotion_classify: p.promotion_classify || undefined,
|
|
92
|
+
is_activated: p.is_activated,
|
|
93
|
+
start_time: p.start_time || undefined,
|
|
94
|
+
end_time: p.end_time || undefined,
|
|
95
|
+
priority_level: p.priority_level,
|
|
96
|
+
is_free_shipping: p.is_free_shipping || undefined,
|
|
97
|
+
coupon_info: p.coupon_info || undefined,
|
|
98
|
+
level_order_prices: p.level_order_prices || undefined,
|
|
99
|
+
arr_level_promotion: p.arr_level_promotion || undefined,
|
|
100
|
+
arr_price_promotion: p.arr_price_promotion || undefined,
|
|
101
|
+
}))
|
|
102
|
+
: promotions,
|
|
103
|
+
total: Array.isArray(promotions) ? promotions.length : 0,
|
|
104
|
+
};
|
|
105
|
+
}));
|
|
106
|
+
server.tool("search_promotions", "Search/filter promotions with advanced filters: by type, status (coming_soon/in_progress/finished), keyword, date range", {
|
|
107
|
+
term: z.string().optional().describe("Search by promotion name"),
|
|
108
|
+
type: z.string().optional().describe("Filter by type: normal, same_price, coupon, coupon_id_multiple_times, discount_by_coupon_id, promotion_order, promotion_category, x_get_y_prod, x_get_y_category"),
|
|
109
|
+
status: z.number().optional().describe("Filter by time status: 1=coming_soon, 2=in_progress, 3=finished"),
|
|
110
|
+
is_activated: z.boolean().optional().describe("Filter by active status"),
|
|
111
|
+
page: z.number().optional().describe("Page number"),
|
|
112
|
+
limit: z.number().optional().describe("Items per page"),
|
|
113
|
+
}, ({ term, type, status, is_activated, page, limit }) => handle(async () => {
|
|
114
|
+
const query = {};
|
|
115
|
+
if (term)
|
|
116
|
+
query.term = term;
|
|
117
|
+
if (type)
|
|
118
|
+
query.type = type;
|
|
119
|
+
if (status != null)
|
|
120
|
+
query.status = status;
|
|
121
|
+
if (is_activated != null)
|
|
122
|
+
query.is_activated = is_activated;
|
|
123
|
+
if (page)
|
|
124
|
+
query.page = page;
|
|
125
|
+
if (limit)
|
|
126
|
+
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;
|
|
130
|
+
}));
|
|
131
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const TEMPLATE_THEMES_URL = "https://api.storecake.io/api/v1/templates/list_themes";
|
|
3
|
+
const THEME_CATALOG_TTL_MS = 60 * 60 * 1000;
|
|
4
|
+
const FETCH_TIMEOUT_MS = 15000;
|
|
5
|
+
let _themeCatalogCache = null;
|
|
6
|
+
async function fetchTemplateThemes({ page = 1, limit = 12, lang = "vi", q = "" } = {}) {
|
|
7
|
+
const url = new URL(TEMPLATE_THEMES_URL);
|
|
8
|
+
url.searchParams.set("page", String(page));
|
|
9
|
+
url.searchParams.set("limit", String(limit));
|
|
10
|
+
url.searchParams.set("lang", lang);
|
|
11
|
+
if (q)
|
|
12
|
+
url.searchParams.set("q", q);
|
|
13
|
+
const controller = new AbortController();
|
|
14
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url, {
|
|
17
|
+
headers: {
|
|
18
|
+
accept: "application/json, text/plain, */*",
|
|
19
|
+
"accept-language": lang,
|
|
20
|
+
authorization: "Bearer null",
|
|
21
|
+
origin: "https://webcake.io",
|
|
22
|
+
referer: "https://webcake.io/",
|
|
23
|
+
},
|
|
24
|
+
signal: controller.signal,
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const body = await res.text().catch(() => "");
|
|
28
|
+
throw new Error(`list_template_themes ${res.status}: ${body}`);
|
|
29
|
+
}
|
|
30
|
+
return await res.json();
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function getThemeCatalog(force = false) {
|
|
37
|
+
if (!force && _themeCatalogCache && Date.now() - _themeCatalogCache.at < THEME_CATALOG_TTL_MS) {
|
|
38
|
+
return _themeCatalogCache.map;
|
|
39
|
+
}
|
|
40
|
+
const map = new Map();
|
|
41
|
+
let page = 1;
|
|
42
|
+
const limit = 100;
|
|
43
|
+
for (let i = 0; i < 5; i++) {
|
|
44
|
+
const res = await fetchTemplateThemes({ page, limit });
|
|
45
|
+
const themes = (res && res.themes) || [];
|
|
46
|
+
for (const t of themes) {
|
|
47
|
+
map.set(t.id, {
|
|
48
|
+
id: t.id,
|
|
49
|
+
name: t.name,
|
|
50
|
+
preview_url: t.preview_url || "",
|
|
51
|
+
thumbnail: t.thumbnail || "",
|
|
52
|
+
categories: (t.categories || []).map((c) => c.name).filter(Boolean),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (themes.length < limit)
|
|
56
|
+
break;
|
|
57
|
+
page += 1;
|
|
58
|
+
}
|
|
59
|
+
_themeCatalogCache = { at: Date.now(), map };
|
|
60
|
+
return map;
|
|
61
|
+
}
|
|
62
|
+
function parseThemeDescription(d) {
|
|
63
|
+
if (typeof d !== "string")
|
|
64
|
+
return { description_vi: "", description_en: "" };
|
|
65
|
+
try {
|
|
66
|
+
const j = JSON.parse(d);
|
|
67
|
+
return {
|
|
68
|
+
description_vi: (j.description_vi || "").slice(0, 600),
|
|
69
|
+
description_en: (j.description_en || "").slice(0, 300),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return { description_vi: d.slice(0, 600), description_en: "" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function registerSiteStyleTools(server, api, handle) {
|
|
77
|
+
server.tool("get_site_info", "Get full site information: name, domain, settings (colors, typography, layout, language, payment methods, etc.)", {}, () => handle(async () => {
|
|
78
|
+
const res = await api.getSite();
|
|
79
|
+
const site = (res && res.data) || res;
|
|
80
|
+
if (!site)
|
|
81
|
+
return { error: "Site not found" };
|
|
82
|
+
return {
|
|
83
|
+
id: site.id,
|
|
84
|
+
name: site.name,
|
|
85
|
+
domain: site.domain,
|
|
86
|
+
custom_domain: site.custom_domain || undefined,
|
|
87
|
+
logo: site.logo || undefined,
|
|
88
|
+
favicon: site.favicon || undefined,
|
|
89
|
+
settings: site.settings || {},
|
|
90
|
+
created_at: site.created_at,
|
|
91
|
+
};
|
|
92
|
+
}));
|
|
93
|
+
server.tool("list_themes", "List all custom themes of the site. Returns theme name, colors, typographies, transitions, and which one is active", {}, () => handle(() => api.listThemes()));
|
|
94
|
+
server.tool("list_template_themes", "Search/list the public Webcake template marketplace (api.storecake.io). Use to match customer brief against existing templates by keyword. Returns id, name, preview_url, thumbnail, categories", {
|
|
95
|
+
q: z.string().optional().describe("Keyword to search themes"),
|
|
96
|
+
page: z.number().optional().describe("Page number (default 1)"),
|
|
97
|
+
limit: z.number().optional().describe("Items per page (default 12)"),
|
|
98
|
+
lang: z.string().optional().describe("Language code (default 'vi')"),
|
|
99
|
+
}, ({ q, page, limit, lang }) => handle(async () => {
|
|
100
|
+
const res = await fetchTemplateThemes({
|
|
101
|
+
q: q || "",
|
|
102
|
+
page: page || 1,
|
|
103
|
+
limit: limit || 12,
|
|
104
|
+
lang: lang || "vi",
|
|
105
|
+
});
|
|
106
|
+
const themes = ((res && res.themes) || []).map((t) => ({
|
|
107
|
+
id: t.id,
|
|
108
|
+
name: t.name,
|
|
109
|
+
preview_url: t.preview_url,
|
|
110
|
+
thumbnail: t.thumbnail,
|
|
111
|
+
categories: (t.categories || []).map((c) => c.name),
|
|
112
|
+
preview_img: (t.site && t.site.preview_img && (t.site.preview_img["1920_1080"] || t.site.preview_img["430_932"])) ||
|
|
113
|
+
null,
|
|
114
|
+
}));
|
|
115
|
+
return {
|
|
116
|
+
page: res && res.page,
|
|
117
|
+
limit: res && res.limit,
|
|
118
|
+
total: res && res.total_entries,
|
|
119
|
+
count: themes.length,
|
|
120
|
+
themes,
|
|
121
|
+
};
|
|
122
|
+
}));
|
|
123
|
+
server.tool("semantic_search_themes", "Semantic search across the theme marketplace using bge-m3 embeddings (cosine similarity). Use when the brief is a natural-language description of industry + features (e.g. 'website mỹ phẩm có popup minigame và loyalty'), not just keywords. Returns top matches with theme_id, score, name, preview_url, thumbnail, description_vi/en", {
|
|
124
|
+
query: z.string().describe("Natural-language description of the desired website (industry + features)"),
|
|
125
|
+
limit: z.number().optional().describe("Number of matches to return (default 5, max 10)"),
|
|
126
|
+
}, ({ query, limit }) => handle(async () => {
|
|
127
|
+
const q = (query || "").trim();
|
|
128
|
+
if (!q)
|
|
129
|
+
return { error: "query is required" };
|
|
130
|
+
const cap = Math.min(Math.max(limit || 5, 1), 10);
|
|
131
|
+
const raw = await api.semanticSearchThemes(q);
|
|
132
|
+
const pairs = Array.isArray(raw && raw.results) ? raw.results : [];
|
|
133
|
+
let catalog;
|
|
134
|
+
try {
|
|
135
|
+
catalog = await getThemeCatalog();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
catalog = new Map();
|
|
139
|
+
}
|
|
140
|
+
const matches = pairs.slice(0, cap).map(([te, score]) => {
|
|
141
|
+
const themeId = te && te.theme_id;
|
|
142
|
+
const info = catalog.get(themeId) || {};
|
|
143
|
+
const desc = parseThemeDescription(te && te.description);
|
|
144
|
+
return {
|
|
145
|
+
theme_id: themeId,
|
|
146
|
+
score: typeof score === "number" ? Number(score.toFixed(4)) : null,
|
|
147
|
+
name: info.name || null,
|
|
148
|
+
preview_url: info.preview_url || null,
|
|
149
|
+
thumbnail: info.thumbnail || null,
|
|
150
|
+
categories: info.categories || [],
|
|
151
|
+
description_vi: desc.description_vi,
|
|
152
|
+
description_en: desc.description_en,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
return { query: q, count: matches.length, matches };
|
|
156
|
+
}));
|
|
157
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "webcake-storefront-mcp",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "MCP server for the WebCake/StoreCake storefront builder — page CRUD, page authoring, products, orders, and more",
|
|
5
|
+
"mcpName": "io.github.vuluu2k/webcake-storefront-mcp",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"webcake-storefront-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc && node scripts/copy-assets.mjs",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"serve": "node dist/index.js serve",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"smoke": "node dist/smoke.js",
|
|
22
|
+
"prepare": "npm run build",
|
|
23
|
+
"prepublishOnly": "npm run build && npm run smoke"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
27
|
+
"better-sqlite3": "^12.8.0",
|
|
28
|
+
"mongodb": "^6.21.0",
|
|
29
|
+
"node-html-parser": "^8.0.2",
|
|
30
|
+
"sharp": "^0.34.5",
|
|
31
|
+
"zod": "^3.25.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
35
|
+
"@types/node": "^26.0.0",
|
|
36
|
+
"typescript": "^6.0.3"
|
|
37
|
+
}
|
|
38
|
+
}
|