This commit is contained in:
Your Name committed 2026-06-15 21:04:50 -04:00
1 parent 9370869dc2
commit b8873f88b0
4 files changed
+351 -3

No files matched your search

+237 -1
View File
@@ -1,3 +1,4 @@
import crypto from "crypto";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
@@ -5,9 +6,36 @@ import {
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import * as FormData from "form-data";
import FormData from "form-data";
import { PAGE_SECTION_TYPES } from "@homaroma/types";
function getChildrenArray(node: any): 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;
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;
}
let BACKEND_URL = "http://localhost:3051/api";
const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin";
const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD || "homaroma123"; // เปลี่ยนรหัสผ่านตรงนี้ให้ตรงกับ Production ด้วยนะครับ
@@ -348,6 +376,22 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
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",
@@ -452,6 +496,62 @@ 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"]
}
}
],
};
@@ -655,6 +755,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
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,
@@ -784,6 +901,125 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
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);
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}`);
}