UNPKG

auto-publishing-mcp-server

Version:

Enterprise-grade MCP Server for Auto-Publishing with pre-publish validation, multi-cloud deployment, and monitoring

678 lines (592 loc) 23.5 kB
import { exec } from 'child_process'; import { promisify } from 'util'; import { promises as fs } from 'fs'; const execAsync = promisify(exec); class FeatureFlagTool { constructor() { this.flags = new Map(); this.flagHistory = new Map(); this.configPath = '/tmp/feature-flags-config.json'; this.loadConfiguration(); } async loadConfiguration() { try { const config = await fs.readFile(this.configPath, 'utf8'); const parsedConfig = JSON.parse(config); this.flags = new Map(Object.entries(parsedConfig.flags || {})); this.flagHistory = new Map(Object.entries(parsedConfig.flagHistory || {})); } catch (error) { console.log('No existing feature flag configuration found, starting fresh'); } } async saveConfiguration() { const config = { flags: Object.fromEntries(this.flags), flagHistory: Object.fromEntries(this.flagHistory), lastUpdated: new Date().toISOString() }; await fs.writeFile(this.configPath, JSON.stringify(config, null, 2)); } async createFlag(args) { const { flagName, description, defaultValue = false, environments = ['dev', 'staging', 'prod'], rolloutStrategy = 'all_users', rolloutPercentage = 100, targetAudience = [], dependencies = [] } = args; if (this.flags.has(flagName)) { return { success: false, message: `Feature flag '${flagName}' already exists` }; } const flag = { name: flagName, description, defaultValue, environments: {}, rolloutStrategy, rolloutPercentage, targetAudience, dependencies, createdAt: new Date().toISOString(), lastModified: new Date().toISOString(), version: 1 }; // Initialize environment configurations for (const env of environments) { flag.environments[env] = { enabled: defaultValue, rolloutPercentage: rolloutPercentage, lastModified: new Date().toISOString() }; } this.flags.set(flagName, flag); // Record creation in history this.recordFlagChange(flagName, 'created', { flag }); await this.saveConfiguration(); await this.propagateToContainers(flagName, flag); return { success: true, message: `Feature flag '${flagName}' created successfully`, flag: flag }; } async updateFlag(args) { const { flagName, environment, enabled, rolloutPercentage, targetAudience, description } = args; if (!this.flags.has(flagName)) { return { success: false, message: `Feature flag '${flagName}' not found` }; } const flag = this.flags.get(flagName); const oldFlag = JSON.parse(JSON.stringify(flag)); // Update flag properties if (description !== undefined) flag.description = description; if (targetAudience !== undefined) flag.targetAudience = targetAudience; // Update environment-specific settings if (environment) { if (!flag.environments[environment]) { flag.environments[environment] = {}; } if (enabled !== undefined) { flag.environments[environment].enabled = enabled; } if (rolloutPercentage !== undefined) { flag.environments[environment].rolloutPercentage = rolloutPercentage; } flag.environments[environment].lastModified = new Date().toISOString(); } flag.lastModified = new Date().toISOString(); flag.version += 1; this.flags.set(flagName, flag); // Record change in history this.recordFlagChange(flagName, 'updated', { oldFlag, newFlag: flag, environment }); await this.saveConfiguration(); await this.propagateToContainers(flagName, flag); return { success: true, message: `Feature flag '${flagName}' updated successfully`, flag: flag }; } async toggleFlag(args) { const { flagName, environment } = args; if (!this.flags.has(flagName)) { return { success: false, message: `Feature flag '${flagName}' not found` }; } const flag = this.flags.get(flagName); if (!flag.environments[environment]) { return { success: false, message: `Environment '${environment}' not configured for flag '${flagName}'` }; } const oldValue = flag.environments[environment].enabled; flag.environments[environment].enabled = !oldValue; flag.environments[environment].lastModified = new Date().toISOString(); flag.lastModified = new Date().toISOString(); flag.version += 1; this.flags.set(flagName, flag); // Record toggle in history this.recordFlagChange(flagName, 'toggled', { environment, oldValue, newValue: !oldValue }); await this.saveConfiguration(); await this.propagateToContainers(flagName, flag); return { success: true, message: `Feature flag '${flagName}' toggled from ${oldValue} to ${!oldValue} in ${environment}`, flag: flag }; } async getFlag(args) { const { flagName, environment, userId } = args; if (!this.flags.has(flagName)) { return { success: false, message: `Feature flag '${flagName}' not found` }; } const flag = this.flags.get(flagName); if (environment && !flag.environments[environment]) { return { success: false, message: `Environment '${environment}' not configured for flag '${flagName}'` }; } // Evaluate flag for specific environment and user let result = flag; if (environment) { const envConfig = flag.environments[environment]; const isEnabled = await this.evaluateFlag(flag, envConfig, userId); result = { ...flag, currentValue: isEnabled, environment: environment, evaluatedAt: new Date().toISOString() }; } return { success: true, flag: result }; } async evaluateFlag(flag, envConfig, userId) { if (!envConfig.enabled) { return false; } // Check rollout percentage if (envConfig.rolloutPercentage < 100) { const userHash = userId ? this.hashUserId(userId) : Math.random() * 100; if (userHash > envConfig.rolloutPercentage) { return false; } } // Check target audience if (flag.targetAudience.length > 0 && userId) { const userInAudience = flag.targetAudience.some(audience => { return this.matchesAudience(userId, audience); }); if (!userInAudience) { return false; } } // Check dependencies for (const dependency of flag.dependencies) { const depFlag = this.flags.get(dependency); if (!depFlag || !depFlag.environments[envConfig.environment]?.enabled) { return false; } } return true; } hashUserId(userId) { // Simple hash function for consistent user bucketing let hash = 0; for (let i = 0; i < userId.length; i++) { const char = userId.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash) % 100; } matchesAudience(userId, audience) { // Simple audience matching - can be extended if (audience.type === 'user_id') { return audience.values.includes(userId); } if (audience.type === 'user_segment') { // This would typically integrate with user segmentation service return true; // Simplified } return false; } async listFlags(args) { const { environment, status = 'all' } = args; const flagList = []; for (const [name, flag] of this.flags) { let flagInfo = { name, description: flag.description, rolloutStrategy: flag.rolloutStrategy, version: flag.version, createdAt: flag.createdAt, lastModified: flag.lastModified }; if (environment) { const envConfig = flag.environments[environment]; if (envConfig) { flagInfo.environmentStatus = { environment, enabled: envConfig.enabled, rolloutPercentage: envConfig.rolloutPercentage, lastModified: envConfig.lastModified }; } } else { flagInfo.environments = flag.environments; } if (status === 'all' || (status === 'enabled' && Object.values(flag.environments).some(env => env.enabled)) || (status === 'disabled' && !Object.values(flag.environments).some(env => env.enabled))) { flagList.push(flagInfo); } } return { success: true, flags: flagList, totalCount: flagList.length }; } async deleteFlag(args) { const { flagName, force = false } = args; if (!this.flags.has(flagName)) { return { success: false, message: `Feature flag '${flagName}' not found` }; } const flag = this.flags.get(flagName); // Check if flag is still enabled in any environment const enabledEnvironments = Object.entries(flag.environments) .filter(([env, config]) => config.enabled) .map(([env]) => env); if (enabledEnvironments.length > 0 && !force) { return { success: false, message: `Flag '${flagName}' is still enabled in: ${enabledEnvironments.join(', ')}. Use force=true to delete anyway.` }; } // Record deletion this.recordFlagChange(flagName, 'deleted', { flag }); this.flags.delete(flagName); await this.saveConfiguration(); await this.removeFlagFromContainers(flagName); return { success: true, message: `Feature flag '${flagName}' deleted successfully` }; } async getFlagHistory(args) { const { flagName, limit = 50 } = args; if (flagName && !this.flags.has(flagName) && !this.flagHistory.has(flagName)) { return { success: false, message: `No history found for feature flag '${flagName}'` }; } let history = []; if (flagName) { history = this.flagHistory.get(flagName) || []; } else { // Return history for all flags for (const [name, flagHistory] of this.flagHistory) { history.push(...flagHistory.map(entry => ({ flagName: name, ...entry }))); } } // Sort by timestamp (newest first) and limit history = history .sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp)) .slice(0, limit); return { success: true, history: history, totalEntries: history.length }; } recordFlagChange(flagName, action, details) { if (!this.flagHistory.has(flagName)) { this.flagHistory.set(flagName, []); } const entry = { action, timestamp: new Date().toISOString(), details }; const history = this.flagHistory.get(flagName); history.unshift(entry); // Keep only last 100 entries per flag if (history.length > 100) { history.splice(100); } this.flagHistory.set(flagName, history); } async propagateToContainers(flagName, flag) { try { // Get all running containers const { stdout } = await execAsync('docker ps --format "{{.Names}}"'); const containers = stdout.trim().split('\n').filter(name => name.trim()); for (const container of containers) { try { // Create feature flag configuration file const flagConfig = { [flagName]: flag }; const configJson = JSON.stringify(flagConfig, null, 2); const tempFile = `/tmp/feature-flag-${flagName}-${Date.now()}.json`; await fs.writeFile(tempFile, configJson); // Copy to container (assuming containers have /etc/feature-flags directory) await execAsync(`docker cp ${tempFile} ${container}:/etc/feature-flags/${flagName}.json`); // Signal container to reload flags (if it supports it) await execAsync(`docker exec ${container} pkill -USR1 -f "feature-flag-service" || true`); // Clean up temp file await fs.unlink(tempFile); } catch (containerError) { console.warn(`Failed to propagate flag to container ${container}:`, containerError.message); } } } catch (error) { console.warn('Failed to propagate feature flag to containers:', error.message); } } async removeFlagFromContainers(flagName) { try { const { stdout } = await execAsync('docker ps --format "{{.Names}}"'); const containers = stdout.trim().split('\n').filter(name => name.trim()); for (const container of containers) { try { await execAsync(`docker exec ${container} rm -f /etc/feature-flags/${flagName}.json || true`); await execAsync(`docker exec ${container} pkill -USR1 -f "feature-flag-service" || true`); } catch (containerError) { console.warn(`Failed to remove flag from container ${container}:`, containerError.message); } } } catch (error) { console.warn('Failed to remove feature flag from containers:', error.message); } } getToolDefinitions() { return [ { name: 'featureflags/create', description: 'Create a new feature flag', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Unique name for the feature flag' }, description: { type: 'string', description: 'Description of what this flag controls' }, defaultValue: { type: 'boolean', description: 'Default value for the flag', default: false }, environments: { type: 'array', items: { type: 'string' }, description: 'Environments where this flag should be available', default: ['dev', 'staging', 'prod'] }, rolloutStrategy: { type: 'string', enum: ['all_users', 'percentage', 'target_audience'], description: 'Strategy for rolling out the flag', default: 'all_users' }, rolloutPercentage: { type: 'number', minimum: 0, maximum: 100, description: 'Percentage of users to enable flag for', default: 100 }, targetAudience: { type: 'array', items: { type: 'object' }, description: 'Specific audience to target', default: [] }, dependencies: { type: 'array', items: { type: 'string' }, description: 'Other flags this flag depends on', default: [] } }, required: ['flagName', 'description'] } }, { name: 'featureflags/update', description: 'Update an existing feature flag', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Name of the feature flag to update' }, environment: { type: 'string', description: 'Environment to update (optional)' }, enabled: { type: 'boolean', description: 'Whether the flag is enabled' }, rolloutPercentage: { type: 'number', minimum: 0, maximum: 100, description: 'Percentage rollout for the flag' }, targetAudience: { type: 'array', items: { type: 'object' }, description: 'Target audience for the flag' }, description: { type: 'string', description: 'Updated description' } }, required: ['flagName'] } }, { name: 'featureflags/toggle', description: 'Toggle a feature flag on/off for a specific environment', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Name of the feature flag' }, environment: { type: 'string', description: 'Environment to toggle the flag in' } }, required: ['flagName', 'environment'] } }, { name: 'featureflags/get', description: 'Retrieve a feature flag and evaluate it for specific context', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Name of the feature flag' }, environment: { type: 'string', description: 'Environment to evaluate flag for' }, userId: { type: 'string', description: 'User ID for evaluation (optional)' } }, required: ['flagName'] } }, { name: 'featureflags/list', description: 'List all feature flags', inputSchema: { type: 'object', properties: { environment: { type: 'string', description: 'Filter by environment (optional)' }, status: { type: 'string', enum: ['all', 'enabled', 'disabled'], description: 'Filter by status', default: 'all' } } } }, { name: 'featureflags/delete', description: 'Delete a feature flag', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Name of the feature flag to delete' }, force: { type: 'boolean', description: 'Force delete even if flag is enabled', default: false } }, required: ['flagName'] } }, { name: 'featureflags/history', description: 'Get change history for feature flags', inputSchema: { type: 'object', properties: { flagName: { type: 'string', description: 'Name of specific flag (optional - returns all if omitted)' }, limit: { type: 'number', description: 'Maximum number of history entries to return', default: 50 } } } } ]; } } export default FeatureFlagTool;