UNPKG

@spaik/mcp-server-roi

Version:

MCP server for AI ROI prediction and tracking with Monte Carlo simulations

211 lines 8.21 kB
import { parentPort } from 'worker_threads'; import { calculateNPV, calculatePaybackPeriod, calculateMonthlyFlow, FINANCIAL_CONSTANTS } from '../core/calculators/financial-utils.js'; import { WorkerError, CalculationError } from '../utils/errors.js'; // Worker-specific logger (writes to stderr to avoid stdout conflicts) const workerLog = (level, message, data) => { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, worker: 'monte-carlo', message, ...(data && { data }) }; console.error(JSON.stringify(logEntry)); }; class Random { seed; constructor(seed) { this.seed = seed; } // Linear Congruential Generator next() { this.seed = (this.seed * 1664525 + 1013904223) % 2147483647; return this.seed / 2147483647; } uniform(min, max) { return min + this.next() * (max - min); } normal(mean, stdDev) { // Box-Muller transform const u1 = this.next(); const u2 = this.next(); const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); return mean + z0 * stdDev; } beta(alpha, beta) { // Simple approximation using uniform distribution // For production, use a proper beta distribution implementation const u = this.next(); return Math.pow(u, alpha) / (Math.pow(u, alpha) + Math.pow(1 - u, beta)); } } function generateRandomValue(variable, random) { switch (variable.distribution) { case 'normal': { const mean = (variable.min + variable.max) / 2; const stdDev = (variable.max - variable.min) / 6; // 99.7% within range let value = random.normal(mean, stdDev); // Clamp to bounds return Math.max(variable.min, Math.min(variable.max, value)); } case 'beta': { const value = random.beta(2, 2); // Beta(2,2) is bell-shaped return variable.min + value * (variable.max - variable.min); } case 'uniform': default: return random.uniform(variable.min, variable.max); } } function runSimulation(task, random) { const { baseCase, variables } = task; // Generate random values for this iteration const adoptionRate = generateRandomValue(variables.adoptionRate, random); const efficiencyGain = generateRandomValue(variables.efficiencyGain, random); const implementationDelay = generateRandomValue(variables.implementationDelay, random); const costOverrun = generateRandomValue(variables.costOverrun, random); // Adjust base case with random factors const adjustedMonthlyBenefit = baseCase.monthlyBenefit * adoptionRate * efficiencyGain; const adjustedInvestment = baseCase.totalInvestment * costOverrun; const adjustedImplementationMonths = baseCase.implementationMonths + implementationDelay; // Calculate cash flows const cashFlows = [-adjustedInvestment]; for (let month = 1; month <= baseCase.timelineMonths; month++) { const monthlyFlow = calculateMonthlyFlow(month, adjustedMonthlyBenefit, adjustedImplementationMonths, baseCase.rampUpMonths, baseCase.ongoingMonthlyCosts); cashFlows.push(monthlyFlow); } // Calculate metrics const totalBenefit = cashFlows.slice(1).reduce((sum, flow) => sum + Math.max(0, flow), 0); const totalCost = adjustedInvestment + baseCase.ongoingMonthlyCosts * baseCase.timelineMonths; const roi = ((totalBenefit - totalCost) / totalCost) * 100; // NPV calculation using shared utility const npv = calculateNPV(cashFlows, FINANCIAL_CONSTANTS.DEFAULT_DISCOUNT_RATE); // Payback period using shared utility const paybackMonths = calculatePaybackPeriod(cashFlows); return { roi, npv, paybackMonths, finalAdoptionRate: adoptionRate, finalEfficiencyGain: efficiencyGain, totalCost }; } // Worker message handler parentPort?.on('message', (task) => { const startTime = Date.now(); workerLog('info', 'Starting simulation task', { iterations: task.iterations, seed: task.seed }); try { // Validate task input if (!task || typeof task !== 'object') { throw new WorkerError('Invalid task received', { task }); } if (!task.iterations || task.iterations < 1 || task.iterations > 1000000) { throw new WorkerError('Invalid iteration count', { iterations: task.iterations, min: 1, max: 1000000 }); } if (!task.baseCase || typeof task.baseCase !== 'object') { throw new WorkerError('Invalid base case data', { baseCase: task.baseCase }); } // Validate financial values const { baseCase } = task; if (baseCase.monthlyBenefit < 0 || !isFinite(baseCase.monthlyBenefit)) { throw new CalculationError('Invalid monthly benefit', { value: baseCase.monthlyBenefit }); } if (baseCase.totalInvestment < 0 || !isFinite(baseCase.totalInvestment)) { throw new CalculationError('Invalid total investment', { value: baseCase.totalInvestment }); } const results = []; const random = new Random(task.seed); // Run simulations with progress logging const logInterval = Math.max(1, Math.floor(task.iterations / 10)); for (let i = 0; i < task.iterations; i++) { try { const result = runSimulation(task, random); // Validate simulation result if (!isFinite(result.roi) || !isFinite(result.npv)) { throw new CalculationError('Simulation produced invalid results', { iteration: i, roi: result.roi, npv: result.npv }); } results.push(result); // Log progress if (i > 0 && i % logInterval === 0) { workerLog('debug', 'Simulation progress', { completed: i, total: task.iterations, percentage: Math.round((i / task.iterations) * 100) }); } } catch (error) { workerLog('error', `Simulation failed at iteration ${i}`, { error: error.message }); // Continue with next iteration instead of failing completely } } const duration = Date.now() - startTime; workerLog('info', 'Simulation task completed', { iterations: task.iterations, results: results.length, duration_ms: duration }); // Send results back to main thread parentPort?.postMessage({ success: true, results, metadata: { totalIterations: task.iterations, successfulIterations: results.length, duration } }); } catch (error) { const duration = Date.now() - startTime; workerLog('error', 'Worker task failed', { error: error.message, stack: error.stack, duration_ms: duration }); // Send error back to main thread parentPort?.postMessage({ success: false, error: { message: error.message, type: error instanceof Error ? error.constructor.name : 'UnknownError', context: error.context } }); } }); // Handle worker errors process.on('uncaughtException', (error) => { workerLog('error', 'Uncaught exception in worker', { error: error.message, stack: error.stack }); process.exit(1); }); process.on('unhandledRejection', (reason, promise) => { workerLog('error', 'Unhandled rejection in worker', { reason, promise }); process.exit(1); }); //# sourceMappingURL=monte-carlo-worker.js.map