webcake-storefront-mcp 1.4.0 → 1.5.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/api.js CHANGED
@@ -266,6 +266,22 @@ export class WebcakeCmsApi {
266
266
  deleteArticle(id) {
267
267
  return this.request("DELETE", `/api/v1/cms_function/${this.siteId}/blog/article/${id}`);
268
268
  }
269
+ /** Create a blog/article category. Command-based: pass a `commands` array whose entries
270
+ * each carry a caller-generated `data.id` (the new category id). Response is generic. */
271
+ createBlogCategory(commands) {
272
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/categories/create`, {
273
+ body: { site_id: this.siteId, commands },
274
+ timeout: 60000,
275
+ });
276
+ }
277
+ /** Create a blog article via the dashboard command pipeline (supports category linkage,
278
+ * images, summary, content). Caller generates the article id in each command's data.id. */
279
+ createBlogArticle(commands) {
280
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/articles/create`, {
281
+ body: { site_id: this.siteId, commands },
282
+ timeout: 60000,
283
+ });
284
+ }
269
285
  // ── Products ──
270
286
  listProducts(query) {
271
287
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/products/all`, { query });
@@ -282,6 +298,22 @@ export class WebcakeCmsApi {
282
298
  getProductsByCategory(categoryId) {
283
299
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/categories/products`, { query: { category_id: categoryId } });
284
300
  }
301
+ /** Create a product. Body wraps fields in `product_params`; the backend generates the
302
+ * id/slug and sets is_published. Pass variations (price/stock per SKU) + categories. */
303
+ createProduct(productParams) {
304
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/products/create`, {
305
+ body: { site_id: this.siteId, product_params: productParams },
306
+ timeout: 60000,
307
+ });
308
+ }
309
+ /** Create a product category. Command-based: pass a `commands` array whose entries each
310
+ * carry a caller-generated `data.id` (the new category id). Response is generic. */
311
+ createProductCategory(commands) {
312
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/categories/create`, {
313
+ body: { site_id: this.siteId, commands },
314
+ timeout: 60000,
315
+ });
316
+ }
285
317
  // ── Orders ──
286
318
  listOrders(query) {
287
319
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/orders/all`, { query });
@@ -1,4 +1,11 @@
1
1
  [
2
+ {
3
+ "v": "1.5.0",
4
+ "d": "23/06/2026",
5
+ "type": "Added",
6
+ "en": "New create_product tool creates a storefront product (simple name + price, or advanced with named attributes and per-SKU variations); accepts hosted…",
7
+ "vi": "Tool mới create_product tạo sản phẩm cho storefront (đơn giản với tên + giá, hoặc nâng cao với attributes và variations theo từng SKU); nhận URL ảnh…"
8
+ },
2
9
  {
3
10
  "v": "1.4.0",
4
11
  "d": "23/06/2026",
@@ -33,12 +40,5 @@
33
40
  "type": "Changed",
34
41
  "en": "The install command's interactive wizard now runs a three-step flow: environment selection (prod / staging / local), authentication, then IDE…",
35
42
  "vi": "Trình hướng dẫn tương tác của lệnh install nay chạy theo 3 bước: chọn môi trường (prod / staging / local), xác thực, rồi cấu hình IDE."
36
- },
37
- {
38
- "v": "1.1.2",
39
- "d": "23/06/2026",
40
- "type": "Fixed",
41
- "en": "The server no longer crashes at startup in container environments built with npm ci --ignore-scripts; the better-sqlite3 native SQLite module has…",
42
- "vi": "Server không còn bị crash khi khởi động trong môi trường container được build bằng npm ci --ignore-scripts; module SQLite native better-sqlite3 đã…"
43
43
  }
44
44
  ]
package/dist/server.js CHANGED
@@ -7,6 +7,7 @@ import { registerArticleTools } from "./tools/articles.js";
7
7
  import { registerCustomerTools } from "./tools/customers.js";
8
8
  import { registerAutomationTools } from "./tools/automation.js";
9
9
  import { registerProductTools } from "./tools/products.js";
10
+ import { registerCatalogWriteTools } from "./tools/catalog-write.js";
10
11
  import { registerOrderTools } from "./tools/orders.js";
11
12
  import { registerSiteStyleTools } from "./tools/site-style.js";
12
13
  import { registerAppTools } from "./tools/apps.js";
@@ -22,6 +23,8 @@ IMPORTANT: When the user asks ANY question about their website, store, products,
22
23
 
23
24
  You can also BUILD pages: use get_build_guide, list_elements, get_element to learn the BuilderX component model, new_section/new_element to compose, validate_page to check, then build_page (dry_run first) to create. Publishing is site-level via publish_site.
24
25
 
26
+ To make a generated site look real, also CREATE DATA so dataset bindings resolve: create_product_category + create_product (storefront), create_blog_category + create_article (blog). Get image URLs from search_images / upload_images first, then reference them. A good flow for a fresh site: create_site → create a few categories → create products in them → build_page (home + store/blog pages) → publish_site.
27
+
25
28
  Workflow:
26
29
  1. On first interaction, call get_current_context. The site is NOT set from env — if no site is selected yet, call list_my_sites and ask the user which site to work on, then switch_site (the choice is saved and reused next session). To start from scratch, create_site makes a new site and switches to it; then build a homepage with build_page (type:'main', is_homepage:true).
27
30
  2. Before answering a site-specific question, query the relevant tool.
@@ -50,6 +53,7 @@ export function createServer(api, opts = {}) {
50
53
  registerCustomerTools(server, api, handle);
51
54
  registerAutomationTools(server, api, handle);
52
55
  registerProductTools(server, api, handle);
56
+ registerCatalogWriteTools(server, api, handle);
53
57
  registerOrderTools(server, api, handle);
54
58
  registerSiteStyleTools(server, api, handle);
55
59
  registerAppTools(server, api, handle);
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { randomUUID } from "node:crypto";
2
3
  export function registerArticleTools(server, api, handle) {
3
4
  server.tool("list_articles", "List blog articles (metadata only, without HTML content). Use get_article to get full content", {
4
5
  page: z.number().optional().describe("Page number"),
@@ -27,16 +28,36 @@ export function registerArticleTools(server, api, handle) {
27
28
  server.tool("get_article", "Get article details by ID", {
28
29
  id: z.string().describe("Article ID"),
29
30
  }, ({ id }) => handle(() => api.getArticle(id)));
30
- server.tool("create_article", "Create a new blog article", {
31
+ server.tool("create_article", `Create a blog article so blog/post pages (post-list, grid-blog, post-overlay) have content.
32
+ Built via the dashboard command pipeline: title + optional summary, HTML content, image URLs, and
33
+ category linkage. Pass category_ids from create_blog_category / list articles' categories so the
34
+ post shows up under those categories (it is also auto-filed under the default category). Image URLs
35
+ must be hosted (search_images / upload_images). The backend generates the id and slug.`, {
31
36
  name: z.string().describe("Article title"),
32
- slug: z.string().describe("URL slug"),
33
- content: z.string().describe("HTML content"),
34
- summary: z.string().optional().describe("Summary"),
35
- category_id: z.string().optional().describe("Category ID"),
36
- tags: z.array(z.string()).optional().describe("Tags"),
37
- images: z.array(z.string()).optional().describe("Image URLs"),
38
- is_hidden: z.boolean().default(false).describe("Hide from public"),
39
- }, (params) => handle(() => api.createArticle(params)));
37
+ content: z.string().optional().describe("HTML content of the post"),
38
+ summary: z.string().optional().describe("Short summary / excerpt"),
39
+ images: z.array(z.string()).optional().describe("Hosted image URLs; the first is the cover image"),
40
+ category_ids: z.array(z.string()).optional().describe("Blog category IDs to file the post under (from create_blog_category)"),
41
+ }, ({ name, content, summary, images, category_ids }) => handle(async () => {
42
+ const id = randomUUID();
43
+ const commands = [{ name: "create_article", data: { id, name } }];
44
+ if (summary)
45
+ commands.push({ name: "summary_article", data: { id, summary } });
46
+ if (images && images.length)
47
+ commands.push({ name: "image_article", data: { id, images } });
48
+ if (content)
49
+ commands.push({ name: "content_article", data: { id, content } });
50
+ if (category_ids && category_ids.length)
51
+ commands.push({ name: "bulk_add_category_to_article", data: { id, ids: category_ids } });
52
+ await api.createBlogArticle(commands);
53
+ return {
54
+ success: true,
55
+ article_id: id,
56
+ name,
57
+ categories: category_ids || [],
58
+ cover: images?.[0] || null,
59
+ };
60
+ }));
40
61
  server.tool("update_article", "Update a blog article", {
41
62
  id: z.string().describe("Article ID"),
42
63
  name: z.string().optional().describe("New title"),
@@ -0,0 +1,126 @@
1
+ // Write tools for catalog DATA — products, product categories, blog categories — so a
2
+ // generated site has real content to render (grid-product / grid-category / post-list
3
+ // bindings resolve only when these exist). Articles already have create_article.
4
+ //
5
+ // The category endpoints are COMMAND-based and the CALLER must generate the new id
6
+ // (the response is a generic {code:success}), so we mint a UUID and return it.
7
+ import { z } from "zod";
8
+ import { randomUUID } from "node:crypto";
9
+ const variationSpec = z.object({
10
+ retail_price: z.number().describe("Selling price (what the customer pays)"),
11
+ original_price: z.number().optional().describe("List/compare-at price (defaults to retail_price)"),
12
+ remain_quantity: z.number().optional().describe("Stock quantity (default 100)"),
13
+ custom_id: z.string().optional().describe("SKU for this variation (auto-generated if omitted)"),
14
+ images: z.array(z.string()).optional().describe("Hosted image URLs for this variation"),
15
+ weight: z.number().optional().describe("Weight in grams (default 0)"),
16
+ fields: z
17
+ .array(z.object({ name: z.string(), value: z.string() }))
18
+ .optional()
19
+ .describe("Attribute values for this variation, e.g. [{name:'Color',value:'Đen'},{name:'Size',value:'M'}]"),
20
+ });
21
+ export function registerCatalogWriteTools(server, api, handle) {
22
+ server.tool("create_product", `Create a product so the storefront has real merchandise (grid-product / slider-product bindings need this).
23
+ Simple use: pass name + price (+ images, category_ids). One default variation with the price/stock is created for you.
24
+ Advanced use: pass attributes (e.g. Color/Size) + variations for a multi-SKU product.
25
+ Images must be HOSTED URLs — get them from search_images or upload_images first. The backend generates id, slug and publishes the product.`, {
26
+ name: z.string().describe("Product name"),
27
+ price: z.number().optional().describe("Selling price (used when you don't pass variations). Required unless variations are given."),
28
+ original_price: z.number().optional().describe("List/compare-at price for the simple single-variation case"),
29
+ stock: z.number().default(100).describe("Stock quantity for the simple single-variation case"),
30
+ sku: z.string().optional().describe("SKU / custom_id for the simple case (auto-generated if omitted)"),
31
+ images: z.array(z.string()).optional().describe("Hosted image URLs (search_images/upload_images). First image becomes the product thumbnail."),
32
+ description: z.string().optional().describe("Product description (HTML allowed)"),
33
+ category_ids: z.array(z.string()).optional().describe("Product category IDs to file the product under (from create_product_category / list_categories)"),
34
+ attributes: z
35
+ .array(z.object({ name: z.string(), values: z.array(z.string()) }))
36
+ .optional()
37
+ .describe("Variant axes, e.g. [{name:'Color',values:['Đen','Trắng']},{name:'Size',values:['S','M','L']}]"),
38
+ variations: z.array(variationSpec).optional().describe("Explicit per-SKU variations. Omit to auto-build one from price/stock/sku."),
39
+ }, ({ name, price, original_price, stock, sku, images, description, category_ids, attributes, variations }) => handle(async () => {
40
+ let vars = variations;
41
+ if (!vars || !vars.length) {
42
+ if (price == null)
43
+ throw new Error("Provide `price` (or explicit `variations`) to create a product.");
44
+ vars = [
45
+ {
46
+ custom_id: sku || `SKU-${randomUUID().slice(0, 8)}`,
47
+ retail_price: price,
48
+ original_price: original_price ?? price,
49
+ remain_quantity: stock ?? 100,
50
+ images: images || [],
51
+ weight: 0,
52
+ fields: [],
53
+ },
54
+ ];
55
+ }
56
+ else {
57
+ // Normalise: fill SKU / original_price / stock defaults per variation.
58
+ vars = vars.map((v) => ({
59
+ custom_id: v.custom_id || `SKU-${randomUUID().slice(0, 8)}`,
60
+ retail_price: v.retail_price,
61
+ original_price: v.original_price ?? v.retail_price,
62
+ remain_quantity: v.remain_quantity ?? 100,
63
+ images: v.images || [],
64
+ weight: v.weight ?? 0,
65
+ fields: v.fields || [],
66
+ is_hidden: false,
67
+ }));
68
+ }
69
+ const productParams = {
70
+ name,
71
+ variations: vars,
72
+ ...(description ? { description } : {}),
73
+ ...(attributes ? { product_attributes: attributes } : {}),
74
+ ...(category_ids ? { categories: category_ids } : {}),
75
+ ...(images && images.length ? { image: images[0] } : {}),
76
+ };
77
+ const res = await api.createProduct(productParams);
78
+ const data = res?.data;
79
+ const productId = data?.attributes?.id || data?.id || data?.product?.id || null;
80
+ return {
81
+ success: true,
82
+ product_id: productId,
83
+ name,
84
+ slug: data?.attributes?.slug || data?.slug || null,
85
+ variations: vars.length,
86
+ categories: category_ids || [],
87
+ raw: productId ? undefined : res, // surface raw response only if we couldn't find the id
88
+ };
89
+ }));
90
+ 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).
91
+ Returns the new category id — pass it to create_product's category_ids. Image must be a hosted URL.`, {
92
+ name: z.string().describe("Category name"),
93
+ image: z.string().optional().describe("Hosted image URL for the category card"),
94
+ description: z.string().optional().describe("Category description"),
95
+ parent_id: z.string().optional().describe("Parent category id for a sub-category"),
96
+ }, ({ name, image, description, parent_id }) => handle(async () => {
97
+ const id = randomUUID();
98
+ const commands = [
99
+ { name: "create_category", data: { id, name, ...(parent_id ? { parent_id } : {}) } },
100
+ ];
101
+ if (image)
102
+ commands.push({ name: "image_category", data: { id, image } });
103
+ if (description) {
104
+ commands.push({
105
+ name: "multi_description",
106
+ data: { id, multi_description: [{ id: randomUUID(), title: name, description }] },
107
+ });
108
+ }
109
+ await api.createProductCategory(commands);
110
+ return { success: true, category_id: id, name, ...(parent_id ? { parent_id } : {}) };
111
+ }));
112
+ 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.`, {
113
+ name: z.string().describe("Blog category name"),
114
+ image: z.string().optional().describe("Hosted image URL"),
115
+ description: z.string().optional().describe("Category description"),
116
+ }, ({ name, image, description }) => handle(async () => {
117
+ const id = randomUUID();
118
+ const commands = [{ name: "create_category", data: { id, name } }];
119
+ if (description)
120
+ commands.push({ name: "description_category", data: { id, description } });
121
+ if (image)
122
+ commands.push({ name: "image_category", data: { id, image } });
123
+ await api.createBlogCategory(commands);
124
+ return { success: true, category_id: id, name };
125
+ }));
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",