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