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"; const BACKEND_URL = process.env.BACKEND_URL || "http://localhost:3000/api"; const ADMIN_USERNAME = process.env.ADMIN_USERNAME || "admin"; const 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: "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" } } } }, { name: "get_product_detail", description: "Get full product details including variants, images, and category", inputSchema: { type: "object", properties: { id: { type: "number", description: "Product ID" } }, required: ["id"] } }, { 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: "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"] } } ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const params = request.params.arguments as any; try { switch (request.params.name) { case "ping_backend": { const res = await api.get("/products?limit=1"); return { content: [{ type: "text", text: `Connection successful!` }] }; } case "search_products": { const search = params.search ? `&search=${encodeURIComponent(params.search)}` : ""; const limit = params.limit ? `&limit=${params.limit}` : "&limit=10"; const res = await api.get(`/products?${search}${limit}`); return { content: [{ type: "text", text: JSON.stringify(res.data, null, 2) }] }; } case "get_product_detail": { const res = await api.get(`/products/${params.id}`); 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 "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) }] }; } 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);