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
|
# 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
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();
|
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";
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user