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:
Ho Ngoc Hai
2026-03-15 17:03:18 +07:00
parent b7a194f14b
commit 20cf8781b8
5 changed files with 49 additions and 34 deletions

View File

@@ -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
View File

@@ -0,0 +1,3 @@
node_modules/
dist/
.env

View File

@@ -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";

View File

@@ -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

View File

@@ -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 },
);