Skip to main content
Glama

firewalla-mcp-server

resource-validator.ts11.5 kB
/** * Resource Existence Validation Utilities for Firewalla MCP Server * Provides pre-flight checks for resource operations to prevent timeout errors */ import type { FirewallaClient } from '../firewalla/client.js'; import { ErrorType, createErrorResponse } from './error-handler.js'; /** * Resource types that can be validated */ export type ResourceType = | 'rule' | 'alarm' | 'device' | 'target_list' | 'box' | 'flow'; /** * Resource existence check result */ export interface ResourceExistenceResult { exists: boolean; resourceId: string; resourceType: ResourceType; error?: string; metadata?: Record<string, unknown>; } /** * Resource operation wrapper result */ export interface ResourceOperationResult<T> { success: boolean; result?: T; error?: any; existenceCheck?: ResourceExistenceResult; } /** * Configuration for resource operations */ export interface ResourceOperationConfig { /** Whether to skip existence check (for performance) */ skipExistenceCheck?: boolean; /** Custom timeout for existence check */ checkTimeoutMs?: number; /** Whether to cache existence results */ cacheResults?: boolean; } /** * Resource validator class with existence checking capabilities */ export class ResourceValidator { private static existenceCache = new Map<string, { exists: boolean; timestamp: number }>(); private static readonly CACHE_TTL_MS = 30000; // 30 seconds cache for existence checks /** * Check if a rule exists */ static async checkRuleExists( ruleId: string, firewalla: FirewallaClient ): Promise<ResourceExistenceResult> { 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: any) => 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: string, firewalla: FirewallaClient ): Promise<ResourceExistenceResult> { 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: string, firewalla: FirewallaClient ): Promise<ResourceExistenceResult> { 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: any) => 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: ResourceType, resourceId: string, firewalla: FirewallaClient ): Promise<ResourceExistenceResult> { 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<T>( resourceType: ResourceType, resourceId: string, firewalla: FirewallaClient, operation: () => Promise<T>, config: ResourceOperationConfig = {} ): Promise<ResourceOperationResult<T>> { 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: string, resourceType: ResourceType, resourceId: string, existenceCheck?: ResourceExistenceResult ) { 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(): void { this.existenceCache.clear(); } /** * Get cache statistics */ static getCacheStats(): { size: number; entries: Array<{ key: string; age: number; exists: boolean }>; } { 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(): number { 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; } } /** * Convenience function for checking rule existence and creating error response */ export async function validateRuleExists( ruleId: string, toolName: string, firewalla: FirewallaClient ): Promise<{ exists: true } | { exists: false; errorResponse: any }> { 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: string, toolName: string, firewalla: FirewallaClient ): Promise<{ exists: true } | { exists: false; errorResponse: any }> { const existenceCheck = await ResourceValidator.checkAlarmExists(alarmId, firewalla); if (!existenceCheck.exists) { return { exists: false, errorResponse: ResourceValidator.createResourceNotFoundResponse( toolName, 'alarm', alarmId, existenceCheck ), }; } return { exists: true }; }

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/amittell/firewalla-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server