Files
goodgo-platform/libs/mcp-servers/src/reports/reports.server.ts
Ho Ngoc Hai 53c33a1c50 feat(mcp): add industrial parks and reports MCP tool servers
Add IndustrialParkServer for KCN/KCX search and analytics, and
ReportsServer for market report generation. Include unit tests
for industrial parks server.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 05:16:11 +07:00

196 lines
6.0 KiB
TypeScript

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod/v3';
import type { ReportsDeps } from '../shared/types';
const REPORT_TYPES = [
'market_overview',
'district_analysis',
'industrial_zone',
'investment_feasibility',
'price_forecast',
] as const;
const PROPERTY_TYPES = ['apartment', 'house', 'townhouse', 'villa', 'land', 'shophouse', 'industrial'] as const;
const MACRO_DATA_CATEGORIES = [
'gdp',
'population',
'fdi',
'infrastructure',
'real_estate_index',
'construction_permits',
'urbanization',
] as const;
const GenerateReportSchema = {
reportType: z.enum(REPORT_TYPES).describe('Type of report to generate'),
location: z.string().describe('City, district, or province for the report'),
propertyType: z.enum(PROPERTY_TYPES).optional().describe('Filter by property type'),
period: z.enum(['1m', '3m', '6m', '1y', '2y', '5y']).default('1y').describe('Time period for data analysis'),
includeForecasts: z.boolean().default(false).describe('Include price/demand forecasts'),
includeMacro: z.boolean().default(false).describe('Include macro-economic data in the report'),
language: z.enum(['vi', 'en']).default('vi').describe('Report language'),
};
const GetMacroDataSchema = {
province: z.string().describe('Province name (e.g. "Bình Dương", "Hồ Chí Minh")'),
categories: z.array(z.enum(MACRO_DATA_CATEGORIES)).min(1).describe('Data categories to retrieve'),
fromYear: z.number().int().min(2010).default(2020).describe('Start year'),
toYear: z.number().int().max(2030).default(2025).describe('End year'),
};
export function createReportsServer(deps: ReportsDeps): McpServer {
const baseUrl = deps.aiServiceBaseUrl.replace(/\/$/, '');
const server = new McpServer({
name: 'goodgo-reports',
version: '0.1.0',
});
server.tool(
'generate_report',
'Generate a comprehensive real estate market report for a given location and property type.',
GenerateReportSchema,
async (params: z.infer<z.ZodObject<typeof GenerateReportSchema>>) => {
const response = await fetch(`${baseUrl}/reports/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
report_type: params.reportType,
location: params.location,
property_type: params.propertyType ?? null,
period: params.period,
include_forecasts: params.includeForecasts,
include_macro: params.includeMacro,
language: params.language,
}),
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ error: `Report generation error (${response.status}): ${errorText}` }),
}],
isError: true,
};
}
const data = (await response.json()) as GeneratedReport;
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
reportId: data.report_id,
reportType: data.report_type,
title: data.title,
location: data.location,
generatedAt: data.generated_at,
summary: data.summary,
sections: (data.sections ?? []).map((s) => ({
title: s.title,
content: s.content,
charts: s.charts ?? [],
})),
keyMetrics: data.key_metrics ?? {},
forecasts: data.forecasts ?? null,
macroData: data.macro_data ?? null,
}, null, 2),
}],
};
},
);
server.tool(
'get_macro_data',
'Retrieve macro-economic data (GDP, population, FDI, infrastructure) for a Vietnamese province.',
GetMacroDataSchema,
async (params: z.infer<z.ZodObject<typeof GetMacroDataSchema>>) => {
const response = await fetch(`${baseUrl}/reports/macro-data`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
province: params.province,
categories: params.categories,
from_year: params.fromYear,
to_year: params.toYear,
}),
});
if (!response.ok) {
const errorText = await response.text();
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ error: `Macro data error (${response.status}): ${errorText}` }),
}],
isError: true,
};
}
const data = (await response.json()) as MacroDataResponse;
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
province: data.province,
period: { from: params.fromYear, to: params.toYear },
data: Object.fromEntries(
Object.entries(data.data ?? {}).map(([category, series]) => [
category,
(series as MacroDataPoint[]).map((point) => ({
year: point.year,
value: point.value,
unit: point.unit,
yoyChange: point.yoy_change ?? null,
})),
]),
),
highlights: data.highlights ?? [],
}, null, 2),
}],
};
},
);
return server;
}
// Response types from the AI service
interface GeneratedReport {
report_id: string;
report_type: string;
title: string;
location: string;
generated_at: string;
summary: string;
sections?: {
title: string;
content: string;
charts?: { type: string; title: string; data: unknown }[];
}[];
key_metrics?: Record<string, unknown>;
forecasts?: {
price_trend: { period: string; predicted_change_pct: number }[];
demand_trend: { period: string; predicted_change_pct: number }[];
confidence: number;
};
macro_data?: Record<string, unknown>;
}
interface MacroDataPoint {
year: number;
value: number;
unit: string;
yoy_change?: number;
}
interface MacroDataResponse {
province: string;
data?: Record<string, MacroDataPoint[]>;
highlights?: string[];
}