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