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