1264 lines
50 KiB
TypeScript
1264 lines
50 KiB
TypeScript
import crypto from "crypto";
|
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
import {
|
|
CallToolRequestSchema,
|
|
ListToolsRequestSchema,
|
|
} 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 SECTION_TOOL_CONFIGS = [
|
|
{
|
|
name: "update_hero_article",
|
|
sectionType: "HERO_ARTICLE",
|
|
description: "Update a HERO_ARTICLE section with structured editorial fields.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized article title" },
|
|
subtitle: { ...localizedStringSchema, description: "Localized subtitle" },
|
|
author: { ...localizedStringSchema, description: "Localized author name" },
|
|
category: { ...localizedStringSchema, description: "Localized category or tag" },
|
|
publishDate: { type: "string", description: "Publish date or display date" },
|
|
date: { type: "string", description: "Alternative date field accepted by backend" },
|
|
imageUrl: { type: "string", description: "Main image URL" },
|
|
backgroundImageUrl: { type: "string", description: "Background image URL" },
|
|
overlayColor: { type: "string", description: "Overlay color in hex or rgba" }
|
|
},
|
|
required: ["sectionId"]
|
|
},
|
|
{
|
|
name: "update_rich_text",
|
|
sectionType: "RICH_TEXT",
|
|
description: "Update a RICH_TEXT section with localized title and content.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized title displayed above the content" },
|
|
content: { ...localizedStringSchema, description: "Localized HTML or plain text content" }
|
|
},
|
|
required: ["sectionId", "content"]
|
|
},
|
|
{
|
|
name: "update_scent_notes_breakdown",
|
|
sectionType: "SCENT_NOTES_BREAKDOWN",
|
|
description: "Update a SCENT_NOTES_BREAKDOWN section with localized title, description, and note groups.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized title" },
|
|
description: { ...localizedStringSchema, description: "Localized supporting description" },
|
|
topNotes: { type: "array", items: localizedStringSchema, description: "Top note values" },
|
|
heartNotes: { type: "array", items: localizedStringSchema, description: "Heart note values" },
|
|
middleNotes: { type: "array", items: localizedStringSchema, description: "Middle note values" },
|
|
baseNotes: { type: "array", items: localizedStringSchema, description: "Base note values" }
|
|
},
|
|
required: ["sectionId"]
|
|
},
|
|
{
|
|
name: "update_quote_callout",
|
|
sectionType: "QUOTE_CALLOUT",
|
|
description: "Update a QUOTE_CALLOUT section with a localized quote, author, and role.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
quote: { ...localizedStringSchema, description: "Localized quote text" },
|
|
author: { ...localizedStringSchema, description: "Localized author name" },
|
|
role: { ...localizedStringSchema, description: "Localized author role or title" }
|
|
},
|
|
required: ["sectionId"]
|
|
},
|
|
{
|
|
name: "update_product_carousel",
|
|
sectionType: "PRODUCT_CAROUSEL",
|
|
description: "Update a PRODUCT_CAROUSEL section with localized copy and carousel options.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized title" },
|
|
description: { ...localizedStringSchema, description: "Localized supporting description" },
|
|
collectionId: { type: "string", description: "Optional 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" }
|
|
},
|
|
required: ["sectionId"]
|
|
},
|
|
{
|
|
name: "update_accordion",
|
|
sectionType: "ACCORDION",
|
|
description: "Update an ACCORDION section with categories, questions, and localized answers.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized section title" },
|
|
categories: {
|
|
type: "array",
|
|
description: "Accordion categories",
|
|
items: {
|
|
type: "object",
|
|
properties: {
|
|
name: { ...localizedStringSchema, description: "Localized category name" },
|
|
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" },
|
|
a: { ...localizedStringSchema, description: "Localized answer" }
|
|
},
|
|
required: ["q", "a"]
|
|
}
|
|
}
|
|
},
|
|
required: ["name", "questions"]
|
|
}
|
|
}
|
|
},
|
|
required: ["sectionId", "categories"]
|
|
},
|
|
{
|
|
name: "update_contact_info",
|
|
sectionType: "CONTACT_INFO",
|
|
description: "Update a CONTACT_INFO section with localized address, email, line, and hours.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized section title" },
|
|
address: { ...localizedStringSchema, description: "Localized address" },
|
|
email: { ...localizedStringSchema, description: "Localized email" },
|
|
line: { ...localizedStringSchema, description: "Localized Line ID or contact" },
|
|
hours: { ...localizedStringSchema, description: "Localized business hours" }
|
|
},
|
|
required: ["sectionId", "address", "email", "line", "hours"]
|
|
},
|
|
{
|
|
name: "update_contact_form",
|
|
sectionType: "CONTACT_FORM",
|
|
description: "Update a CONTACT_FORM section with localized title and subtitle.",
|
|
properties: {
|
|
sectionId: { type: "number", description: "The numeric ID of the section to update" },
|
|
title: { ...localizedStringSchema, description: "Localized form title" },
|
|
subtitle: { ...localizedStringSchema, description: "Localized form subtitle" }
|
|
},
|
|
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>) {
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: JSON.stringify(
|
|
{
|
|
success: true,
|
|
tool: toolName,
|
|
sectionType,
|
|
sectionId: updatedSection?.id,
|
|
updatedFields: Object.keys(payload),
|
|
section: updatedSection
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
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";
|
|
|
|
let BACKEND_URL = process.env.MCP_BACKEND_URL || LOCAL_BACKEND_URL;
|
|
const ADMIN_USERNAME = process.env.MCP_ADMIN_USERNAME || process.env.ADMIN_USERNAME || "admin";
|
|
const ADMIN_PASSWORD = process.env.MCP_ADMIN_PASSWORD || process.env.ADMIN_PASSWORD || "homaroma123";
|
|
|
|
let authToken = "";
|
|
|
|
export const api = axios.create({
|
|
baseURL: BACKEND_URL,
|
|
});
|
|
|
|
api.interceptors.request.use(async (config) => {
|
|
if (!authToken && config.url !== "/auth/login") {
|
|
console.error("[MCP] No token found. Attempting auto-login...");
|
|
await performLogin();
|
|
}
|
|
if (authToken) {
|
|
config.headers.Authorization = `Bearer ${authToken}`;
|
|
}
|
|
return config;
|
|
});
|
|
|
|
async function performLogin() {
|
|
try {
|
|
const res = await axios.post(`${BACKEND_URL}/auth/login`, {
|
|
username: ADMIN_USERNAME,
|
|
password: ADMIN_PASSWORD,
|
|
});
|
|
if (res.data && res.data.token) {
|
|
authToken = res.data.token;
|
|
console.error("[MCP] Successfully logged in to Backend API.");
|
|
}
|
|
} catch (err: any) {
|
|
console.error("[MCP] Auto-login failed:", err.message);
|
|
}
|
|
}
|
|
|
|
const server = new Server(
|
|
{
|
|
name: "homaroma-mcp",
|
|
version: "1.0.0",
|
|
},
|
|
{
|
|
capabilities: {
|
|
tools: {},
|
|
},
|
|
}
|
|
);
|
|
|
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
return {
|
|
tools: [
|
|
{
|
|
name: "set_target_environment",
|
|
description: "Switch the target environment for the MCP server (local or production)",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
env: { type: "string", enum: ["local", "production"] }
|
|
},
|
|
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"]
|
|
}
|
|
},
|
|
{
|
|
name: "ping_backend",
|
|
description: "Ping the backend to verify connection and authentication",
|
|
inputSchema: { type: "object", properties: {} },
|
|
},
|
|
{
|
|
name: "search_products",
|
|
description: "Search for products and get their basic info",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
search: { type: "string", description: "Search term" },
|
|
limit: { type: "number", description: "Max results" },
|
|
lang: { type: "string", description: "Language code (e.g. th, en, all)" }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: "get_product_detail",
|
|
description: "Get full product details including variants, images, and category",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Product ID" },
|
|
lang: { type: "string", description: "Language code (e.g. th, en, all)" }
|
|
},
|
|
required: ["id"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_category_translation",
|
|
description: "Add or update translations for a specific category. Do NOT use this tool for products.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Category ID" },
|
|
lang: { type: "string", description: "Language code (th, en, ja, zh, vi, ko)" },
|
|
name: { type: "string", description: "Translated category name" }
|
|
},
|
|
required: ["id", "lang", "name"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_scent_translation",
|
|
description: "Add or update translations for a specific scent.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Scent ID" },
|
|
lang: { type: "string", description: "Language code (th, en, ja, zh, vi, ko)" },
|
|
name: { type: "string", description: "Translated scent name" }
|
|
},
|
|
required: ["id", "lang", "name"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_page_translation",
|
|
description: "Add or update translations for a specific page.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Page ID" },
|
|
lang: { type: "string", description: "Language code (th, en, ja, zh, vi, ko)" },
|
|
title: { type: "string", description: "Translated page title" }
|
|
},
|
|
required: ["id", "lang", "title"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_hero_translation",
|
|
description: "Add or update translations for a specific hero.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Hero ID" },
|
|
lang: { type: "string", description: "Language code" },
|
|
tagline: { type: "string" },
|
|
title: { type: "string" },
|
|
description: { type: "string" },
|
|
buttonText: { type: "string" }
|
|
},
|
|
required: ["id", "lang"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_menu_item_translation",
|
|
description: "Add or update translations for a specific menu item.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Menu Item ID" },
|
|
lang: { type: "string", description: "Language code" },
|
|
label: { type: "string", description: "Translated menu item label" }
|
|
},
|
|
required: ["id", "lang", "label"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_store_settings_translation",
|
|
description: "Add or update translations for store settings.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
lang: { type: "string", description: "Language code" },
|
|
footerDescription: { type: "string" },
|
|
newsletterTitle: { type: "string" },
|
|
newsletterDescription: { type: "string" },
|
|
newsletterPlaceholder: { type: "string" },
|
|
newsletterButtonText: { type: "string" }
|
|
},
|
|
required: ["lang"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_article_translation",
|
|
description: "Add or update translations for a specific article.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Article ID" },
|
|
lang: { type: "string", description: "Language code" },
|
|
title: { type: "string" },
|
|
content: { type: "string" }
|
|
},
|
|
required: ["id", "lang"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_product_content",
|
|
description: "Update product basic content (name, description, details)",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number" },
|
|
name: { type: "string" },
|
|
description: { type: "string" },
|
|
details: { type: "string" },
|
|
},
|
|
required: ["id"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_product_translation",
|
|
description: "Update product translation for a specific language. Use this to add missing translations.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
id: { type: "number", description: "Product ID" },
|
|
lang: { type: "string", description: "Language code (e.g., en, ja, zh, vi, ko)" },
|
|
name: { type: "string", description: "Translated product name" },
|
|
description: { type: "string", description: "Translated description" },
|
|
details: { type: "string", description: "Translated details (HTML or Markdown allowed)" }
|
|
},
|
|
required: ["id", "lang"]
|
|
}
|
|
},
|
|
{
|
|
name: "upload_product_image_from_url",
|
|
description: "Download image from URL, upload to backend, and append to product images",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
productId: { type: "number" },
|
|
imageUrl: { type: "string", description: "URL of the image to download" }
|
|
},
|
|
required: ["productId", "imageUrl"]
|
|
}
|
|
},
|
|
{
|
|
name: "generate_mock_review",
|
|
description: "Create a dummy review for a product",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
productId: { type: "number" },
|
|
rating: { type: "number", description: "1-5" },
|
|
comment: { type: "string" },
|
|
customerName: { type: "string", description: "Name of the mock reviewer" }
|
|
},
|
|
required: ["productId", "rating", "customerName"]
|
|
}
|
|
},
|
|
{
|
|
name: "get_order_status",
|
|
description: "Get the status and details of a specific order",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
orderId: { type: "number" }
|
|
},
|
|
required: ["orderId"]
|
|
}
|
|
},
|
|
{
|
|
name: "list_pending_orders",
|
|
description: "List orders that are pending payment or processing",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
limit: { type: "number", description: "Max results" }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: "update_order_tracking",
|
|
description: "Update the tracking number of an order and optionally its status",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
orderId: { type: "number" },
|
|
trackingNo: { type: "string" },
|
|
status: { type: "string", description: "Optional status to update (e.g. SHIPPED)" }
|
|
},
|
|
required: ["orderId", "trackingNo"]
|
|
}
|
|
},
|
|
{
|
|
name: "search_customer",
|
|
description: "Search for customers by name, email, phone, or line ID",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
search: { type: "string", description: "Search term" }
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: "list_dynamic_pages",
|
|
description: "List all dynamic pages and layouts managed by the CMS",
|
|
inputSchema: { type: "object", properties: {} }
|
|
},
|
|
{
|
|
name: "list_dynamic_pages_metadata",
|
|
description: "List all dynamic pages and layouts managed by the CMS (metadata only)",
|
|
inputSchema: { type: "object", properties: {} }
|
|
},
|
|
{
|
|
name: "get_dynamic_page_detail",
|
|
description: "Get full detail of a specific dynamic page by slug",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
slug: { type: "string", description: "Slug of the page" }
|
|
},
|
|
required: ["slug"]
|
|
}
|
|
},
|
|
{
|
|
name: "create_dynamic_page",
|
|
description: "Create or update a dynamic page or layout",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
title: { type: "string", description: "Title of the page" },
|
|
slug: { type: "string", description: "Unique slug identifier (e.g. about-us)" },
|
|
isPublished: { type: "boolean", description: "Publish status" },
|
|
status: { type: "string", description: "Publish status (e.g. 'published' or 'draft')" },
|
|
metadata: { type: "object", description: "SEO metadata" },
|
|
id: { type: "number", description: "Numeric page ID (if updating existing page)" }
|
|
},
|
|
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"]
|
|
}
|
|
},
|
|
{
|
|
name: "update_footer_content",
|
|
description: "Update the content and links of the 4 Footer Blocks (Brand, Explore, Collections, Newsletter)",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
brandDescription: { type: "string", description: "Text for Block 1 (Brand tagline)" },
|
|
socialLinks: {
|
|
type: "object",
|
|
description: "URLs for facebook, instagram, tiktok (e.g. { facebookUrl: '...' })",
|
|
properties: {
|
|
facebookUrl: { type: "string" },
|
|
instagramUrl: { type: "string" },
|
|
tiktokUrl: { type: "string" }
|
|
}
|
|
},
|
|
exploreLinks: {
|
|
type: "array",
|
|
description: "Array of { label, url, order } for Block 2 (Explore)",
|
|
items: {
|
|
type: "object",
|
|
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
|
}
|
|
},
|
|
collectionLinks: {
|
|
type: "array",
|
|
description: "Array of { label, url, order } for Block 3 (Collections)",
|
|
items: {
|
|
type: "object",
|
|
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
|
}
|
|
},
|
|
supportLinks: {
|
|
type: "array",
|
|
description: "Array of { label, url, order } for Block 4 (Support/Legal)",
|
|
items: {
|
|
type: "object",
|
|
properties: { label: { type: "string" }, url: { type: "string" }, order: { type: "number" } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
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"]
|
|
}
|
|
}
|
|
],
|
|
};
|
|
});
|
|
|
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
const params = request.params.arguments as any;
|
|
|
|
try {
|
|
switch (request.params.name) {
|
|
case "set_target_environment": {
|
|
const env = params.env;
|
|
if (env === "production") {
|
|
BACKEND_URL = PRODUCTION_BACKEND_URL;
|
|
} else {
|
|
BACKEND_URL = LOCAL_BACKEND_URL;
|
|
}
|
|
api.defaults.baseURL = BACKEND_URL;
|
|
authToken = ""; // reset token so it logs in again on next request
|
|
return {
|
|
content: [
|
|
{
|
|
type: "text",
|
|
text: `✅ MCP Server target environment switched to [${env}]. Base URL is now: ${BACKEND_URL}`
|
|
}
|
|
]
|
|
};
|
|
}
|
|
case "ping_backend": {
|
|
const res = await api.get("/products?limit=1");
|
|
return { content: [{ type: "text", text: `Connection successful!` }] };
|
|
}
|
|
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 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";
|
|
const lang = params.lang ? `&lang=${params.lang}` : "";
|
|
const res = await api.get(`/products?${search}${limit}${lang}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "get_product_detail": {
|
|
const lang = params.lang ? `?lang=${params.lang}` : "";
|
|
const res = await api.get(`/products/${params.id}${lang}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_category_translation": {
|
|
const { id, lang, name } = params as any;
|
|
const res = await api.patch(`/categories/${id}/translations`, {
|
|
name: { [lang]: name }
|
|
});
|
|
return {
|
|
content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }]
|
|
};
|
|
}
|
|
case "update_scent_translation": {
|
|
const { id, lang, name } = params as any;
|
|
const res = await api.patch(`/products/scents/${id}/translations`, {
|
|
name: { [lang]: name }
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_page_translation": {
|
|
const { id, lang, title } = params as any;
|
|
const res = await api.patch(`/pages/${id}/translations`, {
|
|
title: { [lang]: title }
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_hero_translation": {
|
|
const { id, lang, tagline, title, description, buttonText } = params as any;
|
|
const payload: any = {};
|
|
if (tagline) payload.tagline = { [lang]: tagline };
|
|
if (title) payload.title = { [lang]: title };
|
|
if (description) payload.description = { [lang]: description };
|
|
if (buttonText) payload.buttonText = { [lang]: buttonText };
|
|
const res = await api.patch(`/hero/${id}/translations`, payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_menu_item_translation": {
|
|
const { id, lang, label } = params as any;
|
|
const res = await api.patch(`/menus/items/${id}/translations`, {
|
|
label: { [lang]: label }
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_store_settings_translation": {
|
|
const { lang, footerDescription, newsletterTitle, newsletterDescription, newsletterPlaceholder, newsletterButtonText } = params as any;
|
|
const payload: any = {};
|
|
if (footerDescription) payload.footerDescription = { [lang]: footerDescription };
|
|
if (newsletterTitle) payload.newsletterTitle = { [lang]: newsletterTitle };
|
|
if (newsletterDescription) payload.newsletterDescription = { [lang]: newsletterDescription };
|
|
if (newsletterPlaceholder) payload.newsletterPlaceholder = { [lang]: newsletterPlaceholder };
|
|
if (newsletterButtonText) payload.newsletterButtonText = { [lang]: newsletterButtonText };
|
|
const res = await api.patch(`/settings/translations`, payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_article_translation": {
|
|
const { id, lang, title, content } = params as any;
|
|
const payload: any = {};
|
|
if (title) payload.title = { [lang]: title };
|
|
if (content) payload.content = { [lang]: content };
|
|
const res = await api.patch(`/articles/${id}/translations`, payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
|
|
case "update_product_content": {
|
|
const payload: any = {};
|
|
if (params.name) payload.name = params.name;
|
|
if (params.description) payload.description = params.description;
|
|
if (params.details) payload.details = params.details;
|
|
|
|
const res = await api.patch(`/products/${params.id}`, payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "update_product_translation": {
|
|
const payload: any = {};
|
|
if (params.name) payload.name = { [params.lang]: params.name };
|
|
if (params.description) payload.description = { [params.lang]: params.description };
|
|
if (params.details) payload.details = { [params.lang]: params.details };
|
|
|
|
const res = await api.patch(`/products/${params.id}`, payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "upload_product_image_from_url": {
|
|
// 1. Download image
|
|
const imgRes = await axios.get(params.imageUrl, { responseType: 'arraybuffer' });
|
|
const buffer = Buffer.from(imgRes.data, 'binary');
|
|
const ext = params.imageUrl.split('.').pop()?.split('?')[0] || 'jpg';
|
|
|
|
// 2. Upload to backend
|
|
const formData = new FormData();
|
|
formData.append('file', buffer, { filename: `download.${ext}` });
|
|
|
|
const uploadRes = await api.post('/upload', formData, {
|
|
headers: formData.getHeaders()
|
|
});
|
|
const uploadedUrl = uploadRes.data.url;
|
|
|
|
// 3. Get current product to append image
|
|
const prodRes = await api.get(`/products/${params.productId}`);
|
|
const product = prodRes.data;
|
|
const newImages = [...product.images, { url: uploadedUrl, order: product.images.length }];
|
|
|
|
// 4. Update product (using PUT to sync variants and images)
|
|
// Ensure we pass required fields
|
|
const putRes = await api.put(`/products/${params.productId}`, {
|
|
categoryId: product.categoryId,
|
|
description: product.description,
|
|
details: product.details,
|
|
variants: product.variants,
|
|
images: newImages
|
|
});
|
|
|
|
return { content: [{ type: "text", text: `Image uploaded and product updated. New image URL: ${uploadedUrl}` }] };
|
|
}
|
|
case "generate_mock_review": {
|
|
const res = await api.post("/reviews", {
|
|
productId: params.productId,
|
|
rating: params.rating,
|
|
comment: params.comment,
|
|
customerName: params.customerName
|
|
});
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "get_order_status": {
|
|
const res = await api.get(`/orders/${params.orderId}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "list_pending_orders": {
|
|
const res = await api.get(`/orders`);
|
|
const pending = res.data.filter((o: any) => o.status === "PENDING_PAYMENT" || o.status === "PENDING" || o.status === "PROCESSING");
|
|
if (params.limit) {
|
|
pending.splice(params.limit);
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(pending, null, 2) }] };
|
|
}
|
|
case "update_order_tracking": {
|
|
const payload: any = { trackingNo: params.trackingNo };
|
|
const res1 = await api.patch(`/orders/${params.orderId}/tracking`, payload);
|
|
|
|
let result = res1.data;
|
|
if (params.status) {
|
|
const res2 = await api.patch(`/orders/${params.orderId}/status`, { status: params.status });
|
|
result = res2.data;
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
}
|
|
case "search_customer": {
|
|
const search = params.search ? `?search=${encodeURIComponent(params.search)}` : "";
|
|
const res = await api.get(`/customers${search}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "list_dynamic_pages": {
|
|
const res = await api.get("/pages");
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "list_dynamic_pages_metadata": {
|
|
const res = await api.get("/pages");
|
|
let metadata = res.data;
|
|
if (Array.isArray(metadata)) {
|
|
metadata = metadata.map((p: any) => ({ id: p.id, slug: p.slug, title: p.title }));
|
|
} else if (metadata && Array.isArray(metadata.data)) {
|
|
metadata = {
|
|
...metadata,
|
|
data: metadata.data.map((p: any) => ({ id: p.id, slug: p.slug, title: p.title }))
|
|
};
|
|
}
|
|
return { content: [{ type: "text", text: JSON.stringify(metadata, null, 2) }] };
|
|
}
|
|
case "get_dynamic_page_detail": {
|
|
const res = await api.get(`/pages/${params.slug}`);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] };
|
|
}
|
|
case "create_dynamic_page": {
|
|
const payload: any = {
|
|
title: params.title,
|
|
slug: params.slug,
|
|
};
|
|
if (params.isPublished !== undefined) payload.isPublished = params.isPublished;
|
|
if (params.status !== undefined) payload.status = params.status;
|
|
if (params.metadata !== undefined) payload.metadata = params.metadata;
|
|
if (params.id) {
|
|
payload.id = params.id;
|
|
}
|
|
const res = await api.post("/pages", payload);
|
|
return { content: [{ type: "text", text: JSON.stringify(res.data, 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"));
|
|
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 = [];
|
|
|
|
if (params.brandDescription !== undefined || params.socialLinks) {
|
|
const settingsPayload: any = {};
|
|
if (params.brandDescription !== undefined) settingsPayload.footerDescription = params.brandDescription;
|
|
if (params.socialLinks?.facebookUrl !== undefined) settingsPayload.facebookUrl = params.socialLinks.facebookUrl;
|
|
if (params.socialLinks?.instagramUrl !== undefined) settingsPayload.instagramUrl = params.socialLinks.instagramUrl;
|
|
if (params.socialLinks?.tiktokUrl !== undefined) settingsPayload.tiktokUrl = params.socialLinks.tiktokUrl;
|
|
|
|
if (Object.keys(settingsPayload).length > 0) {
|
|
// First fetch existing settings because the backend requires full payload validation
|
|
const currentSettingsRes = await api.get("/settings");
|
|
const currentSettings = currentSettingsRes.data || {};
|
|
|
|
const mergedSettings = { ...currentSettings, ...settingsPayload };
|
|
// Remove null values so Elysia validation doesn't fail on t.Optional(t.String())
|
|
for (const key in mergedSettings) {
|
|
if (mergedSettings[key] === null) {
|
|
delete mergedSettings[key];
|
|
}
|
|
}
|
|
await api.put("/settings", mergedSettings);
|
|
results.push({ type: "settings", status: "success" });
|
|
}
|
|
}
|
|
|
|
if (params.exploreLinks) {
|
|
await api.put("/menus/footer-explore", { items: params.exploreLinks });
|
|
results.push({ type: "explore_links", status: "success" });
|
|
}
|
|
|
|
if (params.collectionLinks) {
|
|
await api.put("/menus/footer-collections", { items: params.collectionLinks });
|
|
results.push({ type: "collection_links", status: "success" });
|
|
}
|
|
|
|
if (params.supportLinks) {
|
|
await api.put("/menus/footer-support", { items: params.supportLinks });
|
|
results.push({ type: "support_links", status: "success" });
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
} catch (err: any) {
|
|
const errorMsg = err.response?.data ? JSON.stringify(err.response.data) : err.message;
|
|
return {
|
|
content: [{ type: "text", text: `Error executing tool: ${errorMsg}` }],
|
|
isError: true,
|
|
};
|
|
}
|
|
});
|
|
|
|
async function main() {
|
|
await performLogin();
|
|
const transport = new StdioServerTransport();
|
|
await server.connect(transport);
|
|
console.error("HomAroma MCP Server running on stdio");
|
|
}
|
|
|
|
main().catch(console.error);
|