Skip to main content
Glama

Optimizely DXP MCP Server

by JaxonDigital
project-tools.js65.7 kB
/** * 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'); const OutputLogger = require('../output-logger'); class ProjectTools { // In-memory storage for dynamically added API key configurations (session only) static dynamicConfigurations = []; /** * Add or update an API key configuration dynamically (session only) */ static addConfiguration(configInfo) { // Check if configuration already exists by ID or name // Special handling: If upgrading from Unknown, match by name only const existingIndex = this.dynamicConfigurations.findIndex(c => { // Match by name (case-insensitive) const nameMatch = c.name.toLowerCase() === configInfo.name.toLowerCase(); // If names match and one is Unknown being upgraded, that's a match if (nameMatch && (c.isUnknown || configInfo.wasUnknown)) { return true; } // Otherwise match by projectId or name return 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() { // Dynamic configurations are kept in memory only for current session 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" // DEBUG: Log all environment variables that contain our format const OutputLogger = require('../output-logger'); const relevantEnvVars = Object.keys(process.env).filter(key => { const value = process.env[key]; return value && typeof value === 'string' && ((value.includes('id=') && value.includes('key=') && value.includes('secret=')) || value.startsWith('DefaultEndpointsProtocol=') || ((value.includes('blobPath=') || value.includes('logPath=')) && !value.includes('id=') && !value.includes('key=') && !value.includes('secret='))); }); OutputLogger.debug(`Checking environment variables...`); OutputLogger.debug(`Found ${relevantEnvVars.length} relevant env vars (DXP, self-hosted, and unknown):`, relevantEnvVars); Object.keys(process.env).forEach(key => { const value = process.env[key]; // Skip if not a string if (typeof value !== 'string') { return; } // Check if empty string (placeholder project) // Only treat as project if name looks like a project name if (value === '') { // Skip if this doesn't look like a project name // Project names typically follow patterns like: // - "ACME-int", "CONTOSO-prod", "FABRIKAM-staging" (project-environment) // - "DEMO-self", "TEST-local" (project-type) // - Single short words like "zilch", "test", "demo" const hasProjectPattern = // Pattern: WORD-env (like ACME-int, CONTOSO-prod) key.match(/^[A-Z0-9]+-(?:int|prod|staging|test|dev|qa|uat|demo|local|self)$/i) || // Pattern: Short single word (max 10 chars) (key.match(/^[A-Z0-9]+$/i) && key.length <= 10) || // Explicitly starts with common project prefixes key.match(/^(?:TEST|DEMO|DEV|PROD|STAGING|QA)[-_]/i); if (!hasProjectPattern) { return; } // Create Unknown placeholder project const projectName = key.replace(/_/g, ' '); const projectConfig = { name: projectName, projectId: `unknown-${projectName.toLowerCase().replace(/\s+/g, '-')}`, isUnknown: true, projectType: 'unknown', needsConfiguration: true, configurationHint: 'Empty project - add connectionString for self-hosted or id/key/secret for DXP', environments: ['Unknown'], configSource: 'environment' }; projects.push(projectConfig); return; } // Check if this looks like our API key format OR a connection string OR self-hosted paths // Must contain either: // 1. DXP format: (id=, key=, secret=) // 2. Azure connection string: (DefaultEndpointsProtocol=) // 3. Self-hosted with paths only: (blobPath= or logPath=) but no id/key/secret const hasDxpFormat = value.includes('id=') && value.includes('key=') && value.includes('secret='); const hasConnectionString = value.startsWith('DefaultEndpointsProtocol='); const hasSelfHostedPaths = (value.includes('blobPath=') || value.includes('logPath=')) && !value.includes('id=') && !value.includes('key=') && !value.includes('secret='); const hasCorrectFormat = hasDxpFormat || hasConnectionString || hasSelfHostedPaths; if (!hasCorrectFormat) { return; } // Use the environment variable name as the project name (underscores become spaces) const projectName = key.replace(/_/g, ' '); try { // Check if this is a raw Azure connection string (self-hosted mode) if (value.startsWith('DefaultEndpointsProtocol=')) { // Extract connection string and any additional parameters // Format: DefaultEndpointsProtocol=...;EndpointSuffix=core.windows.net;blobPath=/path;logPath=/path // Find where the connection string ends (after EndpointSuffix) const endpointMatch = value.match(/EndpointSuffix=[^;]+/); let connectionString = value; let additionalParams = {}; if (endpointMatch) { const endIndex = value.indexOf(endpointMatch[0]) + endpointMatch[0].length; connectionString = value.substring(0, endIndex); // Parse any additional parameters after the connection string const remaining = value.substring(endIndex); if (remaining) { const extraParts = remaining.split(';').filter(p => p.trim()); extraParts.forEach(part => { const [key, val] = part.split('='); if (key && val) { additionalParams[key] = val; } }); } } const projectConfig = { name: projectName, projectId: `self-hosted-${projectName.toLowerCase().replace(/\s+/g, '-')}`, apiKey: '', apiSecret: '', connectionString: connectionString, isSelfHosted: true, environments: ['Production'], // Self-hosted typically has one environment configSource: 'environment' }; // Add optional paths if provided if (additionalParams.blobPath) { projectConfig.blobPath = additionalParams.blobPath; } if (additionalParams.logPath) { projectConfig.logPath = additionalParams.logPath; } if (additionalParams.dbPath) { projectConfig.dbPath = additionalParams.dbPath; } projects.push(projectConfig); return; } // Otherwise parse semicolon-separated key=value pairs for DXP projects const params = {}; const parts = value.split(';').filter(p => p.trim()); if (parts.length === 0) { // Empty configuration - treat as Unknown project placeholder const projectConfig = { name: projectName, projectId: `unknown-${projectName.toLowerCase().replace(/\s+/g, '-')}`, isUnknown: true, projectType: 'unknown', needsConfiguration: true, configurationHint: 'Empty project - add connectionString for self-hosted or id/key/secret for DXP', environments: ['Unknown'], configSource: 'environment' }; projects.push(projectConfig); 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; }); // Extract credentials using standard format let projectId = params.id; // Changed to let to allow reassignment const apiKey = params.key; const apiSecret = params.secret; let connectionString = params.connectionString || params.connStr; // Check if this is a path-only project (has paths but no DXP credentials or connection string) const isPathOnlyProject = (params.blobPath || params.logPath || params.dbPath) && !params.id && !params.key && !params.secret && !connectionString; // Determine project type and handle accordingly if (connectionString) { // Self-hosted mode with connection string if (!projectId) { // Generate a simple ID from the project name const projectIdBase = projectName.toLowerCase().replace(/\s+/g, '-'); projectId = `self-hosted-${projectIdBase}`; } } else if (isPathOnlyProject) { // Unknown type - has paths but no clear indication of type if (!projectId) { // Generate a simple ID from the project name const projectIdBase = projectName.toLowerCase().replace(/\s+/g, '-'); projectId = `unknown-${projectIdBase}`; } OutputLogger.debug(`Unknown project type "${projectName}" configured with paths only`); } else if (!params.id || !params.key || !params.secret) { // DXP mode - needs full API credentials const missingFields = []; if (!projectId) missingFields.push('id'); if (!apiKey) missingFields.push('key'); if (!apiSecret) missingFields.push('secret'); if (missingFields.length > 0) { configErrors.push({ project: projectName, error: `Missing required fields: ${missingFields.join(', ')}`, variable: key, hint: `Format: "id=<uuid>;key=<key>;secret=<secret>"` }); return; } } // Validate UUID format for project ID (skip for self-hosted and unknown) const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const isSelfHostedId = projectId && projectId.startsWith('self-hosted-'); const isUnknownId = projectId && projectId.startsWith('unknown-'); if (!isSelfHostedId && !isUnknownId && !uuidRegex.test(projectId)) { configErrors.push({ project: projectName, error: `Invalid project ID format: "${projectId}"`, 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 const projectConfig = { name: projectName, projectId: projectId, apiKey: apiKey || '', apiSecret: apiSecret || '', environments: params.environments ? params.environments.split(',').map(e => e.trim()) : ['Integration', 'Preproduction', 'Production'], configSource: 'environment' }; // Determine project type based on available credentials if (connectionString) { // Self-hosted with connection string projectConfig.connectionString = connectionString; projectConfig.isSelfHosted = true; projectConfig.projectType = 'self-hosted'; } else if (isPathOnlyProject) { // Unknown type - has paths but no credentials projectConfig.isUnknown = true; projectConfig.projectType = 'unknown'; projectConfig.needsConfiguration = true; // For unknown projects, we need to guide users to add credentials projectConfig.configurationHint = 'Add connectionString for self-hosted or id/key/secret for DXP'; } else if (apiKey && apiSecret) { // DXP PaaS project projectConfig.projectType = 'dxp-paas'; } // Add compact configuration fields if present if (params.blobPath) { projectConfig.blobPath = params.blobPath; } if (params.dbPath) { projectConfig.dbPath = params.dbPath; } if (params.logPath) { projectConfig.logPath = params.logPath; } if (params.telemetry) { projectConfig.telemetry = params.telemetry.toLowerCase() === 'true'; } projects.push(projectConfig); } 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 => { // Check if this dynamic config should replace an existing project const existingIndex = projects.findIndex(p => { // Match by original name (for renames) if (dynConfig.originalName && p.name === dynConfig.originalName) { return true; } // Match by current name if (p.name === dynConfig.name || p.name.toLowerCase() === dynConfig.name.toLowerCase()) { return true; } // Match by project ID if (p.projectId === dynConfig.projectId) { return true; } return false; }); if (existingIndex >= 0) { // Replace existing project with dynamic configuration (upgrade/rename scenario) projects[existingIndex] = dynConfig; } else if (!projects.find(p => p.projectId === dynConfig.projectId)) { // Only add if not already in list (avoid duplicates by ID) projects.push(dynConfig); } }); // First project is always the default (simplified logic) // DEBUG: Log final projects found OutputLogger.debug('Final projects found:'); projects.forEach((p, i) => { OutputLogger.debug(` ${i + 1}. Name: "${p.name}", ID: ${p.projectId || 'undefined'}, Source: ${p.configSource || 'unknown'}`); }); 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; } // Check for last used project from switch_project const lastUsed = process.env.MCP_LAST_USED_PROJECT; if (lastUsed) { const project = projects.find(p => p.name === lastUsed || p.name.toLowerCase() === lastUsed.toLowerCase() ); if (project) return project; } // Return first project as default return 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: [] }; // First project is always the default if (projects.length > 0) { diagnostics.hasDefault = true; } // Validate each project projects.forEach(project => { const projectDiag = { name: project.name, valid: true, errors: [], warnings: [] }; // Check credentials based on project type if (project.isUnknown) { // Unknown projects need configuration guidance projectDiag.warnings.push('Project type unknown - needs connection string (self-hosted) or API credentials (DXP)'); projectDiag.warnings.push('Currently using local paths only'); // Unknown projects are valid but need configuration for full functionality projectDiag.valid = true; } else if (project.isSelfHosted) { // Self-hosted projects use connection strings if (!project.connectionString || project.connectionString.length < 50) { projectDiag.errors.push('Connection string appears invalid or too short'); projectDiag.valid = false; } } else { // DXP projects use API Key/Secret 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 && (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 && (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; } /** * Show the current project */ static async showCurrentProject() { try { // Check for last used project first (from switch_project) let currentProject = null; const lastUsed = process.env.MCP_LAST_USED_PROJECT; if (lastUsed) { const projects = this.getConfiguredProjects(); currentProject = projects.find(p => p.name === lastUsed || p.name.toLowerCase() === lastUsed.toLowerCase() ); } // Fall back to default (first project) if (!currentProject) { currentProject = this.getCurrentProject(); } if (!currentProject) { return ResponseBuilder.error('No project currently selected'); } let response = `📌 **Current Project: ${currentProject.name}**\n\n`; response += `• Project ID: ${currentProject.projectId}\n`; // Show project type if (currentProject.isSelfHosted) { response += `• Type: Self-hosted Azure\n`; response += `• Environment: Production\n`; } else if (currentProject.isUnknown) { response += `• Type: Unconfigured\n`; response += `• Status: Needs API credentials or connection string\n`; } else { response += `• Type: DXP PaaS\n`; response += `• Configured Environments: ${currentProject.environments ? currentProject.environments.join(', ') : 'N/A'}\n`; response += `• Note: Use \`test_connection\` to check actual permissions\n`; } response += `${currentProject.isDefault ? '• Default: Yes ⭐\n' : ''}`; return ResponseBuilder.success( response, 'current-project', { projectName: currentProject.name, projectId: currentProject.projectId, projectType: currentProject.projectType || 'dxp-paas', isDefault: currentProject.isDefault || false } ); } catch (error) { return ResponseBuilder.error(`Failed to get current project: ${error.message}`); } } /** * List all configured projects */ static async listProjects(args = {}) { try { // List all projects 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 = (index === 0) ? ' ⭐ (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(''); let typeLabel = ' ☁️ (DXP PaaS)'; if (project.isSelfHosted) { typeLabel = ' 🏠 (Self-Hosted)'; } else if (project.isUnknown) { typeLabel = ' ❓ (Unknown - Needs Config)'; } sections.push(`${index + 1}. **${project.name}**${typeLabel}${defaultLabel}${dynamicLabel}${hasErrors ? ' ⚠️' : ''}`); sections.push(` Project ID: ${project.projectId}`); if (project.isUnknown) { // Show Unknown project info and guidance sections.push(` Type: Unknown (Paths Only)`); sections.push(` Status: ⚠️ Needs Configuration`); if (project.blobPath) sections.push(` Blob Path: ${project.blobPath}`); if (project.logPath) sections.push(` Log Path: ${project.logPath}`); if (project.dbPath) sections.push(` DB Path: ${project.dbPath}`); sections.push(` 💡 To configure: Add connectionString (self-hosted) or id/key/secret (DXP)`); } else if (project.isSelfHosted) { // Show connection string status for self-hosted sections.push(` Type: Self-Hosted Azure`); sections.push(` Connection String: ${sanitized.connectionString ? '✅ Configured' : '❌ Not configured'}`); if (project.blobPath) sections.push(` Blob Path: ${project.blobPath}`); if (project.logPath) sections.push(` Log Path: ${project.logPath}`); } else { // Show API credentials for DXP PaaS sections.push(` Type: DXP PaaS`); 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 { // Handle multiple ways connection string might be passed let connectionString = args.connectionString; // Check if connection string was mistakenly passed as projectId if (!connectionString && args.projectId && args.projectId.startsWith('DefaultEndpointsProtocol=')) { connectionString = args.projectId; args.projectId = null; // Clear the misused field } // Check if connection string was mistakenly passed as apiKey if (!connectionString && args.apiKey && args.apiKey.startsWith('DefaultEndpointsProtocol=')) { connectionString = args.apiKey; args.apiKey = null; // Clear the misused field args.apiSecret = null; // Also clear apiSecret as it's not needed for self-hosted } // Check if connection string was mistakenly passed as apiSecret if (!connectionString && args.apiSecret && args.apiSecret.startsWith('DefaultEndpointsProtocol=')) { connectionString = args.apiSecret; args.apiSecret = null; // Clear the misused field args.apiKey = null; // Also clear apiKey as it's not needed for self-hosted } // Handle rename request if (args.projectName && args.renameTo) { const projects = this.getConfiguredProjects(); const project = projects.find(p => p.name === args.projectName || p.name.toLowerCase() === args.projectName.toLowerCase() ); if (project) { // Create renamed configuration const renamedConfig = { ...project, name: args.renameTo, originalName: project.name, // Track original name for replacement configSource: 'dynamic', lastUpdated: new Date().toISOString() }; // Add to dynamic configurations (will replace by projectId or originalName) this.addConfiguration(renamedConfig); OutputLogger.log(`Project '${project.name}' renamed to '${args.renameTo}'`); // Return renamed project info return this.formatProjectInfo(renamedConfig, projects.length); } else { return ResponseBuilder.formatResponse({ success: false, message: `Project '${args.projectName}' not found`, details: `Available projects: ${projects.map(p => p.name).join(', ')}` }); } } // Check if this is an attempt to upgrade a project with inline connection string if (args.projectName && connectionString) { // User is providing a connection string for a project const projects = this.getConfiguredProjects(); const project = projects.find(p => p.name === args.projectName || p.name.toLowerCase() === args.projectName.toLowerCase() ); if (project) { // Upgrade or update the existing project to Self-Hosted const upgradedConfig = { ...project, connectionString: connectionString, isSelfHosted: true, isUnknown: false, projectType: 'self-hosted', configSource: 'dynamic', lastUpdated: new Date().toISOString() }; // Update project ID for unknown projects if (upgradedConfig.projectId.startsWith('unknown-')) { upgradedConfig.projectId = upgradedConfig.projectId.replace('unknown-', 'self-hosted-'); } // Remove fields not relevant to self-hosted delete upgradedConfig.needsConfiguration; delete upgradedConfig.configurationHint; delete upgradedConfig.apiKey; delete upgradedConfig.apiSecret; // Add to dynamic configurations this.addConfiguration(upgradedConfig); const actionType = project.isUnknown ? 'upgraded' : 'updated'; OutputLogger.log(`Project '${project.name}' ${actionType} with connection string`); // Return updated project info return this.formatProjectInfo(upgradedConfig, projects.length); } else { // Project doesn't exist yet - create new self-hosted project const newProject = { name: args.projectName, projectId: `self-hosted-${args.projectName.toLowerCase().replace(/\s+/g, '-')}`, connectionString: connectionString, isSelfHosted: true, projectType: 'self-hosted', environments: ['Production'], configSource: 'dynamic', addedAt: new Date().toISOString(), lastUsed: new Date().toISOString() }; // Add to dynamic configurations this.addConfiguration(newProject); OutputLogger.log(`New self-hosted project '${newProject.name}' created`); // Return new project info return this.formatProjectInfo(newProject, projects.length + 1); } } // If inline credentials provided, check for existing project to upgrade or create new if (args.projectName && args.projectId && args.apiKey && args.apiSecret) { const projects = this.getConfiguredProjects(); // Check if there's ANY existing project with this name (Unknown or otherwise) const existingProject = projects.find(p => p.name === args.projectName || p.name.toLowerCase() === args.projectName.toLowerCase() ); if (existingProject) { // Upgrade or update existing project const upgradedConfig = { ...existingProject, projectId: args.projectId, apiKey: args.apiKey, apiSecret: args.apiSecret, isUnknown: false, projectType: 'dxp-paas', configSource: 'dynamic', lastUpdated: new Date().toISOString() }; // Remove Unknown project specific fields delete upgradedConfig.needsConfiguration; delete upgradedConfig.configurationHint; delete upgradedConfig.isSelfHosted; delete upgradedConfig.connectionString; // Add to dynamic configurations this.addConfiguration(upgradedConfig); const actionType = existingProject.isUnknown ? 'upgraded' : 'updated'; OutputLogger.log(`Project '${existingProject.name}' ${actionType} with DXP credentials`); // Return upgraded project info return this.formatProjectInfo(upgradedConfig, projects.length); } else { // No existing project - create new const project = { name: args.projectName, projectId: args.projectId, apiKey: args.apiKey, apiSecret: args.apiSecret, environments: ['Integration', 'Preproduction', 'Production'], projectType: 'dxp-paas', configSource: 'dynamic', addedAt: new Date().toISOString(), lastUsed: new Date().toISOString() }; // Add to dynamic configurations this.addConfiguration(project); OutputLogger.log(`New DXP project '${project.name}' created`); // Get total projects including this one 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('🏢 **Project Configuration**'); sections.push('📋 Found ' + totalProjects + ' configured project' + (totalProjects > 1 ? 's' : '') + ':'); let typeLabel = ' ☁️ (DXP PaaS)'; if (project.isSelfHosted) { typeLabel = ' 🏠 (Self-Hosted)'; } else if (project.isUnknown) { typeLabel = ' ❓ (Unknown - Needs Config)'; } sections.push(' ' + project.name + typeLabel); sections.push(''); sections.push('✅ **Connection Details:**'); sections.push(' Project ID: ' + project.projectId.substring(0, 8) + '...'); // Show appropriate credentials based on project type let hasCredentials; if (project.isUnknown) { sections.push(' Type: Unknown (Paths Only)'); sections.push(' Status: ⚠️ Needs Configuration'); if (project.blobPath) sections.push(' Blob Path: ' + project.blobPath); if (project.logPath) sections.push(' Log Path: ' + project.logPath); if (project.dbPath) sections.push(' DB Path: ' + project.dbPath); sections.push(''); sections.push(' 💡 **How to configure this project:**'); sections.push(' • For self-hosted: Add connectionString=DefaultEndpointsProtocol=...'); sections.push(' • For DXP PaaS: Add id=UUID;key=...;secret=...'); hasCredentials = false; // Unknown projects need configuration } else if (project.isSelfHosted) { sections.push(' Type: Self-Hosted Azure'); sections.push(' Connection String: ' + (project.connectionString ? '✅ Configured' : '❌ Not configured')); if (project.blobPath) sections.push(' Blob Path: ' + project.blobPath); if (project.logPath) sections.push(' Log Path: ' + project.logPath); hasCredentials = !!project.connectionString; } else { sections.push(' Type: DXP PaaS'); sections.push(' API Key: ' + (project.apiKey ? '✅ Configured' : '❌ Not configured')); sections.push(' API Secret: ' + (project.apiSecret ? '✅ Configured' : '❌ Not configured')); hasCredentials = project.apiKey && project.apiSecret; } // Show permissions if known if (project.environments && project.environments.length > 0) { sections.push(''); sections.push('🔑 **Configured Environments:**'); project.environments.forEach(env => { sections.push(' • ' + env); }); } // Status sections.push(''); sections.push(hasCredentials ? '✅ **Ready to Use!**' : '❌ **Missing Credentials**'); if (hasCredentials) { sections.push(''); sections.push('💡 Run "test connection" to see your exact permissions.'); } // 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) { // Handle both string and object input const identifier = typeof projectIdentifier === 'object' ? (projectIdentifier.projectName || projectIdentifier.project || projectIdentifier.projectId) : projectIdentifier; if (!identifier) { return { success: false, message: 'No project identifier provided', credentials: null }; } const projects = this.getConfiguredProjects(); const project = projects.find(p => p.projectId === identifier || p.name === identifier || p.name.toLowerCase() === identifier.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, name: current.name }; } // No credentials available return { projectId: null, apiKey: null, apiSecret: null, name: 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)); } /** * Resolve credentials from various sources * Used by monitoring-tools and other tools that need flexible credential resolution * * @param {Object} args - Arguments that may contain credentials or project reference * @returns {Object} Result object with success status, credentials, and project info */ static resolveCredentials(args = {}) { const { projectName, projectId, apiKey, apiSecret } = args; // If full credentials are provided directly, use them if (apiKey && apiSecret && projectId) { // Try to find matching project for additional info const projects = this.getConfiguredProjects(); const project = projects.find(p => p.projectId === projectId || p.apiKey === apiKey ); return { success: true, credentials: { projectId: projectId, apiKey: apiKey, apiSecret: apiSecret, projectName: projectName || (project ? project.name : 'Direct Credentials') }, project: project || { projectId: projectId, name: projectName || 'Direct Credentials', apiKey: apiKey, apiSecret: apiSecret, environments: ['Integration', 'Preproduction', 'Production'] } }; } // Try to find project by name or ID if (projectName || projectId) { const result = this.switchProject(projectName || projectId); if (result.success) { return { success: true, credentials: result.credentials, project: result.project }; } return { success: false, message: result.message || `Project '${projectName || projectId}' not found`, suggestion: 'Check project name or provide full credentials' }; } // Try to get default or last used project const current = this.getCurrentProject(); if (current) { return { success: true, credentials: { projectId: current.projectId, apiKey: current.apiKey, apiSecret: current.apiSecret, projectName: current.name }, project: current }; } // No credentials available return { success: false, message: 'No project credentials available', suggestion: 'Provide project name, project ID, or full API credentials' }; } /** * Get project information (read-only) * Replaces get_api_key_info for reading project details */ static getProject(args) { const { projectName, projectId } = args; const projects = this.getConfiguredProjects(); // Find the requested project let project; if (projectName || projectId) { project = projects.find(p => p.name === projectName || p.name?.toLowerCase() === projectName?.toLowerCase() || p.projectId === projectId ); } else { // Get default/current project project = this.getCurrentProject(); } if (!project) { const availableNames = projects.map(p => p.name).join(', '); return ResponseBuilder.formatResponse({ success: false, message: `Project '${projectName || projectId}' not found`, details: availableNames ? `Available projects: ${availableNames}` : 'No projects configured' }); } return this.formatProjectInfo(project, projects.length); } /** * Update project configuration (handles all modifications) * Consolidates: rename, credentials, paths, settings */ static updateProject(args) { const { projectName, projectId, // Rename renameTo, // Credentials apiKey, apiSecret, connectionString, // Paths blobPath, dbPath, logPath, // Settings makeDefault } = args; // Find the project to update const projects = this.getConfiguredProjects(); const project = projects.find(p => p.name === projectName || p.name?.toLowerCase() === projectName?.toLowerCase() || p.projectId === projectId ); if (!project) { // If no existing project, create new one if we have enough info if (projectName && (apiKey || connectionString)) { return this.getProjectInfo(args); // Use existing creation logic } return ResponseBuilder.formatResponse({ success: false, message: `Project '${projectName || projectId}' not found`, details: 'Specify an existing project to update or provide credentials to create a new one' }); } // Build updated configuration const updatedConfig = { ...project }; let changes = []; // Handle rename if (renameTo && renameTo !== project.name) { updatedConfig.name = renameTo; updatedConfig.originalName = project.name; changes.push(`Renamed from '${project.name}' to '${renameTo}'`); } // Handle credentials update if (apiKey && apiKey !== project.apiKey) { updatedConfig.apiKey = apiKey; changes.push('Updated API key'); // If providing API credentials, this becomes a DXP project if (apiSecret) { if (project.projectType === 'self-hosted' || project.isSelfHosted) { changes.push('Converted from Self-Hosted to DXP PaaS'); } updatedConfig.projectType = 'dxp-paas'; updatedConfig.isSelfHosted = false; delete updatedConfig.connectionString; updatedConfig.environments = ['Integration', 'Preproduction', 'Production']; } } if (apiSecret && apiSecret !== project.apiSecret) { updatedConfig.apiSecret = apiSecret; changes.push('Updated API secret'); // If providing API credentials, this becomes a DXP project if (apiKey || updatedConfig.apiKey) { if (project.projectType === 'self-hosted' || project.isSelfHosted) { changes.push('Converted from Self-Hosted to DXP PaaS'); } updatedConfig.projectType = 'dxp-paas'; updatedConfig.isSelfHosted = false; delete updatedConfig.connectionString; updatedConfig.environments = ['Integration', 'Preproduction', 'Production']; } } if (connectionString && connectionString !== project.connectionString) { updatedConfig.connectionString = connectionString; if (project.projectType === 'dxp-paas' || (!project.isSelfHosted && project.apiKey)) { changes.push('Converted from DXP PaaS to Self-Hosted'); } updatedConfig.isSelfHosted = true; updatedConfig.projectType = 'self-hosted'; updatedConfig.environments = ['Production']; // Remove DXP-specific fields delete updatedConfig.apiKey; delete updatedConfig.apiSecret; changes.push('Updated connection string'); } // Handle project ID update (for DXP projects) if (projectId && projectId !== project.projectId && !project.projectId.startsWith('unknown-')) { updatedConfig.projectId = projectId; changes.push('Updated project ID'); } // Handle paths if (blobPath && blobPath !== project.blobPath) { updatedConfig.blobPath = blobPath; changes.push('Updated blob path'); } if (dbPath && dbPath !== project.dbPath) { updatedConfig.dbPath = dbPath; changes.push('Updated database path'); } if (logPath && logPath !== project.logPath) { updatedConfig.logPath = logPath; changes.push('Updated log path'); } // Handle default setting if (makeDefault) { updatedConfig.isDefault = true; // Remove default from other projects this.dynamicConfigurations.forEach(c => { if (c.name !== updatedConfig.name) { c.isDefault = false; } }); changes.push('Set as default project'); } // If project was Unknown and now has credentials, mark as configured if (project.isUnknown && (apiKey || connectionString)) { updatedConfig.isUnknown = false; updatedConfig.needsConfiguration = false; delete updatedConfig.configurationHint; if (apiKey) { updatedConfig.projectType = 'dxp-paas'; changes.push('Upgraded from Unknown to DXP PaaS'); } else if (connectionString) { updatedConfig.projectType = 'self-hosted'; changes.push('Upgraded from Unknown to Self-Hosted'); } } if (changes.length === 0) { return ResponseBuilder.formatResponse({ success: true, message: 'No changes to apply', details: `Project '${project.name}' is already up to date` }); } // Update timestamps updatedConfig.configSource = 'dynamic'; updatedConfig.lastUpdated = new Date().toISOString(); // Save the configuration this.addConfiguration(updatedConfig); // Build response const sections = []; sections.push(`✅ **Project Updated Successfully**`); sections.push(''); sections.push(`**${updatedConfig.name}**`); sections.push(`Type: ${updatedConfig.projectType === 'self-hosted' ? 'Self-Hosted' : 'DXP PaaS'}`); sections.push(''); sections.push('**Changes Applied:**'); changes.forEach(change => sections.push(`• ${change}`)); if (updatedConfig.blobPath || updatedConfig.dbPath || updatedConfig.logPath) { sections.push(''); sections.push('**Download Paths:**'); if (updatedConfig.blobPath) sections.push(`• Blobs: ${updatedConfig.blobPath}`); if (updatedConfig.dbPath) sections.push(`• Database: ${updatedConfig.dbPath}`); if (updatedConfig.logPath) sections.push(`• Logs: ${updatedConfig.logPath}`); } return ResponseBuilder.success(sections.join('\n')); } /** * Debug environment variables to understand what's available to the MCP process */ static debugEnvironmentVariables() { // Check for all variables that look like our format const relevantVars = []; Object.keys(process.env).forEach(key => { const value = process.env[key]; if (value && typeof value === 'string') { const hasCorrectFormat = value.includes('id=') && value.includes('key=') && value.includes('secret='); if (hasCorrectFormat) { relevantVars.push({ key: key, value: value.substring(0, 80) + '...' }); } } }); const debugInfo = { totalRelevantVars: relevantVars.length, variables: relevantVars }; return debugInfo; } } module.exports = ProjectTools;

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/JaxonDigital/optimizely-dxp-mcp'

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