Skip to main content
Glama
JaxonDigital

Optimizely DXP MCP Server

by JaxonDigital

download_blobs

Download files from Azure blob storage to local storage with background processing, date filtering, and progress monitoring for Optimizely DXP environments.

Instructions

šŸ“¦ Download files from Azure blob storage container to local path. ASYNC/BACKGROUND: returns immediately with download ID, continues in background. Supports date filtering to download specific time ranges. Use download_status() to monitor progress. Required: container, environment. Optional: downloadPath, dateFilter. Returns downloadId and estimated file count/size.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
environmentNoProduction
containerNameNoStorage container name (auto-detected if not specified)
downloadPathNoWhere to save files (auto-detected based on project)
previewOnlyNoShow download preview without actually downloading
filterNoFilter for specific files: exact name ("logo.png"), glob pattern ("*.pdf", "2024/*.jpg"), or substring ("report")
incrementalNoUse smart incremental download (skip unchanged files). Default: true
forceFullDownloadNoForce full download even if files exist locally. Default: false
skipConfirmationNoSkip confirmation preview (WARNING: downloads immediately without preview). Default: false - always show preview
monitorNoDXP-3: Enable real-time progress monitoring during download. Shows progress updates every 10 seconds or 50 files. Default: false (opt-in)
backgroundNoDXP-3: Start download in background and return immediately with downloadId. Use download_status({ downloadId, monitor: true }) to watch progress. Default: false (blocking download)
projectNameNo
projectIdNo
apiKeyNo
apiSecretNo

Implementation Reference

  • Main execution handler for the download_blobs tool. Handles project resolution, permissions, SAS URL generation, preview/confirmation, incremental downloads, and actual blob downloading using Azure Storage API.
    static async handleDownloadBlobs(args: BlobDownloadArgs): Promise<any> {
        try {
            // Parse natural language container names
            if (args.containerName) {
                args.containerName = this.parseNaturalLanguageContainer(args.containerName);
            }
    
            OutputLogger.info('šŸš€ Starting blob download process...');
    
            const {
                environment,
                project,
                containerName,
                downloadPath,
                filter,
                previewOnly,
                monitor = false,  // DXP-3: Progress monitoring (default: false for opt-in)
                // Legacy parameters for compatibility
                projectName,
                projectId,
                apiKey,
                apiSecret
            } = args;
    
            // DXP-3: Extract monitor and background parameters for progress tracking
            const monitorProgress = monitor === true;
            const backgroundMode = args.background === true;
    
            if (process.env.DEBUG === 'true') {
                if (monitorProgress) console.error('[DEBUG] Blob download progress monitoring enabled');
                if (backgroundMode) console.error('[DEBUG] Background mode enabled - will return immediately');
            }
    
            OutputLogger.info(`šŸ“‹ Args: env=${environment}, project=${project || projectName}, container=${containerName}`);
    
            // Get project configuration (needed even for background mode to get proper project name)
            OutputLogger.info('šŸ”‘ Resolving project configuration...');
            const projectConfig = await this.getProjectConfig(
                project || projectName,
                {
                    ...args,
                    projectId: projectId || args.projectId,
                    apiKey: apiKey || args.apiKey,
                    apiSecret: apiSecret || args.apiSecret
                }
            );
    
            OutputLogger.info(`āœ… Project config resolved: ${projectConfig.name} (${projectConfig.projectId ? projectConfig.projectId.substring(0, 8) + '...' : 'no ID'})`);
    
            // DXP-3: If background mode, delegate to background download handler with resolved project name
            if (backgroundMode) {
                return await this.handleBackgroundBlobDownload({...args, projectName: projectConfig.name}, monitorProgress);
            }
    
            // Check if we're in self-hosted mode FIRST before checking permissions
            // CRITICAL: Only self-hosted if has connectionString AND no DXP credentials
            const hasDxpCredentials = projectConfig.projectId && projectConfig.apiKey && projectConfig.apiSecret;
            const isSelfHosted = (projectConfig.connectionString || projectConfig.isSelfHosted) && !hasDxpCredentials;
    
            if (isSelfHosted) {
                OutputLogger.info('šŸ¢ Self-hosted Azure Storage mode detected');
    
                // Determine download location using new config system
                const targetPath = await DownloadConfig.getDownloadPath(
                    'blobs',
                    projectConfig.name,
                    downloadPath || null,
                    'self-hosted'
                );
    
                return await this.handleSelfHostedDownload({...args, ...projectConfig}, targetPath);
            }
    
            // Check environment permissions if not explicitly specified (only for DXP projects)
            let targetEnv: string;
            if (!environment) {
                // Get or check permissions
                const permissions = await PermissionChecker.getOrCheckPermissionsSafe(projectConfig);
    
                // Use Production as default for downloads (safer for production data)
                const defaultEnv = PermissionChecker.getDefaultEnvironment(permissions, 'download');
                if (defaultEnv) {
                    targetEnv = defaultEnv;
                    OutputLogger.info(`šŸŽÆ Using default environment for downloads: ${targetEnv}`);
    
                    // Show permissions info on first use
                    if (!this._permissionsShown) {
                        const permissionMsg = PermissionChecker.formatPermissionsMessage(permissions);
                        OutputLogger.info(permissionMsg);
                        this._permissionsShown = true;
                    }
                } else {
                    // No accessible environments
                    return ResponseBuilder.error(
                        `āŒ **No Accessible Environments**\n\n` +
                        `This API key does not have access to any environments.\n` +
                        `Please check your API key configuration in the Optimizely DXP Portal.`
                    );
                }
            } else {
                // Environment was explicitly specified
                const envToUse = environment;
                OutputLogger.info(`šŸŽÆ Environment explicitly specified: ${envToUse}`);
                targetEnv = this.parseEnvironment(envToUse);
    
                // Verify access to specified environment
                const permissions = await PermissionChecker.getOrCheckPermissionsSafe(projectConfig);
                if (!permissions.accessible.includes(targetEnv)) {
                    return ResponseBuilder.error(
                        `āŒ **Access Denied to ${targetEnv}**\n\n` +
                        `Your API key does not have access to the ${targetEnv} environment.\n\n` +
                        `**Available environments:** ${permissions.accessible.join(', ') || 'None'}\n\n` +
                        `Please use one of the available environments or update your API key permissions.`
                    );
                }
            }
    
            // Determine download location using new config system
            const targetPath = await DownloadConfig.getDownloadPath(
                'blobs',
                projectConfig.name,
                downloadPath || null,
                targetEnv
            );
    
            OutputLogger.info(`šŸ” Discovering storage containers for ${projectConfig.name} in ${targetEnv}...`);
    
            // List available containers
            let containersResult: any;
            try {
                containersResult = await StorageTools.handleListStorageContainers({
                    apiKey: projectConfig.apiKey,
                    apiSecret: projectConfig.apiSecret,
                    projectId: projectConfig.projectId,
                    environment: targetEnv
                });
            } catch (error: any) {
                return ResponseBuilder.error(
                    `Failed to list storage containers: ${error.message}`
                );
            }
    
            // Parse container list
            const containers = this.parseContainerList(containersResult);
    
            OutputLogger.info(`šŸ“‹ Found ${containers.length} containers: ${containers.join(', ')}`);
    
            if (!containers || containers.length === 0) {
                // Add helpful debug info only in development
                if (process.env.DEBUG) {
                    OutputLogger.error(`Container list result type: ${typeof containersResult}`);
                    OutputLogger.error(`Container list result keys: ${containersResult ? Object.keys(containersResult) : 'null'}`);
                    if (containersResult && typeof containersResult === 'object') {
                        OutputLogger.error(`Container list raw content: ${JSON.stringify(containersResult, null, 2)}`);
                    }
                }
                return ResponseBuilder.error(
                    `No storage containers found in ${targetEnv} environment`
                );
            }
    
            // Determine which container to download
            let targetContainer = containerName;
    
            if (!targetContainer) {
                // Try to auto-detect the media/assets container
                targetContainer = this.detectMediaContainer(containers) || undefined;
    
                if (!targetContainer) {
                    // If multiple containers, ask user to specify
                    return this.formatContainerChoice(containers, targetEnv);
                }
    
                OutputLogger.info(`šŸ“¦ Auto-selected container: ${targetContainer}`);
            }
    
            // Verify container exists (case-insensitive)
            // DXP-178 FIX: parseContainerList returns lowercase names, so compare case-insensitively
            const targetContainerLower = targetContainer.toLowerCase();
            if (!containers.includes(targetContainerLower)) {
                return ResponseBuilder.error(
                    `Container '${targetContainer}' not found. Available: ${containers.join(', ')}`
                );
            }
    
            // DXP-178 FIX: Use the lowercase version for API calls (Azure container names are case-sensitive lowercase)
            targetContainer = targetContainerLower;
    
            OutputLogger.info(`šŸ”‘ Generating SAS link for container: ${targetContainer}...`);
    
            // Log what we're sending (without secrets)
            OutputLogger.info(`Request params: env=${targetEnv}, container=${targetContainer}, project=${projectConfig.projectId ? projectConfig.projectId.substring(0, 8) + '...' : 'missing'}`);
    
            // Call the handler method which returns a properly formatted response
            const sasResponse = await StorageTools.handleGenerateStorageSasLink({
                apiKey: projectConfig.apiKey,
                apiSecret: projectConfig.apiSecret,
                projectId: projectConfig.projectId,
                environment: targetEnv,
                containerName: targetContainer,
                permissions: 'Read',
                expiryHours: 2 // 2 hours should be enough for download
            });
    
            // Extract SAS URL from the response
            const sasUrl = this.extractSasUrl(sasResponse);
    
            if (!sasUrl) {
                OutputLogger.error('Failed to extract SAS URL from response');
                OutputLogger.error(`SAS Response type: ${typeof sasResponse}`);
                OutputLogger.error(`SAS Response keys: ${sasResponse ? Object.keys(sasResponse) : 'null'}`);
    
                // Check if it's an error response
                if (sasResponse && sasResponse.error) {
                    return ResponseBuilder.error(`Storage API Error: ${sasResponse.error.message || 'Unknown error'}`);
                }
    
                // Log the actual response for debugging
                if (sasResponse && sasResponse.result && sasResponse.result.content && sasResponse.result.content[0]) {
                    const content = sasResponse.result.content[0];
                    if (content && content.text) {
                        OutputLogger.error(`Response text (first 500 chars): ${content.text.substring(0, 500)}`);
    
                        // Check if the response contains an error message
                        if (content.text.includes('Error') || content.text.includes('Failed')) {
                            return ResponseBuilder.error(content.text);
                        }
                    }
                }
    
                if (typeof sasResponse === 'string' && sasResponse.includes('Error')) {
                    return ResponseBuilder.error(sasResponse);
                }
    
                return ResponseBuilder.error(
                    'Failed to generate SAS link for container. Please verify the container exists and you have access.'
                );
            }
    
            // Always show preview first (unless explicitly skipped)
            // CRITICAL: When previewOnly is true, never skip confirmation to ensure preview runs
            const skipConfirmation = args.skipConfirmation === true && !previewOnly;
    
            // Debug log to see what's being passed
            if (process.env.DEBUG === 'true') {
                console.error('[DEBUG] Confirmation check:');
                console.error('[DEBUG]   args.skipConfirmation:', args.skipConfirmation, typeof args.skipConfirmation);
                console.error('[DEBUG]   previewOnly:', previewOnly, typeof previewOnly);
                console.error('[DEBUG]   skipConfirmation (computed):', skipConfirmation);
                console.error('[DEBUG]   __backgroundDownload:', args.__backgroundDownload);
                console.error('[DEBUG]   Will show confirmation:', !skipConfirmation);
            }
    
            if (!skipConfirmation) {
                if (process.env.DEBUG === 'true') {
                    console.error('[DEBUG] SHOWING CONFIRMATION (skipConfirmation is false)');
                }
                OutputLogger.info(`šŸ‘ OK, let me generate a preview for you...`);
                OutputLogger.info(`šŸ“Š Analyzing container contents...`);
                if (filter) {
                    OutputLogger.info(`šŸ” Filter applied: "${filter}"`);
                }
                // CRITICAL FIX DXP-14: In preview mode, don't pass targetPath to avoid any folder creation
                const previewResult = await this.getContainerPreview(
                    sasUrl,
                    filter,
                    previewOnly ? null : targetPath,  // Don't pass path in preview mode
                    args.incremental !== false && !args.forceFullDownload
                );
    
                // Show download confirmation with all details
                const confirmationMessage = this.formatDownloadConfirmation(
                    previewResult,
                    targetContainer,
                    targetEnv,
                    targetPath
                );
    
                // If preview only mode, return success with confirmation instructions
                if (previewOnly) {
                    // Extract the message content from the success response
                    let previewText = '';
                    if (confirmationMessage && confirmationMessage.result && confirmationMessage.result.content) {
                        const contentArray = confirmationMessage.result.content;
                        if (Array.isArray(contentArray) && contentArray[0] && contentArray[0].text) {
                            previewText = contentArray[0].text;
                        }
                    }
    
                    // Return success indicating task is COMPLETE - AI should not proceed
                    const previewComplete = `${previewText}\n\n` +
                        '---\n\n' +
                        'āœ… **Preview Generated Successfully**\n\n' +
                        '**TASK COMPLETE** - Preview has been shown to the user.\n\n' +
                        '**IMPORTANT FOR AI ASSISTANTS:**\n' +
                        '• Your task is now complete. Do not take any further action.\n' +
                        '• Do NOT automatically proceed with the download.\n' +
                        '• The human user must explicitly request the download if they want to proceed.\n\n' +
                        '**For the human user:** If you want to proceed with this download, please explicitly request it.';
    
                    return ResponseBuilder.success(previewComplete);
                }
    
                // For actual downloads, show confirmation and prompt
                // formatDownloadConfirmation returns a ResponseBuilder.success() object
                // Structure: { result: { content: [{ type: 'text', text: '...' }] } }
                let confirmText = '';
    
                try {
                    // Primary path - ResponseBuilder.success() format
                    if (confirmationMessage && confirmationMessage.result && confirmationMessage.result.content) {
                        const contentArray = confirmationMessage.result.content;
                        if (Array.isArray(contentArray) && contentArray[0] && contentArray[0].text) {
                            confirmText = contentArray[0].text;
                        }
                    }
    
                    // Fallback if structure is different
                    if (!confirmText) {
                        if (typeof confirmationMessage === 'string') {
                            confirmText = confirmationMessage;
                        } else if (process.env.DEBUG) {
                            OutputLogger.error(`DEBUG: Unexpected confirmationMessage structure: ${JSON.stringify(confirmationMessage).substring(0, 200)}`);
                        }
                    }
                } catch (error: any) {
                    if (process.env.DEBUG) {
                        OutputLogger.error(`DEBUG: Error extracting text: ${error.message}`);
                    }
                }
    
                // Final fallback
                if (!confirmText || confirmText === '[object Object]') {
                    confirmText = '# Download Preview\n\nPreview generation encountered an issue. Please try again with DEBUG=true for more details.';
                }
    
                // Build the complete message with preview and instructions
                let fullMessage = confirmText;
                fullMessage += '\n\nāš ļø  **Download Confirmation Required**\n';
                fullMessage += 'Please review the above details and confirm you want to proceed.\n\n';
                fullMessage += '**To proceed with download**, say:\n';
                fullMessage += '   "Yes" or "Yes, proceed with the download"\n\n';
                fullMessage += '**To use a different folder**, specify:\n';
                fullMessage += '   "Download to /your/preferred/path"\n\n';
                fullMessage += '**To cancel**, say "No" or just ignore this message.';
    
                // Also log for debugging
                if (process.env.DEBUG) {
                    OutputLogger.info(fullMessage);
                }
    
                if (process.env.DEBUG === 'true') {
                    console.error('[DEBUG] Returning confirmation message (download NOT started)');
                }
    
                return ResponseBuilder.success(fullMessage);
            }
    
            if (process.env.DEBUG === 'true') {
                console.error('[DEBUG] Confirmation skipped - proceeding with download');
            }
    
            // Register the download (skip if already registered by background handler)
            let downloadKey: string;
    
            if (args.__backgroundDownload && args.__downloadKey) {
                // Background download - use existing registration
                downloadKey = args.__downloadKey;
    
                if (process.env.DEBUG === 'true') {
                    console.error('[DEBUG] Using existing download key from background handler:', downloadKey);
                }
            } else {
                // Normal download - register new download
                const downloadInfo = {
                    projectName: projectConfig.name,
                    containerName: targetContainer,
                    environment: targetEnv,
                    downloadPath: targetPath,
                    filter: filter || null,
                    type: 'blobs'
                };
    
                const overlaps = downloadManager.checkOverlap(downloadInfo);
                if (overlaps.length > 0 && !args.force) {
                    const activeDownload = overlaps[0].active;
                    return ResponseBuilder.error(
                        `āš ļø **Download Already In Progress**\n\n` +
                        `There's already an active download for this container:\n` +
                        `• **Project**: ${activeDownload.projectName}\n` +
                        `• **Container**: ${activeDownload.containerName}\n` +
                        `• **Progress**: ${activeDownload.progress}%\n\n` +
                        `**Options:**\n` +
                        `• Wait for the current download to complete\n` +
                        `• Use \`list_active_downloads\` to see all active downloads\n` +
                        `• Use \`cancel_download\` to cancel the active download\n` +
                        `• Add \`force: true\` to override and start anyway`
                    );
                }
    
                downloadKey = downloadManager.registerDownload(downloadInfo);
    
                if (process.env.DEBUG === 'true') {
                    console.error('[DEBUG] Registered new download:', downloadKey);
                }
            }
    
            OutputLogger.info(`\nšŸ“¦āž”ļøšŸ’¾ DOWNLOADING: ${targetContainer} āž”ļø ${targetPath}\n`);
            OutputLogger.info(`šŸ“„ Starting download process...`);
    
            try {
                // Start the download process
                const downloadResult = await this.downloadContainerContents(
                    sasUrl,
                    targetPath,
                    filter,
                    downloadKey,  // Pass the key for progress updates
                    args.incremental !== false,  // Default to true
                    args.forceFullDownload === true,  // Default to false
                    monitorProgress  // DXP-3: Pass monitor flag
                );
    
                // Mark download as complete
                downloadManager.completeDownload(downloadKey, {
                    filesDownloaded: downloadResult.downloadedFiles.length,
                    totalSize: downloadResult.totalSize,
                    failed: downloadResult.failedFiles.length
                });
    
                // Format success response
                return this.formatDownloadResult(
                    downloadResult,
                    targetContainer,
                    targetEnv,
                    targetPath
                );
            } catch (error: any) {
                // Mark download as failed
                downloadManager.failDownload(downloadKey, error.message);
                throw error;
            }
    
        } catch (error: any) {
            return ErrorHandler.handleError(error, 'download-blobs', args);
        }
    }
  • Input schema defining parameters for the download_blobs tool, including environment, project, container, filters, preview, incremental mode, etc.
    interface BlobDownloadArgs {
        environment?: string;
        project?: string;
        containerName?: string;
        downloadPath?: string;
        filter?: string;
        previewOnly?: boolean;
        incremental?: boolean;
        forceFullDownload?: boolean;
        monitor?: boolean;
        background?: boolean;
        skipConfirmation?: boolean;
        force?: boolean;
        // Legacy parameters for compatibility
        projectName?: string;
        projectId?: string;
        apiKey?: string;
        apiSecret?: string;
        isSelfHosted?: boolean;
        connectionString?: string;
        apiUrl?: string;
        // Internal flags
        __backgroundDownload?: boolean;
        __downloadKey?: string;
    }
  • Core helper function that performs the actual listing and downloading of blobs from the SAS URL-protected container.
    static async downloadContainerContents(
        sasUrl: string,
        targetPath: string,
        filter: string | undefined,
        downloadKey: string | null = null,
        incremental = true,
        forceFullDownload = false,
        monitorProgress = false
    ): Promise<DownloadResult> {
        const downloadedFiles: Array<{ name: string; size?: number }> = [];
        const failedFiles: Array<{ name: string; error: string }> = [];
        let totalSize = 0;
    
        // DXP-3: Initialize progress monitor
        let progressMonitor: any = null;
    
        try {
            // Ensure target directory exists
            await fsPromises.mkdir(targetPath, { recursive: true });
    
            // Parse the SAS URL
            const url = new URL(sasUrl);
            const containerUrl = `${url.protocol}//${url.host}${url.pathname}`;
            const sasToken = url.search;
    
            OutputLogger.info('šŸ“‹ Listing blobs in container (supports >5000 files via pagination)...');
    
            // List blobs in the container
            const blobResult = await this.listBlobsInContainer(containerUrl, sasToken);
            const blobs = blobResult.blobs;
    
            if (blobs.length === 0) {
                OutputLogger.warn('No blobs found in container');
                return { downloadedFiles, failedFiles, totalSize };
            }
    
            // Apply filter if specified
            let blobsToDownload = blobs;
            if (filter) {
                OutputLogger.info(`šŸ” Applying filter: "${filter}"`);
                const regexPattern = this.globToRegex(filter);
                const filterRegex = new RegExp(regexPattern!, 'i');
                blobsToDownload = blobs.filter(blob => filterRegex.test(blob.name));
                OutputLogger.info(`āœ… Filtered: ${blobsToDownload.length} of ${blobs.length} files match filter`);
    
                // If only 1-3 files match, list them
                if (blobsToDownload.length > 0 && blobsToDownload.length <= 3) {
                    blobsToDownload.forEach(blob => {
                        OutputLogger.info(`  • ${blob.name} (${this.formatBytes(blob.size || 0)})`);
                    });
                }
            }
    
            // Check for incremental download opportunities
            let incrementalInfo: IncrementalInfo | null = null;
            let skippedFiles: ManifestFileInfo[] = [];
    
            if (incremental && !forceFullDownload) {
                OutputLogger.info('šŸ”„ Checking for incremental download opportunities...');
    
                const manifestCheck = await ManifestManager.getFilesToDownload(
                    targetPath,
                    blobsToDownload.map(blob => ({
                        name: blob.name,
                        size: blob.size || 0,
                        lastModified: blob.lastModified || null,
                        source: containerUrl
                    })) as any
                );
    
                incrementalInfo = manifestCheck as any;
                skippedFiles = manifestCheck.skippedFiles as any;
                blobsToDownload = manifestCheck.filesToDownload.map(f => {
                    // Map back to original blob format
                    const originalBlob = blobsToDownload.find(b => b.name === f.name);
                    return originalBlob || f;
                });
    
                if (skippedFiles.length > 0) {
                    OutputLogger.info(`✨ Smart download: Skipping ${skippedFiles.length} unchanged files`);
                    OutputLogger.info(`   Bandwidth saved: ${ManifestManager.formatBytes(skippedFiles.reduce((sum, f) => sum + (f.size || 0), 0))}`);
                }
            }
    
            // Calculate total size and show preview
            const totalBlobSize = blobsToDownload.reduce((sum, blob) => sum + (blob.size || 0), 0);
            const avgSpeedBytesPerSec = 5 * 1024 * 1024; // Assume 5MB/s average download speed
            const estimatedSeconds = totalBlobSize / avgSpeedBytesPerSec; // This is correct - uses filtered size
    
            OutputLogger.info('');
            OutputLogger.info('šŸ“Š **Download Preview**');
            OutputLogger.info(`   Files to download: ${blobsToDownload.length}`);
            OutputLogger.info(`   Total size: ${this.formatBytes(totalBlobSize)}`);
            OutputLogger.info(`   Estimated time: ${this.formatDuration(estimatedSeconds)}`);
            OutputLogger.info('');
    
            // Show sample of files to be downloaded
            if (blobsToDownload.length > 0) {
                OutputLogger.info('šŸ“ Sample files:');
                const sampleCount = Math.min(5, blobsToDownload.length);
                for (let i = 0; i < sampleCount; i++) {
                    const blob = blobsToDownload[i];
                    OutputLogger.info(`   • ${blob.name} (${this.formatBytes(blob.size || 0)})`);
                }
                if (blobsToDownload.length > sampleCount) {
                    OutputLogger.info(`   ... and ${blobsToDownload.length - sampleCount} more files`);
                }
                OutputLogger.info('');
            }
    
            // Show warning for large downloads (over 1GB)
            const oneGB = 1024 * 1024 * 1024;
            if (totalBlobSize > oneGB) {
                OutputLogger.warn(`āš ļø  This is a large download (${this.formatBytes(totalBlobSize)})`);
                OutputLogger.info('   Consider using filters to download specific files if needed.');
                OutputLogger.info('   Example: filter="*.jpg" to download only JPG files\n');
            }
    
            OutputLogger.info('ā³ Starting download...\n');
    
            // DXP-3: Initialize progress monitor if enabled
            progressMonitor = new ProgressMonitor({
                totalFiles: blobsToDownload.length,
                totalBytes: totalBlobSize,
                enabled: monitorProgress || true,  // Always enable for background downloads
                downloadType: 'blobs',
                updateInterval: 10000, // Update every 10 seconds
                updateThreshold: 10 // Or every 10 files (DXP-3: reduced from 50)
            });
    
            // DXP-3: Store ProgressMonitor in DownloadManager for live queries
            if (downloadKey) {
                downloadManager.setProgressMonitor(downloadKey, progressMonitor);
            }
    
            if (monitorProgress) {
                OutputLogger.info(`šŸ“Š Progress monitoring enabled - updates every 10s or 10 files`);
                if (process.env.DEBUG === 'true') {
                    console.error(`[DEBUG] ProgressMonitor initialized: enabled=${progressMonitor.enabled}, totalFiles=${progressMonitor.totalFiles}`);
                }
            }
    
            // Track progress
            let downloadedSize = 0;
            const startTime = Date.now();
    
            // Download each blob
            for (let i = 0; i < blobsToDownload.length; i++) {
                const blob = blobsToDownload[i];
                const progressNum = i + 1;
                const percentage = Math.round((progressNum / blobsToDownload.length) * 100);
    
                // Update download manager progress if we have a key
                if (downloadKey) {
                    downloadManager.updateProgress(downloadKey, percentage, 'downloading');
                }
    
                try {
                    // Show detailed progress (only if monitoring disabled - monitor handles its own display)
                    if (!monitorProgress) {
                        const elapsedMs = Date.now() - startTime;
                        const avgTimePerFile = elapsedMs / progressNum;
                        const remainingFiles = blobsToDownload.length - progressNum;
                        const etaMs = remainingFiles * avgTimePerFile;
    
                        OutputLogger.progress(
                            `[${progressNum}/${blobsToDownload.length}] ${percentage}% | ` +
                            `${this.formatBytes(downloadedSize)}/${this.formatBytes(totalBlobSize)} | ` +
                            `ETA: ${this.formatDuration(etaMs / 1000)} | ` +
                            `Downloading: ${blob.name}`
                        );
                    }
    
                    const localPath = path.join(targetPath, blob.name);
                    const blobUrl = `${containerUrl}/${blob.name}${sasToken}`;
    
                    // Ensure parent directory exists
                    await fsPromises.mkdir(path.dirname(localPath), { recursive: true});
    
                    // Download the blob
                    const size = await this.downloadBlob(blobUrl, localPath);
    
                    downloadedFiles.push({ name: blob.name, size });
                    totalSize += size;
                    downloadedSize += size;
    
                    // DXP-3: Update progress monitor if enabled
                    if (monitorProgress && progressMonitor) {
                        progressMonitor.update(progressNum, downloadedSize, blob.name);
                    }
    
                    // Add to manifest for future incremental downloads
                    if (incrementalInfo) {
                        ManifestManager.addFileToManifest(incrementalInfo.manifest, blob.name, {
                            size: size,
                            lastModified: blob.lastModified || new Date().toISOString(),
                            source: containerUrl
                        });
                    }
    
                } catch (error: any) {
                    OutputLogger.error(`Failed to download ${blob.name}: ${error.message}`);
                    failedFiles.push({ name: blob.name, error: error.message });
                }
            }
    
            // DXP-3: Mark download as complete in progress monitor
            if (monitorProgress && progressMonitor) {
                progressMonitor.complete();
            }
    
            // Only show summary if monitoring is disabled (monitor already showed completion)
            if (!monitorProgress) {
                OutputLogger.success(`āœ… Downloaded ${downloadedFiles.length} files (${this.formatBytes(totalSize)})`);
            }
    
            if (failedFiles.length > 0) {
                OutputLogger.warn(`āš ļø Failed to download ${failedFiles.length} files`);
            }
    
        } catch (error: any) {
            // DXP-3: Mark download as failed in progress monitor
            if (monitorProgress && progressMonitor) {
                progressMonitor.error(error.message);
            }
    
            OutputLogger.error(`Container download failed: ${error.message}`);
            throw error;
        }
    
        return { downloadedFiles, failedFiles, totalSize };
    }
  • Helper for generating container preview (file count, size, types, incremental info) used in confirmation step.
        if (typeof sasResponse === 'string' && sasResponse.includes('Error')) {
            return ResponseBuilder.error(sasResponse);
        }
    
        return ResponseBuilder.error(
            'Failed to generate SAS link for container. Please verify the container exists and you have access.'
        );
    }
    
    // Always show preview first (unless explicitly skipped)
    // CRITICAL: When previewOnly is true, never skip confirmation to ensure preview runs
    const skipConfirmation = args.skipConfirmation === true && !previewOnly;
    
    // Debug log to see what's being passed
    if (process.env.DEBUG === 'true') {
        console.error('[DEBUG] Confirmation check:');
        console.error('[DEBUG]   args.skipConfirmation:', args.skipConfirmation, typeof args.skipConfirmation);
        console.error('[DEBUG]   previewOnly:', previewOnly, typeof previewOnly);
        console.error('[DEBUG]   skipConfirmation (computed):', skipConfirmation);
        console.error('[DEBUG]   __backgroundDownload:', args.__backgroundDownload);
        console.error('[DEBUG]   Will show confirmation:', !skipConfirmation);
    }
    
    if (!skipConfirmation) {
        if (process.env.DEBUG === 'true') {
            console.error('[DEBUG] SHOWING CONFIRMATION (skipConfirmation is false)');
        }
        OutputLogger.info(`šŸ‘ OK, let me generate a preview for you...`);
        OutputLogger.info(`šŸ“Š Analyzing container contents...`);
        if (filter) {
            OutputLogger.info(`šŸ” Filter applied: "${filter}"`);
        }
        // CRITICAL FIX DXP-14: In preview mode, don't pass targetPath to avoid any folder creation
        const previewResult = await this.getContainerPreview(
            sasUrl,
            filter,
            previewOnly ? null : targetPath,  // Don't pass path in preview mode
            args.incremental !== false && !args.forceFullDownload
        );
    
        // Show download confirmation with all details
        const confirmationMessage = this.formatDownloadConfirmation(
            previewResult,
            targetContainer,
            targetEnv,
            targetPath
        );
    
        // If preview only mode, return success with confirmation instructions
  • Example usage and documentation of download_blobs tool in quick status response for self-hosted projects.
    response += `• \`download_blobs containerName: "mysitemedia"\` - Download media\n`;

Latest Blog Posts

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