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

292 lines 11 kB
/** * Resource Existence Validation Utilities for Firewalla MCP Server * Provides pre-flight checks for resource operations to prevent timeout errors */ import { ErrorType, createErrorResponse } from './error-handler.js'; /** * Resource validator class with existence checking capabilities */ export class ResourceValidator { /** * Check if a rule exists */ static async checkRuleExists(ruleId, firewalla) { try { // Use a cached check if available and recent const cacheKey = `rule:${ruleId}`; const cached = this.existenceCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL_MS) { return { exists: cached.exists, resourceId: ruleId, resourceType: 'rule', metadata: { cached: true }, }; } // Get all rules to check for existence (with a reasonable limit) const ruleResponse = await firewalla.getNetworkRules(undefined, 1000); if (!ruleResponse || !ruleResponse.results) { throw new Error('Invalid response from getNetworkRules'); } const exists = ruleResponse.results.some((rule) => rule.gid === ruleId || rule.id === ruleId); // Cache the result this.existenceCache.set(cacheKey, { exists, timestamp: Date.now() }); return { exists, resourceId: ruleId, resourceType: 'rule', metadata: { totalRules: ruleResponse.results.length, matchFound: exists, }, }; } catch (error) { return { exists: false, resourceId: ruleId, resourceType: 'rule', error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Check if an alarm exists */ static async checkAlarmExists(alarmId, firewalla) { try { const cacheKey = `alarm:${alarmId}`; const cached = this.existenceCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL_MS) { return { exists: cached.exists, resourceId: alarmId, resourceType: 'alarm', metadata: { cached: true }, }; } // Try to get the specific alarm const alarmResponse = await firewalla.getSpecificAlarm(alarmId); const exists = !!(alarmResponse && alarmResponse.results && alarmResponse.results.length > 0); // Cache the result this.existenceCache.set(cacheKey, { exists, timestamp: Date.now() }); return { exists, resourceId: alarmId, resourceType: 'alarm', metadata: { alarmFound: exists, }, }; } catch (error) { // If we get a 404 or similar, the alarm doesn't exist const errorMessage = error instanceof Error ? error.message : 'Unknown error'; const exists = !errorMessage.includes('404') && !errorMessage.includes('not found'); return { exists, resourceId: alarmId, resourceType: 'alarm', error: errorMessage, }; } } /** * Check if a device exists */ static async checkDeviceExists(deviceId, firewalla) { try { const cacheKey = `device:${deviceId}`; const cached = this.existenceCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < this.CACHE_TTL_MS) { return { exists: cached.exists, resourceId: deviceId, resourceType: 'device', metadata: { cached: true }, }; } // Get device status to check existence const deviceResponse = await firewalla.getDeviceStatus(undefined, true, 1000); // Get all devices if (!deviceResponse || !deviceResponse.results) { throw new Error('Invalid response from getDeviceStatus'); } const exists = deviceResponse.results.some((device) => device.gid === deviceId || device.id === deviceId || device.mac === deviceId); // Cache the result this.existenceCache.set(cacheKey, { exists, timestamp: Date.now() }); return { exists, resourceId: deviceId, resourceType: 'device', metadata: { totalDevices: deviceResponse.results.length, deviceFound: exists, }, }; } catch (error) { return { exists: false, resourceId: deviceId, resourceType: 'device', error: error instanceof Error ? error.message : 'Unknown error', }; } } /** * Generic resource existence checker */ static async checkResourceExists(resourceType, resourceId, firewalla) { switch (resourceType) { case 'rule': return this.checkRuleExists(resourceId, firewalla); case 'alarm': return this.checkAlarmExists(resourceId, firewalla); case 'device': return this.checkDeviceExists(resourceId, firewalla); case 'box': return { exists: true, resourceId, resourceType, metadata: { note: 'Box existence not implemented yet' }, }; case 'flow': return { exists: true, resourceId, resourceType, metadata: { note: 'Flow existence not implemented yet' }, }; case 'target_list': return { exists: true, resourceId, resourceType, metadata: { note: 'Target list existence not implemented yet' }, }; default: return { exists: false, resourceId, resourceType, error: `Resource type '${resourceType}' not supported for existence checking`, }; } } /** * Wrapper for resource operations that includes existence checking */ static async withResourceCheck(resourceType, resourceId, firewalla, operation, config = {}) { try { // Skip existence check if configured if (config.skipExistenceCheck) { const result = await operation(); return { success: true, result }; } // Check if resource exists const existenceCheck = await this.checkResourceExists(resourceType, resourceId, firewalla); if (!existenceCheck.exists) { return { success: false, existenceCheck, error: new Error(`${resourceType} with ID '${resourceId}' not found`), }; } // Resource exists, proceed with operation const result = await operation(); return { success: true, result, existenceCheck, }; } catch (error) { return { success: false, error, }; } } /** * Create standardized "resource not found" error response */ static createResourceNotFoundResponse(toolName, resourceType, resourceId, existenceCheck) { return createErrorResponse(toolName, `${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} not found`, ErrorType.API_ERROR, { resource_type: resourceType, resource_id: resourceId, existence_check: existenceCheck, troubleshooting: [ `Verify that ${resourceType} '${resourceId}' exists`, `Check if the ${resourceType} ID is correct`, `Ensure you have permission to access this ${resourceType}`, `The ${resourceType} may have been deleted or moved`, ], documentation: `/docs/firewalla-api-reference.md#${resourceType}-operations`, }, [`${resourceType} with ID '${resourceId}' not found`]); } /** * Clear existence cache (useful for testing or when resources change frequently) */ static clearCache() { this.existenceCache.clear(); } /** * Get cache statistics */ static getCacheStats() { const now = Date.now(); const entries = Array.from(this.existenceCache.entries()).map(([key, value]) => ({ key, age: now - value.timestamp, exists: value.exists, })); return { size: this.existenceCache.size, entries, }; } /** * Clean up expired cache entries */ static cleanupCache() { const now = Date.now(); let removed = 0; for (const [key, value] of this.existenceCache.entries()) { if (now - value.timestamp > this.CACHE_TTL_MS) { this.existenceCache.delete(key); removed++; } } return removed; } } ResourceValidator.existenceCache = new Map(); ResourceValidator.CACHE_TTL_MS = 30000; // 30 seconds cache for existence checks /** * Convenience function for checking rule existence and creating error response */ export async function validateRuleExists(ruleId, toolName, firewalla) { const existenceCheck = await ResourceValidator.checkRuleExists(ruleId, firewalla); if (!existenceCheck.exists) { return { exists: false, errorResponse: ResourceValidator.createResourceNotFoundResponse(toolName, 'rule', ruleId, existenceCheck), }; } return { exists: true }; } /** * Convenience function for checking alarm existence and creating error response */ export async function validateAlarmExists(alarmId, toolName, firewalla) { const existenceCheck = await ResourceValidator.checkAlarmExists(alarmId, firewalla); if (!existenceCheck.exists) { return { exists: false, errorResponse: ResourceValidator.createResourceNotFoundResponse(toolName, 'alarm', alarmId, existenceCheck), }; } return { exists: true }; } //# sourceMappingURL=resource-validator.js.map