import type { AxiosInstance } from "axios"; import { applyBulkMenuOperations, applyMenuAddNode, applyMenuMoveNode, applyMenuRemoveNode, applyMenuUpdateTranslation, buildMenuMutationPreview, collectMenuLinksFromTree, createMenuSnapshot, normalizeMenuPayloadForWrite, restoreMenuSnapshot, validateMenuItems, validateMenuNodeData, validateMenuTranslations, validateMenuTreeSnapshot, isPlainObject, SUPPORTED_MENU_LOCALES, } from "./menu-tools"; const localizedTextSchema = { type: "object", description: `Localized text. Allowed locale keys: ${SUPPORTED_MENU_LOCALES.join(", ")}. Provide at least th or en; include lo and it when creating production-ready header menu content.`, properties: Object.fromEntries(SUPPORTED_MENU_LOCALES.map((locale) => [locale, { type: "string" }])), additionalProperties: false, } as const; const footerMenuNameSchema = { type: "string", enum: ["footer-explore", "footer-support"], description: "Footer menu managed by apps/admin Menu Manager.", } as const; const footerWriteControlSchema = { dryRun: { type: "boolean", description: "Validate and preview the operation without saving. Defaults to false.", }, expectedVersion: { type: "string", description: "Version returned by get_footer_menu. The backend rejects stale writes.", }, } as const; const menuLinkNodeSchema = { type: "object", description: "Mega Menu Link node. Valid under a Column or another Link. Renders as a clickable link; child links are shown inline up to the desktop limit.", properties: { nodeId: { type: "string", description: "Existing node UUID. Omit for new nodes; MCP will generate one." }, label: localizedTextSchema, url: { type: "string", description: "Internal path starting with /, or http(s)://, mailto:, tel:" }, icon: { type: "string", description: "Optional icon registry key" }, imageUrl: { type: "string", description: "Optional hover/preview image URL" }, links: { type: "array", description: "Optional child links", items: { type: "object" } }, }, required: ["label", "url"], additionalProperties: true, } as const; const menuColumnNodeSchema = { type: "object", description: "Mega Menu Column node. Valid only under Root. Contains links; it is not directly clickable.", properties: { nodeId: { type: "string", description: "Existing node UUID. Omit for new nodes; MCP will generate one." }, title: localizedTextSchema, icon: { type: "string", description: "Optional icon registry key" }, links: { type: "array", items: menuLinkNodeSchema }, }, required: ["title", "links"], additionalProperties: true, } as const; const rootMenuNodeSchema = { type: "object", description: "Mega Menu Root node. Valid only under root. Desktop renders root labels in the header; columns open in the mega panel on hover.", properties: { nodeId: { type: "string", description: "Existing node UUID. Omit for new nodes; MCP will generate one." }, label: localizedTextSchema, url: { type: "string", description: "Internal path starting with /, or http(s)://, mailto:, tel:" }, rootImageUrl: { type: "string", description: "Optional fallback image for featured preview" }, columns: { type: "array", items: menuColumnNodeSchema }, featured: { type: "object", description: "Optional desktop featured card. If enabled, provide meaningful localized text and imageUrl/url where possible.", properties: { enabled: { type: "boolean" }, sectionTitle: localizedTextSchema, badge: localizedTextSchema, title: localizedTextSchema, subtitle: localizedTextSchema, ctaLabel: localizedTextSchema, price: { type: "string" }, imageUrl: { type: "string" }, url: { type: "string" }, focalPoint: { type: "object", properties: { x: { type: "string" }, y: { type: "string" } }, }, }, additionalProperties: true, }, }, required: ["label", "url", "columns"], additionalProperties: true, } as const; export const MENU_TOOL_DEFINITIONS = [ { name: "get_footer_menu", description: "Read footer-explore or footer-support in a compact, language-specific format. Use lang=all only when every translation is required.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, lang: { type: "string", enum: [...SUPPORTED_MENU_LOCALES, "all"], description: "Language to resolve. Defaults to th; all returns all translations.", }, fallback: { type: "boolean", description: "Allow the backend to fall back to the base label when the requested translation is missing. Defaults to true.", }, }, required: ["menuName"], additionalProperties: false, }, }, { name: "add_footer_menu_item", description: "Add one item to footer-explore or footer-support without replacing the menu. Use dryRun first and pass the version from get_footer_menu.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, label: { type: "string", description: "Base label for the item." }, url: { type: "string", description: "Internal path or supported external URL." }, order: { type: "number", description: "Display order." }, translations: { type: "object", properties: Object.fromEntries(SUPPORTED_MENU_LOCALES.map((locale) => [locale, { type: "string" }])), additionalProperties: false, description: "Optional translated labels keyed by locale.", }, ...footerWriteControlSchema, }, required: ["menuName", "label", "url"], additionalProperties: false, }, }, { name: "update_footer_menu_item", description: "Partially update one footer menu item while preserving omitted fields and translations.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, itemId: { type: "number", description: "Existing footer menu item ID." }, label: { type: "string" }, url: { type: "string" }, order: { type: "number" }, ...footerWriteControlSchema, }, required: ["menuName", "itemId"], additionalProperties: false, }, }, { name: "delete_footer_menu_item", description: "Delete one item from footer-explore or footer-support. Use dryRun first and pass expectedVersion.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, itemId: { type: "number", description: "Existing footer menu item ID." }, ...footerWriteControlSchema, }, required: ["menuName", "itemId"], additionalProperties: false, }, }, { name: "reorder_footer_menu_items", description: "Reorder a footer menu using the complete ordered list of its item IDs.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, itemIds: { type: "array", items: { type: "number" }, minItems: 1, description: "Every current item ID, exactly once, in the desired order.", }, ...footerWriteControlSchema, }, required: ["menuName", "itemIds"], additionalProperties: false, }, }, { name: "update_footer_menu_item_translation", description: "Update one translated label on a footer menu item without changing its base label, URL, order, or other languages.", inputSchema: { type: "object", properties: { menuName: footerMenuNameSchema, itemId: { type: "number", description: "Existing footer menu item ID." }, lang: { type: "string", enum: SUPPORTED_MENU_LOCALES, description: "Translation locale.", }, label: { type: "string", description: "Translated label." }, ...footerWriteControlSchema, }, required: ["menuName", "itemId", "lang", "label"], additionalProperties: false, }, }, { name: "get_menu", description: "Fetch a specific menu and its items (including translations)", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the menu (e.g. main, footer-explore)" } }, required: ["name"] } }, { name: "upsert_menu", description: "Update or create a complete menu by replacing its items. Optional payload/isActive support covers tree-based menus and menu state. Uses proper diff & sync for translations.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the menu (e.g. main, footer-explore)" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" }, payload: { oneOf: [{ type: "object" }, { type: "array" }], description: "Optional menu payload for tree-based menus such as main" }, isActive: { type: "boolean", description: "Optional active state for the menu" }, rootImage: { type: "string", description: "Optional root image for the menu" }, items: { type: "array", description: "Array of menu items", items: { type: "object", properties: { id: { type: "number", description: "ID of existing item (optional)" }, label: { type: "string", description: "Label of the menu item" }, url: { type: "string", description: "URL of the menu item" }, order: { type: "number", description: "Order sequence" }, columnImage: { type: "string", description: "Optional column image for the menu item" }, hoverImage: { type: "string", description: "Optional hover image for the menu item" }, translations: { type: "array", description: "Optional array of translations", items: { type: "object", properties: { langCode: { type: "string" }, label: { type: "string" } } } } }, required: ["label", "url", "order"] } } }, required: ["name"] } }, { name: "audit_missing_pages", description: "Audit all menus and find links that do not have a corresponding dynamic page or hardcoded route", inputSchema: { type: "object", properties: {} } }, { name: "get_mega_menu_tree", description: `Read Mega Menu (main) payload, auto-generate nodeId for all structural nodes if missing, and return the tree. Supported locales: ${SUPPORTED_MENU_LOCALES.join(", ")}.`, inputSchema: { type: "object", properties: {} } }, { name: "validate_mega_menu_tree", description: "Validate the current Mega Menu (main) tree for LLM-safe editing. Reports missing locales, invalid node shape, duplicate nodeId, unsupported translation keys, placeholder text, URL issues, and desktop render limits.", inputSchema: { type: "object", properties: { includeTree: { type: "boolean", description: "Include the normalized menu tree in the response. Default false." } } } }, { name: "capture_menu_snapshot", description: "Capture the current state of a menu so it can be restored later.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the menu to snapshot (e.g. main, footer-explore)" } }, required: ["name"] } }, { name: "restore_menu_snapshot", description: "Restore a previously captured menu snapshot by snapshotId.", inputSchema: { type: "object", properties: { snapshotId: { type: "string", description: "Snapshot identifier returned by capture_menu_snapshot" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" } }, required: ["snapshotId"] } }, { name: "bulk_menu_operations", description: `Apply multiple Mega Menu operations in a single transaction. Workflow: capture_menu_snapshot -> validate_mega_menu_tree -> bulk_menu_operations with dryRun true -> user approval -> apply. Supports add_node, update_translation, move_node, and remove_node. Supported locales: ${SUPPORTED_MENU_LOCALES.join(", ")}.`, inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the menu to modify (e.g. main, footer-explore)" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" }, operations: { type: "array", description: "Array of menu operations to apply in order", items: { type: "object", properties: { type: { type: "string", enum: ["add_node", "update_translation", "move_node", "remove_node"] }, parentId: { type: "string", description: "Parent node ID for add_node" }, nodeId: { type: "string", description: "Target node ID for update/move/remove" }, newParentId: { type: "string", description: "New parent node ID for move_node" }, orderIndex: { type: "number", description: "Insertion index for move_node" }, translations: { ...localizedTextSchema, description: "Translation map for update_translation. Only locale keys are accepted." }, nodeData: { oneOf: [rootMenuNodeSchema, menuColumnNodeSchema, menuLinkNodeSchema], description: "Node data for add_node. Shape depends on parent: parentId=root => Root node; parent is Root => Column node; parent is Column/Link => Link node." } }, required: ["type"] } } }, required: ["name", "operations"] } }, { name: "add_menu_node", description: `Add a new node to the Mega Menu tree. Structure is Root -> Column -> Link -> Child Link. Do not add a Link directly under root. Use dryRun first. Supported locales: ${SUPPORTED_MENU_LOCALES.join(", ")}.`, inputSchema: { type: "object", properties: { parentId: { type: "string", description: "UUID of parent node, or 'root'" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" }, nodeData: { oneOf: [rootMenuNodeSchema, menuColumnNodeSchema, menuLinkNodeSchema], description: "Node data. If parentId is root/omitted, provide a Root node with label, url, and columns. If parent is a Root node, provide a Column node with title and links. If parent is a Column/Link, provide a Link node with label and url." } }, required: ["nodeData"] } }, { name: "update_menu_translation", description: `Update localized text for a specific node in the Mega Menu. Accepts only supported locale keys: ${SUPPORTED_MENU_LOCALES.join(", ")}. This updates label for root/link nodes or title for column nodes.`, inputSchema: { type: "object", properties: { nodeId: { type: "string", description: "UUID of the node" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" }, translations: { ...localizedTextSchema, description: "Object containing locale codes as keys and translated strings as values. Unsupported keys are rejected." } }, required: ["nodeId", "translations"] } }, { name: "move_menu_node", description: "Move a node to a new parent or change its order", inputSchema: { type: "object", properties: { nodeId: { type: "string" }, newParentId: { type: "string", description: "UUID of new parent, or 'root'" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" }, orderIndex: { type: "number", description: "New index in the parent's children array" } }, required: ["nodeId"] } }, { name: "remove_menu_node", description: "Remove a node and all its children from the Mega Menu", inputSchema: { type: "object", properties: { nodeId: { type: "string" }, dryRun: { type: "boolean", description: "Preview the mutation without saving it" } }, required: ["nodeId"] } } ] as const; export const MENU_TOOL_NAMES = new Set(MENU_TOOL_DEFINITIONS.map((tool) => tool.name)); export function getLegacyFooterMenuDeprecation(params: any) { const rejectedFields = ["exploreLinks", "collectionLinks", "supportLinks"] .filter((field) => params?.[field] !== undefined); if (rejectedFields.length === 0) return null; return { success: false, code: "FOOTER_MENU_REPLACEMENT_DEPRECATED", partialWrite: false, rejectedFields, message: "Footer menu arrays cannot be replaced through update_footer_content. Use get_footer_menu and the Footer-specific item tools. Website Collections are managed through Categories.", }; } function compactFooterResponse(data: any, operation: string, params: any) { const source = data?.data ?? data ?? {}; const compact: Record = { success: source.success ?? true, operation: source.operation ?? operation, menuName: source.menuName ?? source.name ?? params.menuName, }; for (const key of [ "dryRun", "version", "lang", "item", "items", "itemId", "itemIds", "itemCount", "label", "before", "after", "deletedItemId", "translatedItemId", "translatedPaths", "changes", "warnings", "code", "error", "partialWrite", ]) { if (source[key] !== undefined) compact[key] = source[key]; } if (compact.dryRun === undefined && params.dryRun !== undefined) { compact.dryRun = Boolean(params.dryRun); } return { content: [{ type: "text", text: JSON.stringify(compact, null, 2) }] }; } function footerWriteBody(params: any, fields: string[]) { const body: Record = {}; for (const field of fields) { if (params[field] !== undefined) body[field] = params[field]; } body.dryRun = Boolean(params.dryRun); if (params.expectedVersion !== undefined) body.expectedVersion = params.expectedVersion; return body; } export async function handleMenuTool(api: AxiosInstance, name: string, params: any) { if (!MENU_TOOL_NAMES.has(name)) return null; switch (name) { case "get_footer_menu": { const query = new URLSearchParams(); query.set("lang", params.lang || "th"); query.set("fallback", params.fallback === false ? "false" : "true"); const res = await api.get(`/menus/footer/${params.menuName}?${query.toString()}`); return compactFooterResponse(res.data, name, params); } case "add_footer_menu_item": { const body = footerWriteBody(params, ["label", "url", "order", "translations"]); const res = await api.post(`/menus/footer/${params.menuName}/items`, body); return compactFooterResponse(res.data, name, params); } case "update_footer_menu_item": { const body = footerWriteBody(params, ["label", "url", "order"]); const res = await api.patch(`/menus/footer/${params.menuName}/items/${params.itemId}`, body); return compactFooterResponse(res.data, name, params); } case "delete_footer_menu_item": { const body = footerWriteBody(params, []); const res = await api.delete(`/menus/footer/${params.menuName}/items/${params.itemId}`, { data: body }); return compactFooterResponse(res.data, name, params); } case "reorder_footer_menu_items": { const body = footerWriteBody(params, ["itemIds"]); const res = await api.put(`/menus/footer/${params.menuName}/items/reorder`, body); return compactFooterResponse(res.data, name, params); } case "update_footer_menu_item_translation": { const body = footerWriteBody(params, ["label"]); const res = await api.patch( `/menus/footer/${params.menuName}/items/${params.itemId}/translations/${params.lang}`, body, ); return compactFooterResponse(res.data, name, params); } case "get_menu": { const res = await api.get(`/menus/${params.name}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } case "upsert_menu": { const dryRun = !!params.dryRun; let existingMenu: any = null; try { const getRes = await api.get(`/menus/${params.name}`); existingMenu = getRes.data; } catch (e) { // Menu might not exist yet } const items = params.items !== undefined ? (Array.isArray(params.items) ? params.items : []) : (existingMenu?.items || []); const body: any = { items }; const itemCheck = validateMenuItems(items); if (itemCheck.issues.length > 0) { throw new Error(`Invalid menu items: ${itemCheck.issues.join("; ")}`); } if (params.payload !== undefined) { body.payload = params.payload; } else if (existingMenu?.payload !== undefined) { body.payload = existingMenu.payload; } if (params.isActive !== undefined) { body.isActive = params.isActive; } else if (existingMenu?.isActive !== undefined) { body.isActive = existingMenu.isActive; } if (params.rootImage !== undefined) { body.rootImage = params.rootImage; } else if (existingMenu?.rootImage !== undefined) { body.rootImage = existingMenu.rootImage; } if (params.payload !== undefined && !isPlainObject(params.payload) && !Array.isArray(params.payload)) { throw new Error("Invalid menu payload: payload must be an object or array"); } if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: true, dryRun: true, operation: "upsert_menu", menuName: params.name, preview: { name: params.name, itemCount: items.length, hasPayload: !!body.payload, isActive: body.isActive, rootImage: body.rootImage || null }, warnings: itemCheck.warnings }, null, 2 ) } ] }; } const res = await api.put(`/menus/${params.name}`, body); return { content: [ { type: "text", text: JSON.stringify( { success: true, operation: "upsert_menu", name: res.data.name, isActive: res.data.isActive, itemCount: res.data.items?.length || 0, hasPayload: !!res.data.payload, rootImage: res.data.rootImage || null }, null, 2 ) } ] }; } case "audit_missing_pages": { const menusRes = await api.get("/menus"); const menus = menusRes.data || []; const pagesRes = await api.get("/pages"); const pages = pagesRes.data || []; const createdSlugs = pages.map((p: any) => `/${p.slug}`); const hardcodedRoutes = ["/", "/shop", "/login", "/cart", "/checkout", "/account", "/search", "/contact"]; const missingLinks: any[] = []; const duplicates: any[] = []; const treeIssues: any[] = []; menus.forEach((menu: any) => { const seenUrls = new Set(); const urls = [ ...(Array.isArray(menu.items) ? menu.items.filter((item: any) => item?.url).map((item: any) => ({ url: item.url, label: item.label, source: "items" as const })) : []), ...(Array.isArray(menu.payload) ? collectMenuLinksFromTree(menu.payload) : []), ]; const uniqueUrls = [...new Map(urls.map((entry) => [entry.url, entry])).values()]; uniqueUrls.forEach(({ url, label, source }) => { if (seenUrls.has(url)) { duplicates.push({ menuName: menu.name, url, label: label || url, source }); return; } seenUrls.add(url); if (url.startsWith("http") || url.startsWith("mailto:") || url.startsWith("tel:")) return; if (hardcodedRoutes.includes(url)) return; if (url.startsWith("/collections/")) return; if (!createdSlugs.includes(url)) { missingLinks.push({ menuName: menu.name, label: label || url, url, suggestedSlug: url.startsWith("/") ? url.substring(1) : url, source, }); } }); if (Array.isArray(menu.payload)) { const treeCheck = validateMenuTreeSnapshot(menu.payload); treeIssues.push({ menuName: menu.name, issues: treeCheck.issues, warnings: treeCheck.warnings }); } }); return { content: [ { type: "text", text: JSON.stringify({ missingPages: missingLinks, duplicates, treeIssues }, null, 2) } ] }; } case "get_mega_menu_tree": { const res = await api.get(`/menus/main`); const menu = res.data; const normalized = normalizeMenuPayloadForWrite(menu.payload); if (normalized.modified) { await api.put(`/menus/main`, { payload: normalized.payload, items: menu.items || [] }); } return { content: [{ type: "text", text: JSON.stringify(normalized.payload, null, 2) }] }; } case "validate_mega_menu_tree": { const res = await api.get(`/menus/main`); const menu = res.data; const normalized = normalizeMenuPayloadForWrite(menu.payload); if (normalized.modified) { await api.put(`/menus/main`, { payload: normalized.payload, items: menu.items || [] }); } const validation = validateMenuTreeSnapshot(normalized.payload); return { content: [ { type: "text", text: JSON.stringify( { success: validation.issues.length === 0, operation: "validate_mega_menu_tree", menuName: "main", supportedLocales: validation.supportedLocales, summary: validation.summary, issueCount: validation.issues.length, warningCount: validation.warnings.length, issues: validation.issues, warnings: validation.warnings, diagnostics: validation.diagnostics, tree: params.includeTree ? normalized.payload : undefined, }, null, 2 ) } ] }; } case "capture_menu_snapshot": { const res = await api.get(`/menus/${params.name}`); const snapshot = createMenuSnapshot(res.data); return { content: [ { type: "text", text: JSON.stringify( { success: true, operation: "capture_menu_snapshot", snapshotId: snapshot.snapshotId, name: snapshot.name, createdAt: snapshot.createdAt, itemCount: snapshot.items.length, hasTreePayload: snapshot.payload.length > 0 }, null, 2 ) } ] }; } case "restore_menu_snapshot": { const dryRun = !!params.dryRun; const snapshot = restoreMenuSnapshot(params.snapshotId); const preview = { name: snapshot.name, payload: snapshot.payload, items: snapshot.items, isActive: snapshot.isActive }; if (dryRun) { return { content: [ { type: "text", text: JSON.stringify({ success: true, dryRun: true, operation: "restore_menu_snapshot", snapshotId: params.snapshotId, preview }, null, 2) } ] }; } const res = await api.put(`/menus/${snapshot.name}`, { payload: snapshot.payload, items: snapshot.items, isActive: snapshot.isActive }); return { content: [{ type: "text", text: JSON.stringify({ success: true, snapshotId: params.snapshotId, menu: res.data }, null, 2) }] }; } case "bulk_menu_operations": { const dryRun = !!params.dryRun; if (!Array.isArray(params.operations) || params.operations.length === 0) { throw new Error("operations must be a non-empty array"); } const res = await api.get(`/menus/${params.name}`); const menu = res.data; const beforePayload = Array.isArray(menu.payload) ? menu.payload : []; const result = applyBulkMenuOperations(beforePayload, params.operations); const validation = validateMenuTreeSnapshot(result.payload); if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: validation.issues.length === 0, dryRun: true, operation: "bulk_menu_operations", menuName: params.name, warnings: [...result.warnings, ...validation.warnings], issues: validation.issues, diagnostics: validation.diagnostics, applied: result.applied, preview: buildMenuMutationPreview(beforePayload, result.payload), }, null, 2 ) } ] }; } const normalized = normalizeMenuPayloadForWrite(result.payload); const writeBody: any = { payload: normalized.payload, items: menu.items || [] }; if (menu.isActive !== undefined) { writeBody.isActive = menu.isActive; } const writeRes = await api.put(`/menus/${params.name}`, writeBody); const writtenCheck = validateMenuTreeSnapshot(normalized.payload); return { content: [ { type: "text", text: JSON.stringify( { success: true, operation: "bulk_menu_operations", menuName: params.name, warnings: [...result.warnings, ...writtenCheck.warnings], applied: result.applied, summary: writtenCheck.summary, }, null, 2 ) } ] }; } case "add_menu_node": { const { parentId, nodeData } = params; const dryRun = !!params.dryRun; const res = await api.get(`/menus/main`); const menu = res.data; const validation = validateMenuNodeData(nodeData, parentId); if (validation.issues.length > 0) { throw new Error(`Invalid menu node: ${validation.issues.join("; ")}`); } const beforePayload = Array.isArray(menu.payload) ? menu.payload : []; const result = applyMenuAddNode(beforePayload, parentId, nodeData); const treeCheck = validateMenuTreeSnapshot(result.payload); if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: treeCheck.issues.length === 0, dryRun: true, operation: "add_menu_node", nodeId: result.nodeId, warnings: [...validation.warnings, ...treeCheck.warnings], issues: treeCheck.issues, diagnostics: treeCheck.diagnostics, preview: buildMenuMutationPreview(beforePayload, result.payload, result.nodeId), }, null, 2 ) } ] }; } await api.put(`/menus/main`, { payload: result.payload, items: menu.items || [] }); return { content: [{ type: "text", text: JSON.stringify({ success: true, operation: "add_menu_node", nodeId: result.nodeId, summary: validateMenuTreeSnapshot(result.payload).summary }, null, 2) }] }; } case "update_menu_translation": { const { nodeId, translations } = params; const dryRun = !!params.dryRun; const validation = validateMenuTranslations(translations); if (validation.issues.length > 0) { throw new Error(`Invalid translations: ${validation.issues.join("; ")}`); } const res = await api.get(`/menus/main`); const menu = res.data; const beforePayload = Array.isArray(menu.payload) ? menu.payload : []; const result = applyMenuUpdateTranslation(beforePayload, nodeId, translations); const treeCheck = validateMenuTreeSnapshot(result.payload); if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: treeCheck.issues.length === 0, dryRun: true, operation: "update_menu_translation", nodeId, warnings: [...validation.warnings, ...treeCheck.warnings], issues: treeCheck.issues, diagnostics: treeCheck.diagnostics, preview: buildMenuMutationPreview(beforePayload, result.payload, nodeId), }, null, 2 ) } ] }; } await api.put(`/menus/main`, { payload: result.payload, items: menu.items || [] }); return { content: [{ type: "text", text: JSON.stringify({ success: true, operation: "update_menu_translation", nodeId, summary: validateMenuTreeSnapshot(result.payload).summary }, null, 2) }] }; } case "move_menu_node": { const { nodeId, newParentId, orderIndex } = params; const dryRun = !!params.dryRun; const res = await api.get(`/menus/main`); const menu = res.data; const beforePayload = Array.isArray(menu.payload) ? menu.payload : []; const result = applyMenuMoveNode(beforePayload, nodeId, newParentId, orderIndex); const treeCheck = validateMenuTreeSnapshot(result.payload); if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: treeCheck.issues.length === 0, dryRun: true, operation: "move_menu_node", nodeId, warnings: treeCheck.warnings, issues: treeCheck.issues, diagnostics: treeCheck.diagnostics, preview: buildMenuMutationPreview(beforePayload, result.payload, nodeId), }, null, 2 ) } ] }; } await api.put(`/menus/main`, { payload: result.payload, items: menu.items || [] }); return { content: [{ type: "text", text: JSON.stringify({ success: true, operation: "move_menu_node", nodeId, summary: validateMenuTreeSnapshot(result.payload).summary }, null, 2) }] }; } case "remove_menu_node": { const { nodeId } = params; const dryRun = !!params.dryRun; const res = await api.get(`/menus/main`); const menu = res.data; const beforePayload = Array.isArray(menu.payload) ? menu.payload : []; const result = applyMenuRemoveNode(beforePayload, nodeId); const treeCheck = validateMenuTreeSnapshot(result.payload); if (dryRun) { return { content: [ { type: "text", text: JSON.stringify( { success: treeCheck.issues.length === 0, dryRun: true, operation: "remove_menu_node", removedNode: result.removedNode, warnings: treeCheck.warnings, issues: treeCheck.issues, diagnostics: treeCheck.diagnostics, preview: buildMenuMutationPreview(beforePayload, result.payload, nodeId), }, null, 2 ) } ] }; } await api.put(`/menus/main`, { payload: result.payload, items: menu.items || [] }); return { content: [{ type: "text", text: JSON.stringify({ success: true, operation: "remove_menu_node", removedNode: result.removedNode, summary: validateMenuTreeSnapshot(result.payload).summary }, null, 2) }] }; } default: return null; } }