@spaik/mcp-server-roi
Version:
MCP server for AI ROI prediction and tracking with Monte Carlo simulations
359 lines • 13.7 kB
JavaScript
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