UNPKG

@juspay/neurolink

Version:

Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio

580 lines (579 loc) 20.8 kB
/** * Multi-Server Manager * * Coordinates multiple MCP servers with load balancing, failover, * and unified tool discovery across all registered servers. * * Features: * - Load balancing strategies (round-robin, least-loaded, random) * - Health-aware routing * - Automatic failover * - Unified tool namespace management * - Cross-server tool discovery * * @module mcp/multiServerManager * @since 8.39.0 */ import { EventEmitter } from "events"; import { logger } from "../utils/logger.js"; import { ErrorFactory } from "../utils/errorHandling.js"; /** * Multi-Server Manager * * Coordinates multiple MCP servers for unified tool access * with load balancing and failover capabilities. * * @example * ```typescript * const manager = new MultiServerManager({ * defaultStrategy: "round-robin", * healthAwareRouting: true, * autoNamespace: true, * }); * * // Add servers * manager.addServer(server1Info); * manager.addServer(server2Info); * * // Create a group for redundant servers * manager.createGroup({ * id: "data-servers", * name: "Data Processing Servers", * servers: ["server1", "server2"], * strategy: "least-loaded", * }); * * // Get unified tool list * const tools = manager.getUnifiedTools(); * * // Execute with automatic routing * const result = await manager.executeTool("readFile", { path: "/data" }); * ``` */ export class MultiServerManager extends EventEmitter { config; servers = new Map(); groups = new Map(); metrics = new Map(); roundRobinCounters = new Map(); toolPreferences = new Map(); // toolName -> preferred serverId constructor(config = {}) { super(); this.config = { defaultStrategy: config.defaultStrategy ?? "round-robin", healthAwareRouting: config.healthAwareRouting ?? true, healthCheckInterval: config.healthCheckInterval ?? 30000, maxFailoverRetries: config.maxFailoverRetries ?? 3, namespaceSeparator: config.namespaceSeparator ?? ".", autoNamespace: config.autoNamespace ?? false, conflictResolution: config.conflictResolution ?? "first-wins", }; } /** * Add a server to the manager */ addServer(server) { this.servers.set(server.id, server); // Initialize metrics this.metrics.set(server.id, { activeRequests: 0, totalRequests: 0, completedRequests: 0, averageResponseTime: 0, errorRate: 0, isHealthy: server.status === "connected", }); this.emit("serverAdded", { serverId: server.id, server }); logger.debug(`[MultiServerManager] Added server: ${server.id} (${server.name})`); } /** * Remove a server from the manager */ removeServer(serverId) { const server = this.servers.get(serverId); if (!server) { return false; } // Remove from all groups for (const [groupId, group] of this.groups) { const index = group.servers.indexOf(serverId); if (index !== -1) { group.servers.splice(index, 1); // Remove empty groups if (group.servers.length === 0) { this.groups.delete(groupId); this.roundRobinCounters.delete(groupId); } } } this.servers.delete(serverId); this.metrics.delete(serverId); // Clear tool preferences for this server for (const [toolName, preferredServer] of this.toolPreferences) { if (preferredServer === serverId) { this.toolPreferences.delete(toolName); } } this.emit("serverRemoved", { serverId }); logger.debug(`[MultiServerManager] Removed server: ${serverId}`); return true; } /** * Update server info */ updateServer(serverId, updates) { const server = this.servers.get(serverId); if (!server) { throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId }); } const updatedServer = { ...server, ...updates, id: serverId }; this.servers.set(serverId, updatedServer); // Update health status in metrics const metrics = this.metrics.get(serverId); if (metrics && updates.status !== undefined) { metrics.isHealthy = updates.status === "connected"; } this.emit("serverUpdated", { serverId, server: updatedServer }); } /** * Create a server group */ createGroup(group) { // Validate servers exist for (const serverId of group.servers) { if (!this.servers.has(serverId)) { throw ErrorFactory.invalidConfiguration("serverGroup.servers", `Server '${serverId}' not found when creating group '${group.id}'`, { serverId, groupId: group.id }); } } this.groups.set(group.id, group); this.roundRobinCounters.set(group.id, 0); this.emit("groupCreated", { group }); logger.debug(`[MultiServerManager] Created group: ${group.id} with ${group.servers.length} servers`); } /** * Remove a server group */ removeGroup(groupId) { const removed = this.groups.delete(groupId); if (removed) { this.roundRobinCounters.delete(groupId); this.emit("groupRemoved", { groupId }); } return removed; } /** * Add a server to a group */ addServerToGroup(serverId, groupId) { const group = this.groups.get(groupId); if (!group) { throw ErrorFactory.invalidConfiguration("groupId", `Group '${groupId}' not found`, { groupId }); } if (!this.servers.has(serverId)) { throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId, groupId }); } if (!group.servers.includes(serverId)) { group.servers.push(serverId); this.emit("serverAddedToGroup", { serverId, groupId }); } } /** * Remove a server from a group */ removeServerFromGroup(serverId, groupId) { const group = this.groups.get(groupId); if (!group) { return false; } const index = group.servers.indexOf(serverId); if (index !== -1) { group.servers.splice(index, 1); this.emit("serverRemovedFromGroup", { serverId, groupId }); return true; } return false; } /** * Get unified tool list from all servers */ getUnifiedTools() { const toolMap = new Map(); for (const [serverId, server] of this.servers) { const metrics = this.metrics.get(serverId); const isHealthy = metrics?.isHealthy ?? true; // Skip unhealthy servers in health-aware mode if (this.config.healthAwareRouting && !isHealthy) { continue; } for (const tool of server.tools || []) { const existingTool = toolMap.get(tool.name); if (existingTool) { // Tool exists from another server - mark as conflict existingTool.hasConflict = true; existingTool.servers.push({ serverId, serverName: server.name, inputSchema: tool.inputSchema, priority: this.getServerPriority(serverId), }); } else { // New tool toolMap.set(tool.name, { name: tool.name, description: tool.description, servers: [ { serverId, serverName: server.name, inputSchema: tool.inputSchema, priority: this.getServerPriority(serverId), }, ], hasConflict: false, preferredServerId: this.toolPreferences.get(tool.name), }); } } } // Sort servers by priority within each tool for (const tool of toolMap.values()) { tool.servers.sort((a, b) => a.priority - b.priority); // Set preferred server if not already set if (!tool.preferredServerId && tool.servers.length > 0) { tool.preferredServerId = tool.servers[0].serverId; } } return Array.from(toolMap.values()); } /** * Get namespaced tools (server.toolName format) */ getNamespacedTools() { const tools = []; for (const [serverId, server] of this.servers) { // Skip unhealthy servers in health-aware mode if (this.config.healthAwareRouting) { const metrics = this.metrics.get(serverId); const isHealthy = metrics?.isHealthy ?? true; if (!isHealthy) { continue; } } for (const tool of server.tools || []) { tools.push({ fullName: `${serverId}${this.config.namespaceSeparator}${tool.name}`, toolName: tool.name, serverId, serverName: server.name, description: tool.description, inputSchema: tool.inputSchema, }); } } return tools; } /** * Set tool preference for routing */ setToolPreference(toolName, serverId) { if (!this.servers.has(serverId)) { throw ErrorFactory.invalidConfiguration("serverId", `Server '${serverId}' not found`, { serverId, toolName }); } this.toolPreferences.set(toolName, serverId); this.emit("toolPreferenceSet", { toolName, serverId }); } /** * Clear tool preference */ clearToolPreference(toolName) { this.toolPreferences.delete(toolName); } /** * Select a server for a tool using load balancing */ selectServer(toolName, groupId) { // Check for tool preference first const preferredServerId = this.toolPreferences.get(toolName); if (preferredServerId) { const server = this.servers.get(preferredServerId); const metrics = this.metrics.get(preferredServerId); if (server && (!this.config.healthAwareRouting || metrics?.isHealthy)) { // Check if server has the tool if (server.tools?.some((t) => t.name === toolName)) { return { serverId: preferredServerId, server }; } } } // Get candidate servers (from group or all servers) let candidates; if (groupId) { const group = this.groups.get(groupId); if (!group) { logger.warn(`[MultiServerManager] Group '${groupId}' not found`); return null; } // Filter group servers to only those that have the requested tool candidates = group.servers.filter((serverId) => { const server = this.servers.get(serverId); return server?.tools?.some((t) => t.name === toolName); }); } else { // Find all servers that have this tool candidates = []; for (const [serverId, server] of this.servers) { if (server.tools?.some((t) => t.name === toolName)) { candidates.push(serverId); } } } if (candidates.length === 0) { return null; } // Filter by health if enabled (prefer group-level flag, fall back to global) const healthAware = groupId ? (this.groups.get(groupId)?.healthAware ?? this.config.healthAwareRouting) : this.config.healthAwareRouting; if (healthAware) { candidates = candidates.filter((id) => { const metrics = this.metrics.get(id); return metrics?.isHealthy ?? true; }); if (candidates.length === 0) { logger.warn(`[MultiServerManager] No healthy servers available for tool '${toolName}'`); return null; } } // Apply load balancing strategy const strategy = groupId ? (this.groups.get(groupId)?.strategy ?? this.config.defaultStrategy) : this.config.defaultStrategy; const selectedId = this.applyStrategy(strategy, candidates, groupId); if (!selectedId) { return null; } const server = this.servers.get(selectedId); return server ? { serverId: selectedId, server } : null; } /** * Apply load balancing strategy */ applyStrategy(strategy, candidates, groupId) { if (candidates.length === 0) { return null; } if (candidates.length === 1) { return candidates[0]; } switch (strategy) { case "round-robin": { const counterKey = groupId ?? "default"; const counter = this.roundRobinCounters.get(counterKey) ?? 0; const selected = candidates[counter % candidates.length]; this.roundRobinCounters.set(counterKey, counter + 1); return selected; } case "least-loaded": { let minLoad = Infinity; let selected = candidates[0]; for (const serverId of candidates) { const metrics = this.metrics.get(serverId); const load = metrics?.activeRequests ?? 0; if (load < minLoad) { minLoad = load; selected = serverId; } } return selected; } case "random": { const index = Math.floor(Math.random() * candidates.length); return candidates[index]; } case "weighted": { if (!groupId) { // Fall back to random for non-group selection const index = Math.floor(Math.random() * candidates.length); return candidates[index]; } const group = this.groups.get(groupId); if (!group?.weights) { const index = Math.floor(Math.random() * candidates.length); return candidates[index]; } // Build effective weights: use configured weight or default of 1 for unlisted candidates const DEFAULT_WEIGHT = 1; const effectiveWeights = candidates.map((serverId) => { const weights = group.weights ?? []; const configured = weights.find((w) => w.serverId === serverId); return { serverId, weight: configured?.weight ?? DEFAULT_WEIGHT, }; }); const totalWeight = effectiveWeights.reduce((sum, w) => sum + w.weight, 0); if (totalWeight === 0) { const index = Math.floor(Math.random() * candidates.length); return candidates[index]; } let random = Math.random() * totalWeight; for (const ew of effectiveWeights) { random -= ew.weight; if (random <= 0) { return ew.serverId; } } return candidates[0]; } case "failover-only": { // Return first healthy server by priority const serverPriorities = candidates .map((id) => ({ id, priority: this.getServerPriority(id, groupId), })) .sort((a, b) => a.priority - b.priority); return serverPriorities[0]?.id ?? null; } default: return candidates[0]; } } /** * Get server priority (lower = higher priority) * * @param serverId - The server to look up * @param groupId - Optional group to scope the lookup to, avoiding * nondeterministic iteration across all groups. */ getServerPriority(serverId, groupId) { // Scoped lookup: check only the specified group if (groupId) { const group = this.groups.get(groupId); if (group?.weights) { const weight = group.weights.find((w) => w.serverId === serverId); if (weight) { return weight.priority; } } } // Fallback: check all groups for weight/priority settings for (const group of this.groups.values()) { if (group.weights) { const weight = group.weights.find((w) => w.serverId === serverId); if (weight) { return weight.priority; } } } // Default priority based on order added const serverIds = Array.from(this.servers.keys()); return serverIds.indexOf(serverId); } /** * Update server metrics */ updateMetrics(serverId, updates) { const metrics = this.metrics.get(serverId); if (metrics) { Object.assign(metrics, updates); this.emit("metricsUpdated", { serverId, metrics: { ...metrics } }); } } /** * Mark request started */ requestStarted(serverId) { const metrics = this.metrics.get(serverId); if (metrics) { metrics.activeRequests++; metrics.totalRequests++; } } /** * Mark request completed */ requestCompleted(serverId, duration, success) { const metrics = this.metrics.get(serverId); if (metrics) { metrics.activeRequests = Math.max(0, metrics.activeRequests - 1); metrics.completedRequests++; // Update average response time using only completed requests const totalTime = metrics.averageResponseTime * (metrics.completedRequests - 1) + duration; metrics.averageResponseTime = totalTime / metrics.completedRequests; // Update error rate (simple moving average) const alpha = 0.1; // Smoothing factor metrics.errorRate = metrics.errorRate * (1 - alpha) + (success ? 0 : 1) * alpha; } } /** * Get all servers */ getServers() { return Array.from(this.servers.values()); } /** * Get server by ID */ getServer(serverId) { return this.servers.get(serverId); } /** * Get all groups */ getGroups() { return Array.from(this.groups.values()); } /** * Get group by ID */ getGroup(groupId) { return this.groups.get(groupId); } /** * Get server metrics */ getServerMetrics(serverId) { return this.metrics.get(serverId); } /** * Get all metrics */ getAllMetrics() { return new Map(this.metrics); } /** * Get statistics */ getStatistics() { let healthyServers = 0; let totalRequests = 0; let activeRequests = 0; for (const metrics of this.metrics.values()) { if (metrics.isHealthy) { healthyServers++; } totalRequests += metrics.totalRequests; activeRequests += metrics.activeRequests; } const unifiedTools = this.getUnifiedTools(); const conflictingTools = unifiedTools.filter((t) => t.hasConflict).length; return { totalServers: this.servers.size, healthyServers, totalGroups: this.groups.size, totalTools: unifiedTools.length, conflictingTools, totalRequests, activeRequests, }; } } /** * Global multi-server manager instance */ export const globalMultiServerManager = new MultiServerManager();