jaxon-optimizely-dxp-mcp
Version:
AI-powered automation for Optimizely DXP - deploy, monitor, and manage environments through natural conversations
654 lines (561 loc) • 26.3 kB
JavaScript
/**
* Project Management Tools
* Handles multi-project configuration and switching
* Part of Jaxon Digital Optimizely DXP MCP Server
*/
const ResponseBuilder = require('../response-builder');
const SecurityHelper = require('../security-helper');
const Config = require('../config');
class ProjectTools {
// In-memory storage for dynamically added API key configurations
static dynamicConfigurations = [];
/**
* Add or update an API key configuration dynamically
*/
static addConfiguration(configInfo) {
// Check if configuration already exists by ID or name
const existingIndex = this.dynamicConfigurations.findIndex(c =>
c.projectId === configInfo.projectId ||
c.name === configInfo.name
);
if (existingIndex >= 0) {
// Update existing configuration
this.dynamicConfigurations[existingIndex] = {
...this.dynamicConfigurations[existingIndex],
...configInfo,
lastUsed: new Date().toISOString()
};
} else {
// Add new configuration
this.dynamicConfigurations.push({
...configInfo,
addedAt: new Date().toISOString(),
lastUsed: new Date().toISOString()
});
}
return configInfo;
}
/**
* Parse API key configurations from environment and dynamic entries
*/
static getConfiguredProjects() {
const projects = [];
const configErrors = [];
// Check ALL environment variables for our specific format
// Any env var with format: "id=uuid;key=value;secret=value" is treated as an API key configuration
// Examples:
// ACME="id=uuid;key=value;secret=value"
// PRODUCTION="id=uuid;key=value;secret=value"
// CLIENT_A_STAGING="id=uuid;key=value;secret=value"
Object.keys(process.env).forEach(key => {
const value = process.env[key];
// Skip if not a string or empty
if (!value || typeof value !== 'string') {
return;
}
// Check if this looks like our API key format
// Must contain id=, key=, and secret= to be considered
if (!value.includes('id=') || !value.includes('key=') || !value.includes('secret=')) {
return;
}
// Use the environment variable name as the project name (underscores become spaces)
const projectName = key.replace(/_/g, ' ');
try {
// Parse semicolon-separated key=value pairs
const params = {};
const parts = value.split(';').filter(p => p.trim());
if (parts.length === 0) {
configErrors.push({
project: projectName,
error: `Empty configuration string`,
variable: key,
value: value
});
return;
}
parts.forEach(param => {
const equalIndex = param.indexOf('=');
if (equalIndex === -1) {
configErrors.push({
project: projectName,
error: `Invalid parameter format: "${param}" (expected key=value)`,
variable: key
});
return;
}
const paramKey = param.substring(0, equalIndex).trim();
const paramValue = param.substring(equalIndex + 1).trim();
if (!paramKey || !paramValue) {
configErrors.push({
project: projectName,
error: `Empty key or value in parameter: "${param}"`,
variable: key
});
return;
}
params[paramKey] = paramValue;
});
// Validate required fields
const missingFields = [];
if (!params.id) missingFields.push('id');
if (!params.key) missingFields.push('key');
if (!params.secret) missingFields.push('secret');
if (missingFields.length > 0) {
configErrors.push({
project: projectName,
error: `Missing required fields: ${missingFields.join(', ')}`,
variable: key,
hint: `Format: "id=<uuid>;key=<apikey>;secret=<apisecret>"`
});
return;
}
// Validate UUID format for project ID
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(params.id)) {
configErrors.push({
project: projectName,
error: `Invalid project ID format: "${params.id}"`,
variable: key,
hint: `Project ID should be a UUID like: abc12345-1234-5678-9abc-def123456789`
});
}
// Validate environments if specified
if (params.environments) {
const validEnvs = ['Integration', 'Preproduction', 'Production'];
const envs = params.environments.split(',').map(e => e.trim());
const invalidEnvs = envs.filter(e => !validEnvs.includes(e));
if (invalidEnvs.length > 0) {
configErrors.push({
project: projectName,
error: `Invalid environments: ${invalidEnvs.join(', ')}`,
variable: key,
hint: `Valid environments are: Integration, Preproduction, Production`
});
}
}
// Add project if validation passed
projects.push({
name: projectName,
projectId: params.id,
apiKey: params.key,
apiSecret: params.secret,
environments: params.environments
? params.environments.split(',').map(e => e.trim())
: ['Integration', 'Preproduction', 'Production'],
isDefault: params.default === 'true',
configSource: 'environment'
});
} catch (error) {
configErrors.push({
project: projectName,
error: `Failed to parse configuration: ${error.message}`,
variable: key
});
}
});
// Log configuration errors if any
if (configErrors.length > 0) {
console.error('\n⚠️ Configuration Errors Found:');
configErrors.forEach(err => {
console.error(`\n Project: ${err.project}`);
console.error(` Variable: ${err.variable}`);
console.error(` Error: ${err.error}`);
if (err.hint) {
console.error(` Hint: ${err.hint}`);
}
if (err.value) {
console.error(` Value: ${err.value.substring(0, 50)}...`);
}
});
console.error('\n');
}
// Add dynamically added configurations
this.dynamicConfigurations.forEach(dynConfig => {
// Only add if not already in list (avoid duplicates)
if (!projects.find(p => p.projectId === dynConfig.projectId)) {
projects.push(dynConfig);
}
});
// Auto-assign default: If no default is set and we have projects, make the first one default
if (projects.length > 0 && !projects.find(p => p.isDefault)) {
projects[0].isDefault = true;
}
return projects;
}
/**
* Get current active project
*/
static getCurrentProject(projectId = null) {
const projects = this.getConfiguredProjects();
// If projectId specified, find that project
if (projectId) {
const project = projects.find(p => p.projectId === projectId || p.name === projectId);
if (project) return project;
}
// Return default project or first project
return projects.find(p => p.isDefault) || projects[0] || null;
}
/**
* Validate project configuration and return diagnostic info
*/
static validateConfiguration() {
const projects = this.getConfiguredProjects();
const diagnostics = {
valid: true,
projectCount: projects.length,
hasDefault: false,
errors: [],
warnings: [],
projects: []
};
// Check for default project
const defaultProjects = projects.filter(p => p.isDefault);
if (defaultProjects.length > 1) {
diagnostics.warnings.push(`Multiple default projects found: ${defaultProjects.map(p => p.name).join(', ')}`);
} else if (defaultProjects.length === 1) {
diagnostics.hasDefault = true;
}
// Validate each project
projects.forEach(project => {
const projectDiag = {
name: project.name,
valid: true,
errors: [],
warnings: []
};
// Check credentials
if (!project.apiKey || project.apiKey.length < 20) {
projectDiag.errors.push('API Key appears invalid or too short');
projectDiag.valid = false;
}
if (!project.apiSecret || project.apiSecret.length < 20) {
projectDiag.errors.push('API Secret appears invalid or too short');
projectDiag.valid = false;
}
// Check for common mistakes
if (project.apiKey.includes('REPLACE_WITH') || project.apiKey.includes('PLACEHOLDER') || project.apiKey.includes('SAMPLE')) {
projectDiag.errors.push('API Key is a placeholder value');
projectDiag.valid = false;
}
if (project.apiSecret.includes('REPLACE_WITH') || project.apiSecret.includes('PLACEHOLDER') || project.apiSecret.includes('SAMPLE')) {
projectDiag.errors.push('API Secret is a placeholder value');
projectDiag.valid = false;
}
// Removed warning about environment names in project names
// This is actually a valid use case - some organizations create
// separate API keys per environment for security reasons
if (projectDiag.errors.length > 0) {
diagnostics.valid = false;
}
diagnostics.projects.push(projectDiag);
});
return diagnostics;
}
/**
* List all configured projects
*/
static async listProjects(args) {
try {
const projects = this.getConfiguredProjects();
const diagnostics = this.validateConfiguration();
if (projects.length === 0) {
return ResponseBuilder.formatResponse({
success: false,
message: 'No projects configured yet',
details: [
'⚠️ No Optimizely projects found.',
'',
'**Quick Start - Just provide credentials when using any command:**',
'',
'Simply include ALL these parameters with your first command:',
'• projectName: "Your Project Name" (e.g., "Production", "Staging")',
'• projectId: "REPLACE_WITH_UUID"',
'• apiKey: "REPLACE_WITH_ACTUAL_KEY"',
'• apiSecret: "REPLACE_WITH_ACTUAL_SECRET"',
'',
'**Example:**',
'"List deployments for Production with projectName Production, projectId abc-123, apiKey SAMPLE_API_KEY, apiSecret SAMPLE_API_SECRET"',
'',
'**After the first use:**',
'The project will be auto-registered and you can simply say:',
'"List deployments for Production"',
'"Deploy on Production"',
'',
'💡 **Why Project Names Matter:**',
'Project names make it easy to reference your projects without remembering UUIDs!',
'',
'**Alternative: Pre-configure projects:**',
'Set environment variables like:',
'PRODUCTION="id=uuid;key=value;secret=value"',
'STAGING="id=uuid;key=value;secret=value"',
'ACME_CORP="id=uuid;key=value;secret=value"'
].join('\n')
});
}
const sections = [];
// Header
sections.push('📂 Configured Optimizely Projects');
sections.push('=' .repeat(50));
// List each project (name first for easier reference)
projects.forEach((project, index) => {
const sanitized = SecurityHelper.sanitizeObject(project);
const defaultLabel = project.isDefault ? ' ⭐ (Default)' : '';
const dynamicLabel = project.addedAt ? ' 📝 (Added)' : '';
// Check for configuration issues for this project
const projectDiag = diagnostics.projects.find(p => p.name === project.name);
const hasErrors = projectDiag && projectDiag.errors.length > 0;
const hasWarnings = projectDiag && projectDiag.warnings.length > 0;
sections.push('');
sections.push(`${index + 1}. **${project.name}**${defaultLabel}${dynamicLabel}${hasErrors ? ' ⚠️' : ''}`);
sections.push(` Project ID: ${project.projectId}`);
sections.push(` API Key: ${sanitized.apiKey ? sanitized.apiKey : '❌ Not configured'}`);
sections.push(` API Secret: ${sanitized.apiSecret ? '✅ Configured' : '❌ Not configured'}`);
// Show configuration errors/warnings
if (hasErrors) {
projectDiag.errors.forEach(err => {
sections.push(` ❌ Error: ${err}`);
});
}
if (hasWarnings) {
projectDiag.warnings.forEach(warn => {
sections.push(` ⚠️ Warning: ${warn}`);
});
}
if (project.lastUsed) {
const lastUsed = new Date(project.lastUsed);
const now = new Date();
const diffHours = Math.floor((now - lastUsed) / (1000 * 60 * 60));
if (diffHours < 1) {
sections.push(` Last used: Just now`);
} else if (diffHours < 24) {
sections.push(` Last used: ${diffHours} hour${diffHours > 1 ? 's' : ''} ago`);
} else {
const diffDays = Math.floor(diffHours / 24);
sections.push(` Last used: ${diffDays} day${diffDays > 1 ? 's' : ''} ago`);
}
}
});
// Footer with usage instructions
sections.push('');
sections.push('=' .repeat(50));
sections.push('💡 Usage Tips:');
sections.push('• Use project name or ID in commands');
sections.push('• Example: "Deploy on Project 1"');
sections.push('• Example: "List deployments for ' + (projects[0]?.name || 'project-name') + '"');
return ResponseBuilder.formatResponse({
success: true,
message: `Found ${projects.length} configured project${projects.length !== 1 ? 's' : ''}`,
details: sections.join('\n')
});
} catch (error) {
return ResponseBuilder.formatResponse({
success: false,
message: 'Failed to list projects',
error: error.message
});
}
}
/**
* Get detailed project information
*/
static async getProjectInfo(args) {
try {
// If inline credentials provided, display that project's info
if (args.projectName && args.projectId && args.apiKey && args.apiSecret) {
// This project will be auto-registered by the main handler
const project = {
name: args.projectName,
projectId: args.projectId,
apiKey: args.apiKey,
apiSecret: args.apiSecret,
environments: ['Integration', 'Preproduction', 'Production'],
isDefault: false
};
// Get total projects including this one
const projects = this.getConfiguredProjects();
const totalProjects = projects.find(p => p.projectId === project.projectId) ? projects.length : projects.length + 1;
return this.formatProjectInfo(project, totalProjects);
}
const requestedProjectId = args.projectId || args.projectName;
const projects = this.getConfiguredProjects();
// If no projects configured
if (projects.length === 0) {
return ResponseBuilder.formatResponse({
success: false,
message: 'No projects configured',
details: 'Please configure at least one project with API credentials.'
});
}
// If specific project requested
if (requestedProjectId) {
const project = projects.find(p =>
p.projectId === requestedProjectId ||
p.name === requestedProjectId ||
p.name.toLowerCase() === requestedProjectId.toLowerCase()
);
if (!project) {
return ResponseBuilder.formatResponse({
success: false,
message: `Project '${requestedProjectId}' not found`,
details: `Available projects: ${projects.map(p => p.name).join(', ')}`
});
}
return this.formatProjectInfo(project, projects.length);
}
// Show current/default project
const currentProject = this.getCurrentProject();
if (currentProject) {
return this.formatProjectInfo(currentProject, projects.length);
}
// Fallback to listing all projects
return this.listProjects(args);
} catch (error) {
return ResponseBuilder.formatResponse({
success: false,
message: 'Failed to get project information',
error: error.message
});
}
}
/**
* Format project information display
*/
static formatProjectInfo(project, totalProjects) {
const sanitized = SecurityHelper.sanitizeObject(project);
const sections = [];
// Header
sections.push('🎯 Optimizely Project Configuration');
sections.push('=' .repeat(50));
// Active project details
sections.push('');
sections.push(`📌 Active Project: ${project.name}${project.isDefault ? ' ⭐' : ''}`);
sections.push(` Project ID: ${project.projectId}`);
sections.push(` API Key: ${sanitized.apiKey}`);
sections.push(` API Secret: ${sanitized.apiSecret}`);
sections.push(` Allowed Environments: ${project.environments.join(', ')}`);
// Credentials status
const hasCredentials = project.apiKey && project.apiSecret;
sections.push('');
sections.push(` Status: ${hasCredentials ? '✅ Configured' : '❌ Missing credentials'}`);
// Other projects summary
if (totalProjects > 1) {
sections.push('');
sections.push('=' .repeat(50));
sections.push(`📊 Total Projects: ${totalProjects}`);
sections.push(' Use "list projects" to see all configured projects');
}
// Configuration tips
sections.push('');
sections.push('=' .repeat(50));
sections.push('💡 Tips:');
if (!hasCredentials) {
sections.push('• Add API credentials to use this project');
sections.push('• Get credentials from your DXP Portal');
} else {
sections.push('• This project is ready to use');
sections.push('• All commands will use this project by default');
}
if (totalProjects > 1) {
sections.push('• Switch projects by using project name in commands');
}
return ResponseBuilder.formatResponse({
success: true,
message: hasCredentials ? 'Project configured and ready' : 'Project needs configuration',
details: sections.join('\n')
});
}
/**
* Switch to a different project (returns credentials for use)
*/
static switchProject(projectIdentifier) {
const projects = this.getConfiguredProjects();
const project = projects.find(p =>
p.projectId === projectIdentifier ||
p.name === projectIdentifier ||
p.name.toLowerCase() === projectIdentifier.toLowerCase()
);
if (!project) {
return {
success: false,
message: `Project '${projectIdentifier}' not found`,
credentials: null
};
}
return {
success: true,
message: `Switched to project: ${project.name}`,
credentials: {
projectId: project.projectId,
apiKey: project.apiKey,
apiSecret: project.apiSecret
},
project: project
};
}
/**
* Get credentials for a specific project or default
*/
static getProjectCredentials(projectIdentifier = null) {
if (projectIdentifier) {
const result = this.switchProject(projectIdentifier);
if (result.success) {
return result.credentials;
}
}
// Check for last used project in environment (session persistence)
const lastUsedProject = process.env.MCP_LAST_USED_PROJECT;
if (lastUsedProject && !projectIdentifier) {
const result = this.switchProject(lastUsedProject);
if (result.success) {
return result.credentials;
}
}
// Return default/current project credentials
const current = this.getCurrentProject();
if (current) {
return {
projectId: current.projectId,
apiKey: current.apiKey,
apiSecret: current.apiSecret
};
}
// No credentials available
return {
projectId: null,
apiKey: null,
apiSecret: null
};
}
/**
* Set the last used project (for session persistence)
*/
static setLastUsedProject(projectName) {
if (projectName) {
process.env.MCP_LAST_USED_PROJECT = projectName;
}
}
/**
* Get support information
*/
static async handleGetSupport(args) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let response = `${STATUS_ICONS.INFO} **Jaxon Digital Support**\n\n`;
response += `We're here to help with your Optimizely DXP MCP needs!\n\n`;
response += `**📧 Email Support**\n`;
response += `support@jaxondigital.com\n\n`;
response += `**🐛 Report Issues**\n`;
response += `GitHub: https://github.com/JaxonDigital/optimizely-dxp-mcp/issues\n\n`;
response += `**💬 Get Help With:**\n`;
response += `• Configuration and setup\n`;
response += `• Deployment issues\n`;
response += `• API authentication\n`;
response += `• Feature requests\n`;
response += `• Custom integrations\n\n`;
response += `**🏢 Enterprise Support**\n`;
response += `For priority support and SLAs, contact us about enterprise plans.\n\n`;
response += `**🌐 Learn More**\n`;
response += `Visit: www.jaxondigital.com\n`;
return ResponseBuilder.success(ResponseBuilder.addFooter(response));
}
}
module.exports = ProjectTools;