This commit is contained in:
Your Name committed 2026-06-19 07:46:36 -04:00
1 parent c84f7bb3ae
commit ce6fa69b09
8 files changed
+1307 -613

No files matched your search

+15 -611
View File
@@ -1,4 +1,3 @@
import crypto from "crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
@@ -7,236 +6,8 @@ import {
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import FormData from "form-data";
import { PAGE_SECTION_TYPES } from "@homaroma/types";
function getChildrenArray(node: any, isRoot?: boolean): any[] {
if (Array.isArray(node.columns)) return node.columns;
if (Array.isArray(node.links)) return node.links;
if (Array.isArray(node.items)) return node.items;
if (Array.isArray(node.children)) return node.children;
if (isRoot) {
node.columns = [];
return node.columns;
}
node.links = [];
return node.links;
}
function findNodeAndParent(tree: any[], targetId: string, parent: any = null, list: any[] = tree): { node: any, parent: any, list: any[], index: number } | null {
for (let i = 0; i < tree.length; i++) {
const item = tree[i];
if (item && typeof item === 'object') {
if (item.nodeId === targetId) {
return { node: item, parent, list: tree, index: i };
}
for (const key of Object.keys(item)) {
if (Array.isArray(item[key])) {
const res = findNodeAndParent(item[key], targetId, item, item[key]);
if (res) return res;
}
}
}
}
return null;
}
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 SECTION_TOOL_CONFIGS = [
{
name: "update_hero_article",
sectionType: "HERO_ARTICLE",
description: `Update a HERO_ARTICLE section. 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_rich_text",
sectionType: "RICH_TEXT",
description: `Update a RICH_TEXT section. 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. 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. 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. Title and description are localized. collectionId, limit, showPrice, showAddToCart, and products are plain values. If products is omitted, backend can auto-populate products on read-back using collectionId/type/productIds/limit. Example payload: { "sectionId": 52, "title": ${localizedStringExample}, "collectionId": "summer", "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}` },
collectionId: { type: "string", description: "Optional plain string collection identifier" },
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 auto-populate on read-back.", items: { type: "object" } }
},
required: ["sectionId"]
},
{
name: "update_accordion",
sectionType: "ACCORDION",
description: `Update an ACCORDION section. 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}` },
categories: {
type: "array",
description: "Accordion 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", "categories"]
},
{
name: "update_contact_info",
sectionType: "CONTACT_INFO",
description: `Update a CONTACT_INFO section. 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. 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"]
}
] as const;
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<string, any> = {};
for (const field of fields) {
if (params[field] !== undefined) {
payload[field] = params[field];
}
}
return payload;
}
function buildSectionUpdateResult(toolName: string, sectionType: string, updatedSection: any, payload: Record<string, any>) {
const notes: string[] = [];
if (sectionType === "SCENT_NOTES_BREAKDOWN") {
notes.push("heartNotes and middleNotes are mirrored automatically by the backend");
}
if (sectionType === "PRODUCT_CAROUSEL") {
notes.push("If products was omitted, the backend may populate products on read-back");
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
tool: toolName,
sectionType,
sectionId: updatedSection?.id,
updatedFields: Object.keys(payload),
appliedPayload: payload,
notes,
section: updatedSection
},
null,
2
)
}
]
};
}
import { MENU_TOOL_DEFINITIONS, handleMenuTool } from "./menu-toolkit";
import { SECTION_TOOL_DEFINITIONS, handleSectionTool } from "./section-toolkit";
const LOCAL_BACKEND_URL = process.env.MCP_BACKEND_URL_LOCAL || "http://localhost:3051/api";
const PRODUCTION_BACKEND_URL = process.env.MCP_BACKEND_URL_PRODUCTION || "https://api.homaroma.shop/api";
@@ -302,53 +73,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["env"]
}
},
{
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. Uses proper diff & sync for translations.",
inputSchema: {
type: "object",
properties: {
name: { type: "string", description: "Name of the menu (e.g. main, footer-explore)" },
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" },
translations: {
type: "array",
description: "Optional array of translations",
items: {
type: "object",
properties: {
langCode: { type: "string" },
label: { type: "string" }
}
}
}
},
required: ["label", "url", "order"]
}
}
},
required: ["name", "items"]
}
},
...MENU_TOOL_DEFINITIONS,
...SECTION_TOOL_DEFINITIONS,
{
name: "ping_backend",
description: "Ping the backend to verify connection and authentication",
@@ -638,56 +364,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ["title", "slug"]
}
},
{
name: "add_page_section",
description: "Add a block section to a dynamic page layout",
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 section type rules" },
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",
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: "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"]
}
},
...SECTION_TOOL_DEFINITIONS,
{
name: "update_footer_content",
description: "Update the content and links of the 4 Footer Blocks (Brand, Explore, Collections, Newsletter)",
@@ -731,67 +408,6 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
}
}
},
{
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 nodes if missing, and return the tree.",
inputSchema: { type: "object", properties: {} }
},
{
name: "add_menu_node",
description: "Add a new node to the Mega Menu tree. CRITICAL RULE: Mega Menu structure is Root -> Column -> Link. Do NOT add a Link directly under Root. If you want to add a Link, you MUST check if a Column exists first. If no Column exists, create a Column node first, then add the Link under that Column.",
inputSchema: {
type: "object",
properties: {
parentId: { type: "string", description: "UUID of parent node, or 'root'" },
nodeData: { type: "object", description: "Node data including label, url, translations, etc." }
},
required: ["nodeData"]
}
},
{
name: "update_menu_translation",
description: "Update 6-language translations for a specific node in the Mega Menu",
inputSchema: {
type: "object",
properties: {
nodeId: { type: "string", description: "UUID of the node" },
translations: {
type: "object",
description: "Object containing language codes as keys (th, en, etc.) and translated strings as values."
}
},
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'" },
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" }
},
required: ["nodeId"]
}
}
],
};
});
@@ -800,6 +416,16 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const params = request.params.arguments as any;
try {
const menuToolResponse = await handleMenuTool(api, request.params.name, params);
if (menuToolResponse) {
return menuToolResponse;
}
const sectionToolResponse = await handleSectionTool(api, request.params.name, params);
if (sectionToolResponse) {
return sectionToolResponse;
}
switch (request.params.name) {
case "set_target_environment": {
const env = params.env;
@@ -827,10 +453,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
const res = await api.get(`/menus/${params.name}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
case "upsert_menu": {
const res = await api.put(`/menus/${params.name}`, { items: params.items });
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
case "search_products": {
const search = params.search ? `&search=${encodeURIComponent(params.search)}` : "";
const limit = params.limit ? `&limit=${params.limit}` : "&limit=10";
@@ -1074,57 +696,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
};
return { content: [{ type: "text", text: JSON.stringify(normalized, null, 2) }] };
}
case "add_page_section": {
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_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": {
const config = SECTION_TOOL_CONFIG_BY_NAME.get(request.params.name);
if (!config) {
throw new Error(`Unknown tool config for: ${request.params.name}`);
}
const payload = buildSectionPayload(params, Object.keys(config.properties).filter((key) => key !== "sectionId"));
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;
}
}
const res = await api.put(`/pages/sections/${params.sectionId}`, {
type: config.sectionType,
...payload
});
return buildSectionUpdateResult(request.params.name, config.sectionType, res.data, payload);
}
case "delete_page_section": {
const res = await api.delete(`/pages/sections/${params.sectionId}`);
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
}
case "update_footer_content": {
const results = [];
@@ -1169,173 +740,6 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
}
case "audit_missing_pages": {
// 1. Fetch all menus
const menusRes = await api.get("/menus");
const menus = menusRes.data || [];
// 2. Fetch all dynamic pages
const pagesRes = await api.get("/pages");
const pages = pagesRes.data || [];
const createdSlugs = pages.map((p: any) => `/${p.slug}`);
// 3. Known SPA Hardcoded Routes
const hardcodedRoutes = [
"/", "/shop", "/login", "/cart", "/checkout", "/account", "/search", "/contact"
];
const missingLinks: any[] = [];
// 4. Audit
menus.forEach((menu: any) => {
if (menu.items && Array.isArray(menu.items)) {
menu.items.forEach((item: any) => {
const url = item.url;
if (!url) return;
// Skip external links
if (url.startsWith("http") || url.startsWith("mailto:") || url.startsWith("tel:")) return;
// Skip SPA Hardcoded routes
if (hardcodedRoutes.includes(url)) return;
// Skip dynamic collection routes natively handled by SPA (e.g. /collections/...)
if (url.startsWith("/collections/")) return;
// Check if it exists in created dynamic pages
if (!createdSlugs.includes(url)) {
missingLinks.push({
menuName: menu.name,
label: item.label,
url: url,
suggestedSlug: url.startsWith("/") ? url.substring(1) : url
});
}
});
}
});
return { content: [{ type: "text", text: JSON.stringify({ missingPages: missingLinks }, null, 2) }] };
}
case "get_mega_menu_tree": {
const res = await api.get(`/menus/main`);
const menu = res.data;
if (!menu.payload) menu.payload = [];
let modified = false;
function ensureNodeIds(obj: any) {
if (Array.isArray(obj)) {
obj.forEach(ensureNodeIds);
} else if (obj !== null && typeof obj === 'object') {
if (!obj.nodeId) {
obj.nodeId = crypto.randomUUID();
modified = true;
}
Object.values(obj).forEach(ensureNodeIds);
}
}
ensureNodeIds(menu.payload);
if (modified) {
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
}
return { content: [{ type: "text", text: JSON.stringify(menu.payload, null, 2) }] };
}
case "add_menu_node": {
const { parentId, nodeData } = params;
const res = await api.get(`/menus/main`);
const menu = res.data;
if (!menu.payload) menu.payload = [];
nodeData.nodeId = crypto.randomUUID();
// Map translations to label/title
if (nodeData.translations) {
const labelKey = nodeData.title !== undefined ? 'title' : 'label';
if (typeof nodeData[labelKey] === 'string') {
// fallback if it was a string
nodeData[labelKey] = { en: nodeData[labelKey] };
}
nodeData[labelKey] = { ...(nodeData[labelKey] || {}), ...nodeData.translations };
delete nodeData.translations;
}
if (!parentId || parentId === 'root') {
if (nodeData.url && !nodeData.columns && !nodeData.items && !nodeData.children) {
throw new Error("Invalid schema: Cannot add a Link directly under Root. Please create a Column first.");
}
menu.payload.push(nodeData);
} else {
const found = findNodeAndParent(menu.payload, parentId);
if (!found) {
throw new Error(`Parent node ${parentId} not found`);
}
const children = getChildrenArray(found.node, found.parent === null);
children.push(nodeData);
}
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
return { content: [{ type: "text", text: JSON.stringify({ success: true, nodeId: nodeData.nodeId, payload: menu.payload }, null, 2) }] };
}
case "update_menu_translation": {
const { nodeId, translations } = params;
const res = await api.get(`/menus/main`);
const menu = res.data;
if (!menu.payload) menu.payload = [];
const found = findNodeAndParent(menu.payload, nodeId);
if (!found) throw new Error(`Node ${nodeId} not found`);
const labelKey = found.node.title !== undefined ? 'title' : 'label';
found.node[labelKey] = { ...(found.node[labelKey] || {}), ...translations };
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
return { content: [{ type: "text", text: JSON.stringify({ success: true, node: found.node }, null, 2) }] };
}
case "move_menu_node": {
const { nodeId, newParentId, orderIndex } = params;
const res = await api.get(`/menus/main`);
const menu = res.data;
if (!menu.payload) menu.payload = [];
const found = findNodeAndParent(menu.payload, nodeId);
if (!found) throw new Error(`Node ${nodeId} not found`);
// Remove from current list
found.list.splice(found.index, 1);
let targetList = menu.payload;
if (newParentId && newParentId !== 'root') {
const newParent = findNodeAndParent(menu.payload, newParentId);
if (!newParent) throw new Error(`New parent ${newParentId} not found`);
targetList = getChildrenArray(newParent.node);
}
if (typeof orderIndex === 'number' && orderIndex >= 0) {
targetList.splice(orderIndex, 0, found.node);
} else {
targetList.push(found.node);
}
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
return { content: [{ type: "text", text: JSON.stringify({ success: true, payload: menu.payload }, null, 2) }] };
}
case "remove_menu_node": {
const { nodeId } = params;
const res = await api.get(`/menus/main`);
const menu = res.data;
if (!menu.payload) menu.payload = [];
const found = findNodeAndParent(menu.payload, nodeId);
if (!found) throw new Error(`Node ${nodeId} not found`);
found.list.splice(found.index, 1);
await api.put(`/menus/main`, { payload: menu.payload, items: menu.items || [] });
return { content: [{ type: "text", text: JSON.stringify({ success: true, payload: menu.payload }, null, 2) }] };
}
default:
throw new Error(`Unknown tool: ${request.params.name}`);
}