Files
homaroma/apps/mcp-server/index.ts
T
2026-06-07 07:33:41 +07:00

220 lines
7.2 KiB
TypeScript

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);