webcake-storefront-mcp 1.31.12 → 1.32.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 +12 -0
- package/dist/changelog.json +14 -14
- package/dist/http.js +9 -2
- package/dist/icon.png +0 -0
- package/dist/server.js +9 -1
- package/dist/tools/articles.js +12 -0
- package/dist/tools/catalog-write.js +54 -5
- package/dist/tools/multilingual.js +35 -0
- package/dist/web-guide.js +3 -0
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -485,6 +485,18 @@ export class WebcakeCmsApi {
|
|
|
485
485
|
timeout: 60000,
|
|
486
486
|
});
|
|
487
487
|
}
|
|
488
|
+
/** All blog categories (dashboard; includes meta_tags). */
|
|
489
|
+
listBlogCategories() {
|
|
490
|
+
return this.request("GET", `/api/v1/dashboard/site/${this.siteId}/blog/categories/all`);
|
|
491
|
+
}
|
|
492
|
+
/** Update blog categories — same command pipeline as create (name_category / description_category /
|
|
493
|
+
* image_category / set_category_custom_slug / set_category_seo …), each with data.id. */
|
|
494
|
+
updateBlogCategory(commands) {
|
|
495
|
+
return this.request("POST", `/api/v1/dashboard/site/${this.siteId}/blog/categories/update`, {
|
|
496
|
+
body: { site_id: this.siteId, commands },
|
|
497
|
+
timeout: 60000,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
488
500
|
/** Create a blog article via the dashboard command pipeline (supports category linkage,
|
|
489
501
|
* images, summary, content). Caller generates the article id in each command's data.id. */
|
|
490
502
|
createBlogArticle(commands) {
|
package/dist/changelog.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"v": "1.32.0",
|
|
4
|
+
"d": "14/10/2026",
|
|
5
|
+
"type": "Changed",
|
|
6
|
+
"en": "The server now serves a raster PNG icon (/favicon.ico, /favicon.png, /icon.png, /apple-touch-icon.png) alongside the existing SVG favicon, and…",
|
|
7
|
+
"vi": "Server nay phục vụ thêm icon dạng raster PNG (/favicon.ico, /favicon.png, /icon.png, /apple-touch-icon.png) bên cạnh favicon SVG hiện có, và khai…"
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
"v": "1.31.13",
|
|
11
|
+
"d": "09/10/2026",
|
|
12
|
+
"type": "Added",
|
|
13
|
+
"en": "create_product, update_product, create_product_category, update_product_category, and create_blog_category accept a new seo object to write…",
|
|
14
|
+
"vi": "create_product, update_product, create_product_category, update_product_category và create_blog_category nay nhận thêm đối tượng seo để ghi…"
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"v": "1.31.12",
|
|
4
18
|
"d": "09/10/2026",
|
|
@@ -26,19 +40,5 @@
|
|
|
26
40
|
"type": "Added",
|
|
27
41
|
"en": "Pages created by create_page, build_page, and commit_page_draft are now stamped with by_ai: \"mcp\" (persisted to the backend pages.by_ai column) so…",
|
|
28
42
|
"vi": "Các trang được tạo bởi create_page, build_page và commit_page_draft nay được đánh dấu by_ai: \"mcp\" (lưu vào cột pages.by_ai ở backend) để builder có…"
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
"v": "1.31.8",
|
|
32
|
-
"d": "29/06/2026",
|
|
33
|
-
"type": "Fixed",
|
|
34
|
-
"en": "Elements placed by new_section, new_row, and build_page that fill their grid cell (text, image, container, columns, repeaters) now receive…",
|
|
35
|
-
"vi": "Các phần tử được đặt bởi new_section, new_row và build_page mà lấp đầy ô lưới (text, image, container, columns, repeaters) nay nhận constraintX:…"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
"v": "1.31.7",
|
|
39
|
-
"d": "26/06/2026",
|
|
40
|
-
"type": "Fixed",
|
|
41
|
-
"en": "build_page, create_page, update_page, and commit_page_draft now strip a leading / from slug before saving; a slug like /cart would previously 404 on…",
|
|
42
|
-
"vi": "build_page, create_page, update_page và commit_page_draft nay tự động loại bỏ dấu / đầu dòng khỏi slug trước khi lưu; các slug kiểu /cart trước đây…"
|
|
43
43
|
}
|
|
44
44
|
]
|
package/dist/http.js
CHANGED
|
@@ -11,7 +11,7 @@ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/
|
|
|
11
11
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
import { createServer } from "./server.js";
|
|
13
13
|
import { makeApi, resolveEnv, ENVIRONMENTS, DEFAULT_ENV } from "./config.js";
|
|
14
|
-
import { landingHtml, faviconSvg, ogImageSvg, normalizeLang } from "./web-guide.js";
|
|
14
|
+
import { landingHtml, faviconSvg, ogImageSvg, normalizeLang, ICON_PNG } from "./web-guide.js";
|
|
15
15
|
import { privacyHtml, termsHtml } from "./legal.js";
|
|
16
16
|
import { registerClient, startAuthorize, completeAuthorize, exchangeToken, resolveAccessToken, revokeToken, authServerMetadata, protectedResourceMetadata, } from "./auth/oauth-server.js";
|
|
17
17
|
const MCP_PATH = "/mcp";
|
|
@@ -291,7 +291,14 @@ export async function startHttpServer(port) {
|
|
|
291
291
|
const httpServer = createHttpServer(async (req, res) => {
|
|
292
292
|
const path = (req.url ?? "").split("?")[0];
|
|
293
293
|
// ---- Favicon / brand icon ----
|
|
294
|
-
|
|
294
|
+
// Google s2 (what the claude.ai connector list uses) skips SVG favicons and shows a
|
|
295
|
+
// globe, so /favicon.ico and friends serve the raster src/icon.png (→ dist/icon.png).
|
|
296
|
+
if ((req.method === "GET" || req.method === "HEAD") && ["/favicon.ico", "/favicon.png", "/icon.png", "/apple-touch-icon.png"].includes(path)) {
|
|
297
|
+
res.writeHead(200, { "content-type": "image/png", "cache-control": "public, max-age=86400" });
|
|
298
|
+
res.end(ICON_PNG);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (req.method === "GET" && path === "/favicon.svg") {
|
|
295
302
|
res.writeHead(200, { "content-type": "image/svg+xml", "cache-control": "public, max-age=86400" });
|
|
296
303
|
res.end(faviconSvg());
|
|
297
304
|
return;
|
package/dist/icon.png
ADDED
|
Binary file
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { installTools, buildToolGroupIndex } from "./tools/registry.js";
|
|
4
|
+
import { ICON_PNG } from "./web-guide.js";
|
|
4
5
|
/** Server version, read from package.json so it always tracks the published release. */
|
|
5
6
|
export const SERVER_VERSION = (() => {
|
|
6
7
|
try {
|
|
@@ -36,7 +37,14 @@ function makeResult(data) {
|
|
|
36
37
|
/** Build a fully-wired MCP server bound to the given API client. */
|
|
37
38
|
export function createServer(api, opts = {}) {
|
|
38
39
|
const toolsSpec = opts.tools ?? process.env.WEBCAKE_TOOLS;
|
|
39
|
-
const server = new McpServer({
|
|
40
|
+
const server = new McpServer({
|
|
41
|
+
name: "webcake-storefront",
|
|
42
|
+
version: SERVER_VERSION,
|
|
43
|
+
// Shown by MCP clients (e.g. claude.ai connectors) instead of a generic globe.
|
|
44
|
+
title: "WebCake Storefront",
|
|
45
|
+
websiteUrl: "https://webcake.io",
|
|
46
|
+
icons: [{ src: `data:image/png;base64,${ICON_PNG.toString("base64")}`, mimeType: "image/png", sizes: ["192x192"] }],
|
|
47
|
+
}, { instructions: `${INSTRUCTIONS}\n\n${buildToolGroupIndex(toolsSpec)}` });
|
|
40
48
|
const handle = async (fn) => {
|
|
41
49
|
try {
|
|
42
50
|
return makeResult(await fn());
|
package/dist/tools/articles.js
CHANGED
|
@@ -79,14 +79,22 @@ export const seoSchema = z
|
|
|
79
79
|
.array(z.object({ name: z.string().describe("Display name"), json: z.string().describe("JSON-LD object as a string, without <script>") }))
|
|
80
80
|
.optional()
|
|
81
81
|
.describe("Structured data markup (JSON-LD). REPLACES all existing markups; [] removes them."),
|
|
82
|
+
custom_tags: z
|
|
83
|
+
.array(z.object({ type: z.enum(["meta", "link"]).default("meta"), props: z.record(z.string()) }))
|
|
84
|
+
.optional()
|
|
85
|
+
.describe('Extra tags ("Thẻ bổ sung"), e.g. { "type": "meta", "props": { "name": "author", "content": "Togi" } }. Replaces a tag with the same name/property/rel.'),
|
|
86
|
+
remove_tags: z.array(z.string()).optional().describe('Remove tags by name/property/rel, e.g. ["author", "og:url"]'),
|
|
82
87
|
})
|
|
83
88
|
.optional()
|
|
84
89
|
.describe("SEO settings (\"Cài đặt SEO\": basic, social share, advanced). Without a meta description the site falls back to the whole body text.");
|
|
85
90
|
/** The SEO save replaces the whole meta_tags array, so merge into the existing tags: passed
|
|
86
91
|
* fields overwrite their tag, "" removes it, structured_data replaces all JSON-LD scripts,
|
|
87
92
|
* everything else (custom tags) is kept. */
|
|
93
|
+
const tagKey = (t) => t?.props?.name || t?.props?.property || t?.props?.rel || t?.props?.["http-equiv"];
|
|
88
94
|
export function mergeSeoTags(seo, existing = []) {
|
|
89
95
|
let tags = [...existing];
|
|
96
|
+
if (seo.remove_tags)
|
|
97
|
+
tags = tags.filter((t) => !seo.remove_tags.includes(tagKey(t)));
|
|
90
98
|
for (const [key, def] of Object.entries(SEO_TAGS)) {
|
|
91
99
|
const value = seo[key];
|
|
92
100
|
if (value === undefined)
|
|
@@ -100,6 +108,10 @@ export function mergeSeoTags(seo, existing = []) {
|
|
|
100
108
|
for (const m of seo.structured_data)
|
|
101
109
|
tags.push({ type: "script", props: { type: "application/ld+json" }, meta: { displayName: m.name }, children: m.json });
|
|
102
110
|
}
|
|
111
|
+
for (const t of seo.custom_tags || []) {
|
|
112
|
+
tags = tags.filter((x) => tagKey(x) !== tagKey(t));
|
|
113
|
+
tags.push({ type: t.type || "meta", props: t.props });
|
|
114
|
+
}
|
|
103
115
|
return tags;
|
|
104
116
|
}
|
|
105
117
|
export function registerArticleTools(server, api, handle) {
|
|
@@ -37,6 +37,21 @@ async function applyProductSeo(api, id, seo) {
|
|
|
37
37
|
const p = res?.data || res?.product || res;
|
|
38
38
|
await api.setProductSeo(id, mergeSeoTags(seo, p?.meta_tags || []), p?.slug ?? null);
|
|
39
39
|
}
|
|
40
|
+
/** Find a category's current meta_tags in a list response (flat or tree-shaped). */
|
|
41
|
+
function findMetaTags(res, id) {
|
|
42
|
+
const stack = [res];
|
|
43
|
+
while (stack.length) {
|
|
44
|
+
const x = stack.pop();
|
|
45
|
+
if (Array.isArray(x))
|
|
46
|
+
stack.push(...x);
|
|
47
|
+
else if (x && typeof x === "object") {
|
|
48
|
+
if (x.id === id && "meta_tags" in x)
|
|
49
|
+
return x.meta_tags || [];
|
|
50
|
+
stack.push(...Object.values(x).filter((v) => v && typeof v === "object"));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
40
55
|
export function registerCatalogWriteTools(server, api, handle) {
|
|
41
56
|
server.tool("create_product", `Create a product so the storefront has real merchandise (grid-product / slider-product bindings need this).
|
|
42
57
|
Simple use: pass name + price (+ images, category_ids). One default variation with the price/stock is created for you.
|
|
@@ -151,7 +166,8 @@ Returns the new category id — pass it to create_product's category_ids. Image
|
|
|
151
166
|
image: z.string().optional().describe("Hosted image URL for the category card"),
|
|
152
167
|
description: z.string().optional().describe("Category description"),
|
|
153
168
|
parent_id: z.string().optional().describe("Parent category id for a sub-category"),
|
|
154
|
-
|
|
169
|
+
seo: seoSchema,
|
|
170
|
+
}, ({ name, image, description, parent_id, seo }) => handle(async () => {
|
|
155
171
|
const id = randomUUID();
|
|
156
172
|
const commands = [
|
|
157
173
|
{ name: "create_category", data: { id, name, ...(parent_id ? { parent_id } : {}) } },
|
|
@@ -164,6 +180,8 @@ Returns the new category id — pass it to create_product's category_ids. Image
|
|
|
164
180
|
data: { id, multi_description: [{ id: randomUUID(), title: name, description }] },
|
|
165
181
|
});
|
|
166
182
|
}
|
|
183
|
+
if (seo)
|
|
184
|
+
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo) } });
|
|
167
185
|
await api.createProductCategory(commands);
|
|
168
186
|
return { success: true, category_id: id, name, ...(parent_id ? { parent_id } : {}) };
|
|
169
187
|
}));
|
|
@@ -171,16 +189,44 @@ Returns the new category id — pass it to create_product's category_ids. Image
|
|
|
171
189
|
name: z.string().describe("Blog category name"),
|
|
172
190
|
image: z.string().optional().describe("Hosted image URL"),
|
|
173
191
|
description: z.string().optional().describe("Category description"),
|
|
174
|
-
|
|
192
|
+
seo: seoSchema,
|
|
193
|
+
}, ({ name, image, description, seo }) => handle(async () => {
|
|
175
194
|
const id = randomUUID();
|
|
176
195
|
const commands = [{ name: "create_category", data: { id, name } }];
|
|
177
196
|
if (description)
|
|
178
197
|
commands.push({ name: "description_category", data: { id, description } });
|
|
179
198
|
if (image)
|
|
180
199
|
commands.push({ name: "image_category", data: { id, image } });
|
|
200
|
+
if (seo)
|
|
201
|
+
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo) } });
|
|
181
202
|
await api.createBlogCategory(commands);
|
|
182
203
|
return { success: true, category_id: id, name };
|
|
183
204
|
}));
|
|
205
|
+
server.tool("update_blog_category", "Update a blog/article category (name, description, image, slug, SEO). Pass id + fields to change.", {
|
|
206
|
+
id: z.string().describe("Blog category id"),
|
|
207
|
+
name: z.string().optional().describe("New name"),
|
|
208
|
+
description: z.string().optional().describe("New description"),
|
|
209
|
+
image: z.string().optional().describe("New hosted image URL"),
|
|
210
|
+
slug: z.string().optional().describe("New custom slug"),
|
|
211
|
+
seo: seoSchema.describe("SEO settings to change; other existing SEO tags are kept. Pass \"\" to clear a field."),
|
|
212
|
+
}, ({ id, name, description, image, slug, seo }) => handle(async () => {
|
|
213
|
+
const commands = [];
|
|
214
|
+
// name_category may rewrite the slug, so the custom slug goes after it.
|
|
215
|
+
if (name != null)
|
|
216
|
+
commands.push({ name: "name_category", data: { id, name } });
|
|
217
|
+
if (slug != null)
|
|
218
|
+
commands.push({ name: "set_category_custom_slug", data: { id, custom_slug: slug } });
|
|
219
|
+
if (description != null)
|
|
220
|
+
commands.push({ name: "description_category", data: { id, description } });
|
|
221
|
+
if (image != null)
|
|
222
|
+
commands.push({ name: "image_category", data: { id, image } });
|
|
223
|
+
if (seo)
|
|
224
|
+
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo, findMetaTags(await api.listBlogCategories(), id)) } });
|
|
225
|
+
if (!commands.length)
|
|
226
|
+
throw new Error("Nothing to update — pass at least one field besides id.");
|
|
227
|
+
await api.updateBlogCategory(commands);
|
|
228
|
+
return { success: true, category_id: id, updated: commands.map((c) => c.name) };
|
|
229
|
+
}));
|
|
184
230
|
// ── Edits: products & product categories ─────────────────────────────────────
|
|
185
231
|
server.tool("update_product", `Update an existing product. Pass product_id + only the fields to change. To change
|
|
186
232
|
price/stock, pass variations (get_product first to see the existing variation shape).
|
|
@@ -243,13 +289,14 @@ Images must be hosted CDN urls (search_images cdn_url / upload_images).`, {
|
|
|
243
289
|
await api.removeProducts(product_ids);
|
|
244
290
|
return { success: true, deleted: product_ids };
|
|
245
291
|
}));
|
|
246
|
-
server.tool("update_product_category", "Update a product category (name, image, description,
|
|
292
|
+
server.tool("update_product_category", "Update a product category (name, image, description, visibility, SEO). Pass id + fields to change.", {
|
|
247
293
|
id: z.string().describe("Category id"),
|
|
248
294
|
name: z.string().optional().describe("New name"),
|
|
249
295
|
image: z.string().optional().describe("New hosted image URL"),
|
|
250
296
|
description: z.string().optional().describe("New description"),
|
|
251
297
|
hidden: z.boolean().optional().describe("Hide (true) or show (false) the category"),
|
|
252
|
-
|
|
298
|
+
seo: seoSchema.describe("SEO settings to change; other existing SEO tags are kept. Pass \"\" to clear a field."),
|
|
299
|
+
}, ({ id, name, image, description, hidden, seo }) => handle(async () => {
|
|
253
300
|
const commands = [];
|
|
254
301
|
if (name != null)
|
|
255
302
|
commands.push({ name: "name_category", data: { id, name } });
|
|
@@ -259,8 +306,10 @@ Images must be hosted CDN urls (search_images cdn_url / upload_images).`, {
|
|
|
259
306
|
commands.push({ name: "multi_description", data: { id, multi_description: [{ id: randomUUID(), title: name || "", description }] } });
|
|
260
307
|
if (hidden != null)
|
|
261
308
|
commands.push({ name: "set_category_visible", data: { id, is_hidden: hidden } });
|
|
309
|
+
if (seo)
|
|
310
|
+
commands.push({ name: "set_category_seo", data: { id, meta_tags: mergeSeoTags(seo, findMetaTags(await api.listCategories(), id)) } });
|
|
262
311
|
if (!commands.length)
|
|
263
|
-
throw new Error("Nothing to update — pass at least one of name/image/description/hidden.");
|
|
312
|
+
throw new Error("Nothing to update — pass at least one of name/image/description/hidden/seo.");
|
|
264
313
|
await api.updateProductCategory(commands);
|
|
265
314
|
return { success: true, category_id: id, updated: commands.map((c) => c.name) };
|
|
266
315
|
}));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { seoSchema, mergeSeoTags } from "./articles.js";
|
|
2
3
|
// Multilingual app (Enum.Application multilingual = 5). Install first via
|
|
3
4
|
// install_app({ app: "multilingual" }), then enable languages. Each resource type
|
|
4
5
|
// (product, category, article, …) has its own translations list + save endpoint.
|
|
@@ -60,6 +61,40 @@ For most resources \`items\` is an array of translation rows. For 'site' pass th
|
|
|
60
61
|
body = { [def.field || `${resource}_translations`]: items, language_code };
|
|
61
62
|
return api.mlSaveTranslations(def.action, body);
|
|
62
63
|
}));
|
|
64
|
+
server.tool("set_translation_seo", `Set the SEO ("Cài đặt SEO") of ONE item's translation in a target language — product, article,
|
|
65
|
+
product category or blog category. Same fields as the item's own seo param; merged into that
|
|
66
|
+
translation's existing SEO tags (fields you don't pass are kept). Only meta_tags is written, so the
|
|
67
|
+
translated name/slug/content are untouched; the translation row is created if missing.`, {
|
|
68
|
+
resource: z.enum(["product", "article", "category", "article_category"]).describe("category = product category, article_category = blog category"),
|
|
69
|
+
id: z.string().describe("Item id (product / article / category id)"),
|
|
70
|
+
language_code: z.string().describe("Target language code, e.g. 'en'"),
|
|
71
|
+
seo: seoSchema.unwrap(),
|
|
72
|
+
}, ({ resource, id, language_code, seo }) => handle(async () => {
|
|
73
|
+
const path = `${resource}_translations`;
|
|
74
|
+
// The list API only filters by name (term), so scan pages to find the row by id.
|
|
75
|
+
// ponytail: up to 50 pages × 100 items; add an id filter on the backend if sites outgrow it.
|
|
76
|
+
let row;
|
|
77
|
+
for (let page = 1; page <= 50 && !row; page++) {
|
|
78
|
+
const box = unwrap(await api.mlGetTranslations(path, { language_code, page, limit: 100 }), path);
|
|
79
|
+
const rows = box?.data || [];
|
|
80
|
+
row = rows.find((r) => r.id === id);
|
|
81
|
+
if (rows.length < 100)
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
const tr = row?.[`${resource}_translation`];
|
|
85
|
+
const idKey = resource === "article_category" ? "category_id" : `${resource}_id`;
|
|
86
|
+
const meta_tags = mergeSeoTags(seo, tr?.meta_tags || []);
|
|
87
|
+
const item = { [idKey]: id, site_id: api.siteId, meta_tags };
|
|
88
|
+
// Article / category / blog-category saves read `name` unconditionally (and re-derive the
|
|
89
|
+
// translated slug from it), so resend the current translated name, else the original one.
|
|
90
|
+
if (resource !== "product") {
|
|
91
|
+
item.name = tr?.name || row?.name;
|
|
92
|
+
if (!item.name)
|
|
93
|
+
throw new Error(`${resource} ${id} not found in the ${language_code} translation list.`);
|
|
94
|
+
}
|
|
95
|
+
await api.mlSaveTranslations(SAVE[resource].action, { [path]: [item], language_code });
|
|
96
|
+
return { success: true, resource, id, language_code, meta_tags };
|
|
97
|
+
}));
|
|
63
98
|
server.tool("auto_translate", `Machine-translate a JSON blob from one language to another. \`type\` must be a valid translation
|
|
64
99
|
type (e.g. 'product', 'category'). Returns { trans, count }.`, {
|
|
65
100
|
type: z.string().describe("Translation type, e.g. 'product'"),
|
package/dist/web-guide.js
CHANGED
|
@@ -564,6 +564,7 @@ export function guideHtml(origin, lang = "vi") {
|
|
|
564
564
|
<link rel="alternate" hreflang="vi" href="${origin}/">
|
|
565
565
|
<link rel="alternate" hreflang="en" href="${origin}/?lang=en">
|
|
566
566
|
<link rel="alternate" hreflang="x-default" href="${origin}/">
|
|
567
|
+
<link rel="icon" type="image/png" sizes="192x192" href="/favicon.png">
|
|
567
568
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
568
569
|
<meta property="og:type" content="website">
|
|
569
570
|
<meta property="og:site_name" content="WebCake Storefront MCP">
|
|
@@ -1032,6 +1033,8 @@ export function ogImageSvg() {
|
|
|
1032
1033
|
* Favicon SVG — the branded "S" mark with yellow dot.
|
|
1033
1034
|
* Served at /favicon.svg and /favicon.ico by http.ts.
|
|
1034
1035
|
*/
|
|
1036
|
+
// Raster icon (192px, generated from ICON_SVG) — shared by /favicon.ico and serverInfo.icons.
|
|
1037
|
+
export const ICON_PNG = readFileSync(new URL("./icon.png", import.meta.url));
|
|
1035
1038
|
export function faviconSvg() {
|
|
1036
1039
|
return ICON_SVG;
|
|
1037
1040
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webcake-storefront-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.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",
|