Skip to main content
Glama

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