UNPKG

@spaik/mcp-server-roi

Version:

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

220 lines 9.8 kB
import { z } from 'zod'; import { InputValidationError, RangeError } from './errors.js'; import { logger } from './logger.js'; /** * Financial value constraints */ const FINANCIAL_CONSTRAINTS = { MIN_AMOUNT: -1e12, // -1 trillion (for costs) MAX_AMOUNT: 1e12, // 1 trillion MIN_RATE: -1, // -100% (for negative growth) MAX_RATE: 10, // 1000% MIN_PERCENTAGE: 0, MAX_PERCENTAGE: 1, MIN_MONTHS: 0, MAX_MONTHS: 600, // 50 years MIN_VOLUME: 0, MAX_VOLUME: 1e9, // 1 billion MIN_TIME_SECONDS: 0, MAX_TIME_SECONDS: 86400, // 24 hours MIN_ITERATIONS: 100, MAX_ITERATIONS: 1e6 }; /** * Validates financial amounts (can be negative for costs) */ export function validateFinancialAmount(value, fieldName, allowNegative = true) { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (!isFinite(value)) { throw new InputValidationError(fieldName, value, 'must be a finite number'); } const min = allowNegative ? FINANCIAL_CONSTRAINTS.MIN_AMOUNT : 0; const max = FINANCIAL_CONSTRAINTS.MAX_AMOUNT; if (value < min || value > max) { throw new RangeError(fieldName, value, min, max); } return value; } /** * Validates percentage values (0-1) */ export function validatePercentage(value, fieldName) { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (value < FINANCIAL_CONSTRAINTS.MIN_PERCENTAGE || value > FINANCIAL_CONSTRAINTS.MAX_PERCENTAGE) { throw new RangeError(fieldName, value, FINANCIAL_CONSTRAINTS.MIN_PERCENTAGE, FINANCIAL_CONSTRAINTS.MAX_PERCENTAGE); } return value; } /** * Validates rate values (can be negative for decline) */ export function validateRate(value, fieldName) { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (value < FINANCIAL_CONSTRAINTS.MIN_RATE || value > FINANCIAL_CONSTRAINTS.MAX_RATE) { throw new RangeError(fieldName, value, FINANCIAL_CONSTRAINTS.MIN_RATE, FINANCIAL_CONSTRAINTS.MAX_RATE); } return value; } /** * Validates volume/count values */ export function validateVolume(value, fieldName) { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (!Number.isInteger(value)) { logger.warn(`Non-integer volume value for ${fieldName}, rounding`, { value }); value = Math.round(value); } if (value < FINANCIAL_CONSTRAINTS.MIN_VOLUME || value > FINANCIAL_CONSTRAINTS.MAX_VOLUME) { throw new RangeError(fieldName, value, FINANCIAL_CONSTRAINTS.MIN_VOLUME, FINANCIAL_CONSTRAINTS.MAX_VOLUME); } return value; } /** * Validates time duration in months */ export function validateMonths(value, fieldName) { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (!Number.isInteger(value)) { logger.warn(`Non-integer month value for ${fieldName}, rounding`, { value }); value = Math.round(value); } if (value < FINANCIAL_CONSTRAINTS.MIN_MONTHS || value > FINANCIAL_CONSTRAINTS.MAX_MONTHS) { throw new RangeError(fieldName, value, FINANCIAL_CONSTRAINTS.MIN_MONTHS, FINANCIAL_CONSTRAINTS.MAX_MONTHS); } return value; } /** * Validates Monte Carlo iterations */ export function validateIterations(value, fieldName = 'iterations') { if (typeof value !== 'number' || isNaN(value)) { throw new InputValidationError(fieldName, value, 'must be a valid number'); } if (!Number.isInteger(value)) { value = Math.round(value); } if (value < FINANCIAL_CONSTRAINTS.MIN_ITERATIONS || value > FINANCIAL_CONSTRAINTS.MAX_ITERATIONS) { throw new RangeError(fieldName, value, FINANCIAL_CONSTRAINTS.MIN_ITERATIONS, FINANCIAL_CONSTRAINTS.MAX_ITERATIONS); } return value; } /** * Zod schemas with custom refinements */ export const FinancialAmountSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => isFinite(val), { message: 'Must be a finite number' }) .refine(val => val >= FINANCIAL_CONSTRAINTS.MIN_AMOUNT && val <= FINANCIAL_CONSTRAINTS.MAX_AMOUNT, { message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_AMOUNT} and ${FINANCIAL_CONSTRAINTS.MAX_AMOUNT}` }); export const PositiveFinancialAmountSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => isFinite(val), { message: 'Must be a finite number' }) .refine(val => val >= 0, { message: 'Must be non-negative' }) .refine(val => val <= FINANCIAL_CONSTRAINTS.MAX_AMOUNT, { message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_AMOUNT}` }); export const PercentageSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => val >= 0 && val <= 1, { message: 'Must be between 0 and 1' }); export const RateSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => val >= FINANCIAL_CONSTRAINTS.MIN_RATE && val <= FINANCIAL_CONSTRAINTS.MAX_RATE, { message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_RATE} and ${FINANCIAL_CONSTRAINTS.MAX_RATE}` }); export const VolumeSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => val >= 0, { message: 'Must be non-negative' }) .refine(val => val <= FINANCIAL_CONSTRAINTS.MAX_VOLUME, { message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_VOLUME}` }) .transform(val => Math.round(val)); // Auto-round to integer export const MonthsSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => val >= 0, { message: 'Must be non-negative' }) .refine(val => val <= FINANCIAL_CONSTRAINTS.MAX_MONTHS, { message: `Must be less than ${FINANCIAL_CONSTRAINTS.MAX_MONTHS} months` }) .transform(val => Math.round(val)); // Auto-round to integer export const IterationsSchema = z.number() .refine(val => !isNaN(val), { message: 'Must be a valid number' }) .refine(val => val >= FINANCIAL_CONSTRAINTS.MIN_ITERATIONS && val <= FINANCIAL_CONSTRAINTS.MAX_ITERATIONS, { message: `Must be between ${FINANCIAL_CONSTRAINTS.MIN_ITERATIONS} and ${FINANCIAL_CONSTRAINTS.MAX_ITERATIONS}` }) .transform(val => Math.round(val)); // Auto-round to integer /** * Validates an entire use case object */ export function validateUseCase(useCase) { const { current_state, future_state } = useCase; // Validate current state if (current_state) { if (current_state.volume_per_month !== undefined) { validateVolume(current_state.volume_per_month, 'current_state.volume_per_month'); } if (current_state.cost_per_unit !== undefined) { validateFinancialAmount(current_state.cost_per_unit, 'current_state.cost_per_unit'); } if (current_state.time_per_unit_hours !== undefined) { if (current_state.time_per_unit_hours < 0 || current_state.time_per_unit_hours > 168) { throw new RangeError('current_state.time_per_unit_hours', current_state.time_per_unit_hours, 0, 168); } } if (current_state.error_rate !== undefined) { validatePercentage(current_state.error_rate, 'current_state.error_rate'); } if (current_state.revenue_per_unit !== undefined) { validateFinancialAmount(current_state.revenue_per_unit, 'current_state.revenue_per_unit', false); } } // Validate future state if (future_state) { if (future_state.automation_rate !== undefined) { validatePercentage(future_state.automation_rate, 'future_state.automation_rate'); } if (future_state.cost_reduction_rate !== undefined) { validatePercentage(future_state.cost_reduction_rate, 'future_state.cost_reduction_rate'); } if (future_state.error_reduction_rate !== undefined) { validatePercentage(future_state.error_reduction_rate, 'future_state.error_reduction_rate'); } if (future_state.time_reduction_rate !== undefined) { validatePercentage(future_state.time_reduction_rate, 'future_state.time_reduction_rate'); } if (future_state.volume_growth_rate !== undefined) { validateRate(future_state.volume_growth_rate, 'future_state.volume_growth_rate'); } if (future_state.quality_improvement_rate !== undefined) { validatePercentage(future_state.quality_improvement_rate, 'future_state.quality_improvement_rate'); } } } /** * Sanitizes string inputs to prevent injection attacks */ export function sanitizeString(input, maxLength = 1000) { if (typeof input !== 'string') { throw new InputValidationError('input', input, 'must be a string'); } // Trim and limit length input = input.trim().substring(0, maxLength); // Remove control characters input = input.replace(/[\x00-\x1F\x7F]/g, ''); return input; } /** * Validates array inputs */ export function validateArray(arr, fieldName, minLength = 0, maxLength = 1000) { if (!Array.isArray(arr)) { throw new InputValidationError(fieldName, arr, 'must be an array'); } if (arr.length < minLength || arr.length > maxLength) { throw new RangeError(fieldName, arr.length, minLength, maxLength); } return arr; } //# sourceMappingURL=validators.js.map