UNPKG

@spaik/mcp-server-roi

Version:

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

415 lines 19.2 kB
import { z } from 'zod'; import { mcpDb } from '../db/supabase.js'; import { ROIEngine } from '../core/calculators/roi-engine.js'; import { FINANCIAL_CONSTANTS } from '../core/calculators/financial-utils.js'; import { ProjectCreateSchema } from '../schemas/project.js'; import { UseCaseCreateSchema } from '../schemas/use-case.js'; import { createLogger } from '../utils/logger.js'; import { DatabaseError, CalculationError, ValidationError, ConfigurationError } from '../utils/errors.js'; import { validateUseCase, validateFinancialAmount, validateMonths } from '../utils/validators.js'; import { DutchBenchmarkValidator } from '../services/dutch-benchmark-validator.js'; // Transaction manager for atomic operations class TransactionManager { logger = createLogger({ component: 'TransactionManager' }); rollbackStack = []; async executeWithRollback(operation, rollbackOperation) { try { const result = await operation(); this.rollbackStack.push(rollbackOperation); return result; } catch (error) { this.logger.error('Operation failed, will trigger rollback', error); throw error; } } async rollbackAll() { this.logger.info('Starting rollback', { operations: this.rollbackStack.length }); // Execute rollbacks in reverse order while (this.rollbackStack.length > 0) { const rollback = this.rollbackStack.pop(); try { await rollback(); } catch (error) { this.logger.error('Rollback operation failed', error); // Continue with other rollbacks even if one fails } } } } // Retry logic with exponential backoff class RetryManager { logger = createLogger({ component: 'RetryManager' }); async withRetry(operation, options = {}) { const { maxAttempts = 3, initialDelay = 1000, maxDelay = 10000, shouldRetry = (error) => this.defaultShouldRetry(error) } = options; let lastError; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await operation(); } catch (error) { lastError = error; if (attempt === maxAttempts || !shouldRetry(error)) { throw error; } const delay = Math.min(initialDelay * Math.pow(2, attempt - 1), maxDelay); this.logger.warn(`Operation failed, retrying in ${delay}ms`, { attempt, maxAttempts, error: error.message }); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError; } defaultShouldRetry(error) { // Retry on network errors or specific database errors if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') { return true; } // Retry on specific Supabase errors if (error.message?.includes('too many requests') || error.message?.includes('temporarily unavailable')) { return true; } // Don't retry on validation errors or business logic errors if (error instanceof ValidationError || error instanceof CalculationError) { return false; } return false; } } export const PredictROISchema = z.object({ organization_id: z.string(), project: ProjectCreateSchema, use_cases: z.array(UseCaseCreateSchema), implementation_costs: z.object({ software_licenses: z.number().min(0), development_hours: z.number().min(0), training_costs: z.number().min(0), infrastructure: z.number().min(0), ongoing_monthly: z.number().min(0).default(0) }), timeline_months: z.number().min(1).max(120), confidence_level: z.number().min(0).max(1).default(0.95), // Options for transaction behavior enable_retry: z.boolean().default(true), transaction_timeout: z.number().default(30000) // 30 seconds default }); export async function predictROI(input) { const toolLogger = createLogger({ tool: 'predict_roi' }); const transactionManager = new TransactionManager(); const retryManager = new RetryManager(); toolLogger.info('Starting ROI prediction', { organization_id: input.organization_id, project_name: input.project?.project_name, use_case_count: input.use_cases?.length, enable_retry: input.enable_retry }); // Set a timeout for the entire operation const timeoutMs = input.transaction_timeout || 30000; // Default to 30 seconds const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Transaction timeout after ${timeoutMs}ms`)); }, timeoutMs); }); try { return await Promise.race([ performPrediction(), timeoutPromise ]); } catch (error) { if (error.message.includes('Transaction timeout')) { toolLogger.error('Transaction timed out', error); await transactionManager.rollbackAll(); throw new DatabaseError('Operation timed out. Please try again with fewer use cases or contact support.', { timeout: input.transaction_timeout }); } throw error; } async function performPrediction() { let validationResult = null; 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 ROI predictions. Please configure it in your environment.'); } // Step 2: Validate against Dutch market benchmarks (MANDATORY FIRST STEP) toolLogger.info('Validating inputs against Dutch market benchmarks'); const dutchValidator = new DutchBenchmarkValidator(perplexityApiKey); // Ensure project data exists before accessing it if (!input.project) { throw new ValidationError('Missing required project information', { field: 'project', message: 'Project details are required' }); } validationResult = await dutchValidator.validateProjectInputs({ industry: input.project.industry, useCases: input.use_cases, implementationCosts: input.implementation_costs, timelineMonths: input.timeline_months }); if (!validationResult.isValid) { toolLogger.warn('Validation found critical issues', { issueCount: validationResult.validationIssues.length, errors: validationResult.validationIssues.filter((i) => i.severity === 'error') }); } // Log validation adjustments if (validationResult.validationIssues.length > 0) { toolLogger.info('Dutch market validation applied adjustments', { adjustmentCount: validationResult.validationIssues.length, warnings: validationResult.validationIssues.filter((i) => i.severity === 'warning').length }); } // Step 3: Use validated/adjusted input for further processing const validatedInput = PredictROISchema.parse({ ...input, ...validationResult.adjustedInput, // Preserve original organization_id organization_id: input.organization_id }); // Step 4: Additional validation validateInputData(validatedInput); // Step 5: Calculate ROI projection with validated data toolLogger.debug('Calculating ROI projection with validated inputs'); const projection = await calculateProjection(validatedInput); // Step 3: Create database records with calculated values using transaction manager toolLogger.debug('Creating database records with transaction management'); // Create project with retry logic const project = await (input.enable_retry ? retryManager.withRetry.bind(retryManager) : async (op) => op())(async () => { return await transactionManager.executeWithRollback(async () => { const { data, error } = await mcpDb .from('projects') .insert({ client_name: validatedInput.project.client_name, project_name: validatedInput.project.project_name, industry: validatedInput.project.industry, description: validatedInput.project.description, status: validatedInput.project.status || 'active' }) .select() .single(); if (error || !data) { throw new DatabaseError('Failed to create project', { error: error?.message, code: error?.code }); } return data; }, async () => { // Rollback operation for project if (project?.id) { await mcpDb.from('projects').delete().eq('id', project.id); } }); }); toolLogger.debug('Project created', { projectId: project.id }); // Create use cases const useCases = await transactionManager.executeWithRollback(async () => { const useCasesToInsert = validatedInput.use_cases.map((uc) => ({ ...uc, project_id: project.id })); const { data, error } = await mcpDb .from('use_cases') .insert(useCasesToInsert) .select(); if (error || !data || data.length === 0) { throw new DatabaseError('Failed to create use cases', { error: error?.message, code: error?.code, projectId: project.id }); } return data; }, async () => { // Rollback operation for use cases if (project.id) { await mcpDb.from('use_cases').delete().eq('project_id', project.id); } }); toolLogger.debug('Use cases created', { count: useCases.length }); // Create projection with calculated values const createdProjection = await transactionManager.executeWithRollback(async () => { // Update projection with actual project ID const projectionToInsert = { ...projection, project_id: project.id, metadata: { ...projection.metadata, calculated_at: new Date().toISOString(), calculation_version: 'v3.1', retry_enabled: input.enable_retry, dutch_validation_applied: true } }; const { data, error } = await mcpDb .from('projections') .insert(projectionToInsert) .select() .single(); if (error || !data) { throw new DatabaseError('Failed to create projection', { error: error?.message, code: error?.code, projectId: project.id }); } return data; }, async () => { // Rollback operation for projection if (createdProjection?.id) { await mcpDb.from('projections').delete().eq('id', createdProjection.id); } }); toolLogger.debug('Projection created with calculated values', { projectionId: createdProjection.id, roi: createdProjection.calculations.five_year_roi }); // Step 6: Return formatted response with validation results const response = { project_id: project.id, projection_id: createdProjection.id, summary: { total_investment: createdProjection.calculations.total_investment, expected_roi: createdProjection.calculations.five_year_roi, payback_period_months: createdProjection.calculations.payback_period_months, net_present_value: createdProjection.calculations.net_present_value, break_even_date: createdProjection.calculations.break_even_date }, financial_metrics: createdProjection.financial_metrics, assumptions: createdProjection.metadata.assumptions, use_cases: useCases.map(uc => ({ name: uc.name, category: uc.category, monthly_benefit: calculateUseCaseBenefit(uc) })), dutch_market_validation: { validation_applied: true, adjustments_made: validationResult.validationIssues.length, validation_issues: validationResult.validationIssues, market_insights: validationResult.marketInsights, recommendations: validationResult.recommendations, citations: validationResult.citations }, metadata: { dutch_market_validated: true, calculation_timestamp: new Date().toISOString(), confidence_level: input.confidence_level, validation_version: 'v1.3.0' } }; toolLogger.info('Enhanced ROI prediction completed successfully', { projectId: project.id, roi: createdProjection.calculations.five_year_roi, payback_months: createdProjection.calculations.payback_period_months }); return response; } catch (error) { // Rollback all operations on any error await transactionManager.rollbackAll(); // Log and re-throw with error context toolLogger.error('ROI prediction failed', error, { operation: 'predictROI', project_name: input.project?.project_name, use_case_count: input.use_cases?.length }); throw enhanceError(error); } } } function validateInputData(input) { // Validate financial amounts validateFinancialAmount(input.implementation_costs.software_licenses, 'software_licenses'); validateFinancialAmount(input.implementation_costs.development_hours * FINANCIAL_CONSTANTS.DEFAULT_DEVELOPER_HOURLY_RATE, 'development_costs'); validateFinancialAmount(input.implementation_costs.training_costs, 'training_costs'); validateFinancialAmount(input.implementation_costs.infrastructure, 'infrastructure'); validateMonths(input.timeline_months, 'timeline_months'); // Validate each use case input.use_cases.forEach((useCase, index) => { try { validateUseCase(useCase); } catch (error) { throw new ValidationError(`Invalid use case at index ${index}: ${error.message}`, { index, useCase }); } }); } async function calculateProjection(input) { const roiEngine = new ROIEngine(); // Generate temporary IDs for calculation const tempProjectId = 'temp-' + Date.now(); const tempUseCases = input.use_cases.map((uc, index) => ({ ...uc, id: `temp-use-case-${index}`, project_id: tempProjectId, created_at: new Date().toISOString(), updated_at: new Date().toISOString() })); try { const projection = roiEngine.calculateProjection(tempProjectId, tempUseCases, input.implementation_costs, input.timeline_months, 'Base Case'); // Validate calculated values if (!isFinite(projection.calculations.total_investment) || projection.calculations.total_investment <= 0) { throw new CalculationError('Invalid total investment calculated', { total_investment: projection.calculations.total_investment }); } if (!isFinite(projection.calculations.five_year_roi)) { projection.calculations.five_year_roi = 0; } return projection; } catch (error) { throw new CalculationError('Failed to calculate ROI projection. Please check your input values.', { error: error.message, use_case_count: input.use_cases.length }); } } function calculateUseCaseBenefit(useCase) { const { current_state, future_state } = useCase; // Time savings value const timeSavingsValue = current_state.process_time_hours * current_state.volume_per_month * future_state.time_reduction_percentage * FINANCIAL_CONSTANTS.DEFAULT_HOURLY_RATE; // Direct cost savings const costSavings = current_state.cost_per_transaction * current_state.volume_per_month * future_state.automation_percentage; // Quality improvement value (reduced errors) const qualityValue = current_state.cost_per_transaction * current_state.volume_per_month * current_state.error_rate * future_state.error_reduction_percentage * FINANCIAL_CONSTANTS.ERROR_COST_MULTIPLIER; return timeSavingsValue + costSavings + qualityValue; } function enhanceError(error) { if (error instanceof ValidationError || error instanceof CalculationError || error instanceof DatabaseError) { return error; } // Handle specific database errors if (error.message?.includes('duplicate key')) { return new DatabaseError('A project with this name already exists. Please use a different project name.', { originalError: error.message }); } if (error.message?.includes('violates foreign key')) { return new DatabaseError('Invalid reference in data. Please ensure all IDs are valid.', { originalError: error.message }); } if (error.message?.includes('connection')) { return new DatabaseError('Unable to connect to the database. Please check your connection and try again.', { originalError: error.message }); } // Generic error return new Error(`Unexpected error in ROI prediction: ${error.message}. Please try again or contact support.`); } //# sourceMappingURL=predict-roi.js.map