UNPKG

@houmak/minerva-mcp-server

Version:

Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations

308 lines (307 loc) 13.5 kB
import { logger } from '../logger.js'; import { IntelligentRouter } from '../router/intelligent-router.js'; export class DryRunManager { config; router; simulationHistory = []; constructor(config) { this.config = { ...config, enabled: config.enabled ?? false, logActions: config.logActions ?? true, simulateDelays: config.simulateDelays ?? true, validateOnly: config.validateOnly ?? false, maxSimulatedDelay: config.maxSimulatedDelay ?? 5000, }; this.router = new IntelligentRouter(); } isEnabled() { return this.config.enabled; } async simulateAction(action, parameters, availableScopes) { const startTime = Date.now(); // Validate action exists // const validation = this.router.validateAction(action); const validation = { isValid: true, errors: [] }; if (!validation.isValid) { return { action, provider: 'none', parameters, wouldExecute: false, estimatedTime: 0, requiredScopes: [], validationErrors: [validation.errors.join(', ') || 'Action not found'], warnings: [], recommendations: ['Check action name and parameters'], }; } // Get action capabilities // const capabilities = this.router.getActionCapabilities(action); const capabilities = { providers: ['graph', 'pnp'], preferred: 'graph' }; const providerSelection = await this.router.selectProvider(action, availableScopes, { availableScopes, currentRateLimits: {}, complexity: 'medium', priority: 'medium' }); // Validate parameters const validationErrors = this.validateParameters(action, parameters, capabilities); // Check scope requirements const missingScopes = this.checkRequiredScopes(capabilities, availableScopes); // Estimate execution time const estimatedTime = this.estimateExecutionTime(action, parameters, capabilities); // Generate warnings and recommendations const warnings = this.generateWarnings(action, parameters, capabilities, availableScopes); const recommendations = this.generateRecommendations(action, parameters, capabilities, availableScopes); // Simulate delay if enabled if (this.config.simulateDelays && estimatedTime > 0) { const delay = Math.min(estimatedTime, this.config.maxSimulatedDelay); await this.simulateDelay(delay); } const result = { action, provider: providerSelection.provider || 'none', parameters, wouldExecute: validationErrors.length === 0 && missingScopes.length === 0, estimatedTime, requiredScopes: capabilities.requiredScopes || [], validationErrors: [...validationErrors, ...missingScopes], warnings, recommendations, }; if (this.config.logActions) { this.logDryRunResult(result); } return result; } async simulateBatch(actions, availableScopes) { const results = []; const requiredScopes = new Set(); const recommendations = []; let successfulActions = 0; let failedActions = 0; let estimatedTotalTime = 0; logger.info(`Starting dry-run simulation for ${actions.length} actions`); for (const { action, parameters } of actions) { const result = await this.simulateAction(action, parameters, availableScopes); results.push(result); if (result.wouldExecute) { successfulActions++; estimatedTotalTime += result.estimatedTime; } else { failedActions++; } // Collect required scopes result.requiredScopes.forEach(scope => requiredScopes.add(scope)); // Collect recommendations recommendations.push(...result.recommendations); } const simulation = { totalActions: actions.length, successfulActions, failedActions, estimatedTotalTime, requiredScopes, recommendations: [...new Set(recommendations)], // Remove duplicates results, }; this.simulationHistory.push(simulation); this.logSimulationSummary(simulation); return simulation; } validateParameters(action, parameters, capabilities) { const errors = []; // Basic parameter validation if (!parameters && capabilities.requiredParameters) { errors.push('Required parameters missing'); return errors; } // Check for required parameters if (capabilities.requiredParameters) { for (const requiredParam of capabilities.requiredParameters) { if (!parameters[requiredParam]) { errors.push(`Required parameter missing: ${requiredParam}`); } } } // Validate parameter types and formats if (parameters) { for (const [key, value] of Object.entries(parameters)) { const validation = this.validateParameterValue(key, value, capabilities); if (validation) { errors.push(validation); } } } return errors; } validateParameterValue(key, value, capabilities) { // Basic type validation if (capabilities.parameterTypes && capabilities.parameterTypes[key]) { const expectedType = capabilities.parameterTypes[key]; const actualType = typeof value; if (actualType !== expectedType) { return `Parameter ${key} should be of type ${expectedType}, got ${actualType}`; } } // Format validation for specific parameters if (key === 'email' && typeof value === 'string') { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return `Invalid email format for parameter ${key}`; } } if (key === 'userId' && typeof value === 'string') { const guidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; if (!guidRegex.test(value)) { return `Invalid GUID format for parameter ${key}`; } } return null; } checkRequiredScopes(capabilities, availableScopes) { const missing = []; if (capabilities.requiredScopes) { for (const requiredScope of capabilities.requiredScopes) { if (!availableScopes.includes(requiredScope)) { missing.push(`Missing required scope: ${requiredScope}`); } } } return missing; } estimateExecutionTime(action, parameters, capabilities) { // Base time estimation based on action complexity let baseTime = 100; // 100ms base if (capabilities.complexity === 'high') { baseTime = 2000; // 2 seconds for complex operations } else if (capabilities.complexity === 'medium') { baseTime = 500; // 500ms for medium operations } // Adjust based on parameters if (parameters && parameters.bulk) { baseTime *= 3; // Bulk operations take longer } if (parameters && parameters.count) { baseTime += parameters.count * 10; // 10ms per item } // Add network latency simulation baseTime += Math.random() * 200; // 0-200ms random latency return Math.round(baseTime); } generateWarnings(action, parameters, capabilities, availableScopes) { const warnings = []; // Check for potentially dangerous operations if (capabilities.destructive && !parameters.confirm) { warnings.push('This is a destructive operation. Consider adding confirm=true parameter'); } // Check for bulk operations if (parameters && parameters.bulk && !parameters.limit) { warnings.push('Bulk operation without limit. Consider adding a limit parameter'); } // Check for rate limiting if (capabilities.rateLimit && capabilities.rateLimit < 100) { warnings.push('This action has strict rate limits. Consider batching requests'); } // Check for missing optional scopes if (capabilities.optionalScopes) { const missingOptional = capabilities.optionalScopes.filter((scope) => !availableScopes.includes(scope)); if (missingOptional.length > 0) { warnings.push(`Missing optional scopes: ${missingOptional.join(', ')}`); } } return warnings; } generateRecommendations(action, parameters, capabilities, availableScopes) { const recommendations = []; // Provider recommendations if (capabilities.providers && capabilities.providers.length > 1) { const preferred = capabilities.preferredProvider; recommendations.push(`Consider using ${preferred} provider for optimal performance`); } // Caching recommendations if (capabilities.cacheable) { recommendations.push('This action supports caching. Enable cache for better performance'); } // Batch processing recommendations if (parameters && parameters.count && parameters.count > 10) { recommendations.push('Consider using batch processing for large datasets'); } // Scope recommendations if (capabilities.requiredScopes && capabilities.requiredScopes.length > 2) { recommendations.push('This action requires multiple scopes. Verify all permissions are granted'); } return recommendations; } async simulateDelay(delay) { return new Promise(resolve => setTimeout(resolve, delay)); } logDryRunResult(result) { const status = result.wouldExecute ? '✅ WOULD EXECUTE' : '❌ WOULD FAIL'; logger.info(`[DRY-RUN] ${status} - ${result.action}`); logger.info(` Provider: ${result.provider}`); logger.info(` Estimated Time: ${result.estimatedTime}ms`); if (result.validationErrors.length > 0) { logger.warn(` Validation Errors: ${result.validationErrors.join(', ')}`); } if (result.warnings.length > 0) { logger.warn(` Warnings: ${result.warnings.join(', ')}`); } if (result.recommendations.length > 0) { logger.info(` Recommendations: ${result.recommendations.join(', ')}`); } } logSimulationSummary(simulation) { logger.info('=== DRY-RUN SIMULATION SUMMARY ==='); logger.info(`Total Actions: ${simulation.totalActions}`); logger.info(`Successful: ${simulation.successfulActions}`); logger.info(`Failed: ${simulation.failedActions}`); logger.info(`Estimated Total Time: ${simulation.estimatedTotalTime}ms`); logger.info(`Required Scopes: ${Array.from(simulation.requiredScopes).join(', ')}`); if (simulation.recommendations.length > 0) { logger.info(`Recommendations: ${simulation.recommendations.join(', ')}`); } logger.info('==================================='); } getSimulationHistory() { return this.simulationHistory; } clearHistory() { this.simulationHistory = []; } generateReport() { if (this.simulationHistory.length === 0) { return 'No dry-run simulations available'; } const latest = this.simulationHistory[this.simulationHistory.length - 1]; let report = '# Dry-Run Simulation Report\n\n'; report += `## Summary\n`; report += `- **Total Actions**: ${latest.totalActions}\n`; report += `- **Successful**: ${latest.successfulActions}\n`; report += `- **Failed**: ${latest.failedActions}\n`; report += `- **Success Rate**: ${((latest.successfulActions / latest.totalActions) * 100).toFixed(1)}%\n`; report += `- **Estimated Time**: ${latest.estimatedTotalTime}ms\n\n`; report += `## Required Scopes\n`; report += Array.from(latest.requiredScopes).map(scope => `- ${scope}`).join('\n') + '\n\n'; report += `## Recommendations\n`; report += latest.recommendations.map(rec => `- ${rec}`).join('\n') + '\n\n'; report += `## Detailed Results\n`; latest.results.forEach((result, index) => { report += `### ${index + 1}. ${result.action}\n`; report += `- **Status**: ${result.wouldExecute ? '✅ Would Execute' : '❌ Would Fail'}\n`; report += `- **Provider**: ${result.provider}\n`; report += `- **Time**: ${result.estimatedTime}ms\n`; if (result.validationErrors.length > 0) { report += `- **Errors**: ${result.validationErrors.join(', ')}\n`; } if (result.warnings.length > 0) { report += `- **Warnings**: ${result.warnings.join(', ')}\n`; } report += '\n'; }); return report; } }