webcake-storefront-mcp 1.11.0 → 1.12.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
@@ -76,11 +76,12 @@ export class WebcakeCmsApi {
76
76
  createSite(params) {
77
77
  return this.request("POST", `/api/v1/dashboard/site/create`, { body: params, timeout: 60000 });
78
78
  }
79
- /** Duplicate an existing site (its settings AND all pages/page-sources) into a NEW site
80
- * owned by the current account. Used to spin up a site FROM a marketplace template
81
- * (pass the template's source site id). Body: { site_id (source), name, slug? }. */
82
- duplicateSite(params) {
83
- return this.request("POST", `/api/v1/dashboard/site/duplicate`, { body: params, timeout: 90000 });
79
+ /** Create a NEW site from a marketplace TEMPLATE by its theme id — the dedicated
80
+ * "use this template" API. Clones the template's pages, global sections, cart, popups,
81
+ * styles and fonts into a fresh account-owned site. Body: { id: <theme_id>, name, slug }.
82
+ * Returns { data: { site: { id, ... } } }. (403 if the free 4-site quota is reached, prod.) */
83
+ importStoreToTheme(params) {
84
+ return this.request("POST", `/api/v1/dashboard/site/import_store_to_theme`, { body: params, timeout: 120000 });
84
85
  }
85
86
  getSiteInfo() {
86
87
  return this.request("GET", `/api/v1/site/${this.siteId}/`);
@@ -333,6 +334,27 @@ export class WebcakeCmsApi {
333
334
  timeout: 60000,
334
335
  });
335
336
  }
337
+ /** Update a product. Body wraps fields in `product_params` and identifies the product by
338
+ * product_params.product_id. */
339
+ updateProduct(productParams) {
340
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/products/update`, {
341
+ body: { site_id: this.siteId, product_params: productParams },
342
+ timeout: 60000,
343
+ });
344
+ }
345
+ /** Delete products by id. Body: { site_id, ids }. */
346
+ removeProducts(ids) {
347
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/products/remove`, {
348
+ body: { site_id: this.siteId, ids },
349
+ timeout: 60000,
350
+ });
351
+ }
352
+ /** Publish/unpublish products. Body: { site_id, data: [{ product_id, is_published }] }. */
353
+ setProductsPublished(data) {
354
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/products/published`, {
355
+ body: { site_id: this.siteId, data },
356
+ });
357
+ }
336
358
  /** Create a product category. Command-based: pass a `commands` array whose entries each
337
359
  * carry a caller-generated `data.id` (the new category id). Response is generic. */
338
360
  createProductCategory(commands) {
@@ -341,6 +363,21 @@ export class WebcakeCmsApi {
341
363
  timeout: 60000,
342
364
  });
343
365
  }
366
+ /** Update a product category — command-based (name_category / image_category /
367
+ * multi_description / set_category_visible), each command's data.id = the category id. */
368
+ updateProductCategory(commands) {
369
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/categories/update`, {
370
+ body: { site_id: this.siteId, commands },
371
+ timeout: 60000,
372
+ });
373
+ }
374
+ /** Delete product categories — command bulk_delete_category with data.ids. */
375
+ deleteProductCategory(commands) {
376
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/categories/delete`, {
377
+ body: { site_id: this.siteId, commands },
378
+ timeout: 60000,
379
+ });
380
+ }
344
381
  // ── Orders ──
345
382
  listOrders(query) {
346
383
  return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/orders/all`, { query });
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.12.0",
4
+ "d": "24/06/2026",
5
+ "type": "Added",
6
+ "en": "New update_product tool updates an existing product's name, description, images, category_ids, is_published flag, or variations (price/stock/SKU per…",
7
+ "vi": "Tool mới update_product cập nhật name, description, images, category_ids, cờ is_published hoặc variations (giá/tồn kho/SKU theo từng biến thể) của…"
8
+ },
9
+ {
10
+ "v": "1.11.1",
11
+ "d": "24/06/2026",
12
+ "type": "Fixed",
13
+ "en": "create_site_from_template now calls the dedicated import_store_to_theme API instead of the generic site-duplicate endpoint, correctly cloning the…",
14
+ "vi": "create_site_from_template nay gọi đúng API import_store_to_theme thay vì endpoint nhân bản site thông thường, giúp clone đầy đủ các trang, global…"
15
+ },
2
16
  {
3
17
  "v": "1.11.0",
4
18
  "d": "24/06/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Added",
27
41
  "en": "New list_automations tool returns each automation's id, name, status, and trigger info so agents can find the automation_id required by send_mail or…",
28
42
  "vi": "Tool mới list_automations trả về id, name, status và thông tin trigger của từng automation, giúp agent tìm được automation_id cần truyền vào…"
29
- },
30
- {
31
- "v": "1.7.0",
32
- "d": "23/06/2026",
33
- "type": "Changed",
34
- "en": "The web guide landing page (served via the serve command) is reframed to lead on full-storefront creation: the hero, meta description, \"What you…",
35
- "vi": "Trang web guide (phục vụ qua lệnh serve) được viết lại để làm nổi bật việc xây dựng toàn bộ cửa hàng: hero, meta description, thư viện \"Bạn dựng…"
36
- },
37
- {
38
- "v": "1.6.0",
39
- "d": "23/06/2026",
40
- "type": "Added",
41
- "en": "get_element now returns an attributes field with a curated per-element reference covering the meaningful specials, config, events, and bindings keys…",
42
- "vi": "get_element nay trả về trường attributes chứa tài liệu tham chiếu theo từng loại element, bao gồm các key có ý nghĩa của specials, config, events và…"
43
43
  }
44
44
  ]
@@ -126,4 +126,84 @@ Returns the new category id — pass it to create_product's category_ids. Image
126
126
  await api.createBlogCategory(commands);
127
127
  return { success: true, category_id: id, name };
128
128
  }));
129
+ // ── Edits: products & product categories ─────────────────────────────────────
130
+ server.tool("update_product", `Update an existing product. Pass product_id + only the fields to change. To change
131
+ price/stock, pass variations (get_product first to see the existing variation shape).
132
+ Images must be hosted CDN urls (search_images cdn_url / upload_images).`, {
133
+ product_id: z.string().describe("Product id to update"),
134
+ name: z.string().optional().describe("New name"),
135
+ description: z.string().optional().describe("New description (HTML allowed)"),
136
+ images: z.array(z.string()).optional().describe("New hosted image URLs; first becomes the thumbnail"),
137
+ category_ids: z.array(z.string()).optional().describe("Replace the product's categories"),
138
+ is_published: z.boolean().optional().describe("Publish/unpublish the product"),
139
+ variations: z.array(variationSpec).optional().describe("Replace variations (price/stock/SKU per variant). Reuse existing custom_ids from get_product to edit in place."),
140
+ }, ({ product_id, name, description, images, category_ids, is_published, variations }) => handle(async () => {
141
+ const productParams = { product_id };
142
+ if (name != null)
143
+ productParams.name = name;
144
+ if (description != null)
145
+ productParams.description = description;
146
+ if (images)
147
+ productParams.image = images[0];
148
+ if (category_ids) {
149
+ productParams.categories = category_ids;
150
+ productParams.ribbons = [];
151
+ }
152
+ if (is_published != null)
153
+ productParams.is_published = is_published;
154
+ if (variations) {
155
+ productParams.variations = variations.map((v) => ({
156
+ custom_id: v.custom_id || `SKU-${randomUUID().slice(0, 8)}`,
157
+ retail_price: v.retail_price,
158
+ original_price: v.original_price ?? v.retail_price,
159
+ remain_quantity: v.remain_quantity ?? 100,
160
+ images: v.images || (images ? images : []),
161
+ weight: v.weight ?? 0,
162
+ fields: v.fields || [],
163
+ is_hidden: false,
164
+ }));
165
+ }
166
+ await api.updateProduct(productParams);
167
+ return { success: true, product_id, updated: Object.keys(productParams).filter((k) => k !== "product_id") };
168
+ }));
169
+ server.tool("set_product_published", "Publish or unpublish one or more products quickly (without a full update).", {
170
+ product_ids: z.array(z.string()).describe("Product ids"),
171
+ is_published: z.boolean().describe("true = publish (visible), false = hide"),
172
+ }, ({ product_ids, is_published }) => handle(async () => {
173
+ await api.setProductsPublished(product_ids.map((product_id) => ({ product_id, is_published })));
174
+ return { success: true, product_ids, is_published };
175
+ }));
176
+ server.tool("delete_product", "Delete one or more products by id.", {
177
+ product_ids: z.array(z.string()).describe("Product ids to delete"),
178
+ }, ({ product_ids }) => handle(async () => {
179
+ await api.removeProducts(product_ids);
180
+ return { success: true, deleted: product_ids };
181
+ }));
182
+ server.tool("update_product_category", "Update a product category (name, image, description, or visibility). Pass id + fields to change.", {
183
+ id: z.string().describe("Category id"),
184
+ name: z.string().optional().describe("New name"),
185
+ image: z.string().optional().describe("New hosted image URL"),
186
+ description: z.string().optional().describe("New description"),
187
+ hidden: z.boolean().optional().describe("Hide (true) or show (false) the category"),
188
+ }, ({ id, name, image, description, hidden }) => handle(async () => {
189
+ const commands = [];
190
+ if (name != null)
191
+ commands.push({ name: "name_category", data: { id, name } });
192
+ if (image != null)
193
+ commands.push({ name: "image_category", data: { id, image } });
194
+ if (description != null)
195
+ commands.push({ name: "multi_description", data: { id, multi_description: [{ id: randomUUID(), title: name || "", description }] } });
196
+ if (hidden != null)
197
+ commands.push({ name: "set_category_visible", data: { id, is_hidden: hidden } });
198
+ if (!commands.length)
199
+ throw new Error("Nothing to update — pass at least one of name/image/description/hidden.");
200
+ await api.updateProductCategory(commands);
201
+ return { success: true, category_id: id, updated: commands.map((c) => c.name) };
202
+ }));
203
+ server.tool("delete_product_category", "Delete one or more product categories by id.", {
204
+ ids: z.array(z.string()).describe("Category ids to delete"),
205
+ }, ({ ids }) => handle(async () => {
206
+ await api.deleteProductCategory([{ name: "bulk_delete_category", data: { ids } }]);
207
+ return { success: true, deleted: ids };
208
+ }));
129
209
  }
@@ -52,7 +52,6 @@ async function getThemeCatalog(force = false) {
52
52
  preview_url: t.preview_url || "",
53
53
  thumbnail: t.thumbnail || "",
54
54
  categories: (t.categories || []).map((c) => c.name).filter(Boolean),
55
- site_id: (t.site && t.site.id) || undefined,
56
55
  });
57
56
  }
58
57
  if (themes.length < limit)
@@ -146,7 +145,6 @@ export function registerSiteStyleTools(server, api, handle) {
146
145
  const desc = parseThemeDescription(te && te.description);
147
146
  return {
148
147
  theme_id: themeId,
149
- template_site_id: info.site_id || null, // pass to create_site_from_template
150
148
  score: typeof score === "number" ? Number(score.toFixed(4)) : null,
151
149
  name: info.name || null,
152
150
  preview_url: info.preview_url || null,
@@ -163,50 +161,31 @@ export function registerSiteStyleTools(server, api, handle) {
163
161
  hint: "Pick one and call create_site_from_template with its theme_id (or template_site_id) to clone it into a new editable site.",
164
162
  };
165
163
  }));
166
- server.tool("create_site_from_template", `Create a NEW site by CLONING a marketplace template (all its pages + settings), so the
167
- customer starts from a finished design and then edits it. Resolve a template with
168
- semantic_search_themes / list_template_themes first, then pass its theme_id (or
169
- template_site_id). After cloning, switch to the new site and edit layout/content with
170
- update_page_element(s), colours/typography with the site-style tools, and republish.`, {
164
+ server.tool("create_site_from_template", `Create a NEW site from a marketplace TEMPLATE (the dedicated "use this template" API).
165
+ Clones the template's pages, global sections, cart, popups, styles and fonts into a fresh
166
+ site. Pick a template with semantic_search_themes / list_template_themes, then pass its
167
+ theme_id here. Switches to the new site so you can immediately edit layout/content with
168
+ update_page_element(s), colours/fonts via the site-style tools, then publish_site.`, {
171
169
  name: z.string().describe("Name for the new site"),
172
- theme_id: z.string().optional().describe("Marketplace theme id (from semantic_search_themes / list_template_themes)"),
173
- template_site_id: z.string().optional().describe("The template's source site id (alternative to theme_id; semantic_search_themes returns it as template_site_id)"),
170
+ theme_id: z.string().describe("Marketplace theme id (from semantic_search_themes / list_template_themes)"),
174
171
  slug: z.string().optional().describe("URL-safe slug for the new site (auto-generated if omitted)"),
175
- switch_to: z.boolean().default(true).describe("Switch the session to the new site after cloning (saved for next session)"),
176
- }, ({ name, theme_id, template_site_id, slug, switch_to }) => handle(async () => {
177
- // Resolve the template's source site id.
178
- let sourceSiteId = template_site_id;
179
- if (!sourceSiteId && theme_id) {
180
- const catalog = await getThemeCatalog().catch(() => new Map());
181
- sourceSiteId = catalog.get(theme_id)?.site_id;
182
- }
183
- if (!sourceSiteId) {
184
- throw new Error("Could not resolve the template's source site. Pass template_site_id, or a theme_id that exists in the marketplace (list_template_themes / semantic_search_themes).");
185
- }
186
- // The duplicate endpoint returns success-only (no id), so snapshot the site list
187
- // first, clone, then resolve the new site by diff.
188
- const listIds = async () => {
189
- const r = await api.listMySites({ page: 1, limit: 100 }).catch(() => null);
190
- const arr = r?.data?.sites || r?.data || [];
191
- return Array.isArray(arr) ? arr : [];
192
- };
193
- const before = await listIds();
194
- const beforeIds = new Set(before.map((s) => s.id));
172
+ switch_to: z.boolean().default(true).describe("Switch the session to the new site after creating it (saved for next session)"),
173
+ }, ({ name, theme_id, slug, switch_to }) => handle(async () => {
174
+ let res;
195
175
  try {
196
- await api.duplicateSite({ site_id: sourceSiteId, name, ...(slug ? { slug } : {}) });
176
+ res = await api.importStoreToTheme({ id: theme_id, name, ...(slug ? { slug } : {}) });
197
177
  }
198
178
  catch (e) {
199
179
  const msg = e instanceof Error ? e.message : String(e);
200
180
  if (msg.includes("403"))
201
181
  throw new Error("Cannot create site: account site quota reached (free plan allows up to 4 sites).");
202
- throw new Error(`Cloning the template failed: ${msg}`);
182
+ throw new Error(`Creating the site from the template failed: ${msg}. Check the theme_id exists (list_template_themes / semantic_search_themes).`);
203
183
  }
204
- const after = await listIds();
205
- const fresh = after.filter((s) => !beforeIds.has(s.id));
206
- const newSite = fresh.find((s) => s.name === name) || fresh[0];
184
+ const data = res?.data || res;
185
+ const newSite = data?.site || data?.new_site || data;
207
186
  const newId = newSite?.id;
208
187
  if (!newId)
209
- throw new Error("Template was cloned but the new site could not be located in your site list — check list_my_sites.");
188
+ throw new Error("Site created from template but no id was returned.");
210
189
  let switched = false;
211
190
  let previewUrl = null;
212
191
  const previousSiteId = api.siteId;
@@ -221,11 +200,11 @@ update_page_element(s), colours/typography with the site-style tools, and republ
221
200
  success: true,
222
201
  site_id: newId,
223
202
  name: newSite?.name || name,
224
- from_template: { theme_id: theme_id || null, source_site_id: sourceSiteId },
203
+ from_template: theme_id,
225
204
  switched,
226
205
  ...(switched ? { current_site_id: api.siteId, previous_site_id: previousSiteId } : {}),
227
206
  preview_url: previewUrl,
228
- next_step: "Site cloned with all template pages. Edit content with search_page_elements + update_page_element(s), change colours/fonts via list_themes/site-style, then publish_site.",
207
+ next_step: "Site created from the template (pages, sections, popups, styles, fonts). Edit content with search_page_elements + update_page_element(s), colours/fonts via list_themes/site-style, then publish_site.",
229
208
  };
230
209
  }));
231
210
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.11.0",
3
+ "version": "1.12.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",