UNPKG

firewalla-mcp-server

Version:

Model Context Protocol (MCP) server for Firewalla MSP API - Provides real-time network monitoring, security analysis, and firewall management through 28 specialized tools compatible with any MCP client

289 lines 11 kB
/** * Bulk Operation Management for Firewalla MCP Server * Provides infrastructure for performing operations on multiple resources efficiently */ import { ParameterValidator } from '../validation/error-handler.js'; import { withToolTimeout, TimeoutError } from './timeout-manager.js'; /** * Default configuration for bulk operations */ const DEFAULT_BULK_CONFIG = { maxItems: 100, continueOnFailure: true, timeoutMs: 30000, // 30 seconds concurrency: 5, collectDetailedErrors: true, }; /** * Manager class for handling bulk operations */ export class BulkOperationManager { constructor(config = {}) { // Ensure timeoutMs is never undefined this.config = { ...DEFAULT_BULK_CONFIG, ...config, timeoutMs: config.timeoutMs ?? DEFAULT_BULK_CONFIG.timeoutMs, }; } /** * Validate bulk operation parameters */ validateBulkParams(ids) { const errors = []; // Validate that ids is an array if (!Array.isArray(ids)) { errors.push('ids must be an array'); return { isValid: false, errors, sanitizedIds: [] }; } // Validate array is not empty if (ids.length === 0) { errors.push('ids array cannot be empty'); return { isValid: false, errors, sanitizedIds: [] }; } // Validate array size if (ids.length > this.config.maxItems) { errors.push(`Too many items: ${ids.length}. Maximum allowed: ${this.config.maxItems}`); return { isValid: false, errors, sanitizedIds: [] }; } // Validate and sanitize individual IDs const sanitizedIds = []; const seenIds = new Set(); for (let i = 0; i < ids.length; i++) { const id = ids[i]; // Validate ID is a string if (typeof id !== 'string') { errors.push(`Item at index ${i} must be a string, got ${typeof id}`); continue; } // Validate ID is not empty const trimmedId = id.trim(); if (trimmedId.length === 0) { errors.push(`Item at index ${i} cannot be empty`); continue; } // Check for duplicates if (seenIds.has(trimmedId)) { errors.push(`Duplicate ID found: ${trimmedId}`); continue; } seenIds.add(trimmedId); sanitizedIds.push(trimmedId); } return { isValid: errors.length === 0, errors, sanitizedIds, }; } /** * Execute a bulk operation on multiple items */ async executeBulkOperation(ids, operation, firewalla, operationName) { const startTime = Date.now(); const results = []; const errorsByType = {}; const warnings = []; try { // Process items with controlled concurrency const chunks = this.chunkArray(ids, this.config.concurrency); for (const chunk of chunks) { const chunkPromises = chunk.map(async (id, index) => { try { const result = await withToolTimeout(async () => operation(id, firewalla, index), `bulk_${operationName}`, Math.min(this.config.timeoutMs, 10000) // Use per-item timeout, max 10s ); return { id, success: true, data: result, }; } catch (error) { let errorMessage = 'Unknown error occurred'; let errorType = 'unknown_error'; if (error instanceof TimeoutError) { errorMessage = `Operation timed out for item ${id}`; errorType = 'timeout_error'; } else if (error instanceof Error) { errorMessage = error.message; // Try to determine error type from message if (errorMessage.includes('not found') || errorMessage.includes('404')) { errorType = 'not_found_error'; } else if (errorMessage.includes('unauthorized') || errorMessage.includes('403')) { errorType = 'authentication_error'; } else if (errorMessage.includes('network') || errorMessage.includes('connection')) { errorType = 'network_error'; } else { errorType = 'api_error'; } } // Track error types errorsByType[errorType] = (errorsByType[errorType] || 0) + 1; const itemResult = { id, success: false, error: errorMessage, errorType, }; if (this.config.collectDetailedErrors) { itemResult.data = { originalError: error instanceof Error ? error.message : String(error), timestamp: new Date().toISOString(), }; } return itemResult; } }); // Wait for current chunk to complete const chunkResults = await Promise.all(chunkPromises); results.push(...chunkResults); // If not continuing on failure and we have failures, stop if (!this.config.continueOnFailure && chunkResults.some(r => !r.success)) { warnings.push('Operation stopped due to failures (continueOnFailure=false)'); break; } } const successful = results.filter(r => r.success).length; const failed = results.filter(r => !r.success).length; const processingTime = Date.now() - startTime; // Add performance warnings if (processingTime > this.config.timeoutMs * 0.8) { warnings.push('Operation took longer than expected, consider reducing batch size'); } if (failed > successful) { warnings.push('More operations failed than succeeded, check your input data and permissions'); } return { total: results.length, successful, failed, results, summary: { success_rate: results.length > 0 ? Math.round((successful / results.length) * 100) : 0, processing_time_ms: processingTime, errors_by_type: errorsByType, }, warnings: warnings.length > 0 ? warnings : undefined, }; } catch (error) { // Handle catastrophic failures const errorMessage = `Bulk operation failed: ${error instanceof Error ? error.message : 'Unknown error'}`; throw new Error(errorMessage); } } /** * Split array into chunks for controlled concurrency */ chunkArray(array, chunkSize) { const chunks = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i, i + chunkSize)); } return chunks; } /** * Create a configuration optimized for alarm operations */ static forAlarms() { return new BulkOperationManager({ maxItems: 50, // Alarms are typically smaller operations continueOnFailure: true, timeoutMs: 20000, concurrency: 3, collectDetailedErrors: true, }); } /** * Create a configuration optimized for rule operations */ static forRules() { return new BulkOperationManager({ maxItems: 100, // Rules can handle larger batches continueOnFailure: true, timeoutMs: 45000, concurrency: 5, collectDetailedErrors: true, }); } /** * Create a configuration optimized for device operations */ static forDevices() { return new BulkOperationManager({ maxItems: 200, // Device operations are usually lightweight continueOnFailure: true, timeoutMs: 30000, concurrency: 10, collectDetailedErrors: false, // Don't need detailed errors for device ops }); } } /** * Utility function to create standardized bulk operation responses */ export function createBulkOperationResponse(result, operationName) { return { content: [ { type: 'text', text: JSON.stringify({ operation: operationName, ...result, timestamp: new Date().toISOString(), }, null, 2), }, ], isError: false, }; } /** * Validate common bulk operation parameters */ export function validateBulkOperationArgs(args) { const errors = []; // Validate required ids parameter const idsValidation = ParameterValidator.validateRequiredString(Array.isArray(args?.ids) ? JSON.stringify(args.ids) : args?.ids, 'ids'); if (!idsValidation.isValid) { // Special handling for array validation if (!Array.isArray(args?.ids)) { errors.push('ids parameter must be an array of strings'); } else { errors.push(...idsValidation.errors); } } const ids = Array.isArray(args?.ids) ? args.ids : []; // Additional validation for individual IDs const validIds = []; ids.forEach((id, index) => { if (typeof id !== 'string') { errors.push(`ID at index ${index} must be a string, got ${typeof id}`); } else if (id.trim().length === 0) { errors.push(`ID at index ${index} cannot be empty`); } else { validIds.push(id.trim()); } }); return { isValid: errors.length === 0, errors, sanitizedArgs: { ids: validIds, options: args?.options || {}, }, }; } //# sourceMappingURL=bulk-operation-manager.js.map