UNPKG

jaxon-optimizely-dxp-mcp

Version:

AI-powered automation for Optimizely DXP - deploy, monitor, and manage environments through natural conversations

1,143 lines (1,001 loc) β€’ 54.9 kB
#!/usr/bin/env node /** * Jaxon Digital Optimizely DXP MCP Server * Built with official @modelcontextprotocol for full Claude compatibility * * Built by Jaxon Digital - Optimizely Gold Partner * https://www.jaxondigital.com */ // Load environment variables from .env file if it exists const fs = require('fs'); const path = require('path'); const envPath = path.join(process.cwd(), '.env'); if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf8'); envContent.split('\n').forEach(line => { if (line && !line.startsWith('#')) { const [key, ...valueParts] = line.split('='); if (key && valueParts.length > 0) { process.env[key.trim()] = valueParts.join('=').trim(); } } }); } const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { ListToolsRequestSchema, CallToolRequestSchema } = require('@modelcontextprotocol/sdk/types.js'); const { z } = require('zod'); const { zodToJsonSchema } = require('zod-to-json-schema'); // Import existing modules const libPath = path.join(__dirname, 'lib'); const Config = require(path.join(libPath, 'config')); const ErrorHandler = require(path.join(libPath, 'error-handler')); const ResponseBuilder = require(path.join(libPath, 'response-builder')); const OutputLogger = require(path.join(libPath, 'output-logger')); const { DatabaseTools, DeploymentTools, StorageTools, PackageTools, LoggingTools, ContentTools, DeploymentHelperTools } = require(path.join(libPath, 'tools')); const ProjectTools = require(path.join(libPath, 'tools', 'project-tools')); const MonitoringTools = require(path.join(libPath, 'tools', 'monitoring-tools')); const ConnectionTestTools = require(path.join(libPath, 'tools', 'connection-test-tools')); const SetupWizard = require(path.join(libPath, 'tools', 'setup-wizard')); const SimpleTools = require(path.join(libPath, 'tools', 'simple-tools')); const DatabaseSimpleTools = require(path.join(libPath, 'tools', 'database-simple-tools')); const ProjectSwitchTool = require(path.join(libPath, 'tools', 'project-switch-tool')); const VersionChecker = require(path.join(libPath, 'version-check')); const { getTelemetry } = require(path.join(libPath, 'telemetry')); // Initialize telemetry const telemetry = getTelemetry(); // Check for updates on startup (async, non-blocking) (async () => { const updateInfo = await VersionChecker.checkForUpdates(); const notification = VersionChecker.formatUpdateNotification(updateInfo); if (notification) { OutputLogger.info(notification); } })(); // Helper function to normalize environment names function normalizeEnvironmentName(env) { if (!env) return env; const envUpper = env.toUpperCase(); // Map common abbreviations to full names const abbreviations = { 'INT': 'Integration', 'INTE': 'Integration', 'INTEGRATION': 'Integration', 'PREP': 'Preproduction', 'PRE': 'Preproduction', 'PREPRODUCTION': 'Preproduction', 'PROD': 'Production', 'PRODUCTION': 'Production' }; return abbreviations[envUpper] || env; } // Custom Zod transformer for environment names const environmentSchema = z.string().transform(normalizeEnvironmentName).pipe( z.enum(['Integration', 'Preproduction', 'Production']) ); // Define Zod schemas for each tool const schemas = { // Simple Commands - Dead Simple with Smart Defaults deploy: z.object({ target: z.string().optional().describe('Target environment: prod, staging, integration (default: prod)'), source: z.string().optional().describe('Source environment (auto-detected if not specified)'), project: z.string().optional().describe('Project name (uses default if not specified)'), dryRun: z.boolean().optional().describe('Show what would be deployed without executing') }), status: z.object({ project: z.string().optional().describe('Project name (uses default if not specified)'), environment: z.string().optional().describe('Filter to specific environment') }), rollback: z.object({ environment: z.string().optional().describe('Environment to rollback (default: production)'), project: z.string().optional().describe('Project name (uses default if not specified)') }), quick: z.object({ project: z.string().optional().describe('Project name (uses default if not specified)') }), // Database Simple Commands - Natural language for database operations backup: z.object({ environment: z.string().optional().describe('Environment to backup: prod, staging, integration (default: prod)'), project: z.string().optional().describe('Project name (uses default if not specified)'), databaseName: z.string().optional().describe('Database name (default: epicms)'), dryRun: z.boolean().optional().describe('Preview what will happen without executing'), autoDownload: z.boolean().optional().describe('Automatically download backup when complete'), downloadPath: z.string().optional().describe('Path to save downloaded backup (default: ./backups)'), forceNew: z.boolean().optional().describe('Force creation of new backup even if recent one exists') }), backup_status: z.object({ exportId: z.string().optional().describe('Export ID to check (uses latest if not specified)'), project: z.string().optional().describe('Project name (uses default if not specified)'), latest: z.boolean().optional().describe('Check status of latest backup') }), list_backups: z.object({ project: z.string().optional().describe('Project name (uses default if not specified)'), limit: z.number().optional().describe('Number of recent backups to show (default: 5)') }), check_download_capabilities: z.object({ downloadPath: z.string().optional().describe('Path to check for download capability (default: ./backups)') }), // Project switching switch_project: z.object({ projectName: z.string().describe('Name of the project to switch to') }), current_project: z.object({}), // Connection testing test_connection: z.object({ projectId: z.string().optional(), projectName: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), health_check: z.object({ projectId: z.string().optional(), projectName: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), setup_wizard: z.object({ skipChecks: z.boolean().optional(), autoFix: z.boolean().optional() }), // Version information get_version: z.object({}), // Project management get_api_key_info: z.object({ projectId: z.string().optional(), projectName: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), list_api_keys: z.object({}), get_support: z.object({}), list_monitors: z.object({}), update_monitoring_interval: z.object({ deploymentId: z.string().optional(), interval: z.number().min(10).max(600) }), stop_monitoring: z.object({ deploymentId: z.string().optional(), all: z.boolean().optional() }), get_monitoring_stats: z.object({}), disable_telemetry: z.object({}), enable_telemetry: z.object({}), get_rate_limit_status: z.object({ projectName: z.string().optional(), projectId: z.string().optional() }), get_cache_status: z.object({ projectName: z.string().optional(), projectId: z.string().optional(), action: z.enum(['status', 'clear']).optional().default('status') }), // Database operations export_database: z.object({ environment: environmentSchema, databaseName: z.enum(['epicms', 'epicommerce']), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), check_export_status: z.object({ exportId: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), environment: environmentSchema, databaseName: z.enum(['epicms', 'epicommerce']), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Deployment operations list_deployments: z.object({ limit: z.number().min(1).max(100).optional().default(20), offset: z.number().min(0).optional(), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), start_deployment: z.object({ sourceEnvironment: environmentSchema, targetEnvironment: environmentSchema, deploymentType: z.enum(['code', 'content', 'all']).optional(), sourceApps: z.array(z.string()).optional(), includeBlob: z.boolean().optional(), includeDatabase: z.boolean().optional(), directDeploy: z.boolean().optional().default(false), useMaintenancePage: z.boolean().optional().default(false), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), get_deployment_status: z.object({ deploymentId: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), complete_deployment: z.object({ deploymentId: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), reset_deployment: z.object({ deploymentId: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Storage operations list_storage_containers: z.object({ environment: environmentSchema, projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), generate_storage_sas_link: z.object({ environment: environmentSchema, containerName: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), permissions: z.enum(['Read', 'Write', 'Delete', 'List']).optional().default('Read'), expiryHours: z.number().optional().default(24), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Package operations upload_deployment_package: z.object({ environment: environmentSchema, packagePath: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), deploy_package_and_start: z.object({ sourceEnvironment: environmentSchema, targetEnvironment: environmentSchema, packagePath: z.string(), projectName: z.string().optional(), projectId: z.string().optional(), directDeploy: z.boolean().optional().default(true), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Azure DevOps Integration deploy_azure_artifact: z.object({ artifactUrl: z.string().describe('Azure DevOps build artifact URL (e.g. https://dev.azure.com/org/project/_apis/build/builds/12345/artifacts)'), targetEnvironment: environmentSchema.describe('Target environment for deployment'), azureDevOpsPat: z.string().optional().describe('Azure DevOps Personal Access Token (can also use AZURE_DEVOPS_PAT env var)'), artifactName: z.string().optional().default('drop').describe('Name of the artifact to download (default: drop)'), cleanupAfterDeploy: z.boolean().optional().default(true).describe('Clean up downloaded artifact after deployment'), projectName: z.string().optional(), projectId: z.string().optional(), directDeploy: z.boolean().optional().default(true), useMaintenancePage: z.boolean().optional().default(false), zeroDowntimeMode: z.boolean().optional().default(false), warmUpUrl: z.string().optional(), waitForCompletion: z.boolean().optional().default(false), waitTimeoutMinutes: z.number().optional().default(30), apiKey: z.string().optional(), apiSecret: z.string().optional() }), deploy_package_enhanced: z.object({ packagePath: z.string().optional().describe('Local package file path (for traditional deployment)'), artifactUrl: z.string().optional().describe('Azure DevOps artifact URL (for CI/CD integration)'), targetEnvironment: environmentSchema.describe('Target environment for deployment'), azureDevOpsPat: z.string().optional().describe('Azure DevOps PAT (required for artifact URLs)'), artifactName: z.string().optional().default('drop').describe('Artifact name to download'), cleanupAfterDeploy: z.boolean().optional().default(true).describe('Clean up temporary files'), projectName: z.string().optional(), projectId: z.string().optional(), directDeploy: z.boolean().optional().default(true), useMaintenancePage: z.boolean().optional().default(false), zeroDowntimeMode: z.boolean().optional().default(false), warmUpUrl: z.string().optional(), waitForCompletion: z.boolean().optional().default(false), waitTimeoutMinutes: z.number().optional().default(30), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Logging operations get_edge_logs: z.object({ environment: environmentSchema.optional(), projectName: z.string().optional(), projectId: z.string().optional(), hours: z.number().optional().default(1), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Content operations copy_content: z.object({ sourceEnvironment: environmentSchema, targetEnvironment: environmentSchema, projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), // Deployment helper operations analyze_package: z.object({ packagePath: z.string() }), prepare_deployment_package: z.object({ sourcePath: z.string(), outputPath: z.string().optional(), excludePatterns: z.array(z.string()).optional() }), generate_sas_upload_url: z.object({ environment: environmentSchema, projectName: z.string().optional(), projectId: z.string().optional(), apiKey: z.string().optional(), apiSecret: z.string().optional() }), split_package: z.object({ packagePath: z.string(), chunkSizeMB: z.number().optional().default(50) }) }; // Special handler for project info - now delegated to ProjectTools function handleProjectInfo(args) { return ProjectTools.getProjectInfo(args); } // Legacy project info handler (for reference) function handleProjectInfoLegacy() { const projectId = process.env.OPTIMIZELY_PROJECT_ID; const projectName = process.env.OPTIMIZELY_PROJECT_NAME; const hasApiKey = !!process.env.OPTIMIZELY_API_KEY; const hasApiSecret = !!process.env.OPTIMIZELY_API_SECRET; const isConfigured = projectId && hasApiKey && hasApiSecret; let infoText = `πŸ“Š **Optimizely DXP Project Information**\n\n`; if (isConfigured) { if (projectName) { infoText += `βœ… **Active Project: ${projectName}**\n\n`; } else { infoText += `βœ… **Project is configured and ready!**\n\n`; } infoText += `**Project Details:**\n`; if (projectName) { infoText += `β€’ Name: **${projectName}**\n`; } infoText += `β€’ Project ID: \`${projectId}\`\n` + `β€’ API Key: βœ… Configured\n` + `β€’ API Secret: βœ… Configured\n`; } else { infoText += `⚠️ **Configuration Required**\n\n` + `**Current Status:**\n` + `β€’ Project ID: ${projectId ? `\`${projectId}\`` : '❌ Not configured'}\n` + `β€’ API Key: ${hasApiKey ? 'βœ… Configured' : '❌ Not configured'}\n` + `β€’ API Secret: ${hasApiSecret ? 'βœ… Configured' : '❌ Not configured'}\n\n`; if (!projectId || !hasApiKey || !hasApiSecret) { infoText += `**To configure, you have two options:**\n\n` + `**Option 1: Pass credentials with each tool call**\n` + `When using any tool, provide:\n` + `β€’ projectId: "your-project-id"\n` + `β€’ apiKey: "your-api-key"\n` + `β€’ apiSecret: "your-api-secret"\n\n` + `**Option 2: Configure environment variables (recommended)**\n` + `Edit your MCP client config and add:\n\n` + `\`\`\`json\n` + `"env": {\n` + ` "OPTIMIZELY_PROJECT_ID": "your-project-id",\n` + ` "OPTIMIZELY_API_KEY": "your-api-key",\n` + ` "OPTIMIZELY_API_SECRET": "your-api-secret"\n` + `}\n` + `\`\`\`\n\n` + `Then restart your MCP client.\n`; } } infoText += `\nBuilt by Jaxon Digital - Optimizely Gold Partner`; return { result: { content: [{ type: 'text', text: infoText }] } }; } /** * Handle analytics request */ function handleDisableTelemetry(args) { try { // Disable telemetry for this session telemetry.enabled = false; // Also set environment variable for future sessions in this process process.env.OPTIMIZELY_MCP_TELEMETRY = 'false'; return { result: { content: [{ type: 'text', text: `πŸ”’ **Telemetry Disabled**\n\n` + `βœ… Anonymous telemetry has been disabled for this session.\n\n` + `**What this means:**\n` + `β€’ No usage data will be collected\n` + `β€’ No performance metrics will be tracked\n` + `β€’ No error reports will be sent\n\n` + `**To make this permanent across all sessions:**\n\n` + `**Option 1:** Add to your Claude Desktop config:\n` + `\`"OPTIMIZELY_MCP_TELEMETRY": "false"\`\n\n` + `**Option 2:** Set environment variable:\n` + `\`export OPTIMIZELY_MCP_TELEMETRY=false\`\n\n` + `**To re-enable:** Use the \`enable_telemetry\` tool.\n\n` + `Thank you for using Jaxon Digital's Optimizely DXP MCP Server! πŸš€` }] } }; } catch (error) { const errorMessage = ErrorHandler.formatError(error, { tool: 'disable_telemetry' }); return { result: { content: [{ type: 'text', text: errorMessage }] } }; } } function handleEnableTelemetry(args) { try { // Enable telemetry for this session telemetry.enabled = true; // Remove the disable flag from environment delete process.env.OPTIMIZELY_MCP_TELEMETRY; return { result: { content: [{ type: 'text', text: `πŸ“Š **Telemetry Enabled**\n\n` + `βœ… Anonymous telemetry has been re-enabled for this session.\n\n` + `**What we collect (anonymously):**\n` + `β€’ Tool usage patterns (which tools are used most)\n` + `β€’ Performance metrics (operation times)\n` + `β€’ Error categories (no sensitive data)\n\n` + `**Privacy guaranteed:**\n` + `β€’ No personal information\n` + `β€’ No project names or IDs\n` + `β€’ No API keys or secrets\n` + `β€’ No file contents or paths\n\n` + `**To disable again:** Use the \`disable_telemetry\` tool.\n\n` + `Thank you for helping us improve this tool! πŸ™` }] } }; } catch (error) { const errorMessage = ErrorHandler.formatError(error, { tool: 'enable_telemetry' }); return { result: { content: [{ type: 'text', text: errorMessage }] } }; } } /** * Handle rate limit status request */ function handleGetRateLimitStatus(args) { try { const PowerShellHelper = require(path.join(libPath, 'powershell-helper')); const rateLimiter = PowerShellHelper.getRateLimiter(); // Get project credentials for the status check let projectId = args.projectId || process.env.OPTIMIZELY_PROJECT_ID; // If project name provided, try to resolve it if (args.projectName && !projectId) { const ProjectTools = require(path.join(libPath, 'tools', 'project-tools')); const projectCreds = ProjectTools.getProjectCredentials(args.projectName); if (projectCreds) { projectId = projectCreds.projectId; } } if (!projectId) { const defaultCreds = ProjectTools.getProjectCredentials(); projectId = defaultCreds.projectId; } if (!projectId) { return { error: `No project ID found. Please provide a projectId parameter or configure environment variables.\n\nπŸ“§ Need help? Contact us at support@jaxondigital.com` }; } const status = rateLimiter.getStatus(projectId); const suggestedWait = rateLimiter.getSuggestedWaitTime(projectId); let statusText = `⚑ **Rate Limit Status**\n\n`; statusText += `**Project:** \`${projectId}\`\n\n`; statusText += `πŸ“Š **Usage Quotas**\n`; statusText += `β€’ Requests per minute: ${status.requestsLastMinute}/${status.maxRequestsPerMinute}\n`; statusText += `β€’ Requests per hour: ${status.requestsLastHour}/${status.maxRequestsPerHour}\n`; const minutePercent = ((status.requestsLastMinute / status.maxRequestsPerMinute) * 100).toFixed(1); const hourPercent = ((status.requestsLastHour / status.maxRequestsPerHour) * 100).toFixed(1); statusText += `β€’ Minute usage: ${minutePercent}%\n`; statusText += `β€’ Hour usage: ${hourPercent}%\n\n`; if (status.isThrottled) { const waitTime = Math.ceil((status.throttleRetryAfter - Date.now()) / 1000); statusText += `🚨 **Currently Throttled**\n`; statusText += `β€’ Status: API returned 429 (Too Many Requests)\n`; statusText += `β€’ Wait time: ${waitTime} seconds\n`; statusText += `β€’ Retry after: ${new Date(status.throttleRetryAfter).toISOString()}\n\n`; } else if (status.backoffUntil) { const waitTime = Math.ceil((status.backoffUntil - Date.now()) / 1000); statusText += `⏳ **Backing Off**\n`; statusText += `β€’ Reason: Consecutive failures\n`; statusText += `β€’ Wait time: ${waitTime} seconds\n`; statusText += `β€’ Retry after: ${new Date(status.backoffUntil).toISOString()}\n\n`; } else if (suggestedWait > 0) { statusText += `⚠️ **Usage Warning**\n`; statusText += `β€’ Status: Approaching rate limits\n`; statusText += `β€’ Suggested wait: ${Math.ceil(suggestedWait / 1000)} seconds\n`; statusText += `β€’ Recommendation: Space out requests\n\n`; } else { statusText += `βœ… **Status: Good**\n`; statusText += `β€’ No rate limiting active\n`; statusText += `β€’ Requests can proceed normally\n\n`; } if (status.consecutiveFailures > 0) { statusText += `⚠️ **Error History**\n`; statusText += `β€’ Consecutive failures: ${status.consecutiveFailures}\n`; statusText += `β€’ This triggers exponential backoff\n\n`; } if (status.lastRequest > 0) { const lastRequestAge = ((Date.now() - status.lastRequest) / 1000).toFixed(1); statusText += `πŸ“… **Last Request**\n`; statusText += `β€’ ${lastRequestAge} seconds ago\n`; statusText += `β€’ Time: ${new Date(status.lastRequest).toISOString()}\n\n`; } statusText += `πŸ”§ **Rate Limiting Info**\n`; statusText += `β€’ Rate limiting helps prevent API abuse\n`; statusText += `β€’ Limits are per-project and reset automatically\n`; statusText += `β€’ Failed requests don't count against quotas\n`; statusText += `β€’ The system uses exponential backoff for failed requests\n\n`; statusText += `πŸ’‘ **Tips**\n`; statusText += `β€’ Space out requests when approaching limits\n`; statusText += `β€’ Use batch operations when possible\n`; statusText += `β€’ Check this status if requests are being throttled\n\n`; statusText += `πŸ“§ Need help? Contact us at support@jaxondigital.com`; return { result: { content: [{ type: 'text', text: statusText }] } }; } catch (error) { OutputLogger.error('Rate limit status error:', error); return { error: `Failed to get rate limit status: ${error.message}\n\nπŸ“§ Need help? Contact us at support@jaxondigital.com` }; } } /** * Handle cache status request */ function handleGetCacheStatus(args) { try { const PowerShellHelper = require(path.join(libPath, 'powershell-helper')); // Get project credentials for the status check let projectId = args.projectId || process.env.OPTIMIZELY_PROJECT_ID; // If project name provided, try to resolve it if (args.projectName && !projectId) { const ProjectTools = require(path.join(libPath, 'tools', 'project-tools')); const projectCreds = ProjectTools.getProjectCredentials(args.projectName); if (projectCreds) { projectId = projectCreds.projectId; } } if (!projectId && args.action === 'clear') { const defaultCreds = ProjectTools.getProjectCredentials(); projectId = defaultCreds.projectId; } // Handle clear action if (args.action === 'clear') { if (!projectId) { return { error: `No project ID found for cache clearing. Please provide a projectId parameter.\n\nπŸ“§ Need help? Contact us at support@jaxondigital.com` }; } PowerShellHelper.clearCache(projectId); return { result: { content: [{ type: 'text', text: `βœ… **Cache Cleared**\n\n` + `**Project:** \`${projectId}\`\n\n` + `All cached entries for this project have been removed.\n\n` + `πŸ“§ Need help? Contact us at support@jaxondigital.com` }] } }; } // Get cache statistics const stats = PowerShellHelper.getCacheStats(); let statusText = `πŸ’Ύ **Cache Status**\n\n`; statusText += `πŸ“Š **Performance Metrics**\n`; statusText += `β€’ Hit Rate: ${stats.hitRate} (${stats.hits} hits, ${stats.misses} misses)\n`; statusText += `β€’ Total Entries: ${stats.entries}/${stats.maxEntries || 1000}\n`; statusText += `β€’ Cache Size: ${stats.sizeMB} MB / ${stats.maxSizeMB} MB\n`; statusText += `β€’ Operations: ${stats.sets} sets, ${stats.deletes} deletes\n\n`; const efficiency = stats.hits + stats.misses > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100) : 0; if (efficiency >= 70) { statusText += `βœ… **Cache Performance: Excellent**\n`; statusText += `β€’ High hit rate indicates good caching efficiency\n`; statusText += `β€’ Frequently accessed data is being cached effectively\n\n`; } else if (efficiency >= 40) { statusText += `⚠️ **Cache Performance: Good**\n`; statusText += `β€’ Moderate hit rate - caching is helping performance\n`; statusText += `β€’ Consider using operations that benefit from caching more frequently\n\n`; } else if (stats.hits + stats.misses > 10) { statusText += `πŸ”„ **Cache Performance: Low**\n`; statusText += `β€’ Low hit rate - cache may need tuning\n`; statusText += `β€’ Operations may not be benefiting from caching\n\n`; } else { statusText += `πŸ“ˆ **Cache Performance: Starting**\n`; statusText += `β€’ Not enough data to determine efficiency\n`; statusText += `β€’ Performance will improve with usage\n\n`; } if (stats.entries > 0) { statusText += `πŸ”§ **Cache Details**\n`; statusText += `β€’ Cached operations include: list_deployments, get_deployment_status, list_storage_containers\n`; statusText += `β€’ Cache automatically expires based on data type\n`; statusText += `β€’ Write operations automatically invalidate related cache entries\n`; statusText += `β€’ Cache is persistent across sessions\n\n`; } statusText += `πŸ’‘ **How Caching Helps**\n`; statusText += `β€’ Reduces API calls to Optimizely DXP\n`; statusText += `β€’ Improves response times for repeated operations\n`; statusText += `β€’ Respects rate limits by serving cached results\n`; statusText += `β€’ Automatically invalidates when data changes\n\n`; statusText += `πŸ”„ **Cache Management**\n`; statusText += `β€’ Use \`get_cache_status\` with \`action: "clear"\` to clear project cache\n`; statusText += `β€’ Cache automatically cleans expired entries\n`; statusText += `β€’ Size and entry limits prevent unlimited growth\n\n`; if (projectId) { statusText += `**Current Project:** \`${projectId}\`\n\n`; } statusText += `πŸ“§ Need help? Contact us at support@jaxondigital.com`; return { result: { content: [{ type: 'text', text: statusText }] } }; } catch (error) { OutputLogger.error('Cache status error:', error); return { error: `Failed to get cache status: ${error.message}\n\nπŸ“§ Need help? Contact us at support@jaxondigital.com` }; } } // Handle get_version command async function handleGetVersion(args) { try { const packageJson = require('./package.json'); const currentVersion = packageJson.version; let versionText = `πŸ“¦ **Jaxon Optimizely DXP MCP Server**\n\n`; versionText += `**Current Version**: v${currentVersion}\n`; versionText += `**Released**: ${packageJson.publishedAt || 'Unknown'}\n\n`; // Check for updates (with error handling) try { const versionChecker = require('./lib/version-check'); const updateInfo = await versionChecker.checkForUpdates(); if (updateInfo && updateInfo.updateAvailable) { versionText += `⚠️ **Update Available**: v${updateInfo.latestVersion}\n`; versionText += `πŸ“… Released: ${updateInfo.publishedAt || 'Recently'}\n\n`; versionText += `**To Update**:\n`; versionText += `\`\`\`bash\n`; versionText += `npm install -g jaxon-optimizely-dxp-mcp@latest\n`; versionText += `\`\`\`\n\n`; versionText += `Then restart Claude Desktop or your MCP client.\n`; } else if (updateInfo) { versionText += `βœ… **You are on the latest version!**\n`; } else { // updateInfo is null, likely due to network issues versionText += `ℹ️ **Update check unavailable** (offline or timeout)\n`; } } catch (updateError) { // If update check fails, just show current version OutputLogger.error('Version check error:', updateError); versionText += `ℹ️ **Update check failed** - showing current version only\n`; } versionText += `\n**System Information**:\n`; versionText += `β€’ Node.js: ${process.version}\n`; versionText += `β€’ Platform: ${process.platform}\n`; versionText += `β€’ Architecture: ${process.arch}\n`; return { result: { content: [{ type: 'text', text: ResponseBuilder.addFooter(versionText) }] } }; } catch (error) { OutputLogger.error('Error in handleGetVersion:', error); const errorMessage = ErrorHandler.formatError(error, { tool: 'get_version', args }); return { result: { content: [{ type: 'text', text: errorMessage }] } }; } } // Command handler map const commandHandlers = { // Simple Commands - Dead Simple with Smart Defaults 'deploy': (args) => SimpleTools.handleDeploy(args), 'status': (args) => SimpleTools.handleStatus(args), 'rollback': (args) => SimpleTools.handleRollback(args), 'quick': (args) => SimpleTools.handleQuick(args), // Database Simple Commands - Natural language 'backup': (args) => DatabaseSimpleTools.handleBackup(args), 'backup_status': (args) => DatabaseSimpleTools.handleBackupStatus(args), 'list_backups': (args) => DatabaseSimpleTools.handleListBackups(args), 'check_download_capabilities': (args) => DatabaseSimpleTools.handleCheckCapabilities(args), // Project Switching 'switch_project': (args) => ProjectSwitchTool.handleSwitchProject(args), 'current_project': (args) => ProjectSwitchTool.handleGetCurrentProject(args), // Setup & Connection Tools 'test_connection': (args) => ConnectionTestTools.testConnection(args), 'health_check': (args) => ConnectionTestTools.healthCheck(args), 'setup_wizard': (args) => SetupWizard.runSetupWizard(args), 'get_version': handleGetVersion, 'get_api_key_info': handleProjectInfo, 'list_api_keys': (args) => ProjectTools.listProjects(args), 'get_support': (args) => ProjectTools.handleGetSupport(args), 'list_monitors': (args) => MonitoringTools.listMonitors(args), 'update_monitoring_interval': (args) => MonitoringTools.updateMonitoringInterval(args), 'stop_monitoring': (args) => MonitoringTools.stopMonitoring(args), 'get_monitoring_stats': (args) => MonitoringTools.getMonitoringStats(args), 'disable_telemetry': handleDisableTelemetry, 'enable_telemetry': handleEnableTelemetry, 'get_rate_limit_status': handleGetRateLimitStatus, 'get_cache_status': handleGetCacheStatus, 'export_database': (args) => DatabaseTools.handleExportDatabase(args), 'check_export_status': (args) => DatabaseTools.handleCheckExportStatus(args), 'list_deployments': (args) => DeploymentTools.handleListDeployments(args), 'start_deployment': (args) => DeploymentTools.handleStartDeployment(args), 'get_deployment_status': (args) => DeploymentTools.handleGetDeploymentStatus(args), 'complete_deployment': (args) => DeploymentTools.handleCompleteDeployment(args), 'reset_deployment': (args) => DeploymentTools.handleResetDeployment(args), 'list_storage_containers': (args) => StorageTools.handleListStorageContainers(args), 'generate_storage_sas_link': (args) => StorageTools.handleGenerateStorageSasLink(args), 'upload_deployment_package': (args) => PackageTools.handleUploadDeploymentPackage(args), 'deploy_package_and_start': (args) => PackageTools.handleDeployPackageAndStart(args), 'deploy_azure_artifact': (args) => PackageTools.handleDeployAzureArtifact(args), 'deploy_package_enhanced': (args) => PackageTools.handleDeployPackageEnhanced(args), 'get_edge_logs': (args) => LoggingTools.handleGetEdgeLogs(args), 'copy_content': (args) => ContentTools.handleCopyContent(args), 'analyze_package': (args) => DeploymentHelperTools.handleAnalyzePackage(args), 'prepare_deployment_package': (args) => DeploymentHelperTools.handlePrepareDeploymentPackage(args), 'generate_sas_upload_url': (args) => DeploymentHelperTools.handleGenerateSasUploadUrl(args), 'split_package': (args) => DeploymentHelperTools.handleSplitPackage(args) }; // Tool definitions const toolDefinitions = Object.keys(schemas).map(name => { const descriptions = { // Simple Commands - Dead Simple with Smart Defaults 'deploy': 'πŸš€ Deploy to any environment with smart defaults (e.g. "deploy to prod")', 'status': 'πŸ“Š Intelligent status overview showing what matters right now', 'rollback': '🚨 Emergency rollback - one-click safety when things go wrong', 'quick': '⚑ Ultra-fast status check - just the essentials', // Database Simple Commands 'backup': 'πŸ’Ύ Create database backup (defaults to production for safety)', 'backup_status': 'πŸ” Check database backup status (defaults to latest backup)', 'list_backups': 'πŸ“‹ List recent database backups with status', 'check_download_capabilities': 'πŸ”§ Check if auto-download is supported in your environment', // Project Switching 'switch_project': 'πŸ”„ Switch to a different project (persists for session)', 'current_project': 'πŸ“Œ Show currently active project', // Setup & Connection Tools 'test_connection': 'πŸ” Test your MCP setup and validate configuration (run this first!)', 'health_check': 'Quick health check of MCP status (minimal output)', 'setup_wizard': 'πŸ§™ Interactive setup wizard for first-time configuration', 'get_version': 'Check the current MCP server version and available updates', 'get_api_key_info': 'Get current Optimizely API key configuration details or register a new API key', 'list_api_keys': 'List all configured Optimizely API keys', 'get_support': 'Get comprehensive support information and contact details', 'list_monitors': 'List active deployment monitors and monitoring statistics', 'update_monitoring_interval': 'Update the monitoring frequency for active deployment monitors', 'stop_monitoring': 'Stop monitoring for specific deployments or all active monitors', 'get_monitoring_stats': 'Get detailed monitoring system statistics and performance metrics', 'disable_telemetry': 'Disable anonymous telemetry data collection for this session', 'enable_telemetry': 'Re-enable anonymous telemetry data collection for this session', 'get_rate_limit_status': 'View current rate limiting status and usage quotas', 'get_cache_status': 'View cache performance statistics or clear cache entries', 'export_database': 'Export database from an Optimizely DXP environment (uses configured project)', 'check_export_status': 'Check the status of a database export', 'list_deployments': 'List all deployments for the configured project', 'start_deployment': 'Start deployment between environments. Smart defaults: Upward (Intβ†’Pre, Preβ†’Prod) deploys CODE; Downward (Prodβ†’Pre/Int) copies CONTENT. Override with deploymentType: "code", "content", or "all". Commerce: set sourceApps: ["cms", "commerce"]', 'get_deployment_status': 'Get the status of a deployment', 'complete_deployment': 'Complete a deployment that is in Verification state', 'reset_deployment': 'Reset/rollback a deployment', 'list_storage_containers': 'List storage containers for an environment (uses configured project)', 'generate_storage_sas_link': 'Generate SAS link for storage container', 'upload_deployment_package': 'Upload a deployment package', 'deploy_package_and_start': 'Deploy a package and start deployment', 'deploy_azure_artifact': 'πŸ”— Deploy directly from Azure DevOps build artifacts - perfect for CI/CD integration', 'deploy_package_enhanced': 'πŸ“¦ Enhanced package deployment supporting both local files and Azure DevOps artifacts', 'get_edge_logs': 'Get edge/CDN logs for entire project (BETA - requires enablement by Optimizely support)', 'copy_content': 'Copy content between environments (uses configured project)', 'analyze_package': 'Analyze deployment package size and provide upload recommendations', 'prepare_deployment_package': 'Prepare optimized deployment package from source directory', 'generate_sas_upload_url': 'Generate SAS URL for direct package upload (best for large files)', 'split_package': 'Split large package into smaller chunks for easier upload' }; return { name, description: descriptions[name], inputSchema: schemas[name] }; }); // Create server instance const server = new Server( { name: Config.PROJECT.NAME, version: require('./package.json').version }, { capabilities: { tools: {} } } ); // Handle tools/list request server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: toolDefinitions.map(tool => ({ name: tool.name, description: tool.description, inputSchema: zodToJsonSchema(tool.inputSchema) })) }; }); // Handle tools/call request server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name: toolName, arguments: args } = request.params; // Validate input with Zod schema const schema = schemas[toolName]; if (!schema) { throw new Error(`Unknown tool: ${toolName}`); } let validatedArgs; try { validatedArgs = schema.parse(args); } catch (error) { return { content: [{ type: 'text', text: `❌ Invalid arguments: ${error.message}\n\nπŸ“§ Need help? Contact us at support@jaxondigital.com` }], isError: true }; } // Auto-register project when credentials are provided inline (BEFORE credential injection) // This ensures API keys are saved even when used with get_api_key_info if (validatedArgs.projectName && validatedArgs.projectId && validatedArgs.apiKey && validatedArgs.apiSecret) { // Check if this is a new project or update const existingProjects = ProjectTools.getConfiguredProjects(); const isNewProject = !existingProjects.find(p => p.id === validatedArgs.projectId || p.name === validatedArgs.projectName ); // Add or update the API key configuration ProjectTools.addConfiguration({ name: validatedArgs.projectName, id: validatedArgs.projectId, apiKey: validatedArgs.apiKey, apiSecret: validatedArgs.apiSecret, environments: ['Integration', 'Preproduction', 'Production'], isDefault: false }); // Log registration for debugging (to stderr) if (isNewProject) { // console.error(`Registered new project: ${validatedArgs.projectName}`); } else { // console.error(`Updated project: ${validatedArgs.projectName}`); } } // Handle project switching and credential injection // First check if a project name was provided (for easier switching) if (validatedArgs.projectName && !validatedArgs.projectId) { const projectCreds = ProjectTools.getProjectCredentials(validatedArgs.projectName); if (projectCreds) { validatedArgs.projectId = projectCreds.projectId; validatedArgs.apiKey = projectCreds.apiKey; validatedArgs.apiSecret = projectCreds.apiSecret; // Remember this project for subsequent calls in the session ProjectTools.setLastUsedProject(validatedArgs.projectName); } } // Inject environment credentials if not provided and no inline credentials if (!validatedArgs.projectId && !validatedArgs.apiKey && !validatedArgs.apiSecret) { const defaultCreds = ProjectTools.getProjectCredentials(); validatedArgs.projectId = defaultCreds.projectId || process.env.OPTIMIZELY_PROJECT_ID; validatedArgs.apiKey = defaultCreds.apiKey || process.env.OPTIMIZELY_API_KEY; validatedArgs.apiSecret = defaultCreds.apiSecret || process.env.OPTIMIZELY_API_SECRET; } // If still missing apiKey or apiSecret, try to get from configured projects if (!validatedArgs.apiKey || !validatedArgs.apiSecret) { const projectCreds = ProjectTools.getProjectCredentials(validatedArgs.projectId); if (projectCreds) { validatedArgs.apiKey = validatedArgs.apiKey || projectCreds.apiKey; validatedArgs.apiSecret = validatedArgs.apiSecret || projectCreds.apiSecret; } } // Log which project is being used (to stderr to avoid polluting stdout) if (validatedArgs.projectId && toolName !== 'get_api_key_info') { // console.error(`Using project: ${validatedArgs.projectId}`); } // Check for missing credentials (except for project management tools) if (toolName !== 'get_api_key_info' && toolName !== 'list_api_keys') { const missingCreds = []; const hasProjectName = !!validatedArgs.projectName; if (!validatedArgs.projectId) missingCreds.push('Project ID'); if (!validatedArgs.apiKey) missingCreds.push('API Key'); if (!validatedArgs.apiSecret) missingCreds.push('API Secret'); // Only show missing credentials error if we're actually missing required fields if (missingCreds.length > 0) { // Add project name suggestion if other credentials are missing if (!hasProjectName) { missingCreds.unshift('Project Name (strongly recommended for easy reference)'); } return { content: [{ type: 'text', text: `❌ **Missing Required Credentials**\n\n` + `The following credentials are required but not provided:\n` + missingCreds.map(c => `β€’ ${c}`).join('\n') + `\n\n` + `**How to fix this:**\n\n` + `**Option 1:** Pass ALL credentials as parameters to this tool:\n` + `β€’ projectName: "Your Project Name" (e.g., "Production", "Staging", "ClientA")\n` + `β€’ projectId: "your-uuid"\n` + `β€’ apiKey: "your-key"\n` + `β€’ apiSecret: "your-secret"\n\n` + `**Why Project Name is Important:**\n` + `Once you provide a project name, the project is auto-registered and you can reference it by name in future commands!\n\n` + `**Option 2:** Configure environment variables in Claude Desktop:\n` + `Run the \`get_api_key_info\` tool for detailed setup instructions.\n\n` + `πŸ’‘ **Tip:** Use \`list_api_keys\` to see all registered API key configurations.` }], isError: true }; } } // Execute tool using handler map const startTime = Date.now(); try { const handler = commandHandlers[toolName]; if (!handler) { throw new Error(`Tool ${toolName} not implemented`); } // Track tool usage telemetry.trackToolUsage(toolName, { environment: validatedArgs.environment, hasCredentials: !!(validatedArgs.apiKey && validatedArgs.projectId)