UNPKG

@spaik/mcp-server-roi

Version:

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

359 lines 13.7 kB
import { z } from 'zod'; import { SonarBenchmarkService } from './sonar-benchmark-service.js'; import { createLogger } from '../utils/logger.js'; import { getBenchmarks } from '../core/benchmarks/index.js'; import { rateLimiters } from '../utils/rate-limiter.js'; // Aggregated benchmark result export const AggregatedBenchmarkSchema = z.object({ metric: z.string(), value: z.number(), unit: z.string(), confidence: z.number().min(0).max(1), sources: z.array(z.object({ name: z.string(), value: z.number(), date: z.string(), weight: z.number() })), consensus: z.enum(['strong', 'moderate', 'weak']), recommendedValue: z.number(), range: z.object({ min: z.number(), max: z.number(), p25: z.number(), p75: z.number() }) }); export class BenchmarkAggregator { logger = createLogger({ service: 'BenchmarkAggregator' }); sonarService; sources = []; fmpApiKey; constructor(config) { // Initialize Sonar service if API key provided if (config?.sonarApiKey && config?.enableSonar !== false) { this.sonarService = new SonarBenchmarkService({ apiKey: config.sonarApiKey }); this.sources.push({ name: 'Perplexity Sonar', fetchData: (req) => this.sonarService.fetchBenchmarks(req), weight: 0.4, // High weight for real-time data priority: 1 }); } // Store FMP API key if provided if (config?.fmpApiKey && config?.enableFMP !== false) { this.fmpApiKey = config.fmpApiKey; this.sources.push({ name: 'Financial Modeling Prep', fetchData: (req) => this.fetchFMPBenchmarks(req), weight: 0.3, priority: 2 }); } // Always include static benchmarks as fallback this.sources.push({ name: 'Static Benchmarks', fetchData: (req) => this.fetchStaticBenchmarks(req), weight: 0.3, priority: 3 }); // Sort sources by priority this.sources.sort((a, b) => a.priority - b.priority); } /** * Aggregate benchmarks from all available sources */ async aggregateBenchmarks(request) { this.logger.info('Starting benchmark aggregation', { industry: request.industry, sources: this.sources.map(s => s.name) }); // Fetch data from all sources in parallel const sourceResults = await Promise.allSettled(this.sources.map(async (source) => { const startTime = Date.now(); try { const data = await source.fetchData(request); const duration = Date.now() - startTime; this.logger.debug('Source fetch completed', { source: source.name, count: data.length, duration }); return { source, data }; } catch (error) { this.logger.error(`Source fetch failed: ${source.name}`, error); throw error; } })); // Collect successful results const successfulResults = sourceResults .filter((result) => result.status === 'fulfilled') .map(result => result.value); if (successfulResults.length === 0) { throw new Error('All benchmark sources failed'); } // Group benchmarks by metric const metricGroups = this.groupByMetric(successfulResults); // Aggregate each metric group const aggregatedBenchmarks = []; for (const [metric, sources] of metricGroups.entries()) { const aggregated = this.aggregateMetric(metric, sources); aggregatedBenchmarks.push(aggregated); } this.logger.info('Benchmark aggregation completed', { metricsCount: aggregatedBenchmarks.length, averageConfidence: this.calculateAverageConfidence(aggregatedBenchmarks) }); return aggregatedBenchmarks; } /** * Fetch benchmarks from Financial Modeling Prep API */ async fetchFMPBenchmarks(request) { if (!this.fmpApiKey) { throw new Error('FMP API key not configured'); } // Map our industries to FMP sectors const sectorMap = { financial_services: 'Financial Services', healthcare: 'Healthcare', retail: 'Consumer Cyclical', manufacturing: 'Industrials', technology: 'Technology' }; const sector = sectorMap[request.industry] || 'Technology'; try { // Use rate limiter for FMP API calls const data = await rateLimiters.fmp.executeWithRateLimit(async () => { // Fetch industry averages from FMP const response = await fetch(`https://financialmodelingprep.com/api/v3/sector-performance?apikey=${this.fmpApiKey}`); if (!response.ok) { throw new Error(`FMP API error: ${response.status}`); } return response.json(); }, { priority: 'normal', timeout: 15000 }); // Convert FMP data to our benchmark format const benchmarks = []; // Find sector data const sectorData = data.find((s) => s.sector === sector); if (sectorData) { // Revenue growth as a proxy for ROI potential if (sectorData.revenueGrowth) { benchmarks.push({ industry: request.industry, metric: 'revenue growth', value: Math.abs(sectorData.revenueGrowth * 100), unit: '%', source: 'Financial Modeling Prep', date: new Date().toISOString().split('T')[0], confidence: 0.8 }); } // Operating margin as efficiency metric if (sectorData.operatingMargin) { benchmarks.push({ industry: request.industry, metric: 'operating efficiency', value: Math.abs(sectorData.operatingMargin * 100), unit: '%', source: 'Financial Modeling Prep', date: new Date().toISOString().split('T')[0], confidence: 0.8 }); } } return benchmarks; } catch (error) { this.logger.error('FMP API fetch failed', error); return []; } } /** * Fetch static benchmarks from local data */ async fetchStaticBenchmarks(request) { const industryBenchmarks = await getBenchmarks(request.industry); const benchmarks = []; // Convert static benchmarks to our format if (industryBenchmarks) { // Automation savings benchmarks.push({ industry: request.industry, metric: 'automation rate', value: industryBenchmarks.automationSavings.percentage * 100, unit: '%', source: 'Static Industry Data', date: new Date().toISOString().split('T')[0], confidence: industryBenchmarks.automationSavings.confidence }); // Error reduction benchmarks.push({ industry: request.industry, metric: 'error reduction', value: industryBenchmarks.errorReduction.percentage * 100, unit: '%', source: 'Static Industry Data', date: new Date().toISOString().split('T')[0], confidence: industryBenchmarks.errorReduction.confidence }); // Implementation timeline benchmarks.push({ industry: request.industry, metric: 'implementation time', value: industryBenchmarks.implementationTimeline.typicalMonths, unit: 'months', source: 'Static Industry Data', date: new Date().toISOString().split('T')[0], confidence: industryBenchmarks.implementationTimeline.confidence }); // Note: Project-type specific benchmarks could be added here // if the getBenchmarks function is enhanced to return projectTypes data } return benchmarks; } /** * Group benchmarks by metric name */ groupByMetric(results) { const groups = new Map(); for (const { source, data } of results) { for (const benchmark of data) { const normalizedMetric = this.normalizeMetricName(benchmark.metric); if (!groups.has(normalizedMetric)) { groups.set(normalizedMetric, []); } groups.get(normalizedMetric).push({ source, benchmark }); } } return groups; } /** * Normalize metric names for grouping */ normalizeMetricName(metric) { return metric .toLowerCase() .replace(/[_-]/g, ' ') .replace(/\s+/g, ' ') .trim(); } /** * Aggregate multiple sources for a single metric */ aggregateMetric(metric, sources) { // Calculate weighted average let weightedSum = 0; let totalWeight = 0; const sourceDetails = []; const values = []; for (const { source, benchmark } of sources) { const weight = source.weight * benchmark.confidence; weightedSum += benchmark.value * weight; totalWeight += weight; values.push(benchmark.value); sourceDetails.push({ name: source.name, value: benchmark.value, date: benchmark.date, weight }); } const recommendedValue = totalWeight > 0 ? weightedSum / totalWeight : values[0]; // Calculate range statistics values.sort((a, b) => a - b); const range = { min: Math.min(...values), max: Math.max(...values), p25: this.percentile(values, 0.25), p75: this.percentile(values, 0.75) }; // Determine consensus strength const variance = this.calculateVariance(values); const coefficientOfVariation = Math.sqrt(variance) / recommendedValue; let consensus; if (coefficientOfVariation < 0.1) { consensus = 'strong'; } else if (coefficientOfVariation < 0.25) { consensus = 'moderate'; } else { consensus = 'weak'; } // Calculate overall confidence const confidence = Math.min(0.95, totalWeight / sources.length * (consensus === 'strong' ? 1.2 : consensus === 'moderate' ? 1.0 : 0.8)); return { metric, value: recommendedValue, unit: sources[0].benchmark.unit, confidence, sources: sourceDetails, consensus, recommendedValue, range }; } /** * Calculate percentile */ percentile(sortedValues, p) { const index = p * (sortedValues.length - 1); const lower = Math.floor(index); const upper = Math.ceil(index); const weight = index % 1; if (lower === upper) { return sortedValues[lower]; } return sortedValues[lower] * (1 - weight) + sortedValues[upper] * weight; } /** * Calculate variance */ calculateVariance(values) { const mean = values.reduce((a, b) => a + b, 0) / values.length; const squaredDiffs = values.map(v => Math.pow(v - mean, 2)); return squaredDiffs.reduce((a, b) => a + b, 0) / values.length; } /** * Calculate average confidence across all benchmarks */ calculateAverageConfidence(benchmarks) { if (benchmarks.length === 0) return 0; const sum = benchmarks.reduce((acc, b) => acc + b.confidence, 0); return sum / benchmarks.length; } /** * Get industry-specific adjustment factors */ async getIndustryAdjustments(industry, companySize) { // Size adjustments const sizeMultipliers = { small: 0.85, // Smaller companies typically see lower absolute ROI medium: 1.0, // Baseline large: 1.15, // Larger scale benefits enterprise: 1.25 // Maximum scale benefits }; // Industry complexity factors const complexityFactors = { financial_services: 1.3, // High regulation healthcare: 1.4, // Highest regulation retail: 0.9, // Lower complexity manufacturing: 1.1, // Moderate complexity technology: 0.8, // Lowest barriers education: 1.0, // Baseline government: 1.5, // Highest complexity other: 1.0 // Baseline }; return { sizeMultiplier: sizeMultipliers[companySize || 'medium'] || 1.0, complexityFactor: complexityFactors[industry] || 1.0, riskAdjustment: 1.0 // Could be enhanced with Sonar data }; } } //# sourceMappingURL=benchmark-aggregator.js.map