Skip to main content
Glama
server-groups.jsโ€ข10.7 kB
/** * Server Groups Management * Manages groups of servers for batch operations */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { logger } from './logger.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Default groups file location const GROUPS_FILE = path.join(__dirname, '..', '.server-groups.json'); // Group execution strategies export const EXECUTION_STRATEGIES = { PARALLEL: 'parallel', // Execute on all servers at once SEQUENTIAL: 'sequential', // Execute one by one ROLLING: 'rolling' // Execute with delay between servers }; class ServerGroups { constructor() { this.groups = this.loadGroups(); } /** * Load groups from file */ loadGroups() { try { if (fs.existsSync(GROUPS_FILE)) { const data = fs.readFileSync(GROUPS_FILE, 'utf8'); return JSON.parse(data); } } catch (error) { logger.warn('Failed to load server groups', { error: error.message }); } // Return default groups return { all: { description: 'All configured servers', servers: [], dynamic: true // Will be populated from server config }, production: { description: 'Production servers', servers: [], strategy: EXECUTION_STRATEGIES.ROLLING, delay: 5000 // 5 seconds between servers }, staging: { description: 'Staging/test servers', servers: [], strategy: EXECUTION_STRATEGIES.PARALLEL }, development: { description: 'Development servers', servers: [], strategy: EXECUTION_STRATEGIES.PARALLEL } }; } /** * Save groups to file */ saveGroups() { try { // Don't save dynamic groups const groupsToSave = {}; for (const [name, group] of Object.entries(this.groups)) { if (!group.dynamic) { groupsToSave[name] = group; } } fs.writeFileSync(GROUPS_FILE, JSON.stringify(groupsToSave, null, 2)); logger.info('Server groups saved', { count: Object.keys(groupsToSave).length }); return true; } catch (error) { logger.error('Failed to save server groups', { error: error.message }); return false; } } /** * Get a group by name */ getGroup(name) { const group = this.groups[name.toLowerCase()]; if (!group) { throw new Error(`Group '${name}' not found`); } // For 'all' group, return all configured servers if (name.toLowerCase() === 'all' && group.dynamic) { return { ...group, servers: this.getAllServers() }; } return group; } /** * Get all configured servers */ getAllServers() { // This will be populated from the main server config const servers = []; for (const [key, value] of Object.entries(process.env)) { if (key.startsWith('SSH_SERVER_') && key.endsWith('_HOST')) { const serverName = key.replace('SSH_SERVER_', '').replace('_HOST', '').toLowerCase(); servers.push(serverName); } } return servers; } /** * Create a new group */ createGroup(name, servers = [], options = {}) { const groupName = name.toLowerCase(); if (this.groups[groupName] && !options.overwrite) { throw new Error(`Group '${name}' already exists`); } this.groups[groupName] = { description: options.description || `Group: ${name}`, servers: servers, strategy: options.strategy || EXECUTION_STRATEGIES.PARALLEL, delay: options.delay || 0, stopOnError: options.stopOnError || false, created: new Date().toISOString() }; this.saveGroups(); logger.info('Server group created', { name: groupName, servers: servers.length, strategy: this.groups[groupName].strategy }); return this.groups[groupName]; } /** * Update a group */ updateGroup(name, updates) { const groupName = name.toLowerCase(); const group = this.groups[groupName]; if (!group) { throw new Error(`Group '${name}' not found`); } if (group.dynamic) { throw new Error(`Cannot update dynamic group '${name}'`); } // Update group properties if (updates.servers !== undefined) { group.servers = updates.servers; } if (updates.description !== undefined) { group.description = updates.description; } if (updates.strategy !== undefined) { group.strategy = updates.strategy; } if (updates.delay !== undefined) { group.delay = updates.delay; } if (updates.stopOnError !== undefined) { group.stopOnError = updates.stopOnError; } group.updated = new Date().toISOString(); this.saveGroups(); logger.info('Server group updated', { name: groupName, updates: Object.keys(updates) }); return group; } /** * Delete a group */ deleteGroup(name) { const groupName = name.toLowerCase(); const group = this.groups[groupName]; if (!group) { throw new Error(`Group '${name}' not found`); } if (group.dynamic) { throw new Error(`Cannot delete dynamic group '${name}'`); } delete this.groups[groupName]; this.saveGroups(); logger.info('Server group deleted', { name: groupName }); return true; } /** * Add servers to a group */ addServers(name, servers) { const groupName = name.toLowerCase(); const group = this.groups[groupName]; if (!group) { throw new Error(`Group '${name}' not found`); } if (group.dynamic) { throw new Error(`Cannot modify dynamic group '${name}'`); } // Add servers (avoid duplicates) const currentServers = new Set(group.servers); servers.forEach(server => currentServers.add(server.toLowerCase())); group.servers = Array.from(currentServers); this.saveGroups(); logger.info('Servers added to group', { group: groupName, added: servers.length, total: group.servers.length }); return group; } /** * Remove servers from a group */ removeServers(name, servers) { const groupName = name.toLowerCase(); const group = this.groups[groupName]; if (!group) { throw new Error(`Group '${name}' not found`); } if (group.dynamic) { throw new Error(`Cannot modify dynamic group '${name}'`); } // Remove servers const toRemove = new Set(servers.map(s => s.toLowerCase())); group.servers = group.servers.filter(s => !toRemove.has(s)); this.saveGroups(); logger.info('Servers removed from group', { group: groupName, removed: servers.length, remaining: group.servers.length }); return group; } /** * List all groups */ listGroups() { const groups = []; for (const [name, group] of Object.entries(this.groups)) { // Populate dynamic groups if (group.dynamic && name === 'all') { group.servers = this.getAllServers(); } groups.push({ name, ...group, serverCount: group.servers.length }); } return groups; } /** * Execute command on group with strategy */ async executeOnGroup(groupName, executor, options = {}) { const group = this.getGroup(groupName); const results = []; const strategy = options.strategy || group.strategy || EXECUTION_STRATEGIES.PARALLEL; const delay = options.delay || group.delay || 0; const stopOnError = options.stopOnError !== undefined ? options.stopOnError : group.stopOnError; logger.info('Executing on server group', { group: groupName, servers: group.servers.length, strategy, delay }); switch (strategy) { case EXECUTION_STRATEGIES.PARALLEL: { // Execute on all servers simultaneously const promises = group.servers.map(async (server) => { try { const result = await executor(server); return { server, success: true, result }; } catch (error) { logger.error(`Execution failed on ${server}`, { error: error.message }); return { server, success: false, error: error.message }; } }); const parallelResults = await Promise.all(promises); results.push(...parallelResults); break; } case EXECUTION_STRATEGIES.SEQUENTIAL: case EXECUTION_STRATEGIES.ROLLING: // Execute one by one for (const server of group.servers) { try { const result = await executor(server); results.push({ server, success: true, result }); // Add delay for rolling strategy if (strategy === EXECUTION_STRATEGIES.ROLLING && delay > 0) { logger.debug(`Waiting ${delay}ms before next server`); await new Promise(resolve => setTimeout(resolve, delay)); } } catch (error) { logger.error(`Execution failed on ${server}`, { error: error.message }); results.push({ server, success: false, error: error.message }); // Stop on error if configured if (stopOnError) { logger.warn('Stopping execution due to error', { server }); break; } } } break; default: throw new Error(`Unknown execution strategy: ${strategy}`); } // Summary const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; logger.info('Group execution completed', { group: groupName, successful, failed, total: results.length }); return { group: groupName, strategy, results, summary: { total: results.length, successful, failed } }; } } // Export singleton instance export const serverGroups = new ServerGroups(); // Export convenience functions export const getGroup = (name) => serverGroups.getGroup(name); export const createGroup = (name, servers, options) => serverGroups.createGroup(name, servers, options); export const updateGroup = (name, updates) => serverGroups.updateGroup(name, updates); export const deleteGroup = (name) => serverGroups.deleteGroup(name); export const addServersToGroup = (name, servers) => serverGroups.addServers(name, servers); export const removeServersFromGroup = (name, servers) => serverGroups.removeServers(name, servers); export const listGroups = () => serverGroups.listGroups(); export const executeOnGroup = (name, executor, options) => serverGroups.executeOnGroup(name, executor, options); export default serverGroups;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bvisible/mcp-ssh-manager'

If you have feedback or need assistance with the MCP directory API, please join our Discord server