@houmak/minerva-mcp-server
Version:
Minerva Model Context Protocol (MCP) Server for Microsoft 365 and Azure integrations
308 lines (307 loc) • 13.5 kB
JavaScript
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;
}
}