webcake-storefront-mcp 1.31.9 → 1.31.10
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 +35 -8
- package/dist/changelog.json +7 -7
- package/dist/tools/articles.js +107 -34
- package/dist/tools/context.js +5 -8
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -429,20 +429,47 @@ export class WebcakeCmsApi {
|
|
|
429
429
|
return this.request("DELETE", `/api/v1/dashboard/site/${this.siteId}/db_collections/${id}`, { headers });
|
|
430
430
|
}
|
|
431
431
|
// ── Blog Articles ──
|
|
432
|
+
// NOTE: the `/api/v1/cms_function/:site_id/blog/article/*` routes are NOT usable from here.
|
|
433
|
+
// They sit behind builderx_api's CmsFunctionAuth plug, which requires the per-site CMS *admin*
|
|
434
|
+
// token (api_cms_keys, plug_type "admin_cms") as the Bearer — a storefront/session JWT always
|
|
435
|
+
// gets 401 "Token not found". Everything below therefore uses the same dashboard blog API the
|
|
436
|
+
// builder UI calls with the session JWT, except article detail, which has no dashboard route
|
|
437
|
+
// and uses the public read route.
|
|
438
|
+
/** List articles (dashboard). Query: page, limit, term (searches name + slug).
|
|
439
|
+
* Response: { success, articles: { data: [...], total_entries, page, limit, term } }. */
|
|
432
440
|
listArticles(query) {
|
|
433
|
-
return this.request("GET", `/api/v1/
|
|
441
|
+
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/blog/articles/all`, { query });
|
|
434
442
|
}
|
|
435
|
-
|
|
436
|
-
|
|
443
|
+
/** Articles filed under ONE blog category. Query: page, limit, article_id?, category_ids?.
|
|
444
|
+
* Response: { data: { articles: [...] } }. */
|
|
445
|
+
listArticlesByCategory(categoryId, query) {
|
|
446
|
+
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/blog/articles/${categoryId}`, { query });
|
|
437
447
|
}
|
|
438
|
-
|
|
439
|
-
|
|
448
|
+
/** Article detail (full content). Public read route — the dashboard API has no GET-by-id.
|
|
449
|
+
* Response: { success, data: {...article} }; 422 "Article not found!" when missing/removed. */
|
|
450
|
+
getArticle(id) {
|
|
451
|
+
return this.request("GET", `/view/${this.siteId}/article/${id}`);
|
|
452
|
+
}
|
|
453
|
+
/** Update an article through the dashboard command pipeline — same command shape as create
|
|
454
|
+
* (name_article / summary_article / content_article / image_article / set_article_custom_slug /
|
|
455
|
+
* set_article_visible / set_article_tags / bulk_add_category_to_article …). Each command's
|
|
456
|
+
* `data.id` is the article id. Commands run in order, so slug commands must follow name ones. */
|
|
457
|
+
updateBlogArticle(commands) {
|
|
458
|
+
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/articles/update`, {
|
|
459
|
+
body: { site_id: this.siteId, commands },
|
|
460
|
+
timeout: 60000,
|
|
461
|
+
});
|
|
440
462
|
}
|
|
441
|
-
|
|
442
|
-
|
|
463
|
+
/** Soft-delete articles (sets is_removed) via the dashboard command pipeline. */
|
|
464
|
+
deleteArticles(ids) {
|
|
465
|
+
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/articles/delete`, {
|
|
466
|
+
body: { site_id: this.siteId, commands: [{ name: "bulk_delete_article", data: { ids } }] },
|
|
467
|
+
timeout: 60000,
|
|
468
|
+
});
|
|
443
469
|
}
|
|
470
|
+
/** Delete a single article. */
|
|
444
471
|
deleteArticle(id) {
|
|
445
|
-
return this.
|
|
472
|
+
return this.deleteArticles([id]);
|
|
446
473
|
}
|
|
447
474
|
/** Create a blog/article category. Command-based: pass a `commands` array whose entries
|
|
448
475
|
* each carry a caller-generated `data.id` (the new category id). Response is generic. */
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.31.10",
|
|
4
|
+
"d": "08/09/2026",
|
|
5
|
+
"type": "Added",
|
|
6
|
+
"en": "list_articles accepts a term parameter to search articles by title or slug, and its response now includes page and limit alongside total.",
|
|
7
|
+
"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."
|
|
8
|
+
},
|
|
2
9
|
{
|
|
3
10
|
"v": "1.31.9",
|
|
4
11
|
"d": "07/09/2026",
|
|
@@ -33,12 +40,5 @@
|
|
|
33
40
|
"type": "Added",
|
|
34
41
|
"en": "Column definitions accepted by create_collection and update_collection_columns now support five new optional fields: note (help text for the…",
|
|
35
42
|
"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
|
]
|
package/dist/tools/articles.js
CHANGED
|
@@ -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",
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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(() =>
|
|
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",
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
}
|
package/dist/tools/context.js
CHANGED
|
@@ -52,17 +52,14 @@ async function clearSeedData(api) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
catch { /* best-effort */ }
|
|
55
|
-
// Blog articles — list then delete
|
|
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
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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.
|
|
3
|
+
"version": "1.31.10",
|
|
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",
|