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 # GoodGo MCP Server Configuration
CATALOG_API_URL=http://localhost:5002/api/v1
INVENTORY_API_URL=http://localhost:5003/api/v1 # API Gateway URL (Traefik routes all services by path prefix)
FNB_API_URL=http://localhost:5019/api/v1 # Docker local: http://localhost/api/v1 (port 80 via Traefik)
ORDER_API_URL=http://localhost:5004/api/v1 # Staging: https://api.staging.goodgo.vn/api/v1
API_GATEWAY_URL=http://localhost/api/v1
# Default shop for convenience (Cobic Coffee) # Default shop for convenience (Cobic Coffee)
DEFAULT_SHOP_ID=e1f392af-fe95-4c7f-8656-5b74ad5fd0a9 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(); 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 { function createClient(baseURL: string): AxiosInstance {
const client = axios.create({ const client = axios.create({
baseURL, baseURL,
@@ -21,21 +31,14 @@ function createClient(baseURL: string): AxiosInstance {
return client; return client;
} }
export const catalogApi = createClient( // Single gateway client — Traefik routes by path prefix
process.env.CATALOG_API_URL || "http://localhost:5002/api/v1" const gateway = createClient(GATEWAY_URL);
);
export const inventoryApi = createClient( // Export aliases for backward compatibility (all point to same gateway)
process.env.INVENTORY_API_URL || "http://localhost:5003/api/v1" export const catalogApi = gateway;
); export const inventoryApi = gateway;
export const fnbApi = gateway;
export const fnbApi = createClient( export const orderApi = gateway;
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 const DEFAULT_SHOP_ID = export const DEFAULT_SHOP_ID =
process.env.DEFAULT_SHOP_ID || "e1f392af-fe95-4c7f-8656-5b74ad5fd0a9"; process.env.DEFAULT_SHOP_ID || "e1f392af-fe95-4c7f-8656-5b74ad5fd0a9";

View File

@@ -9,12 +9,17 @@ interface TopItem {
} }
interface DashboardData { interface DashboardData {
totalRevenue: number; // API returns "revenue" (not "totalRevenue")
revenue: number;
totalRevenue?: number;
orderCount: number; orderCount: number;
itemsSold: number; itemsSold: number;
avgOrderValue: number; avgOrderValue: number;
topItems: TopItem[]; // API returns "popularItems" (not "topItems")
revenueByHour: any[]; popularItems?: TopItem[];
topItems?: TopItem[];
hourlyRevenue?: any[];
revenueByHour?: any[];
} }
interface InventoryItem { interface InventoryItem {
@@ -47,12 +52,14 @@ export function registerAnalyticsTools(server: McpServer): void {
params: { shopId: resolvedShopId, period }, params: { shopId: resolvedShopId, period },
}); });
const dashboard: DashboardData = // Dashboard API may or may not wrap in {data: ...}
response.data?.data ?? response.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 { return {
content: [ content: [
{ {
@@ -65,19 +72,19 @@ export function registerAnalyticsTools(server: McpServer): void {
const lines = topItems.map((item, i) => { const lines = topItems.map((item, i) => {
const rank = `#${i + 1}`; const rank = `#${i + 1}`;
const revenue = item.revenue.toLocaleString(); const rev = (item.revenue ?? 0).toLocaleString();
return `${rank} ${item.productName}${item.quantitySold} sold (${revenue} VND revenue)`; return `${rank} ${item.productName}${item.quantitySold} sold (${rev} VND revenue)`;
}); });
const summary = [ const summary = [
`Top Selling Products (${period})`, `Top Selling Products (${period})`,
`${"—".repeat(40)}`, `${"—".repeat(40)}`,
`Total Revenue: ${dashboard.totalRevenue.toLocaleString()} VND`, `Total Revenue: ${totalRevenue.toLocaleString()} VND`,
`Total Orders: ${dashboard.orderCount}`, `Total Orders: ${dashboard.orderCount ?? 0}`,
`Items Sold: ${dashboard.itemsSold}`, `Items Sold: ${dashboard.itemsSold ?? 0}`,
`Avg Order Value: ${dashboard.avgOrderValue.toLocaleString()} VND`, `Avg Order Value: ${(dashboard.avgOrderValue ?? 0).toLocaleString()} VND`,
``, ``,
`Rankings:`, topItems.length > 0 ? `Rankings:` : `(No product rankings available)`,
...lines, ...lines,
].join("\n"); ].join("\n");
@@ -134,7 +141,7 @@ export function registerAnalyticsTools(server: McpServer): void {
) )
: 0; : 0;
const totalRevenue = dashboard.totalRevenue ?? 0; const totalRevenue = dashboard.revenue ?? dashboard.totalRevenue ?? 0;
const grossMargin = const grossMargin =
totalRevenue > 0 totalRevenue > 0
? ((totalRevenue - totalInventoryCost) / totalRevenue) * 100 ? ((totalRevenue - totalInventoryCost) / totalRevenue) * 100

View File

@@ -65,6 +65,7 @@ export function registerCatalogTools(server: McpServer): void {
const resolvedShopId = shopId ?? DEFAULT_SHOP_ID; const resolvedShopId = shopId ?? DEFAULT_SHOP_ID;
const params: Record<string, unknown> = { const params: Record<string, unknown> = {
shopId: resolvedShopId,
isActive, isActive,
page, page,
pageSize, pageSize,
@@ -72,7 +73,7 @@ export function registerCatalogTools(server: McpServer): void {
if (categoryId) params.categoryId = categoryId; if (categoryId) params.categoryId = categoryId;
const { data: res } = await catalogApi.get( const { data: res } = await catalogApi.get(
`/shops/${resolvedShopId}/products`, `/products`,
{ params }, { params },
); );