This commit is contained in:
Your Name committed 2026-10-01 06:59:38 +07:00
1 parent 0ceee3dbff
commit 2d39edbd57
18 files changed
+804 -153

No files matched your search

+6 -1
View File
@@ -129,11 +129,16 @@ th, en, ja, zh, vi, ko, lo, it
LLM agents should use this workflow for category edits:
1. `list_categories` with `lang: "all"` to discover category IDs and current translations
2. `create_category`, `update_category`, `update_category_translations`, or `delete_category` with `dryRun: true`
2. `create_category`, `update_category`, `update_category_media`, `update_category_translations`, or `delete_category` with `dryRun: true`
3. Review the preview and product counts
4. Apply only after the intended change is clear
5. Run `list_categories` again to verify
Category media uses semantic roles: `thumbnail`, `hero_desktop`, `hero_mobile`,
`card_portrait`, and `social_share`. `update_category_media` replaces the complete
media set, so omitted roles are removed. `update_category_image` remains available
only as a legacy thumbnail compatibility tool.
Backend category support currently covers:
- base `name`
+70 -8
View File
@@ -104,11 +104,31 @@ function compactCategory(category: any) {
id: category.id,
name: category.name,
imageUrl: category.imageUrl ?? null,
media: category.media ?? [],
layoutType: category.layoutType ?? undefined,
productCount: category._count?.products ?? category.productCount ?? undefined,
translations: category.translations ?? undefined,
};
}
const categoryMediaSchema = {
type: "object",
properties: {
role: {
type: "string",
enum: ["thumbnail", "hero_desktop", "hero_mobile", "card_portrait", "social_share"],
description: "Semantic image usage; do not encode a layout name in this field."
},
url: { type: "string", description: "Image URL or website-relative media path." },
width: { type: ["number", "null"], description: "Optional intrinsic width in pixels." },
height: { type: ["number", "null"], description: "Optional intrinsic height in pixels." },
focalX: { type: "number", minimum: 0, maximum: 1, description: "Horizontal focal point from 0 to 1." },
focalY: { type: "number", minimum: 0, maximum: 1, description: "Vertical focal point from 0 to 1." },
sortOrder: { type: "number", minimum: 0 }
},
required: ["role", "url"]
};
function compactProduct(product: any) {
if (!product) return null;
const images = Array.isArray(product.images) ? product.images : [];
@@ -211,12 +231,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "create_category",
description: "Create a product category. Backend supports base name, imageUrl, and translations. It does not support slug, hierarchy, sort order, or active state for categories.",
description: "Create a product category with semantic responsive media and translations. imageUrl remains a legacy thumbnail fallback.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Base category name. Use Thai or the primary display name." },
imageUrl: { type: ["string", "null"], description: "Optional image URL." },
layoutType: { type: "string", enum: ["split_showcase", "editorial_hero", "dense_catalog"] },
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
translations: {
type: "object",
description: "Optional locale map for category names. Supported locales: th, en, ja, zh, vi, ko, lo, it."
@@ -228,13 +250,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
},
{
name: "update_category",
description: "Update category base name, image URL, and/or translations. Use dryRun first. Backend supports name and imageUrl only for base category fields.",
description: "Update category base fields, layout, semantic responsive media, and/or translations. Use dryRun first.",
inputSchema: {
type: "object",
properties: {
id: { type: "number", description: "Category ID" },
name: { type: "string", description: "Optional base category name." },
imageUrl: { type: ["string", "null"], description: "Optional image URL. Pass null to remove." },
layoutType: { type: "string", enum: ["split_showcase", "editorial_hero", "dense_catalog"] },
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
translations: {
type: "object",
description: "Optional locale map for category names. Supported locales: th, en, ja, zh, vi, ko, lo, it."
@@ -324,9 +348,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["id", "translations"]
}
},
{
name: "update_category_media",
description: "Replace the complete semantic media set for a category. Omitted roles are removed. Use dryRun first.",
inputSchema: {
type: "object",
properties: {
id: { type: "number", description: "Category ID" },
media: { type: "array", items: categoryMediaSchema, maxItems: 5 },
dryRun: { type: "boolean", description: "Preview only. No media is changed." }
},
required: ["id", "media"]
}
},
{
name: "update_category_image",
description: "Update the image URL for a specific category.",
description: "Legacy compatibility tool. Update the category thumbnail only. Prefer update_category_media for responsive assets.",
inputSchema: {
type: "object",
properties: {
@@ -1145,13 +1182,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
case "create_category": {
const { name, imageUrl = null, translations, dryRun } = params as any;
const { name, imageUrl = null, layoutType, media, translations, dryRun } = params as any;
if (typeof name !== "string" || name.trim().length === 0) {
throw new Error("name must be a non-empty string.");
}
if (translations !== undefined) validateCategoryLocaleMap(translations);
const payload = { name: name.trim(), imageUrl };
const payload = { name: name.trim(), imageUrl, layoutType, media };
if (dryRun) {
return {
content: [{
@@ -1164,7 +1201,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
create: payload,
translations: translations ?? null,
backendSupport: {
fields: ["name", "imageUrl", "translations"],
fields: ["name", "imageUrl", "layoutType", "media", "translations"],
unsupportedFields: ["slug", "parentId", "sortOrder", "isActive"],
},
},
@@ -1193,7 +1230,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
}
case "update_category": {
const { id, name, imageUrl, translations, dryRun } = params as any;
const { id, name, imageUrl, layoutType, media, translations, dryRun } = params as any;
if (name !== undefined && (typeof name !== "string" || name.trim().length === 0)) {
throw new Error("name must be a non-empty string when provided.");
}
@@ -1210,6 +1247,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const baseUpdate: any = {};
if (name !== undefined) baseUpdate.name = name.trim();
if (imageUrl !== undefined) baseUpdate.imageUrl = imageUrl;
if (layoutType !== undefined) baseUpdate.layoutType = layoutType;
if (media !== undefined) baseUpdate.media = media;
if (dryRun) {
return {
@@ -1225,7 +1264,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
baseUpdate,
translations: translations ?? null,
backendSupport: {
fields: ["name", "imageUrl", "translations"],
fields: ["name", "imageUrl", "layoutType", "media", "translations"],
unsupportedFields: ["slug", "parentId", "sortOrder", "isActive"],
},
},
@@ -1392,6 +1431,29 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }]
};
}
case "update_category_media": {
const { id, media, dryRun } = params as any;
if (!Array.isArray(media)) throw new Error("media must be an array.");
if (dryRun) {
const before = await api.get(`/categories/${id}?lang=all`);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
dryRun: true,
operation: "update_category_media",
categoryId: id,
before: compactCategory(before.data),
media,
note: "This is a full replacement; omitted roles will be removed."
}, null, 2)
}]
};
}
const res = await api.put(`/categories/${id}/media`, { media });
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
case "update_product_brand_translation": {
const { id, lang, name } = params as any;
const res = await api.patch(`/products/brands/${id}/translations`, {