Skip to main content
Glama

Optimizely DXP MCP Server

by JaxonDigital
error-handler.js23.6 kB
/** * Error Handler Module * Centralized error detection and formatting * Part of Jaxon Digital Optimizely DXP MCP Server */ const SecurityHelper = require('./security-helper'); class ErrorHandler { /** * Common error patterns and their handlers */ static ERROR_PATTERNS = { // Check API permissions - now more specific to avoid false positives PERMISSION_DENIED: { pattern: /(403.*forbidden|access.*denied.*environment|unauthorized.*environment)/i, type: 'PERMISSION_DENIED', title: 'Permission Denied', getMessage: (error, context) => { // Extract environment if mentioned const envMatch = error.match(/(Integration|Preproduction|Production)/i); const environment = envMatch ? envMatch[1] : 'the requested environment'; return `Your API key does not have access to ${environment}.`; }, getSolution: (error, context) => { const envMatch = error.match(/(Integration|Preproduction|Production)/i); const environment = envMatch ? envMatch[1] : null; let solution = ['**❌ Access Denied**', '']; if (environment) { solution.push(`You do not have permission to access the ${environment} environment.`); } else { solution.push('You do not have permission for this operation.'); } solution.push('', '**What you can do:**'); solution.push('• Try using a different environment you have access to'); solution.push('• Contact your DXP administrator to request access'); solution.push('• Check your API key permissions in the Optimizely DXP Portal'); return solution.join('\n'); } }, CONFIG_ERROR: { pattern: /(Missing required fields|Invalid project ID format|Empty configuration|Invalid parameter format)/i, type: 'CONFIG_ERROR', title: 'Configuration Error', getMessage: (error) => error || 'Your project configuration has errors', getSolution: () => [ '**Correct Format:**', '```', 'OPTIMIZELY_API_KEY_ACME="id=<uuid>;key=<apikey>;secret=<apisecret>"', '```', '', '**Example:**', '```', 'OPTIMIZELY_API_KEY_MY_CLIENT="id=abc12345-1234-5678-9abc-def123456789;key=myApiKey123;secret=myApiSecret456"', '```', '(Legacy format OPTIMIZELY_PROJECT_<NAME> also supported)', '', '**Common Issues:**', '- Missing semicolons between parameters', '- Using placeholder values like "example" or "your-key-here"', '- Project ID not in UUID format', '- Empty or missing required fields (id, key, secret)' ].join('\n') }, MODULE_MISSING: { pattern: /EpiCloud/i, type: 'MODULE_MISSING', title: 'EpiCloud PowerShell Module Required', getMessage: () => 'To use this feature, you need the EpiCloud PowerShell module installed', getSolution: () => [ '**Installation:**', '```powershell', 'Install-Module EpiCloud -Force', '```' ].join('\n') }, INVALID_PROJECT_ID: { pattern: /Failed to validate the credentials specified for project with id|project with id.*not found|invalid project.*uuid|project.*does not exist.*portal/i, type: 'INVALID_PROJECT_ID', title: 'Invalid Project ID', getMessage: (context) => { return context.projectId ? `The Project ID '${context.projectId}' is invalid or does not exist in your account` : 'The specified Project ID is invalid or does not exist in your account'; }, getSolution: (context) => [ '**How to Fix:**', '1. **Check your Project ID in Optimizely DXP:**', ' - Log into your Optimizely DXP portal', ' - Navigate to your project settings', ' - Copy the correct Project ID (it should be a UUID format like: abc12345-1234-5678-9abc-def123456789)', '', '2. **Verify your credentials have access to the project:**', ' - Ensure your API key was created for the correct project', ' - Check that you have permission to access this project', ' - Try using a different project from your account', '', '3. **Common Issues:**', ' - Project ID format must be a valid UUID', ' - API credentials might be for a different project', ' - Project may have been deleted or renamed', ' - You may not have access to this project', '', context.projectId ? `**Your Project ID:** ${context.projectId}` : '', '**Expected Format:** xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' ].filter(Boolean).join('\n') }, AUTH_FAILED: { pattern: /(authentication|unauthorized.*credential|401|HTTP 401)/i, type: 'AUTH_FAILED', title: 'Authentication Failed', getMessage: (error, context) => { // Check if it's Azure Storage authentication if (error && error.includes('Azure Storage')) { return 'Azure Storage authentication failed. The account key or SAS token is invalid.'; } return 'The API credentials are invalid or expired'; }, getSolution: (error, context) => { // Azure Storage specific solution if (error && error.includes('Azure Storage')) { return [ '**Azure Storage Authentication Issue:**', '', '**Check your connection string:**', '• Verify the AccountKey is correct and not expired', '• Ensure the AccountName matches your storage account', '• Check that the connection string format is valid', '', '**Connection string format:**', '```', 'DefaultEndpointsProtocol=https;AccountName=myaccount;AccountKey=mykey;EndpointSuffix=core.windows.net', '```', '', '**Common issues:**', '• Account key may have been rotated in Azure Portal', '• Special characters in the key not properly escaped', '• Using an old or expired SAS token instead of account key' ].join('\n'); } // Default API credentials solution return [ '**Troubleshooting:**', '- Verify your API key and secret are correct', '- Check that the credentials haven\'t expired', context.projectId ? `- Ensure the credentials are for project ${context.projectId}` : '', '- Generate new credentials if needed' ].filter(Boolean).join('\n'); } }, OPERATION_IN_PROGRESS: { pattern: /(on-going|already running|already an on-going)/i, type: 'OPERATION_IN_PROGRESS', title: 'Operation Already In Progress', getMessage: () => 'Another operation is currently running on this environment', getSolution: () => [ '**Next Steps:**', '- Wait for the current operation to complete', '- Check deployment status to monitor progress', '- Try again once the current operation finishes' ].join('\n') }, INVALID_STATE: { pattern: /(invalid state|cannot be|not in a valid state)/i, type: 'INVALID_STATE', title: 'Invalid Operation State', getMessage: () => 'This operation cannot be performed in the current state', getSolution: (context) => [ '**Requirements:**', context.expectedState ? `- Resource must be in ${context.expectedState} state` : '', '- Check the current status before retrying', '- Ensure all prerequisites are met' ].filter(Boolean).join('\n') }, LARGE_FILE: { pattern: /(exceeds GitHub|Large files detected|file size limit)/i, type: 'LARGE_FILE', title: 'Large File Detected', getMessage: () => 'File exceeds size limits', getSolution: () => [ '**Solutions:**', '- Remove large files from repository', '- Use Git LFS for large files', '- Add large files to .gitignore' ].join('\n') }, INVALID_DEPLOYMENT: { pattern: /(deployment.*not found|invalid deployment|deployment does not exist|get-epideployment.*cannot find)/i, type: 'INVALID_DEPLOYMENT', title: 'Invalid Deployment ID', getMessage: (context) => { return context.deploymentId ? `The deployment ID '${context.deploymentId}' is invalid or does not exist` : 'The specified deployment ID is invalid or does not exist'; }, getSolution: (context) => [ '**How to Fix:**', '1. List available deployments:', ' Use `list_deployments` to see all deployments', '2. Copy the correct deployment ID from the list', '3. Ensure you\'re using the full deployment ID (usually a GUID)', '', '**Common Issues:**', '- Partial IDs are not accepted (use the full GUID)', '- Deployment may have been cleaned up after 30 days', '- You may be looking in the wrong project', context.deploymentId ? `\n**Provided ID:** ${context.deploymentId}` : '', context.projectId ? `**Project:** ${context.projectId}` : '' ].filter(Boolean).join('\n') }, NOT_FOUND: { pattern: /(not found|404|does not exist)/i, type: 'NOT_FOUND', title: 'Resource Not Found', getMessage: (context) => { // Provide specific message for deployment IDs if (context.deploymentId) { return `Deployment with ID '${context.deploymentId}' was not found`; } // Handle project access issues with structured response if (context.projectId || context.projectName) { return `Unable to access ${context.projectName || 'project'}`; } return `The requested ${context.resourceType || 'resource'} was not found`; }, getSolution: (context) => { const SecurityHelper = require('./security-helper'); // Provide specific guidance for deployment IDs if (context.deploymentId) { return [ '**Troubleshooting Steps:**', '1. Verify the deployment ID is correct (check for typos)', '2. Use `list_deployments` to see available deployments', '3. Ensure the deployment exists in this project', '4. Check that you have permission to view this deployment', '', `**Deployment ID:** ${context.deploymentId}`, context.projectId ? `**Project ID:** ${context.projectId}` : '' ].filter(Boolean).join('\n'); } // Enhanced project access troubleshooting with masked API key if (context.projectId || context.projectName || context.apiKey) { const parts = []; parts.push('🔍 **Configuration Verification:**'); parts.push(''); // Project ID verification if (context.projectId) { parts.push('1. ✅ **Project ID Format**'); parts.push(` • Current: ${context.projectId}`); parts.push(' • Status: Valid UUID format'); parts.push(''); } // API Key verification with masking if (context.apiKey) { const maskedKey = SecurityHelper.maskSecret(context.apiKey, 4); parts.push('2. ✅ **API Key Configuration**'); parts.push(` • Prefix: ${maskedKey} (first 4 chars for identification)`); parts.push(' • Length: ✅ Correct length (40 characters)'); parts.push(' • Status: ❓ Unable to verify - check DXP Portal'); parts.push(''); } parts.push('3. 🔍 **Manual Verification Required**'); parts.push(' • Log into Optimizely DXP Portal'); parts.push(' • Navigate to API Management'); if (context.apiKey) { const maskedKey = SecurityHelper.maskSecret(context.apiKey, 4); parts.push(` • Verify API Key ${maskedKey} has access to project: ${context.projectName || context.projectId}`); } parts.push(' • Check if project name/ID changed recently'); parts.push(' • Confirm API Key has sufficient permissions for this operation'); parts.push(''); parts.push('⚠️ **Note:** Optimizely\'s API returns the same error for invalid credentials,'); parts.push(' missing permissions, or incorrect project IDs.'); return parts.join('\n'); } return [ '**Possible Causes:**', '- Incorrect ID or name provided', '- Resource has been deleted', '- You don\'t have permission to access it', context.resourceId ? `- Resource ID: ${context.resourceId}` : '' ].filter(Boolean).join('\n'); } }, TIMEOUT: { pattern: /(timeout|timed out)/i, type: 'TIMEOUT', title: 'Operation Timed Out', getMessage: () => 'The operation took too long to complete', getSolution: () => [ '**Suggestions:**', '- Try again with a longer timeout', '- Check if the service is responsive', '- Consider breaking the operation into smaller steps' ].join('\n') } }; /** * Detect error type from stderr output * @param {string} stderr - Error output * @param {Object} context - Additional context * @returns {Object|null} Error details or null */ static detectError(stderr, context = {}) { if (!stderr) return null; for (const [key, errorDef] of Object.entries(this.ERROR_PATTERNS)) { if (errorDef.pattern.test(stderr)) { return { type: errorDef.type, title: errorDef.title, message: errorDef.getMessage(stderr, context), solution: errorDef.getSolution(stderr, context) }; } } // Generic error if no pattern matches if (stderr.includes('error') || stderr.includes('Exception')) { // Try to extract specific error message const errorMatch = stderr.match(/"errors":\["(.+?)"/i); const message = errorMatch ? errorMatch[1] : 'An unexpected error occurred'; return { type: 'GENERIC_ERROR', title: 'Error', message: message, solution: '**Troubleshooting:**\n- Check the error message for details\n- Verify all parameters are correct\n- Try again or contact support@jaxondigital.com for help' }; } return null; } /** * Format error for display * @param {Object} error - Error object from detectError * @param {Object} context - Additional context * @returns {string} Formatted error message */ static formatError(error, context = {}) { // Handle limited environment access as informational, not an error if (error.type === 'LIMITED_ENV_ACCESS') { let result = `ℹ️ **${error.title}**\n\n`; result += error.message + '\n\n'; result += error.solution + '\n'; // Immediately check which environments are accessible result += '\n**Checking your actual environment access...**'; return result; } let result = `❌ **${error.title}**\n\n`; if (error.message) { result += error.message + '\n\n'; } if (error.solution) { result += error.solution + '\n'; } // Add context information (sanitized) const safeContext = SecurityHelper.createSafeLogContext(context); const contextItems = []; if (safeContext.operation) contextItems.push(`**Operation:** ${safeContext.operation}`); if (safeContext.projectId) contextItems.push(`**Project ID:** ${safeContext.projectId}`); if (safeContext.environment) contextItems.push(`**Environment:** ${safeContext.environment}`); if (safeContext.deploymentId) contextItems.push(`**Deployment ID:** ${safeContext.deploymentId}`); if (contextItems.length > 0) { result += '\n' + contextItems.join('\n'); } // Add support contact for persistent issues result += '\n\n📧 **Need help?** Contact us at support@jaxondigital.com'; return result; } /** * Extract error message from various formats * @param {string} errorText - Error text to parse * @returns {string} Extracted error message (sanitized) */ static extractErrorMessage(errorText) { let message = ''; // Remove ANSI color codes first const cleanText = errorText.replace(/\[\d+(?:;\d+)*m/g, ''); // Try JSON error format const jsonMatch = cleanText.match(/"errors":\["(.+?)"/i); if (jsonMatch) { message = jsonMatch[1]; } else { // Try specific Optimizely project validation error const optimizelyMatch = cleanText.match(/Failed to validate the credentials specified for project with id (.+?)!/i); if (optimizelyMatch) { message = `Invalid Project ID: ${optimizelyMatch[1]}`; } else { // Try exception message const exceptionMatch = cleanText.match(/Exception: (.+?)$/im); if (exceptionMatch) { message = exceptionMatch[1]; } else { // Try generic error format const errorMatch = cleanText.match(/Error: (.+?)$/im); if (errorMatch) { message = errorMatch[1]; } else { // Return first non-empty line if nothing else matches const lines = cleanText.split('\n').map(line => line.trim()).filter(line => line.length > 0); message = lines[0] || 'An error occurred'; } } } } // Sanitize the extracted message to remove any secrets return SecurityHelper.sanitizeError(message); } /** * Check if error is retryable * @param {Object} error - Error object * @returns {boolean} True if operation can be retried */ static isRetryable(error) { const retryableTypes = [ 'TIMEOUT', 'OPERATION_IN_PROGRESS' ]; return retryableTypes.includes(error.type); } /** * Get suggested wait time for retry * @param {Object} error - Error object * @returns {number} Suggested wait time in milliseconds */ static getRetryDelay(error) { const delays = { 'OPERATION_IN_PROGRESS': 30000, // 30 seconds 'TIMEOUT': 5000, // 5 seconds 'default': 10000 // 10 seconds }; return delays[error.type] || delays.default; } /** * Handle error and return MCP response format * @param {Error|string} error - Error object or message * @param {string|Object} operation - Operation name or context object * @param {Object} additionalContext - Additional context if operation is a string * @returns {Object} MCP response object with error content */ static handleError(error, operation, additionalContext) { // Handle different call signatures let context = {}; if (typeof operation === 'string') { context = { operation, ...additionalContext }; } else if (typeof operation === 'object') { context = operation; } // If error is already formatted (has title/message), use formatError if (error && error.title && error.message) { const formattedError = ErrorHandler.formatError(error, context); return { content: [{ type: 'text', text: formattedError }], isError: true }; } // Otherwise, detect error type first const errorMessage = error instanceof Error ? error.message : String(error); const detectedError = ErrorHandler.detectError(errorMessage); if (detectedError) { const formattedError = ErrorHandler.formatError(detectedError, context); return { content: [{ type: 'text', text: formattedError }], isError: true }; } // Fallback for unrecognized errors return { content: [{ type: 'text', text: `❌ **Error**\n\n${errorMessage}\n\n📧 Need help? Contact us at support@jaxondigital.com` }], isError: true }; } } module.exports = ErrorHandler;

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