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>) => { 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>) => { 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; forecasts?: { price_trend: { period: string; predicted_change_pct: number }[]; demand_trend: { period: string; predicted_change_pct: number }[]; confidence: number; }; macro_data?: Record; } interface MacroDataPoint { year: number; value: number; unit: string; yoy_change?: number; } interface MacroDataResponse { province: string; data?: Record; highlights?: string[]; }