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`;
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It does an excellent job describing key behavioral traits: the async/background nature (returns immediately with download ID), the need for monitoring via another tool, and the date filtering capability. It doesn't mention rate limits, authentication requirements, or error handling, which keeps it from a perfect score.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is perfectly structured and concise. It starts with the core purpose, immediately highlights the critical async behavior, then provides usage guidance, parameter context, and return values - all in 4 sentences with zero wasted words. Every sentence earns its place.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a complex tool with 14 parameters, no annotations, and no output schema, the description does remarkably well. It covers the essential behavior, usage patterns, and key parameters. The main gap is that with 14 parameters, it only explicitly mentions 4 of them, leaving many schema parameters undocumented in the description.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description adds meaningful context beyond the schema's 64% coverage. It explicitly identifies 'container' and 'environment' as required parameters (though schema shows 0 required), mentions 'downloadPath' and 'dateFilter' as optional, and explains the return values (downloadId and estimated file count/size). This compensates well for the schema's incomplete coverage.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool's purpose with specific verb ('Download') and resource ('files from Azure blob storage container to local path'). It distinguishes from sibling tools like 'download_logs' or 'download_status' by focusing on blob storage files rather than logs or status monitoring.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines5/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides explicit usage guidance: it specifies when to use this tool (for downloading files from blob storage), when to use alternatives (use 'download_status()' to monitor progress), and mentions prerequisites (container and environment are required). It also distinguishes this from blocking downloads by highlighting the async/background nature.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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