UNPKG

snow-flow

Version:

Snow-Flow v3.2.0: Complete ServiceNow Enterprise Suite with 180+ MCP Tools. ATF Testing, Knowledge Management, Service Catalog, Change Management with CAB scheduling, Virtual Agent chatbots with NLU, Performance Analytics KPIs, Flow Designer automation, A

441 lines 17.2 kB
"use strict"; /** * MCP Server Manager - Manages background MCP servers for Claude Code integration */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPServerManager = void 0; const child_process_1 = require("child_process"); const fs_1 = require("fs"); const path_1 = require("path"); const events_1 = require("events"); const os_1 = __importDefault(require("os")); const unified_auth_store_js_1 = require("./unified-auth-store.js"); const mcp_singleton_lock_js_1 = require("./mcp-singleton-lock.js"); const mcp_process_manager_js_1 = require("./mcp-process-manager.js"); class MCPServerManager extends events_1.EventEmitter { constructor(configPath) { super(); this.servers = new Map(); this.configPath = configPath || (0, path_1.join)(process.env.SNOW_FLOW_HOME || (0, path_1.join)(os_1.default.homedir(), '.snow-flow'), 'mcp-servers.json'); this.logPath = (0, path_1.join)(process.env.SNOW_FLOW_HOME || (0, path_1.join)(os_1.default.homedir(), '.snow-flow'), 'logs'); } /** * Initialize MCP server manager */ async initialize() { // Ensure directories exist await fs_1.promises.mkdir(process.env.SNOW_FLOW_HOME || (0, path_1.join)(os_1.default.homedir(), '.snow-flow'), { recursive: true }); await fs_1.promises.mkdir(this.logPath, { recursive: true }); // Load existing configuration await this.loadConfiguration(); } /** * Load MCP server configuration */ async loadConfiguration() { try { const configData = await fs_1.promises.readFile(this.configPath, 'utf-8'); const configs = JSON.parse(configData); for (const config of configs) { this.servers.set(config.name, { name: config.name, script: config.script, port: config.port, host: config.host, status: 'stopped' }); } } catch (error) { // No configuration file exists yet, create default await this.createDefaultConfiguration(); } } /** * Create default MCP server configuration */ async createDefaultConfiguration() { const defaultServers = [ { name: 'ServiceNow Deployment MCP', script: 'dist/mcp/servicenow-deployment-mcp.js', autoStart: true, env: { 'SNOW_INSTANCE': process.env.SNOW_INSTANCE || '', 'SNOW_CLIENT_ID': process.env.SNOW_CLIENT_ID || '', 'SNOW_CLIENT_SECRET': process.env.SNOW_CLIENT_SECRET || '' } }, { name: 'ServiceNow Update Set MCP', script: 'dist/mcp/servicenow-update-set-mcp.js', autoStart: true }, { name: 'ServiceNow Intelligent MCP', script: 'dist/mcp/servicenow-intelligent-mcp.js', autoStart: true }, { name: 'ServiceNow Graph Memory MCP', script: 'dist/mcp/servicenow-graph-memory-mcp.js', autoStart: true, env: { 'NEO4J_URI': process.env.NEO4J_URI || '', 'NEO4J_USER': process.env.NEO4J_USER || '', 'NEO4J_PASSWORD': process.env.NEO4J_PASSWORD || '' } }, { name: 'ServiceNow Operations MCP', script: 'dist/mcp/servicenow-operations-mcp.js', autoStart: true }, { name: 'ServiceNow Platform Development MCP', script: 'dist/mcp/servicenow-platform-development-mcp.js', autoStart: true }, { name: 'ServiceNow Integration MCP', script: 'dist/mcp/servicenow-integration-mcp.js', autoStart: true }, { name: 'ServiceNow Automation MCP', script: 'dist/mcp/servicenow-automation-mcp.js', autoStart: true }, { name: 'ServiceNow Security & Compliance MCP', script: 'dist/mcp/servicenow-security-compliance-mcp.js', autoStart: true }, { name: 'ServiceNow Reporting & Analytics MCP', script: 'dist/mcp/servicenow-reporting-analytics-mcp.js', autoStart: true }, ]; await this.saveConfiguration(defaultServers); // Initialize server entries for (const config of defaultServers) { this.servers.set(config.name, { name: config.name, script: config.script, port: config.port, host: config.host, status: 'stopped' }); } } /** * Save MCP server configuration */ async saveConfiguration(configs) { await fs_1.promises.writeFile(this.configPath, JSON.stringify(configs, null, 2)); } /** * Start a specific MCP server */ async startServer(name) { const server = this.servers.get(name); if (!server) { throw new Error(`Server '${name}' not found`); } if (server.status === 'running') { return true; // Already running } // Check if we can spawn a new server const processManager = mcp_process_manager_js_1.MCPProcessManager.getInstance(); if (!processManager.canSpawnServer()) { // Try cleanup first processManager.cleanup(); // Check again after cleanup if (!processManager.canSpawnServer()) { throw new Error('Cannot spawn server: resource limits exceeded. Too many MCP processes running.'); } } server.status = 'starting'; this.emit('serverStarting', name); try { // Determine script path based on installation type let scriptPath = server.script; // If script is a relative path, resolve it if (!scriptPath.startsWith('/')) { // Check if we're in a global npm installation const isGlobalInstall = __dirname.includes('node_modules/snow-flow'); if (isGlobalInstall) { // For global installs, use the absolute path from __dirname const packageRoot = __dirname.split('node_modules/snow-flow')[0] + 'node_modules/snow-flow'; scriptPath = (0, path_1.join)(packageRoot, server.script); } else { // For local development, use process.cwd() scriptPath = (0, path_1.join)(process.cwd(), server.script); } } // Check if script exists await fs_1.promises.access(scriptPath); // Bridge OAuth tokens to MCP servers await unified_auth_store_js_1.unifiedAuthStore.bridgeToMCP(); // Get current tokens for the MCP server const tokens = await unified_auth_store_js_1.unifiedAuthStore.getTokens(); const authEnv = {}; if (tokens) { authEnv.SNOW_OAUTH_TOKENS = JSON.stringify(tokens); authEnv.SNOW_INSTANCE = tokens.instance; authEnv.SNOW_CLIENT_ID = tokens.clientId; authEnv.SNOW_CLIENT_SECRET = tokens.clientSecret; if (tokens.accessToken) { authEnv.SNOW_ACCESS_TOKEN = tokens.accessToken; } if (tokens.refreshToken) { authEnv.SNOW_REFRESH_TOKEN = tokens.refreshToken; } if (tokens.expiresAt) { authEnv.SNOW_TOKEN_EXPIRES_AT = tokens.expiresAt; } } // Start the process with OAuth tokens const childProcess = (0, child_process_1.spawn)('node', [scriptPath], { stdio: ['ignore', 'pipe', 'pipe'], detached: true, env: { ...process.env, ...authEnv } }); // Set up logging const logFile = (0, path_1.join)(this.logPath, `${name.replace(/\\s+/g, '_').toLowerCase()}.log`); const logStream = await fs_1.promises.open(logFile, 'a'); childProcess.stdout?.on('data', (data) => { logStream.write(`[${new Date().toISOString()}] STDOUT: ${data}`); }); childProcess.stderr?.on('data', (data) => { logStream.write(`[${new Date().toISOString()}] STDERR: ${data}`); server.lastError = data.toString(); }); childProcess.on('exit', (code) => { logStream.write(`[${new Date().toISOString()}] Process exited with code: ${code}\\n`); logStream.close(); server.status = code === 0 ? 'stopped' : 'error'; server.process = undefined; server.pid = undefined; this.emit('serverStopped', name, code); }); childProcess.on('error', (error) => { server.status = 'error'; server.lastError = error.message; server.process = undefined; server.pid = undefined; this.emit('serverError', name, error); }); // Update server info server.process = childProcess; server.pid = childProcess.pid; server.status = 'running'; server.startedAt = new Date(); server.lastError = undefined; // Detach process so it runs independently childProcess.unref(); this.emit('serverStarted', name, childProcess.pid); return true; } catch (error) { server.status = 'error'; server.lastError = error instanceof Error ? error.message : String(error); this.emit('serverError', name, error); return false; } } /** * Stop a specific MCP server */ async stopServer(name) { const server = this.servers.get(name); if (!server || !server.process) { return true; // Already stopped } try { // Try graceful shutdown first server.process.kill('SIGTERM'); // Wait for graceful shutdown with shorter timeout to prevent hanging await new Promise((resolve) => { const timeout = setTimeout(() => { // Force kill if graceful shutdown fails if (server.process) { try { server.process.kill('SIGKILL'); } catch (e) { // Process might already be dead } } resolve(undefined); }, 2000); // Reduced from 5000ms to 2000ms to prevent hanging server.process?.on('exit', () => { clearTimeout(timeout); resolve(undefined); }); }); server.status = 'stopped'; server.process = undefined; server.pid = undefined; this.emit('serverStopped', name, 0); return true; } catch (error) { server.lastError = error instanceof Error ? error.message : String(error); this.emit('serverError', name, error); return false; } } /** * Start all configured MCP servers */ async startAllServers() { // 🔒 SINGLETON CHECK - Prevent duplicate instances const singletonLock = (0, mcp_singleton_lock_js_1.getMCPSingletonLock)(); if (!singletonLock.acquire()) { throw new Error('❌ MCP servers already running. Cannot start duplicate instances.'); } // Clean up any existing duplicates first const processManager = mcp_process_manager_js_1.MCPProcessManager.getInstance(); processManager.killDuplicates(); console.log('✅ Starting all MCP servers (singleton protected with resource limits)...'); console.log(processManager.getResourceSummary()); // Start servers sequentially with delay to avoid resource spikes let started = 0; for (const name of Array.from(this.servers.keys())) { try { // Check resource limits before each start if (!processManager.canSpawnServer()) { console.warn(`⚠️ Skipping ${name} - resource limits reached`); continue; } await this.startServer(name); started++; // Small delay between starts to avoid CPU spike await new Promise(resolve => setTimeout(resolve, 200)); } catch (error) { console.error(`Failed to start server '${name}':`, error); } } console.log(`✅ Started ${started}/${this.servers.size} MCP servers`); console.log(processManager.getResourceSummary()); } /** * Stop all running MCP servers */ async stopAllServers() { const promises = Array.from(this.servers.keys()).map(name => this.stopServer(name).catch(error => { console.error(`Failed to stop server '${name}':`, error); return false; })); await Promise.all(promises); // Release singleton lock after stopping all servers const singletonLock = (0, mcp_singleton_lock_js_1.getMCPSingletonLock)(); if (singletonLock.isAcquired()) { singletonLock.release(); console.log('✅ Released MCP singleton lock after stopping all servers'); } } /** * Get status of a specific server */ getServer(name) { return this.servers.get(name); } /** * Check if all servers are running */ areAllServersRunning() { return Array.from(this.servers.values()).every(server => server.status === 'running'); } /** * Get running servers count */ getRunningServersCount() { return Array.from(this.servers.values()).filter(server => server.status === 'running').length; } /** * Add a new server configuration */ async addServer(config) { this.servers.set(config.name, { name: config.name, script: config.script, port: config.port, host: config.host, status: 'stopped' }); // Save updated configuration const configs = Array.from(this.servers.values()).map(server => ({ name: server.name, script: server.script, port: server.port, host: server.host, autoStart: true })); await this.saveConfiguration(configs); } /** * Remove a server configuration */ async removeServer(name) { // Stop server first if running await this.stopServer(name); // Remove from memory this.servers.delete(name); // Save updated configuration const configs = Array.from(this.servers.values()).map(server => ({ name: server.name, script: server.script, port: server.port, host: server.host, autoStart: true })); await this.saveConfiguration(configs); } /** * Get list of all servers (for monitoring/status display) */ getServerList() { return Array.from(this.servers.values()); } /** * Get server status by name */ getServerStatus(name) { return this.servers.get(name); } /** * Get overall system status */ getSystemStatus() { const servers = this.getServerList(); const running = servers.filter(s => s.status === 'running').length; const total = servers.length; return { total, running, stopped: servers.filter(s => s.status === 'stopped').length, starting: servers.filter(s => s.status === 'starting').length, error: servers.filter(s => s.status === 'error').length, health: running === total ? 'healthy' : running > 0 ? 'partial' : 'down' }; } /** * Cleanup - stop all servers and clean up resources */ async cleanup() { await this.stopAllServers(); } } exports.MCPServerManager = MCPServerManager; //# sourceMappingURL=mcp-server-manager.js.map