UNPKG

@spaik/mcp-server-roi

Version:

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

566 lines 24.7 kB
import { z } from 'zod'; import { mcpDb } from '../db/supabase.js'; import { MLComparisonEngine } from '../services/ml-comparison-engine.js'; import { DutchBenchmarkValidator } from '../services/dutch-benchmark-validator.js'; import { createLogger } from '../utils/logger.js'; import { DatabaseError, ValidationError, ConfigurationError } from '../utils/errors.js'; export const CompareProjectsSchema = z.object({ project_ids: z.array(z.string().uuid()).min(2).max(10), comparison_metrics: z.array(z.enum([ 'roi', 'payback_period', 'npv', 'total_investment', 'monthly_benefit', 'risk_score', 'implementation_complexity', 'success_probability', 'synergies' ])).default(['roi', 'payback_period', 'npv', 'risk_score', 'success_probability']), time_horizon: z.number().min(12).max(120).default(60), // months enable_ml_insights: z.boolean().default(true), include_visualizations: z.boolean().default(false) }); export async function compareProjects(input) { const logger = createLogger({ tool: 'compare_projects' }); logger.info('Starting project comparison', { project_count: input.project_ids.length, ml_enabled: input.enable_ml_insights }); try { // Step 1: Check for Perplexity API key const perplexityApiKey = process.env.PERPLEXITY_API_KEY; if (!perplexityApiKey) { throw new ConfigurationError('PERPLEXITY_API_KEY is required for project comparisons. Please configure it in your environment.'); } // Step 2: Validate input const validatedInput = CompareProjectsSchema.parse(input); // Step 3: Fetch all project data in parallel const projectData = await fetchProjectData(validatedInput.project_ids); // Step 4: Initialize Dutch validator const dutchValidator = new DutchBenchmarkValidator(perplexityApiKey); const projectValidations = new Map(); // Step 5: Validate each project against Dutch benchmarks logger.info('Validating projects against Dutch market benchmarks'); for (const pd of projectData) { if (pd.project.id && pd.projections.length > 0) { const projection = pd.projections[0]; // Use latest projection // Extract implementation costs from projection const implementationCosts = projection.implementation_costs || { software_licenses: projection.calculations?.total_investment * 0.3 || 0, development_hours: 1000, // Default estimate training_costs: projection.calculations?.total_investment * 0.1 || 0, infrastructure: projection.calculations?.total_investment * 0.2 || 0, ongoing_monthly: 0 }; const validation = await dutchValidator.validateProjectInputs({ industry: pd.project.industry, useCases: pd.useCases, implementationCosts, timelineMonths: projection.timeline_months || 12 }); projectValidations.set(pd.project.id, validation); if (validation.validationIssues.length > 0) { logger.info('Validation adjustments for project', { projectId: pd.project.id, adjustmentCount: validation.validationIssues.length }); } } } // Initialize ML engine const mlEngine = validatedInput.enable_ml_insights ? new MLComparisonEngine() : null; // Perform ML analysis if enabled let mlResults = []; if (mlEngine) { logger.debug('Running ML comparison analysis'); mlResults = await mlEngine.compareProjects(projectData .filter(pd => pd.project.id) // Filter out projects without IDs .map(pd => ({ id: pd.project.id, projection: pd.projections[0], // Use latest projection useCases: pd.useCases, industry: pd.project.industry, companySize: 'medium' // Could be retrieved from actual data }))); } // Calculate metrics for each project const projectMetrics = calculateProjectMetrics(projectData, validatedInput.comparison_metrics); // Generate rankings const rankings = generateRankings(projectMetrics, mlResults); // Identify insights const insights = generateInsights(projectData, projectMetrics, mlResults); // Generate recommendations including Dutch market insights const recommendations = generateRecommendations(projectData, projectMetrics, mlResults, projectValidations); // Add Dutch market-specific recommendations const dutchRecommendations = generateDutchMarketRecommendations(projectValidations); recommendations.push(...dutchRecommendations); // Create visualizations if requested const visualizations = validatedInput.include_visualizations ? generateVisualizations(projectMetrics, mlResults) : undefined; // Generate Dutch market summary const dutchMarketSummary = generateDutchMarketSummary(projectValidations); // Build the result const result = { projects: projectData .filter(pd => pd.project.id) // Filter out projects without IDs .map((pd, index) => { const projectId = pd.project.id; const mlResult = mlResults.find(ml => ml.projectId === projectId); const validation = projectValidations.get(projectId); return { id: projectId, name: pd.project.project_name, client: pd.project.client_name, industry: pd.project.industry, status: pd.project.status || 'active', metrics: projectMetrics.get(projectId), mlInsights: mlResult, dutchMarketValidation: validation ? { adjustmentsMade: validation.validationIssues.length, validationIssues: validation.validationIssues, marketInsights: validation.marketInsights } : undefined }; }), rankings, insights, recommendations, dutchMarketSummary, visualization: visualizations }; logger.info('Comparison completed with Dutch market validation', { top_performer: insights.bestPerformer.projectId, ml_predictions_generated: mlResults.length, dutch_validations_performed: projectValidations.size, total_adjustments: dutchMarketSummary.totalAdjustments }); return result; } catch (error) { logger.error('Comparison failed', error); if (error instanceof ValidationError) { throw error; } if (error instanceof DatabaseError) { throw new DatabaseError('Failed to fetch project data. Please ensure all project IDs are valid.', { error: error.message }); } throw new Error(`Unexpected error in project comparison: ${error.message}`); } } /** * Fetch all project data including projections and use cases */ async function fetchProjectData(projectIds) { const logger = createLogger({ component: 'fetchProjectData' }); // Fetch all data in parallel const results = await Promise.all(projectIds.map(async (projectId) => { // Fetch project const { data: project, error: projectError } = await mcpDb .from('projects') .select('*') .eq('id', projectId) .single(); if (projectError || !project) { throw new DatabaseError(`Project not found: ${projectId}`, { error: projectError?.message }); } // Fetch projections const { data: projections, error: projectionError } = await mcpDb .from('projections') .select('*') .eq('project_id', projectId) .order('created_at', { ascending: false }); if (projectionError || !projections || projections.length === 0) { throw new DatabaseError(`No projections found for project: ${projectId}`, { error: projectionError?.message }); } // Fetch use cases const { data: useCases, error: useCaseError } = await mcpDb .from('use_cases') .select('*') .eq('project_id', projectId); if (useCaseError || !useCases) { throw new DatabaseError(`Failed to fetch use cases for project: ${projectId}`, { error: useCaseError?.message }); } return { project, projections, useCases }; })); logger.debug('Fetched project data', { count: results.length }); return results; } /** * Calculate metrics for each project */ function calculateProjectMetrics(projectData, requestedMetrics) { const metricsMap = new Map(); for (const pd of projectData) { const projection = pd.projections[0]; // Use latest projection const metrics = {}; // Financial metrics if (requestedMetrics.includes('roi')) { metrics.roi = projection.calculations.five_year_roi; } if (requestedMetrics.includes('payback_period')) { metrics.payback_period = projection.calculations.payback_period_months || 999; } if (requestedMetrics.includes('npv')) { metrics.npv = projection.calculations.net_present_value; } if (requestedMetrics.includes('total_investment')) { metrics.total_investment = projection.calculations.total_investment; } if (requestedMetrics.includes('monthly_benefit')) { metrics.monthly_benefit = calculateMonthlyBenefit(pd.useCases); } // Risk and complexity metrics if (requestedMetrics.includes('risk_score')) { metrics.risk_score = calculateRiskScore(pd.useCases, projection); } if (requestedMetrics.includes('implementation_complexity')) { metrics.implementation_complexity = calculateComplexity(pd.useCases); } if (pd.project.id) { metricsMap.set(pd.project.id, metrics); } } return metricsMap; } /** * Generate rankings based on metrics and ML results */ function generateRankings(projectMetrics, mlResults) { const rankings = { byMetric: {}, overall: [], mlBased: [] }; // Get all projects const projectIds = Array.from(projectMetrics.keys()); // Rank by each metric const firstMetrics = projectMetrics.values().next().value; const metrics = firstMetrics ? Object.keys(firstMetrics) : []; for (const metric of metrics) { const sorted = [...projectIds].sort((a, b) => { const aValue = projectMetrics.get(a)[metric]; const bValue = projectMetrics.get(b)[metric]; // Lower is better for these metrics if (['payback_period', 'risk_score', 'implementation_complexity'].includes(metric)) { return aValue - bValue; } // Higher is better for others return bValue - aValue; }); rankings.byMetric[metric] = sorted; } // Calculate overall ranking (weighted average of ranks) const weights = { roi: 0.3, payback_period: 0.2, npv: 0.2, risk_score: 0.15, total_investment: 0.15 }; const overallScores = new Map(); for (const projectId of projectIds) { let score = 0; let totalWeight = 0; for (const [metric, weight] of Object.entries(weights)) { if (rankings.byMetric[metric]) { const rank = rankings.byMetric[metric].indexOf(projectId) + 1; score += rank * weight; totalWeight += weight; } } overallScores.set(projectId, score / totalWeight); } rankings.overall = [...projectIds].sort((a, b) => overallScores.get(a) - overallScores.get(b)); // ML-based ranking if available if (mlResults.length > 0) { rankings.mlBased = mlResults .sort((a, b) => a.ranking.overall - b.ranking.overall) .map(r => r.projectId); } return rankings; } /** * Generate insights from the comparison */ function generateInsights(projectData, projectMetrics, mlResults) { const insights = { bestPerformer: { projectId: '', reason: '' }, riskiest: { projectId: '', risks: [] }, quickestPayback: { projectId: '', months: 0 } }; // Find best performer let bestROI = -Infinity; for (const [projectId, metrics] of projectMetrics.entries()) { if (metrics.roi > bestROI) { bestROI = metrics.roi; insights.bestPerformer.projectId = projectId; insights.bestPerformer.reason = `Highest ROI of ${bestROI.toFixed(1)}%`; } } // Find riskiest project let highestRisk = -Infinity; let riskiestId = ''; for (const mlResult of mlResults) { if (mlResult.mlPredictions.riskScore > highestRisk) { highestRisk = mlResult.mlPredictions.riskScore; riskiestId = mlResult.projectId; } } if (riskiestId) { const mlResult = mlResults.find(r => r.projectId === riskiestId); insights.riskiest.projectId = riskiestId; insights.riskiest.risks = mlResult.mlPredictions.keyRiskFactors .filter(r => r.impact === 'high') .map(r => r.factor); } // Find quickest payback let quickestPayback = Infinity; for (const [projectId, metrics] of projectMetrics.entries()) { if (metrics.payback_period < quickestPayback) { quickestPayback = metrics.payback_period; insights.quickestPayback.projectId = projectId; insights.quickestPayback.months = quickestPayback; } } // Extract synergies if available if (mlResults.length > 0) { const allSynergies = []; for (const result of mlResults) { if (result.mlPredictions.synergies) { for (const synergy of result.mlPredictions.synergies) { allSynergies.push({ projects: [result.projectId, synergy.withProject], type: synergy.type, value: synergy.estimatedValue }); } } } // Deduplicate synergies const uniqueSynergies = allSynergies.filter((s, index) => allSynergies.findIndex(s2 => s2.projects.sort().join(',') === s.projects.sort().join(',')) === index); if (uniqueSynergies.length > 0) { insights.synergies = uniqueSynergies; } } return insights; } /** * Generate recommendations based on analysis */ function generateRecommendations(projectData, projectMetrics, mlResults, validations) { const recommendations = []; // ML-based recommendations for (const mlResult of mlResults) { if (mlResult.recommendation === 'strongly_recommend') { const project = projectData.find(pd => pd.project.id === mlResult.projectId); recommendations.push(`Strongly recommend proceeding with "${project.project.project_name}" - ` + `ML analysis shows ${(mlResult.mlPredictions.successProbability * 100).toFixed(0)}% success probability`); } else if (mlResult.recommendation === 'not_recommended') { const project = projectData.find(pd => pd.project.id === mlResult.projectId); recommendations.push(`Consider deferring "${project.project.project_name}" - ` + `High risk score (${mlResult.mlPredictions.riskScore.toFixed(1)}/10) and low success probability`); } } // Validation-based recommendations for (const [projectId, validation] of validations.entries()) { const project = projectData.find(pd => pd.project.id === projectId); if (validation.validationIssues.filter(i => i.severity === 'warning').length > 3) { recommendations.push(`Review assumptions for "${project.project.project_name}" - ` + `Multiple values exceed Dutch market norms and have been adjusted`); } // Check for significant adjustments const significantAdjustments = validation.validationIssues.filter(i => i.originalValue / i.adjustedValue > 1.5 || i.adjustedValue / i.originalValue > 1.5); if (significantAdjustments.length > 0) { recommendations.push(`"${project.project.project_name}" had ${significantAdjustments.length} significant adjustments ` + `to align with Dutch market realities. Consider reviewing the business case.`); } } // Portfolio recommendations const totalInvestment = Array.from(projectMetrics.values()) .reduce((sum, m) => sum + (m.total_investment || 0), 0); if (totalInvestment > 5000000) { recommendations.push(`Total portfolio investment exceeds $5M. Consider phased implementation ` + `starting with quick-win projects to generate early ROI`); } // Synergy recommendations const synergies = mlResults .flatMap(r => r.mlPredictions.synergies || []) .filter((s, index, self) => self.findIndex(s2 => s2.withProject === s.withProject) === index); if (synergies.length > 0) { const totalSynergyValue = synergies.reduce((sum, s) => sum + s.estimatedValue, 0); recommendations.push(`Identified synergies worth $${totalSynergyValue.toLocaleString()}. ` + `Consider bundling related projects for maximum value`); } return recommendations; } /** * Generate visualization data */ function generateVisualizations(projectMetrics, mlResults) { const visualizations = []; // ROI vs Risk scatter plot const scatterData = { type: 'scatter', data: { datasets: [{ label: 'Projects', data: Array.from(projectMetrics.entries()).map(([projectId, metrics]) => { const mlResult = mlResults.find(r => r.projectId === projectId); return { x: metrics.roi, y: mlResult?.mlPredictions.riskScore || metrics.risk_score || 5, label: projectId }; }) }] }, options: { scales: { x: { title: { text: 'ROI (%)' } }, y: { title: { text: 'Risk Score' } } } } }; visualizations.push(scatterData); // Timeline comparison const timelineData = { type: 'timeline', data: Array.from(projectMetrics.entries()).map(([projectId, metrics]) => ({ projectId, paybackPeriod: metrics.payback_period, totalDuration: 60 // 5 years })) }; visualizations.push(timelineData); return visualizations; } /** * Format benchmark comparison for display */ function formatBenchmarkComparison(projectMetrics, benchmarks) { const comparisons = []; for (const benchmark of benchmarks) { const metricName = benchmark.metric.toLowerCase().replace(/\s+/g, '_'); const projectValue = projectMetrics[metricName]; if (projectValue !== undefined) { // Calculate percentile const percentile = calculatePercentile(projectValue, benchmark.range); comparisons.push({ metric: benchmark.metric, projectValue, industryAverage: benchmark.recommendedValue, percentile }); } } return comparisons; } /** * Helper functions */ function calculateMonthlyBenefit(useCases) { return useCases.reduce((sum, uc) => { const volume = uc.current_state.volume_per_month; const costPerTransaction = uc.current_state.cost_per_transaction; const automation = uc.future_state.automation_percentage; return sum + (volume * costPerTransaction * automation); }, 0); } function calculateRiskScore(useCases, projection) { // Simple risk calculation based on complexity and timeline const avgComplexity = useCases.reduce((sum, uc) => sum + (uc.implementation?.complexity_score || 5), 0) / useCases.length; const timelineRisk = Math.min(10, projection.timeline_months / 6); return (avgComplexity + timelineRisk) / 2; } function calculateComplexity(useCases) { return useCases.reduce((sum, uc) => sum + (uc.implementation?.complexity_score || 5), 0) / useCases.length; } function calculatePercentile(value, range) { if (value <= range.min) return 0; if (value >= range.max) return 100; if (value <= range.p25) { return (value - range.min) / (range.p25 - range.min) * 25; } else if (value <= range.p75) { return 25 + (value - range.p25) / (range.p75 - range.p25) * 50; } else { return 75 + (value - range.p75) / (range.max - range.p75) * 25; } } /** * Generate Dutch market-specific recommendations */ function generateDutchMarketRecommendations(validations) { const recommendations = []; // Analyze common issues across all projects const allIssues = []; const industries = new Set(); validations.forEach((validation, projectId) => { validation.validationIssues.forEach(issue => { if (issue.severity === 'warning' || issue.severity === 'error') { allIssues.push(issue.field); } }); validation.marketInsights.forEach(insight => { industries.add(insight.metric); }); }); // Generate recommendations based on common patterns if (allIssues.filter(i => i.includes('monthly_cost_reduction')).length > 0) { recommendations.push('Consider phased rollouts aligned with Dutch market maturity levels to achieve more realistic savings targets.'); } if (allIssues.filter(i => i.includes('timelineMonths')).length > 0) { recommendations.push('Account for Dutch regulatory requirements (GDPR, works council) which typically add 2-3 months to implementation timelines.'); } // Add industry-specific Dutch recommendations recommendations.push('Leverage Netherlands-specific AI funding opportunities through RVO and regional development agencies.', 'Ensure compliance with the upcoming EU AI Act which will impact Dutch implementations from 2024 onwards.', 'Consider partnering with Dutch knowledge institutions (TNO, universities) for R&D tax benefits (WBSO).'); return recommendations; } /** * Generate Dutch market summary from validations */ function generateDutchMarketSummary(validations) { let totalAdjustments = 0; const commonIssues = new Map(); const allMarketTrends = []; const allCitations = []; // Aggregate data from all validations validations.forEach(validation => { totalAdjustments += validation.validationIssues.length; // Count common issues validation.validationIssues.forEach(issue => { const key = issue.reason.split('.')[0]; // Get first sentence as key commonIssues.set(key, (commonIssues.get(key) || 0) + 1); }); // Collect market trends validation.marketInsights.forEach(insight => { allMarketTrends.push(`${insight.metric}: ${insight.trend}`); }); // Collect citations validation.citations.forEach(citation => { if (!allCitations.some(c => c.url === citation.url)) { allCitations.push(citation); } }); }); // Get top 3 most common issues const sortedIssues = Array.from(commonIssues.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([issue, _]) => issue); // Get unique market trends const uniqueTrends = [...new Set(allMarketTrends)].slice(0, 5); return { totalAdjustments, commonIssues: sortedIssues, marketTrends: uniqueTrends, citations: allCitations.slice(0, 10) // Limit to 10 most relevant citations }; } //# sourceMappingURL=compare-projects.js.map