webcake-storefront-mcp 1.31.9 → 1.31.11

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
@@ -1,6 +1,6 @@
1
1
  const DEFAULT_TIMEOUT = 15000;
2
- /** Stamped on every page this MCP server creates (backend column `pages.by_ai`),
3
- * so the builder can flag AI-authored pages. Mirrors the landing-page convention. */
2
+ /** Stamped on every site and page this MCP server creates (backend columns `sites.by_ai`
3
+ * and `pages.by_ai`), so the builder can flag AI-authored work. Mirrors the landing-page convention. */
4
4
  export const BY_AI_MARKER = "mcp";
5
5
  export class WebcakeCmsApi {
6
6
  baseUrl;
@@ -78,14 +78,20 @@ export class WebcakeCmsApi {
78
78
  * but NO pages. Returns { data: { site: { id, site_slug:{slug}, ... } } }.
79
79
  * Fails with 403 when the account's site quota is reached (free plan: 4 sites). */
80
80
  createSite(params) {
81
- return this.request("POST", `/api/v1/dashboard/site/create`, { body: params, timeout: 60000 });
81
+ return this.request("POST", `/api/v1/dashboard/site/create`, {
82
+ body: { ...params, by_ai: BY_AI_MARKER },
83
+ timeout: 60000,
84
+ });
82
85
  }
83
86
  /** Create a NEW site from a marketplace TEMPLATE by its theme id — the dedicated
84
87
  * "use this template" API. Clones the template's pages, global sections, cart, popups,
85
88
  * styles and fonts into a fresh account-owned site. Body: { id: <theme_id>, name, slug }.
86
89
  * Returns { data: { site: { id, ... } } }. (403 if the free 4-site quota is reached, prod.) */
87
90
  importStoreToTheme(params) {
88
- return this.request("POST", `/api/v1/dashboard/site/import_store_to_theme`, { body: params, timeout: 120000 });
91
+ return this.request("POST", `/api/v1/dashboard/site/import_store_to_theme`, {
92
+ body: { ...params, by_ai: BY_AI_MARKER },
93
+ timeout: 120000,
94
+ });
89
95
  }
90
96
  getSiteInfo() {
91
97
  return this.request("GET", `/api/v1/site/${this.siteId}/`);
@@ -429,20 +435,47 @@ export class WebcakeCmsApi {
429
435
  return this.request("DELETE", `/api/v1/dashboard/site/${this.siteId}/db_collections/${id}`, { headers });
430
436
  }
431
437
  // ── Blog Articles ──
438
+ // NOTE: the `/api/v1/cms_function/:site_id/blog/article/*` routes are NOT usable from here.
439
+ // They sit behind builderx_api's CmsFunctionAuth plug, which requires the per-site CMS *admin*
440
+ // token (api_cms_keys, plug_type "admin_cms") as the Bearer — a storefront/session JWT always
441
+ // gets 401 "Token not found". Everything below therefore uses the same dashboard blog API the
442
+ // builder UI calls with the session JWT, except article detail, which has no dashboard route
443
+ // and uses the public read route.
444
+ /** List articles (dashboard). Query: page, limit, term (searches name + slug).
445
+ * Response: { success, articles: { data: [...], total_entries, page, limit, term } }. */
432
446
  listArticles(query) {
433
- return this.request("GET", `/api/v1/cms_function/${this.siteId}/blog/article/all`, { query });
447
+ return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/blog/articles/all`, { query });
434
448
  }
435
- getArticle(id) {
436
- return this.request("GET", `/api/v1/cms_function/${this.siteId}/blog/article/${id}`);
449
+ /** Articles filed under ONE blog category. Query: page, limit, article_id?, category_ids?.
450
+ * Response: { data: { articles: [...] } }. */
451
+ listArticlesByCategory(categoryId, query) {
452
+ return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/blog/articles/${categoryId}`, { query });
437
453
  }
438
- createArticle(params) {
439
- return this.request("POST", `/api/v1/cms_function/${this.siteId}/blog/article`, { body: params });
454
+ /** Article detail (full content). Public read route — the dashboard API has no GET-by-id.
455
+ * Response: { success, data: {...article} }; 422 "Article not found!" when missing/removed. */
456
+ getArticle(id) {
457
+ return this.request("GET", `/view/${this.siteId}/article/${id}`);
458
+ }
459
+ /** Update an article through the dashboard command pipeline — same command shape as create
460
+ * (name_article / summary_article / content_article / image_article / set_article_custom_slug /
461
+ * set_article_visible / set_article_tags / bulk_add_category_to_article …). Each command's
462
+ * `data.id` is the article id. Commands run in order, so slug commands must follow name ones. */
463
+ updateBlogArticle(commands) {
464
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/articles/update`, {
465
+ body: { site_id: this.siteId, commands },
466
+ timeout: 60000,
467
+ });
440
468
  }
441
- updateArticle(id, params) {
442
- return this.request("PATCH", `/api/v1/cms_function/${this.siteId}/blog/article/${id}`, { body: params });
469
+ /** Soft-delete articles (sets is_removed) via the dashboard command pipeline. */
470
+ deleteArticles(ids) {
471
+ return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/articles/delete`, {
472
+ body: { site_id: this.siteId, commands: [{ name: "bulk_delete_article", data: { ids } }] },
473
+ timeout: 60000,
474
+ });
443
475
  }
476
+ /** Delete a single article. */
444
477
  deleteArticle(id) {
445
- return this.request("DELETE", `/api/v1/cms_function/${this.siteId}/blog/article/${id}`);
478
+ return this.deleteArticles([id]);
446
479
  }
447
480
  /** Create a blog/article category. Command-based: pass a `commands` array whose entries
448
481
  * each carry a caller-generated `data.id` (the new category id). Response is generic. */
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "v": "1.31.11",
4
+ "d": "11/09/2026",
5
+ "type": "Changed",
6
+ "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…",
7
+ "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ỉ…"
8
+ },
9
+ {
10
+ "v": "1.31.10",
11
+ "d": "08/09/2026",
12
+ "type": "Added",
13
+ "en": "list_articles accepts a term parameter to search articles by title or slug, and its response now includes page and limit alongside total.",
14
+ "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."
15
+ },
2
16
  {
3
17
  "v": "1.31.9",
4
18
  "d": "07/09/2026",
@@ -26,19 +40,5 @@
26
40
  "type": "Removed",
27
41
  "en": "scaffold_store_pages, scaffold_global_sections, and scaffold_popup tools are removed; pages are now composed free-form from elements using…",
28
42
  "vi": "Các tool scaffold_store_pages, scaffold_global_sections và scaffold_popup đã bị xóa; các trang nay được tổ hợp tự do từ các phần tử bằng…"
29
- },
30
- {
31
- "v": "1.31.5",
32
- "d": "26/06/2026",
33
- "type": "Added",
34
- "en": "Column definitions accepted by create_collection and update_collection_columns now support five new optional fields: note (help text for the…",
35
- "vi": "Định nghĩa cột được chấp nhận bởi create_collection và update_collection_columns nay hỗ trợ thêm năm trường tùy chọn mới: note (ghi chú/mô tả cho…"
36
- },
37
- {
38
- "v": "1.31.4",
39
- "d": "26/06/2026",
40
- "type": "Added",
41
- "en": "The guide returned by get_http_function and get_site_custom_code now includes a verified end-to-end \"Custom data TABLES (collections)\" section…",
42
- "vi": "Hướng dẫn trả về bởi get_http_function và get_site_custom_code nay bổ sung phần \"Custom data TABLES (collections)\" đã được xác minh thực tế, ghi lại…"
43
43
  }
44
44
  ]
@@ -1,33 +1,67 @@
1
1
  import { z } from "zod";
2
2
  import { randomUUID } from "node:crypto";
3
+ /** The dashboard blog API answers with two different envelopes:
4
+ * /blog/articles/all → { articles: { data: [...], total_entries, page, limit } }
5
+ * /blog/articles/{category} → { data: { articles: [...] } }
6
+ * Flatten both to a plain array. */
7
+ function pickArticles(res) {
8
+ const candidates = [
9
+ res?.articles?.data,
10
+ res?.articles,
11
+ res?.data?.articles?.data,
12
+ res?.data?.articles,
13
+ res?.data?.data,
14
+ res?.data,
15
+ res,
16
+ ];
17
+ for (const c of candidates)
18
+ if (Array.isArray(c))
19
+ return c;
20
+ return [];
21
+ }
22
+ function summarizeArticle(a) {
23
+ return {
24
+ id: a.id || a._id,
25
+ name: a.name,
26
+ slug: a.slug,
27
+ summary: a.summary || undefined,
28
+ cover: (Array.isArray(a.images) && a.images[0]) || undefined,
29
+ category_ids: Array.isArray(a.article_categories)
30
+ ? a.article_categories.map((ac) => ac.category_id).filter(Boolean)
31
+ : undefined,
32
+ tags: a.tags && a.tags.length ? a.tags : undefined,
33
+ is_hidden: a.is_hidden,
34
+ published_at: a.render_inserted_at || undefined,
35
+ inserted_at: a.inserted_at,
36
+ updated_at: a.updated_at,
37
+ };
38
+ }
3
39
  export function registerArticleTools(server, api, handle) {
4
- server.tool("list_articles", "List blog articles (metadata only, without HTML content). Use get_article to get full content", {
5
- page: z.number().optional().describe("Page number"),
6
- limit: z.number().optional().describe("Items per page"),
7
- category_id: z.string().optional().describe("Filter by category"),
8
- }, ({ page, limit, category_id }) => handle(async () => {
9
- const res = await api.listArticles({ page, limit, category_id });
10
- const articles = (res && res.data) || res || [];
11
- if (!Array.isArray(articles))
12
- return res;
40
+ server.tool("list_articles", `List blog articles (metadata only, without HTML content). Use get_article for the full content.
41
+ Pass term to search by title/slug. Pass category_id to list the posts filed under one blog category —
42
+ that view is the public one, so hidden posts and posts scheduled in the future are left out.`, {
43
+ page: z.number().optional().describe("Page number (default 1)"),
44
+ limit: z.number().optional().describe("Items per page (default 20)"),
45
+ category_id: z.string().optional().describe("Filter by blog category id"),
46
+ term: z.string().optional().describe("Search articles by title or slug"),
47
+ }, ({ page, limit, category_id, term }) => handle(async () => {
48
+ const res = category_id
49
+ ? await api.listArticlesByCategory(category_id, { page, limit })
50
+ : await api.listArticles({ page, limit, term });
51
+ const articles = pickArticles(res);
13
52
  return {
14
- data: articles.map((a) => ({
15
- id: a.id || a._id,
16
- name: a.name,
17
- slug: a.slug,
18
- summary: a.summary || undefined,
19
- category_id: a.category_id || undefined,
20
- tags: a.tags || undefined,
21
- is_hidden: a.is_hidden,
22
- created_at: a.created_at,
23
- updated_at: a.updated_at,
24
- })),
25
- total: res.total || articles.length,
53
+ data: articles.map(summarizeArticle),
54
+ total: res?.articles?.total_entries ?? articles.length,
55
+ page: res?.articles?.page ?? page ?? 1,
56
+ limit: res?.articles?.limit ?? limit ?? articles.length,
26
57
  };
27
58
  }));
28
- server.tool("get_article", "Get article details by ID", {
59
+ server.tool("get_article", "Get article details by ID (includes the full HTML content)", {
29
60
  id: z.string().describe("Article ID"),
30
- }, ({ id }) => handle(() => api.getArticle(id)));
61
+ }, ({ id }) => handle(async () => {
62
+ const res = await api.getArticle(id);
63
+ return (res && res.data) || res;
64
+ }));
31
65
  server.tool("create_article", `Create a blog article so blog/post pages (post-list, grid-blog, post-overlay) have content.
32
66
  Built via the dashboard command pipeline: title + optional summary, HTML content, image URLs, and
33
67
  category linkage. Pass category_ids from create_blog_category / list articles' categories so the
@@ -58,17 +92,56 @@ must be hosted (search_images / upload_images). The backend generates the id and
58
92
  cover: images?.[0] || null,
59
93
  };
60
94
  }));
61
- server.tool("update_article", "Update a blog article", {
95
+ server.tool("update_article", `Update a blog article. Only the fields you pass are changed — each one becomes a command in the
96
+ same dashboard pipeline create_article uses. NOTE: renaming regenerates the slug, so pass slug in
97
+ the SAME call if you want a custom one (it is applied after the rename).`, {
62
98
  id: z.string().describe("Article ID"),
63
- name: z.string().optional().describe("New title"),
64
- slug: z.string().optional().describe("New slug"),
65
- content: z.string().optional().describe("New HTML content"),
66
- summary: z.string().optional().describe("New summary"),
67
- category_id: z.string().optional().describe("Category ID"),
68
- tags: z.array(z.string()).optional().describe("Tags"),
99
+ name: z.string().optional().describe("New title (regenerates the slug unless slug is also passed)"),
100
+ slug: z.string().optional().describe("New custom slug"),
101
+ content: z.string().optional().describe("New HTML content (replaces the old content)"),
102
+ summary: z.string().optional().describe("New summary / excerpt"),
103
+ images: z.array(z.string()).optional().describe("Hosted image URLs; the first is the cover image"),
104
+ category_ids: z.array(z.string()).optional().describe("Blog category IDs to file the post under (added, existing ones are kept)"),
105
+ remove_category_ids: z.array(z.string()).optional().describe("Blog category IDs to unfile the post from"),
106
+ tags: z.array(z.string()).optional().describe("Article TAG IDs (uuids from the blog tag list) — not free text"),
69
107
  is_hidden: z.boolean().optional().describe("Hide from public"),
70
- }, ({ id, ...params }) => handle(() => api.updateArticle(id, params)));
71
- server.tool("delete_article", "Delete a blog article", {
72
- id: z.string().describe("Article ID"),
73
- }, ({ id }) => handle(() => api.deleteArticle(id)));
108
+ published_at: z.string().optional().describe("Publish date, ISO/naive datetime (render_inserted_at)"),
109
+ }, ({ id, name, slug, content, summary, images, category_ids, remove_category_ids, tags, is_hidden, published_at }) => handle(async () => {
110
+ const commands = [];
111
+ // Order matters: name_article rewrites the slug, so the custom slug must come after it.
112
+ if (name != null)
113
+ commands.push({ name: "name_article", data: { id, name } });
114
+ if (slug != null)
115
+ commands.push({ name: "set_article_custom_slug", data: { id, custom_slug: slug } });
116
+ if (summary != null)
117
+ commands.push({ name: "summary_article", data: { id, summary } });
118
+ if (content != null)
119
+ commands.push({ name: "content_article", data: { id, content } });
120
+ if (images)
121
+ commands.push({ name: "image_article", data: { id, images } });
122
+ if (tags)
123
+ commands.push({ name: "set_article_tags", data: { id, article_tags: tags } });
124
+ if (is_hidden != null)
125
+ commands.push({ name: "set_article_visible", data: { id, is_hidden } });
126
+ if (published_at != null)
127
+ commands.push({ name: "set_article_render_inserted_at", data: { id, render_inserted_at: published_at } });
128
+ if (category_ids && category_ids.length)
129
+ commands.push({ name: "bulk_add_category_to_article", data: { id, ids: category_ids } });
130
+ if (remove_category_ids && remove_category_ids.length)
131
+ commands.push({ name: "bulk_remove_category_to_article", data: { id, ids: remove_category_ids } });
132
+ if (!commands.length)
133
+ throw new Error("Nothing to update — pass at least one field besides id.");
134
+ await api.updateBlogArticle(commands);
135
+ return { success: true, article_id: id, updated: commands.map((c) => c.name) };
136
+ }));
137
+ server.tool("delete_article", "Delete blog articles (soft delete — they disappear from the site)", {
138
+ id: z.string().optional().describe("Article ID"),
139
+ ids: z.array(z.string()).optional().describe("Several article IDs to delete in one call"),
140
+ }, ({ id, ids }) => handle(async () => {
141
+ const list = [...(ids || []), ...(id ? [id] : [])].filter(Boolean);
142
+ if (!list.length)
143
+ throw new Error("Pass id or ids.");
144
+ await api.deleteArticles(list);
145
+ return { success: true, deleted: list };
146
+ }));
74
147
  }
@@ -52,17 +52,14 @@ async function clearSeedData(api) {
52
52
  }
53
53
  }
54
54
  catch { /* best-effort */ }
55
- // Blog articles — list then delete one by one (best-effort; blog cleanup must never fail site creation).
55
+ // Blog articles — list then bulk-delete (best-effort; blog cleanup must never fail site creation).
56
56
  try {
57
57
  const res = await api.listArticles({ page: 1, limit: 200 });
58
- const articles = (res && res.data) || res || [];
58
+ const articles = (res && (res.articles?.data || res.articles || res.data)) || [];
59
59
  const ids = (Array.isArray(articles) ? articles : []).map((a) => a.id).filter(Boolean);
60
- for (const id of ids) {
61
- try {
62
- await api.deleteArticle(id);
63
- result.articles++;
64
- }
65
- catch { /* skip the ones that fail */ }
60
+ if (ids.length) {
61
+ await api.deleteArticles(ids);
62
+ result.articles = ids.length;
66
63
  }
67
64
  }
68
65
  catch { /* best-effort */ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webcake-storefront-mcp",
3
- "version": "1.31.9",
3
+ "version": "1.31.11",
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",