import crypto from "crypto"; import type { AxiosInstance } from "axios"; import { PAGE_SECTION_TYPES } from "@homaroma/types"; const localizedStringSchema = { oneOf: [ { type: "string" }, { type: "object", additionalProperties: { type: "string" } } ] }; const localizedStringListSchema = { type: "array", items: localizedStringSchema }; const localizedStringExample = '{ "th": "ข้อความภาษาไทย", "en": "English text" }'; const localizedStringListExample = `[ ${localizedStringExample} ]`; const categoryIdListExample = `[1, 2]`; const productIdListExample = `[101, 102]`; const ADD_PAGE_SECTION_GUIDANCE: Record = { HERO: { required: ["tagline", "title", "description", "imageUrl", "buttonText", "buttonLink"], example: `{ "tagline": ${localizedStringExample}, "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/hero.jpg", "buttonText": ${localizedStringExample}, "buttonLink": "/shop" }` }, ANNOUNCEMENT_BAR: { required: ["slides"], example: `{ "slides": [${localizedStringExample}] }` }, NEWSLETTER: { required: ["title", "description", "buttonText", "showPrivacyCheckbox"], example: `{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "buttonText": ${localizedStringExample}, "showPrivacyCheckbox": true }` }, EDITORIAL_TILE: { required: ["title", "description", "imageUrl", "imagePosition"], example: `{ "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/tile.jpg", "imagePosition": "left" }` }, EDITORIAL_CAROUSEL: { required: ["title", "slides[].category", "slides[].title", "slides[].description", "slides[].imageUrl"], example: `{ "title": ${localizedStringExample}, "slides": [ { "category": ${localizedStringExample}, "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://example.com/slide.jpg" } ] }` }, PRODUCTS: { required: ["title", "type"], example: `{ "title": ${localizedStringExample}, "type": "custom", "productIds": ${productIdListExample}, "limit": 4 }` }, CATEGORIES: { required: ["title"], example: `{ "title": ${localizedStringExample}, "categoryIds": ${categoryIdListExample} }` }, TEXT: { required: ["content"], example: `{ "title": ${localizedStringExample}, "content": ${localizedStringExample} }` }, BANNER: { required: ["imageUrl"], example: `{ "imageUrl": "https://example.com/banner.jpg", "title": ${localizedStringExample}, "buttonText": ${localizedStringExample} }` }, HERO_ARTICLE: { required: ["title"], example: `{ "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "imageUrl": "https://example.com/article.jpg" }` }, RICH_TEXT: { required: ["content"], example: `{ "title": ${localizedStringExample}, "content": ${localizedStringExample} }` }, SCENT_NOTES_BREAKDOWN: { required: [], example: `{ "title": ${localizedStringExample}, "topNotes": ${localizedStringListExample} }` }, QUOTE_CALLOUT: { required: ["quote"], example: `{ "quote": ${localizedStringExample}, "author": ${localizedStringExample} }` }, PRODUCT_CAROUSEL: { required: ["type"], example: `{ "type": "custom", "productIds": ${productIdListExample}, "title": ${localizedStringExample}, "limit": 4 }` }, ACCORDION: { required: [], example: `{ "title": ${localizedStringExample}, "categories": [ { "name": ${localizedStringExample}, "questions": [ { "q": ${localizedStringExample}, "a": ${localizedStringExample} } ] } ] }` }, CONTACT_INFO: { required: ["address", "email", "line", "hours"], example: `{ "title": ${localizedStringExample}, "address": ${localizedStringExample}, "email": "hello@example.com", "line": "@homaroma", "hours": ${localizedStringExample} }` }, CONTACT_FORM: { required: [], example: `{ "title": ${localizedStringExample}, "subtitle": ${localizedStringExample} }` } }; function buildAddPageSectionDescription() { const toolSummaries = PAGE_SECTION_TYPES.map((type) => { const guide = ADD_PAGE_SECTION_GUIDANCE[type] ?? { required: [], example: `{ }` }; return `- ${type}: required ${guide.required.length > 0 ? guide.required.join(", ") : "no always-required payload fields"}. Minimal example ${guide.example}`; }).join("\n"); return `Add a block section to a dynamic page layout. The payload is fully replaced on create and must satisfy the exact schema for the chosen section type. Use the following required fields and minimal examples for first-time use:\n${toolSummaries}`; } const SECTION_TOOL_CONFIGS = [ { name: "update_hero_article", sectionType: "HERO_ARTICLE", description: `Update a HERO_ARTICLE section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Use localized objects for title, subtitle, author, and category. Use plain strings for image URLs, overlayColor, publishDate/date, and buttonLink. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "subtitle": ${localizedStringExample}, "author": ${localizedStringExample}, "imageUrl": "https://..." }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized article title, for example ${localizedStringExample}` }, subtitle: { ...localizedStringSchema, description: `Localized subtitle, for example ${localizedStringExample}` }, author: { ...localizedStringSchema, description: `Localized author name, for example ${localizedStringExample}` }, category: { ...localizedStringSchema, description: `Localized category or tag, for example ${localizedStringExample}` }, publishDate: { type: "string", description: "Plain string publish date or display date" }, date: { type: "string", description: "Plain string alternative date field accepted by backend" }, imageUrl: { type: "string", description: "Plain string main image URL" }, backgroundImageUrl: { type: "string", description: "Plain string background image URL" }, overlayColor: { type: "string", description: "Plain string overlay color in hex or rgba" } }, required: ["sectionId"] }, { name: "update_editorial_tile", sectionType: "EDITORIAL_TILE", description: `Update an EDITORIAL_TILE section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title, description, and buttonText are localized. imageUrl, imagePosition, buttonLink, backgroundColor, and textColor are plain strings. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "description": ${localizedStringExample}, "imageUrl": "https://..." }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, imageUrl: { type: "string", description: "Plain string main image URL" }, imagePosition: { type: "string", description: "Plain string image position ('left' or 'right')" }, title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` }, description: { ...localizedStringSchema, description: `Localized description, for example ${localizedStringExample}` }, buttonText: { ...localizedStringSchema, description: `Localized button text, for example ${localizedStringExample}` }, buttonLink: { type: "string", description: "Plain string button link URL" }, backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" }, textColor: { type: "string", description: "Plain string text color in hex or rgba" } }, required: ["sectionId"] }, { name: "update_rich_text", sectionType: "RICH_TEXT", description: `Update a RICH_TEXT section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title is optional and localized. Content is required and may be HTML or plain text per language. Example payload: { "sectionId": 34, "title": ${localizedStringExample}, "content": ${localizedStringExample} }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized title displayed above the content, for example ${localizedStringExample}` }, content: { ...localizedStringSchema, description: `Localized HTML or plain text content, for example ${localizedStringExample}` } }, required: ["sectionId", "content"] }, { name: "update_scent_notes_breakdown", sectionType: "SCENT_NOTES_BREAKDOWN", description: `Update a SCENT_NOTES_BREAKDOWN section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title and description are localized. Each note item can be a plain string or a localized object. Use either heartNotes or middleNotes; the backend mirrors whichever one you provide so both stay in sync. Example payload: { "sectionId": 40, "title": ${localizedStringExample}, "topNotes": ${localizedStringListExample} }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` }, description: { ...localizedStringSchema, description: `Localized supporting description, for example ${localizedStringExample}` }, topNotes: { ...localizedStringListSchema, description: `Top note values, for example ${localizedStringListExample}` }, heartNotes: { ...localizedStringListSchema, description: `Heart note values. The server mirrors heartNotes and middleNotes, for example ${localizedStringListExample}` }, middleNotes: { ...localizedStringListSchema, description: `Middle note values. The server mirrors heartNotes and middleNotes, for example ${localizedStringListExample}` }, baseNotes: { ...localizedStringListSchema, description: `Base note values, for example ${localizedStringListExample}` } }, required: ["sectionId"] }, { name: "update_quote_callout", sectionType: "QUOTE_CALLOUT", description: `Update a QUOTE_CALLOUT section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Quote is required and localized. Author and role are optional localized fields. Example payload: { "sectionId": 48, "quote": ${localizedStringExample}, "author": ${localizedStringExample}, "role": ${localizedStringExample} }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, quote: { ...localizedStringSchema, description: `Localized quote text, for example ${localizedStringExample}` }, author: { ...localizedStringSchema, description: `Localized author name, for example ${localizedStringExample}` }, role: { ...localizedStringSchema, description: `Localized author role or title, for example ${localizedStringExample}` } }, required: ["sectionId"] }, { name: "update_product_carousel", sectionType: "PRODUCT_CAROUSEL", description: `Update a PRODUCT_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. Title and description are localized. type is required for a valid stored payload. Use categoryId for featured carousels, brandId for brand carousels, or productIds for custom carousels. Example payload: { "sectionId": 52, "type": "custom", "productIds": ${productIdListExample}, "title": ${localizedStringExample}, "limit": 6, "showPrice": true }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized title, for example ${localizedStringExample}` }, description: { ...localizedStringSchema, description: `Localized supporting description, for example ${localizedStringExample}` }, type: { type: "string", enum: ["featured", "brand", "custom"], description: "Canonical carousel mode used for validation and product resolution" }, categoryId: { type: "number", description: "Category ID for featured carousels" }, brandId: { type: "number", description: "Brand ID for brand carousels" }, productIds: { type: "array", description: `Explicit product IDs for custom carousels, for example ${productIdListExample}`, items: { type: "number" } }, limit: { type: "number", description: "Maximum number of products to show" }, showPrice: { type: "boolean", description: "Show product price" }, showAddToCart: { type: "boolean", description: "Show add to cart button" }, products: { type: "array", description: "Optional explicit product list. Omit this to let backend resolve products by type/categoryId/brandId/productIds.", items: { type: "object" } }, includeHydratedProducts: { type: "boolean", description: "Optional. Set true to return hydrated products in the response. Default false for lightweight responses." } }, required: ["sectionId"] }, { name: "update_accordion", sectionType: "ACCORDION", description: `Update an ACCORDION section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title is optional and localized. categories is required. Each category needs a localized name, optional icon, and at least one question. Each question needs localized q and a fields. Example payload: { "sectionId": 60, "categories": [ { "name": ${localizedStringExample}, "questions": [ { "q": ${localizedStringExample}, "a": ${localizedStringExample} } ] } ] }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` }, backgroundColor: { type: "string", description: "Plain string representing a valid CSS color for the background" }, textColor: { type: "string", description: "Plain string representing a valid CSS color for text" }, iconColor: { type: "string", description: "Plain string representing a valid CSS color for icons" }, categories: { type: "array", description: "Accordion categories. IMPORTANT: Providing this will completely overwrite the existing categories array. Ensure you provide all categories.", items: { type: "object", properties: { name: { ...localizedStringSchema, description: `Localized category name, for example ${localizedStringExample}` }, icon: { type: "string", description: "Optional icon or emoji" }, questions: { type: "array", description: "Questions under the category", items: { type: "object", properties: { q: { ...localizedStringSchema, description: `Localized question, for example ${localizedStringExample}` }, a: { ...localizedStringSchema, description: `Localized answer, for example ${localizedStringExample}` } }, required: ["q", "a"] } } }, required: ["name", "questions"] } } }, required: ["sectionId"] }, { name: "update_contact_info", sectionType: "CONTACT_INFO", description: `Update a CONTACT_INFO section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title is optional and localized. address and hours are localized text. email and line are plain strings. Example payload: { "sectionId": 72, "title": ${localizedStringExample}, "address": ${localizedStringExample}, "email": "hello@example.com", "line": "@homaroma", "hours": ${localizedStringExample} }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` }, address: { ...localizedStringSchema, description: `Localized address, for example ${localizedStringExample}` }, email: { type: "string", description: "Plain string email address" }, line: { type: "string", description: "Plain string Line ID or contact" }, hours: { ...localizedStringSchema, description: `Localized business hours, for example ${localizedStringExample}` } }, required: ["sectionId", "address", "email", "line", "hours"] }, { name: "update_contact_form", sectionType: "CONTACT_FORM", description: `Update a CONTACT_FORM section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title and subtitle are localized; this block has no extra fields. Example payload: { "sectionId": 73, "title": ${localizedStringExample}, "subtitle": ${localizedStringExample} }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized form title, for example ${localizedStringExample}` }, subtitle: { ...localizedStringSchema, description: `Localized form subtitle, for example ${localizedStringExample}` } }, required: ["sectionId"] }, { name: "update_editorial_carousel", sectionType: "EDITORIAL_CAROUSEL", description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, layout, imagePosition, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "layout": "split_screen", "imagePosition": "left" }`, properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` }, description: { ...localizedStringSchema, description: `Localized section description, for example ${localizedStringExample}` }, backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" }, textColor: { type: "string", description: "Plain string text color in hex or rgba" }, layout: { type: "string", enum: ["standard", "split_screen"], description: "Layout style of the carousel (standard or split_screen)" }, imagePosition: { type: "string", enum: ["left", "right"], description: "Position of the image relative to text when layout is split_screen (left or right)" }, slides: { type: "array", items: { type: "object", properties: { title: localizedStringSchema, description: localizedStringSchema, category: localizedStringSchema, imageUrl: { type: "string" }, buttonText: localizedStringSchema, buttonLink: { type: "string" } }, required: ["imageUrl"] }, description: "Array of slides. IMPORTANT: Providing this will completely overwrite the existing slides array. Ensure you provide all slides." } }, required: ["sectionId"] } ] as const; type SectionToolConfig = (typeof SECTION_TOOL_CONFIGS)[number]; const SECTION_TOOL_CONFIG_BY_NAME = new Map( SECTION_TOOL_CONFIGS.map((config) => [config.name, config] as const) ); function buildSectionPayload(params: any, fields: readonly string[]) { const payload: Record = {}; for (const field of fields) { if (params[field] !== undefined) { payload[field] = params[field]; } } return payload; } function sanitizeSectionForResponse(sectionType: string, section: any, options?: { includeHydratedProducts?: boolean }) { if (!section || !section.payload) return section; if (sectionType === "PRODUCT_CAROUSEL" && !options?.includeHydratedProducts) { return { ...section, payload: { ...section.payload, products: undefined, } }; } return section; } function buildSectionUpdateResult( toolName: string, sectionType: string, updatedSection: any, payload: Record, options?: { includeHydratedProducts?: boolean } ) { const notes: string[] = []; if (sectionType === "SCENT_NOTES_BREAKDOWN") { notes.push("heartNotes and middleNotes are mirrored automatically by the backend"); } if (sectionType === "PRODUCT_CAROUSEL" && !options?.includeHydratedProducts) { notes.push("Hydrated products are omitted by default. Pass includeHydratedProducts=true to inspect resolved products."); } return { content: [ { type: "text", text: JSON.stringify( { success: true, tool: toolName, sectionType, sectionId: updatedSection?.id, updatedFields: Object.keys(payload), appliedPayload: payload, notes, section: sanitizeSectionForResponse(sectionType, updatedSection, options) }, null, 2 ) } ] }; } export const SECTION_TOOL_DEFINITIONS = [ { name: "get_page_section", description: "Get a specific page section by ID, returning its hydrated payload resolving linked content like categories or products. Optionally pass lang to control translation.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to fetch" }, lang: { type: "string", description: "Optional language code (th, en, all)" } }, required: ["sectionId"] } }, { name: "add_page_section", description: buildAddPageSectionDescription(), inputSchema: { type: "object", properties: { pageId: { type: "number", description: "The numeric ID of the page" }, type: { type: "string", enum: [...PAGE_SECTION_TYPES], description: "Block type (e.g. HERO, ANNOUNCEMENT_BAR, NEWSLETTER, EDITORIAL_TILE, EDITORIAL_CAROUSEL, PRODUCTS, CATEGORIES, TEXT, BANNER, HERO_ARTICLE, RICH_TEXT, SCENT_NOTES_BREAKDOWN, QUOTE_CALLOUT, PRODUCT_CAROUSEL)" }, order: { type: "number", description: "Display order sequence (starts at 1)" }, payload: { type: "object", description: "The JSON payload matching the exact schema for the chosen type. See the tool description for required fields and minimal valid examples per type." }, isActive: { type: "boolean", description: "Initial active state" } }, required: ["pageId", "type", "payload"] } }, { name: "update_page_section", description: "Update an existing block section payload, order, or active state. If 'payload' is provided here, it replaces the stored payload after backend validation. This tool does not merge payload keys. Use the type-specific update tools when you want MCP to merge a partial payload with the existing stored payload before writing.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, type: { type: "string", enum: [...PAGE_SECTION_TYPES], description: "Optional new block type" }, order: { type: "number", description: "Optional new display order sequence" }, payload: { type: "object", description: "Optional new JSON payload matching the section type rules" }, isActive: { type: "boolean", description: "Optional new active state" } }, required: ["sectionId"] } }, ...SECTION_TOOL_CONFIGS.map((config) => ({ name: config.name, description: config.description, inputSchema: { type: "object", properties: config.properties, required: config.required } })), { name: "add_accordion_category", description: "Add a category to an ACCORDION section.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, name: { ...localizedStringSchema, description: "Localized category name" }, icon: { type: "string", description: "Optional plain string for icon/emoji" }, questions: { type: "array", description: "Optional array of questions to add", items: { type: "object", properties: { q: { ...localizedStringSchema, description: "Localized question text" }, a: { ...localizedStringSchema, description: "Localized answer text" } }, required: ["q", "a"] } } }, required: ["sectionId", "name"] } }, { name: "update_accordion_category", description: "Update a category in an ACCORDION section.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, categoryId: { type: "string", description: "ID of the category to update" }, name: { ...localizedStringSchema, description: "Optional localized category name" }, icon: { type: "string", description: "Optional plain string for icon/emoji" } }, required: ["sectionId", "categoryId"] } }, { name: "add_accordion_question", description: "Add a question to a category in an ACCORDION section.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, categoryId: { type: "string", description: "ID of the category" }, question: { ...localizedStringSchema, description: "Localized question text" }, answer: { ...localizedStringSchema, description: "Localized answer text" } }, required: ["sectionId", "categoryId", "question", "answer"] } }, { name: "update_accordion_question", description: "Update a question in an ACCORDION section.", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to update" }, categoryId: { type: "string", description: "ID of the category" }, questionId: { type: "string", description: "ID of the question item to update" }, question: { ...localizedStringSchema, description: "Optional localized question text" }, answer: { ...localizedStringSchema, description: "Optional localized answer text" } }, required: ["sectionId", "categoryId", "questionId"] } }, { name: "delete_page_section", description: "Delete a block section from a dynamic page layout", inputSchema: { type: "object", properties: { sectionId: { type: "number", description: "The numeric ID of the section to delete" } }, required: ["sectionId"] } } ] as const; export const SECTION_TOOL_NAMES = new Set(SECTION_TOOL_DEFINITIONS.map((tool) => tool.name)); export async function handleSectionTool(api: AxiosInstance, name: string, params: any) { if (!SECTION_TOOL_NAMES.has(name)) return null; // Global validation: almost all section tools require sectionId, except add_page_section if (name !== "add_page_section" && params.sectionId === undefined) { throw new Error(`Validation Error: 'sectionId' is required for tool '${name}'`); } switch (name) { case "get_page_section": { const langParam = params.lang ? `?lang=${params.lang}` : ""; const res = await api.get(`/pages/sections/${params.sectionId}${langParam}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } case "add_page_section": { if (params.pageId === undefined) { throw new Error(`Validation Error: 'pageId' is required for tool '${name}'`); } const payload: any = { type: params.type, payload: params.payload, isActive: params.isActive !== false }; if (params.order !== undefined) payload.order = params.order; const res = await api.post(`/pages/${params.pageId}/sections`, payload); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } case "update_page_section": { const payload: any = {}; if (params.type) payload.type = params.type; if (params.order !== undefined) payload.order = params.order; if (params.payload) payload.payload = params.payload; if (params.isActive !== undefined) payload.isActive = params.isActive; const res = await api.put(`/pages/sections/${params.sectionId}`, payload); return buildSectionUpdateResult("update_page_section", res.data?.type ?? params.type ?? "UNKNOWN", res.data, payload); } case "update_editorial_tile": case "update_hero_article": case "update_rich_text": case "update_scent_notes_breakdown": case "update_quote_callout": case "update_product_carousel": case "update_accordion": case "update_contact_info": case "update_contact_form": case "update_editorial_carousel": { const config = SECTION_TOOL_CONFIG_BY_NAME.get(name); if (!config) { throw new Error(`Unknown tool config for: ${name}`); } const payloadFields = Object.keys(config.properties).filter((key) => key !== "sectionId" && key !== "includeHydratedProducts"); const payload = buildSectionPayload(params, payloadFields); if (config.sectionType === "SCENT_NOTES_BREAKDOWN") { if (payload.heartNotes !== undefined && payload.middleNotes === undefined) { payload.middleNotes = payload.heartNotes; } else if (payload.middleNotes !== undefined && payload.heartNotes === undefined) { payload.heartNotes = payload.middleNotes; } } // Fetch existing section to merge payload so AI doesn't have to provide all required fields every time let mergedPayload = payload; try { const existingRes = await api.get(`/pages/sections/${params.sectionId}?hydrate=false`); if (existingRes.data && existingRes.data.payload) { mergedPayload = { ...existingRes.data.payload, ...payload }; } } catch (err) { console.warn(`Could not fetch existing section ${params.sectionId} for merging, proceeding with partial payload.`); } const res = await api.put(`/pages/sections/${params.sectionId}`, { type: config.sectionType, payload: mergedPayload }); return buildSectionUpdateResult(name, config.sectionType, res.data, payload, { includeHydratedProducts: params.includeHydratedProducts === true }); } case "delete_page_section": { await api.delete(`/pages/sections/${params.sectionId}`); return { content: [{ type: "text", text: JSON.stringify({ success: true, sectionId: params.sectionId }, null, 2) }] }; } case "add_accordion_category": case "update_accordion_category": case "add_accordion_question": case "update_accordion_question": { const existingRes = await api.get(`/pages/sections/${params.sectionId}?hydrate=false`); const payload = existingRes.data?.payload || {}; payload.categories = payload.categories || []; let responseBody: Record = { success: true, tool: name, sectionId: params.sectionId, }; if (name === "add_accordion_category") { const questions = params.questions?.map((q: any) => ({ id: crypto.randomUUID(), q: q.q, a: q.a })) || []; const category = { id: crypto.randomUUID(), name: params.name, icon: params.icon, questions }; payload.categories.push(category); responseBody = { ...responseBody, categoryId: category.id, category, categoryCount: payload.categories.length, }; } else if (name === "update_accordion_category") { const cat = payload.categories.find((c: any) => c.id === params.categoryId); if (!cat) throw new Error(`Category with id ${params.categoryId} not found`); const changedFields: string[] = []; if (params.name) { cat.name = typeof params.name === "object" && typeof cat.name === "object" ? { ...cat.name, ...params.name } : params.name; changedFields.push("name"); } if (params.icon !== undefined) { cat.icon = params.icon; changedFields.push("icon"); } responseBody = { ...responseBody, categoryId: cat.id, changedFields, category: cat, }; } else if (name === "add_accordion_question") { const cat = payload.categories.find((c: any) => c.id === params.categoryId); if (!cat) throw new Error(`Category with id ${params.categoryId} not found`); cat.questions = cat.questions || []; const question = { id: crypto.randomUUID(), q: params.question, a: params.answer }; cat.questions.push(question); responseBody = { ...responseBody, categoryId: cat.id, questionId: question.id, question: question, }; } else if (name === "update_accordion_question") { const cat = payload.categories.find((c: any) => c.id === params.categoryId); if (!cat) throw new Error(`Category with id ${params.categoryId} not found`); cat.questions = cat.questions || []; const item = cat.questions.find((q: any) => q.id === params.questionId); if (!item) throw new Error(`Question with id ${params.questionId} not found`); const changedFields: string[] = []; if (params.question) { item.q = typeof params.question === "object" && typeof item.q === "object" ? { ...item.q, ...params.question } : params.question; changedFields.push("question"); } if (params.answer) { item.a = typeof params.answer === "object" && typeof item.a === "object" ? { ...item.a, ...params.answer } : params.answer; changedFields.push("answer"); } responseBody = { ...responseBody, categoryId: cat.id, questionId: item.id, changedFields, question: item, }; } await api.put(`/pages/sections/${params.sectionId}`, { type: "ACCORDION", payload }); return { content: [{ type: "text", text: JSON.stringify(responseBody, null, 2) }] }; } default: return null; } }