UNPKG

@houmak/minerva-mcp-server

Version:

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

680 lines (679 loc) 26.7 kB
import { logger } from "../logger.js"; export class IntelligentRouter { capabilityMatrix; rateLimitCounters = new Map(); providerHealth = new Map(); performanceHistory = new Map(); userPreferences = new Map(); constructor() { // Enhanced capability matrix with all current tools this.capabilityMatrix = { providers: { graph: { name: 'Microsoft Graph API', capabilities: ['users.read', 'sites.read', 'groups.read', 'teams.read', 'files.read'], health: 'healthy', rateLimit: 1000, rateLimits: { requestsPerMinute: 1000 }, stability: 'stable', preferred: ['get-users', 'get-sites', 'get-groups', 'Minerva-Microsoft'] }, pnp: { name: 'PnP PowerShell', capabilities: ['sites.read', 'lists.read', 'permissions.read', 'content.read', 'admin.read'], health: 'healthy', rateLimit: 500, rateLimits: { requestsPerMinute: 500 }, stability: 'stable', preferred: ['getSiteInventory', 'getPermissionAudit', 'exportSiteContent', 'getSiteSharingSettings', 'getExternalUsers', 'applySiteTemplate', 'getListFormatting', 'migrateSharePointList', 'migrateSharePointDocuments', 'bulkCreateLists', 'getFlowsReport', 'createSharePointSite', 'getTeamsReport', 'getAADUsersReport', 'getFlowRunsSummary', 'getAzureResources'] }, cli: { name: 'CLI for Microsoft 365', capabilities: ['teams.read', 'planner.read', 'admin.read'], health: 'healthy', rateLimit: 200, rateLimits: { requestsPerMinute: 200 }, stability: 'beta', preferred: ['getTeamsReport', 'getAADUsersReport'] } }, actions: { // Graph API actions 'Minerva-Microsoft': { providers: ['graph'], scopes: ['users.read', 'sites.read', 'groups.read'], complexity: 'low', stability: 'stable', preferred: 'graph' }, 'getSharePointLists': { providers: ['graph', 'pnp'], scopes: ['sites.read', 'lists.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getPnPSiteDesigns': { providers: ['pnp'], scopes: ['admin.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'testPowerShellAvailability': { providers: ['pnp'], scopes: [], complexity: 'low', stability: 'stable', preferred: 'pnp' }, 'sp_apply_site_design': { providers: ['pnp'], scopes: ['sites.read', 'admin.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'sp_break_role_inheritance': { providers: ['pnp'], scopes: ['permissions.read', 'permissions.write'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'sp_get_site_users': { providers: ['pnp'], scopes: ['users.read', 'permissions.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'sp_get_content_types': { providers: ['pnp'], scopes: ['sites.read', 'lists.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'sp_manage_features': { providers: ['pnp'], scopes: ['admin.read', 'admin.write'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, // Migration tools 'migrateSharePointList': { providers: ['pnp'], scopes: ['sites.read', 'lists.read', 'permissions.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'migrateSharePointDocuments': { providers: ['pnp'], scopes: ['sites.read', 'files.read', 'permissions.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'migrateSharePointContentTypes': { providers: ['pnp'], scopes: ['sites.read', 'admin.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'migrateSharePointPermissions': { providers: ['pnp'], scopes: ['permissions.read', 'permissions.write'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'testMigrationPrerequisites': { providers: ['pnp'], scopes: ['sites.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getMigrationStatus': { providers: ['pnp'], scopes: ['sites.read'], complexity: 'low', stability: 'stable', preferred: 'pnp' }, // PnP tools 'getSiteInventory': { providers: ['pnp'], scopes: ['sites.read', 'lists.read', 'permissions.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'createSharePointSite': { providers: ['pnp'], scopes: ['sites.read', 'admin.write'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getTeamsReport': { providers: ['pnp', 'cli'], scopes: ['teams.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getAADUsersReport': { providers: ['pnp', 'cli'], scopes: ['users.read', 'admin.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getSiteSharingSettings': { providers: ['pnp'], scopes: ['sites.read', 'permissions.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getPermissionAudit': { providers: ['pnp'], scopes: ['permissions.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'exportSiteContent': { providers: ['pnp'], scopes: ['sites.read', 'files.read'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'getListFormatting': { providers: ['pnp'], scopes: ['sites.read', 'lists.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getExternalUsers': { providers: ['pnp'], scopes: ['users.read', 'permissions.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'getFlowRunsSummary': { providers: ['pnp'], scopes: ['flows.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'applySiteTemplate': { providers: ['pnp'], scopes: ['sites.read', 'admin.write'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'getAzureResources': { providers: ['pnp'], scopes: ['azure.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' }, 'bulkCreateLists': { providers: ['pnp'], scopes: ['sites.read', 'lists.write'], complexity: 'high', stability: 'stable', preferred: 'pnp' }, 'getFlowsReport': { providers: ['pnp'], scopes: ['flows.read'], complexity: 'medium', stability: 'stable', preferred: 'pnp' } }, routing_rules: { "complexity-thresholds": { "low": "graph", "medium": "pnp", "high": "pnp" }, "priority-thresholds": { "high": "graph", "medium": "pnp", "low": "cli" }, "fallback-order": { "graph": ["pnp", "cli"], "pnp": ["graph", "cli"], "cli": ["pnp", "graph"] } } }; this.initializeProviderHealth(); this.initializePerformanceHistory(); } /** * Set user preference for a specific action */ setUserPreference(action, provider) { this.userPreferences.set(action, provider); logger.info(`Router: User preference set for action '${action}' to provider '${provider}'`); } /** * Get user preference for a specific action */ getUserPreference(action) { return this.userPreferences.get(action); } /** * Update performance metrics for a provider */ updatePerformanceMetrics(provider, executionTime, success) { const current = this.performanceHistory.get(provider) || { avgTime: 0, successRate: 0, lastUpdate: 0, totalCalls: 0, successfulCalls: 0 }; // Update average execution time (exponential moving average) const alpha = 0.1; // Smoothing factor current.avgTime = alpha * executionTime + (1 - alpha) * current.avgTime; // Update success rate current.totalCalls = (current.totalCalls || 0) + 1; if (success) { current.successfulCalls = (current.successfulCalls || 0) + 1; } current.successRate = current.successfulCalls / current.totalCalls; current.lastUpdate = Date.now(); this.performanceHistory.set(provider, current); logger.info(`Router: Updated performance metrics for '${provider}': avgTime=${current.avgTime}ms, successRate=${(current.successRate * 100).toFixed(1)}%`); } /** * Get performance metrics for a provider */ getPerformanceMetrics(provider) { return this.performanceHistory.get(provider); } /** * Sélectionne le meilleur provider pour une action donnée */ async selectProvider(action, availableScopes, context) { logger.info(`Router: Sélection du provider pour l'action '${action}'`); // Recherche de l'action dans la matrice de capacités const actionConfig = this.findActionConfig(action); if (!actionConfig) { logger.warn(`Action '${action}' non trouvée dans la matrice de capacités`); return null; } // Vérification de la disponibilité des scopes const availableProviders = this.filterProvidersByScopes(actionConfig.providers, actionConfig.scopes, availableScopes); if (availableProviders.length === 0) { logger.warn(`Aucun provider disponible pour l'action '${action}'. Scopes requis: ${actionConfig.scopes.join(", ")}`); return null; } // Sélection du provider optimal const selectedProvider = await this.selectOptimalProvider(availableProviders, actionConfig, context); // Préparation du résultat const result = { provider: selectedProvider.provider, reason: selectedProvider.reason, confidence: selectedProvider.confidence, fallbackProviders: this.getFallbackProviders(availableProviders, selectedProvider.provider), estimatedTime: this.estimateExecutionTime(actionConfig, selectedProvider.provider), requiredScopes: actionConfig.scopes }; logger.info(`Router: Provider sélectionné pour '${action}': ${selectedProvider.provider} (confiance: ${selectedProvider.confidence}%)`); return result; } /** * Find action configuration in capability matrix */ findActionConfig(action) { return this.capabilityMatrix.actions[action]; } /** * Filter providers by available scopes */ filterProvidersByScopes(providers, requiredScopes, availableScopes) { if (requiredScopes.length === 0) { return providers; } const hasRequiredScopes = requiredScopes.every(scope => availableScopes.includes(scope) || availableScopes.includes('*')); return hasRequiredScopes ? providers : []; } /** * Select optimal provider based on scoring system */ async selectOptimalProvider(availableProviders, actionConfig, context) { const scores = []; for (const provider of availableProviders) { let score = 0; const reasons = []; // Score de base selon la préférence définie if (actionConfig.preferred === provider) { score += 30; reasons.push("Provider préféré pour cette action"); } // Score selon la stabilité const stabilityScore = this.getStabilityScore(actionConfig.stability); score += stabilityScore; reasons.push(`Stabilité: ${actionConfig.stability} (+${stabilityScore})`); // Score selon les limites de taux const rateLimitScore = this.getRateLimitScore(provider, context.currentRateLimits); score += rateLimitScore; reasons.push(`Rate limit: +${rateLimitScore}`); // Score selon la santé du provider const healthScore = this.getHealthScore(provider); score += healthScore; reasons.push(`Santé: +${healthScore}`); // Score selon la complexité const complexityScore = this.getComplexityScore(provider, actionConfig.complexity); score += complexityScore; reasons.push(`Complexité: +${complexityScore}`); // Score selon les performances historiques const performanceScore = this.getPerformanceScore(provider); score += performanceScore; reasons.push(`Performance: +${performanceScore}`); // Bonus pour la préférence utilisateur if (context.userPreference === provider) { score += 10; reasons.push("Préférence utilisateur (+10)"); } // Bonus pour la priorité if (context.priority === "high") { const highPriorityProvider = this.capabilityMatrix.routing_rules["complexity-thresholds"][actionConfig.complexity]; if (provider === highPriorityProvider) { score += 15; reasons.push("Provider optimisé pour la priorité haute (+15)"); } } scores.push({ provider, score, reasons }); } // Tri par score décroissant scores.sort((a, b) => b.score - a.score); const bestProvider = scores[0]; const confidence = Math.min(100, Math.max(0, bestProvider.score)); return { provider: bestProvider.provider, reason: bestProvider.reasons.join(", "), confidence }; } /** * Calcule le score de stabilité */ getStabilityScore(stability) { switch (stability) { case "stable": return 25; case "beta": return 15; case "alpha": return 5; default: return 10; } } /** * Calcule le score selon les limites de taux */ getRateLimitScore(provider, currentRateLimits) { const providerConfig = this.capabilityMatrix.providers[provider]; if (!providerConfig) return 0; const currentUsage = currentRateLimits[provider] || 0; const maxRequests = providerConfig.rateLimits.requestsPerMinute; const usagePercentage = (currentUsage / maxRequests) * 100; if (usagePercentage < 50) return 20; if (usagePercentage < 80) return 10; if (usagePercentage < 95) return 5; return -10; // Pénalité si proche de la limite } /** * Calcule le score de santé du provider */ getHealthScore(provider) { const health = this.providerHealth.get(provider); if (!health) return 0; switch (health.status) { case "healthy": return 15; case "degraded": return 5; case "unhealthy": return -10; default: return 0; } } /** * Calcule le score de performance basé sur l'historique */ getPerformanceScore(provider) { const metrics = this.performanceHistory.get(provider); if (!metrics) return 0; // Score basé sur le taux de succès (0-20 points) const successScore = metrics.successRate * 20; // Score basé sur le temps d'exécution (0-10 points) // Plus rapide = plus de points const timeScore = Math.max(0, 10 - (metrics.avgTime / 1000)); // 10 points si < 1s, 0 si > 10s return Math.round(successScore + timeScore); } /** * Calcule le score selon la complexité */ getComplexityScore(provider, complexity) { const routingRules = this.capabilityMatrix.routing_rules; const preferredProvider = routingRules["complexity-thresholds"][complexity]; if (provider === preferredProvider) { return 15; } return 0; } /** * Obtient les providers de fallback */ getFallbackProviders(availableProviders, selectedProvider) { return availableProviders.filter(p => p !== selectedProvider); } /** * Estime le temps d'exécution */ estimateExecutionTime(actionConfig, provider) { const baseTime = this.getBaseExecutionTime(actionConfig.complexity); const providerMultiplier = this.getProviderTimeMultiplier(provider); return Math.round(baseTime * providerMultiplier); } /** * Obtient le temps d'exécution de base selon la complexité */ getBaseExecutionTime(complexity) { switch (complexity) { case "low": return 1000; // 1 seconde case "medium": return 3000; // 3 secondes case "high": return 10000; // 10 secondes default: return 2000; } } /** * Obtient le multiplicateur de temps selon le provider */ getProviderTimeMultiplier(provider) { switch (provider) { case "graph": return 1.0; // Référence case "pnp": return 1.5; // Plus lent (PowerShell) case "cli": return 2.0; // Plus lent (CLI) default: return 1.0; } } /** * Met à jour la santé d'un provider */ updateProviderHealth(provider, status) { this.providerHealth.set(provider, { status, lastCheck: Date.now() }); logger.info(`Router: Santé du provider '${provider}' mise à jour: ${status}`); } /** * Met à jour les compteurs de rate limiting */ updateRateLimitCounter(provider, requestCount) { const now = Date.now(); const resetTime = now + (60 * 1000); // Reset dans 1 minute this.rateLimitCounters.set(provider, { count: requestCount, resetTime }); } /** * Incrémente le compteur de rate limiting pour un provider */ incrementRateLimitCounter(provider) { const now = Date.now(); const currentCounter = this.rateLimitCounters.get(provider); if (currentCounter && now < currentCounter.resetTime) { // Incrémenter le compteur existant this.rateLimitCounters.set(provider, { count: currentCounter.count + 1, resetTime: currentCounter.resetTime }); } else { // Créer un nouveau compteur this.rateLimitCounters.set(provider, { count: 1, resetTime: now + (60 * 1000) // Reset dans 1 minute }); } } /** * Initialise la santé des providers */ initializeProviderHealth() { this.providerHealth.set("graph", { status: "healthy", lastCheck: Date.now() }); this.providerHealth.set("pnp", { status: "healthy", lastCheck: Date.now() }); this.providerHealth.set("cli", { status: "healthy", lastCheck: Date.now() }); } /** * Initialise l'historique des performances */ initializePerformanceHistory() { this.performanceHistory.set("graph", { avgTime: 1000, successRate: 0.95, lastUpdate: Date.now(), totalCalls: 100, successfulCalls: 95 }); this.performanceHistory.set("pnp", { avgTime: 3000, successRate: 0.90, lastUpdate: Date.now(), totalCalls: 80, successfulCalls: 72 }); this.performanceHistory.set("cli", { avgTime: 5000, successRate: 0.85, lastUpdate: Date.now(), totalCalls: 60, successfulCalls: 51 }); } /** * Obtient les statistiques du router */ getRouterStats() { return { totalActions: this.countTotalActions(), providerHealth: Object.fromEntries(this.providerHealth), performanceHistory: Object.fromEntries(this.performanceHistory), rateLimitCounters: Object.fromEntries(this.rateLimitCounters), userPreferences: Object.fromEntries(this.userPreferences) }; } /** * Compte le nombre total d'actions */ countTotalActions() { return Object.keys(this.capabilityMatrix.actions).length; } /** * Obtient les actions supportées par un provider */ getActionsForProvider(provider) { const actions = []; for (const [action, config] of Object.entries(this.capabilityMatrix.actions)) { if (config.providers.includes(provider)) { actions.push(action); } } return actions; } /** * Vérifie si une action est supportée par un provider */ isActionSupported(action, provider) { const actionConfig = this.findActionConfig(action); return actionConfig ? actionConfig.providers.includes(provider) : false; } /** * Obtient les scopes requis pour une action */ getRequiredScopes(action) { const actionConfig = this.findActionConfig(action); return actionConfig ? actionConfig.scopes : []; } /** * Obtient la santé d'un provider */ getProviderHealth(provider) { const health = this.providerHealth.get(provider); return health ? health.status : "healthy"; } /** * Obtient les compteurs de rate limiting */ getRateLimitCounters(provider) { const counter = this.rateLimitCounters.get(provider); return counter ? counter.count : 0; } /** * Obtient la matrice de capacités */ getCapabilityMatrix() { return this.capabilityMatrix; } /** * Obtient les capacités d'une action */ getActionCapabilities(action) { return this.findActionConfig(action); } /** * Valide une action */ validateAction(action) { const errors = []; const actionConfig = this.findActionConfig(action); if (!actionConfig) { errors.push(`Action '${action}' not found in capability matrix`); } return { isValid: errors.length === 0, errors }; } /** * Obtient les recommandations de providers pour une action */ getProviderRecommendations(action, availableScopes) { const actionConfig = this.findActionConfig(action); if (!actionConfig) return []; const availableProviders = this.filterProvidersByScopes(actionConfig.providers, actionConfig.scopes, availableScopes); return availableProviders.map(provider => ({ provider, name: this.capabilityMatrix.providers[provider]?.name || provider, preferred: actionConfig.preferred === provider, health: this.getProviderHealth(provider), performance: this.getPerformanceMetrics(provider) })); } }