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
JavaScript
/**
* 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