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
JavaScript
;
/**
* 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