fix: MCP server audit — correct API routing through Traefik gateway
CTO Audit findings and fixes:
- Port config: all 4 services were using wrong localhost ports (5002/5003/5004/5019).
All services run behind Traefik on port 80 — consolidated to single gateway client.
- Route fix: /shops/{shopId}/products → /products?shopId= (Traefik routes /shops to merchant-service)
- Response parsing: dashboard API uses "revenue"/"popularItems" (not "totalRevenue"/"topItems")
- Added .gitignore to prevent .env with JWT tokens from being committed
Verified all 12 tools against live Docker services via Traefik gateway.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# GoodGo MCP Server Configuration
|
||||
CATALOG_API_URL=http://localhost:5002/api/v1
|
||||
INVENTORY_API_URL=http://localhost:5003/api/v1
|
||||
FNB_API_URL=http://localhost:5019/api/v1
|
||||
ORDER_API_URL=http://localhost:5004/api/v1
|
||||
|
||||
# API Gateway URL (Traefik routes all services by path prefix)
|
||||
# Docker local: http://localhost/api/v1 (port 80 via Traefik)
|
||||
# Staging: https://api.staging.goodgo.vn/api/v1
|
||||
API_GATEWAY_URL=http://localhost/api/v1
|
||||
|
||||
# Default shop for convenience (Cobic Coffee)
|
||||
DEFAULT_SHOP_ID=e1f392af-fe95-4c7f-8656-5b74ad5fd0a9
|
||||
|
||||
3
services/goodgo-mcp-server/.gitignore
vendored
Normal file
3
services/goodgo-mcp-server/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
@@ -3,6 +3,16 @@ import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
/**
|
||||
* All microservices run behind Traefik API Gateway on port 80.
|
||||
* Traefik routes by path prefix:
|
||||
* /api/v1/products, /api/v1/categories → catalog-service
|
||||
* /api/v1/inventory, /api/v1/stock → inventory-service
|
||||
* /api/v1/kitchen, /api/v1/tables → fnb-engine
|
||||
* /api/v1/orders → order-service
|
||||
*/
|
||||
const GATEWAY_URL = process.env.API_GATEWAY_URL || "http://localhost/api/v1";
|
||||
|
||||
function createClient(baseURL: string): AxiosInstance {
|
||||
const client = axios.create({
|
||||
baseURL,
|
||||
@@ -21,21 +31,14 @@ function createClient(baseURL: string): AxiosInstance {
|
||||
return client;
|
||||
}
|
||||
|
||||
export const catalogApi = createClient(
|
||||
process.env.CATALOG_API_URL || "http://localhost:5002/api/v1"
|
||||
);
|
||||
// Single gateway client — Traefik routes by path prefix
|
||||
const gateway = createClient(GATEWAY_URL);
|
||||
|
||||
export const inventoryApi = createClient(
|
||||
process.env.INVENTORY_API_URL || "http://localhost:5003/api/v1"
|
||||
);
|
||||
|
||||
export const fnbApi = createClient(
|
||||
process.env.FNB_API_URL || "http://localhost:5019/api/v1"
|
||||
);
|
||||
|
||||
export const orderApi = createClient(
|
||||
process.env.ORDER_API_URL || "http://localhost:5004/api/v1"
|
||||
);
|
||||
// Export aliases for backward compatibility (all point to same gateway)
|
||||
export const catalogApi = gateway;
|
||||
export const inventoryApi = gateway;
|
||||
export const fnbApi = gateway;
|
||||
export const orderApi = gateway;
|
||||
|
||||
export const DEFAULT_SHOP_ID =
|
||||
process.env.DEFAULT_SHOP_ID || "e1f392af-fe95-4c7f-8656-5b74ad5fd0a9";
|
||||
|
||||
@@ -9,12 +9,17 @@ interface TopItem {
|
||||
}
|
||||
|
||||
interface DashboardData {
|
||||
totalRevenue: number;
|
||||
// API returns "revenue" (not "totalRevenue")
|
||||
revenue: number;
|
||||
totalRevenue?: number;
|
||||
orderCount: number;
|
||||
itemsSold: number;
|
||||
avgOrderValue: number;
|
||||
topItems: TopItem[];
|
||||
revenueByHour: any[];
|
||||
// API returns "popularItems" (not "topItems")
|
||||
popularItems?: TopItem[];
|
||||
topItems?: TopItem[];
|
||||
hourlyRevenue?: any[];
|
||||
revenueByHour?: any[];
|
||||
}
|
||||
|
||||
interface InventoryItem {
|
||||
@@ -47,12 +52,14 @@ export function registerAnalyticsTools(server: McpServer): void {
|
||||
params: { shopId: resolvedShopId, period },
|
||||
});
|
||||
|
||||
const dashboard: DashboardData =
|
||||
response.data?.data ?? response.data ?? {};
|
||||
// Dashboard API may or may not wrap in {data: ...}
|
||||
const raw = response.data?.data ?? response.data ?? {};
|
||||
const dashboard: DashboardData = raw;
|
||||
|
||||
const topItems: TopItem[] = dashboard.topItems ?? [];
|
||||
const topItems: TopItem[] = dashboard.popularItems ?? dashboard.topItems ?? [];
|
||||
const totalRevenue = dashboard.revenue ?? dashboard.totalRevenue ?? 0;
|
||||
|
||||
if (topItems.length === 0) {
|
||||
if (topItems.length === 0 && totalRevenue === 0) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@@ -65,19 +72,19 @@ export function registerAnalyticsTools(server: McpServer): void {
|
||||
|
||||
const lines = topItems.map((item, i) => {
|
||||
const rank = `#${i + 1}`;
|
||||
const revenue = item.revenue.toLocaleString();
|
||||
return `${rank} ${item.productName} — ${item.quantitySold} sold (${revenue} VND revenue)`;
|
||||
const rev = (item.revenue ?? 0).toLocaleString();
|
||||
return `${rank} ${item.productName} — ${item.quantitySold} sold (${rev} VND revenue)`;
|
||||
});
|
||||
|
||||
const summary = [
|
||||
`Top Selling Products (${period})`,
|
||||
`${"—".repeat(40)}`,
|
||||
`Total Revenue: ${dashboard.totalRevenue.toLocaleString()} VND`,
|
||||
`Total Orders: ${dashboard.orderCount}`,
|
||||
`Items Sold: ${dashboard.itemsSold}`,
|
||||
`Avg Order Value: ${dashboard.avgOrderValue.toLocaleString()} VND`,
|
||||
`Total Revenue: ${totalRevenue.toLocaleString()} VND`,
|
||||
`Total Orders: ${dashboard.orderCount ?? 0}`,
|
||||
`Items Sold: ${dashboard.itemsSold ?? 0}`,
|
||||
`Avg Order Value: ${(dashboard.avgOrderValue ?? 0).toLocaleString()} VND`,
|
||||
``,
|
||||
`Rankings:`,
|
||||
topItems.length > 0 ? `Rankings:` : `(No product rankings available)`,
|
||||
...lines,
|
||||
].join("\n");
|
||||
|
||||
@@ -134,7 +141,7 @@ export function registerAnalyticsTools(server: McpServer): void {
|
||||
)
|
||||
: 0;
|
||||
|
||||
const totalRevenue = dashboard.totalRevenue ?? 0;
|
||||
const totalRevenue = dashboard.revenue ?? dashboard.totalRevenue ?? 0;
|
||||
const grossMargin =
|
||||
totalRevenue > 0
|
||||
? ((totalRevenue - totalInventoryCost) / totalRevenue) * 100
|
||||
|
||||
@@ -65,6 +65,7 @@ export function registerCatalogTools(server: McpServer): void {
|
||||
const resolvedShopId = shopId ?? DEFAULT_SHOP_ID;
|
||||
|
||||
const params: Record<string, unknown> = {
|
||||
shopId: resolvedShopId,
|
||||
isActive,
|
||||
page,
|
||||
pageSize,
|
||||
@@ -72,7 +73,7 @@ export function registerCatalogTools(server: McpServer): void {
|
||||
if (categoryId) params.categoryId = categoryId;
|
||||
|
||||
const { data: res } = await catalogApi.get(
|
||||
`/shops/${resolvedShopId}/products`,
|
||||
`/products`,
|
||||
{ params },
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user