/** * Mock Service Server — port 5100 * * Simulates external/internal enterprise systems for the reference implementation. * MCPlet handlers call these endpoints; replacing with real services requires only * updating the mockServiceUrl in reference.yaml. */ import http from 'node:http'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const dataDir = path.join(__dirname, 'data'); const PORT = 5100; // ---- Data loaders ---- function loadJson(filename: string): T { return JSON.parse(fs.readFileSync(path.join(dataDir, filename), 'utf-8')) as T; } interface Customer { customerId: string; name: string; email: string; cancelTendency: string; cancelTendencyReason: string; reservations: string[]; } interface Reservation { reservationId: string; customerId: string; customerName: string; date: string; time: string; partySize: number; status: string; notes: string; } interface SentEmail { to: string; subject: string; body: string; sentAt: string; } interface SentPost { platform: string; content: string; postedAt: string; } // In-memory log for mock sends const sentEmails: SentEmail[] = []; const sentPosts: SentPost[] = []; // ---- Route handlers ---- function handleCrmCustomers(url: URL, res: http.ServerResponse): void { const filter = url.searchParams.get('filter'); const customers = loadJson('customers.json'); const result = filter === 'rain_cancel_tendency' ? customers.filter((c) => c.cancelTendency === 'high') : customers; sendJson(res, 200, { customers: result, total: result.length }); } function handleCrmReservations(_url: URL, res: http.ServerResponse): void { // Fixed mock: return all reservations regardless of date query const reservations = loadJson('reservations.json'); sendJson(res, 200, { reservations, total: reservations.length }); } function handleErpInventory(url: URL, res: http.ServerResponse): void { const item = url.searchParams.get('item'); const data = loadJson<{ items: Array<{ category: string }> }>('inventory.json'); const items = item ? data.items.filter((i) => i.category === item || i.category.includes(item)) : data.items; sendJson(res, 200, { items, total: items.length }); } function handleWeatherForecast(_url: URL, res: http.ServerResponse): void { // Fixed mock: always return rainy forecast regardless of date query const data = loadJson<{ location: string; forecasts: Array<{ date: string }> }>('weather.json'); sendJson(res, 200, { location: data.location, forecasts: data.forecasts }); } function handleSiteStats(_url: URL, res: http.ServerResponse): void { sendJson(res, 200, { site: 'EPARK', period: 'last_7_days', pageViews: 12480, uniqueVisitors: 3920, reservationPageViews: 2150, conversionRate: 0.18, updatedAt: new Date().toISOString(), }); } async function handleEmailSend(req: http.IncomingMessage, res: http.ServerResponse): Promise { const body = await parseJsonBody<{ to: string; subject: string; body: string }>(req); const entry: SentEmail = { ...body, sentAt: new Date().toISOString() }; sentEmails.push(entry); console.log(`[mock-email] Sent to ${body.to}: ${body.subject}`); sendJson(res, 200, { ok: true, messageId: `mock-${Date.now()}`, sentAt: entry.sentAt }); } async function handleSnsPost(req: http.IncomingMessage, res: http.ServerResponse): Promise { const body = await parseJsonBody<{ platform: string; content: string }>(req); const entry: SentPost = { ...body, postedAt: new Date().toISOString() }; sentPosts.push(entry); console.log(`[mock-sns] Posted to ${body.platform}: ${body.content.slice(0, 50)}...`); sendJson(res, 200, { ok: true, postId: `mock-${Date.now()}`, postedAt: entry.postedAt }); } function handleSentEmails(_url: URL, res: http.ServerResponse): void { sendJson(res, 200, { sentEmails, total: sentEmails.length }); } function handleSentPosts(_url: URL, res: http.ServerResponse): void { sendJson(res, 200, { sentPosts, total: sentPosts.length }); } // ---- Router ---- const server = http.createServer((req, res) => { const url = new URL(req.url ?? '/', `http://localhost:${PORT}`); const method = req.method ?? 'GET'; try { if (method === 'GET' && url.pathname === '/crm/customers') { handleCrmCustomers(url, res); return; } if (method === 'GET' && url.pathname === '/crm/reservations') { handleCrmReservations(url, res); return; } if (method === 'GET' && url.pathname === '/erp/inventory') { handleErpInventory(url, res); return; } if (method === 'GET' && url.pathname === '/weather/forecast') { handleWeatherForecast(url, res); return; } if (method === 'GET' && url.pathname === '/site/stats') { handleSiteStats(url, res); return; } if (method === 'POST' && url.pathname === '/email/send') { void handleEmailSend(req, res); return; } if (method === 'POST' && url.pathname === '/sns/post') { void handleSnsPost(req, res); return; } if (method === 'GET' && url.pathname === '/debug/emails') { handleSentEmails(url, res); return; } if (method === 'GET' && url.pathname === '/debug/posts') { handleSentPosts(url, res); return; } sendJson(res, 404, { error: `Not found: ${method} ${url.pathname}` }); } catch (err) { sendJson(res, 500, { error: (err as Error).message }); } }); server.listen(PORT, () => { console.log(`[mock-services] Listening on http://localhost:${PORT}`); console.log('[mock-services] Endpoints:'); console.log(' GET /crm/customers?filter=rain_cancel_tendency'); console.log(' GET /crm/reservations?date=YYYY-MM-DD'); console.log(' GET /erp/inventory?item=dessert'); console.log(' GET /weather/forecast?date=YYYY-MM-DD'); console.log(' GET /site/stats'); console.log(' POST /email/send'); console.log(' POST /sns/post'); console.log(' GET /debug/emails (sent email log)'); console.log(' GET /debug/posts (sent SNS post log)'); }); // ---- Helpers ---- function sendJson(res: http.ServerResponse, status: number, body: unknown): void { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(body, null, 2)); } function parseJsonBody(req: http.IncomingMessage): Promise { return new Promise((resolve, reject) => { let data = ''; req.on('data', (chunk: Buffer) => { data += chunk.toString(); }); req.on('end', () => { try { resolve(JSON.parse(data) as T); } catch { reject(new Error('Invalid JSON')); } }); req.on('error', reject); }); }