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

136 lines 7.59 kB
/** * Device monitoring tool handlers */ import { BaseToolHandler } from './base.js'; import { ParameterValidator, SafeAccess, createErrorResponse, ErrorType, } from '../../validation/error-handler.js'; import { unixToISOStringOrNow } from '../../utils/timestamp.js'; import { sanitizeFieldValue, normalizeUnknownFields, batchNormalize, sanitizeByteCount, } from '../../utils/data-normalizer.js'; import { validateResponseStructure, normalizeTimestamps, createValidationSchema, } from '../../utils/data-validator.js'; import { getLimitValidationConfig } from '../../config/limits.js'; import { withToolTimeout, TimeoutError, createTimeoutErrorResponse, } from '../../utils/timeout-manager.js'; export class GetDeviceStatusHandler extends BaseToolHandler { constructor() { super({ enableGeoEnrichment: true, enableFieldNormalization: true, additionalMeta: { data_source: 'devices', entity_type: 'network_devices', supports_geographic_enrichment: true, supports_field_normalization: true, supports_pagination: true, supports_filtering: true, standardization_version: '2.0.0', }, }); this.name = 'get_device_status'; this.description = 'Check online/offline status of all network devices with detailed information including MAC addresses, IP addresses, device types, and last seen timestamps. Requires limit parameter. Data is cached for 2 minutes for performance.'; this.category = 'device'; } async execute(args, firewalla) { try { // Parameter validation with standardized limits const limitValidation = ParameterValidator.validateNumber(args?.limit, 'limit', { required: false, defaultValue: 200, ...getLimitValidationConfig(this.name), }); if (!limitValidation.isValid) { return createErrorResponse(this.name, 'Parameter validation failed', ErrorType.VALIDATION_ERROR, undefined, limitValidation.errors); } const deviceId = args?.device_id; const includeOffline = args?.include_offline !== false; // Default to true const limit = limitValidation.sanitizedValue; const cursor = args?.cursor; // Cursor for pagination const devicesResponse = await withToolTimeout(async () => firewalla.getDeviceStatus(deviceId, includeOffline, limit, cursor), this.name); // Validate response structure const validationSchema = createValidationSchema('devices'); const validationResult = validateResponseStructure(devicesResponse, validationSchema); if (!validationResult.isValid) { // Validation warnings logged for debugging } // Normalize device data for consistency const deviceResults = SafeAccess.safeArrayAccess(devicesResponse.results, (arr) => arr, []); const normalizedDevices = batchNormalize(deviceResults, { name: (v) => sanitizeFieldValue(v, 'Unknown Device').value, ip: (v) => sanitizeFieldValue(v, 'unknown').value, macVendor: (v) => sanitizeFieldValue(v, 'unknown').value, network: (v) => (v ? normalizeUnknownFields(v) : null), group: (v) => (v ? normalizeUnknownFields(v) : null), online: (v) => Boolean(v), // Ensure consistent boolean handling }); // Optimize device counting to avoid dual array iteration const deviceCounts = normalizedDevices.reduce((acc, d) => { if (d.online === true) { acc.online++; } else { acc.offline++; } return acc; }, { online: 0, offline: 0 }); const startTime = Date.now(); // Process device data with timestamps but preserve original IDs const processedDevices = deviceResults.map((device, index) => { // Apply timestamp normalization to device data const timestampNormalized = normalizeTimestamps(device); const finalDevice = timestampNormalized.data; // Get normalized device for other fields const normalizedDevice = normalizedDevices[index] || {}; return { id: device.id || device.mac || 'unknown', // Use original ID or MAC gid: device.gid || 'unknown', // Use original GID name: normalizedDevice.name || finalDevice.name || device.name || 'unknown', ip: normalizedDevice.ip || finalDevice.ip || device.ip || 'unknown', macVendor: normalizedDevice.macVendor || finalDevice.macVendor || device.macVendor || 'unknown', online: normalizedDevice.online !== undefined ? normalizedDevice.online : finalDevice.online !== undefined ? finalDevice.online : Boolean(device.online), lastSeen: unixToISOStringOrNow(SafeAccess.getNestedValue(finalDevice, 'lastSeen', 0)), ipReserved: SafeAccess.getNestedValue(finalDevice, 'ipReserved', false), network: finalDevice.network, // Already normalized group: finalDevice.group, // Already normalized totalDownload: sanitizeByteCount(SafeAccess.getNestedValue(finalDevice, 'totalDownload', 0)), totalUpload: sanitizeByteCount(SafeAccess.getNestedValue(finalDevice, 'totalUpload', 0)), }; }); // Apply geographic enrichment for IP addresses const enrichedDevices = await this.enrichGeoIfNeeded(processedDevices, [ 'ip', ]); const unifiedResponseData = { total_devices: SafeAccess.getNestedValue(devicesResponse, 'total_count', 0), online_devices: deviceCounts .online, offline_devices: deviceCounts .offline, page_size: SafeAccess.safeArrayAccess(devicesResponse.results, arr => arr.length, 0), has_more: SafeAccess.getNestedValue(devicesResponse, 'has_more', false), devices: enrichedDevices, next_cursor: SafeAccess.getNestedValue(devicesResponse, 'next_cursor', null), }; const executionTime = Date.now() - startTime; return this.createUnifiedResponse(unifiedResponseData, { executionTimeMs: executionTime, }); } catch (error) { // Handle timeout errors specifically if (error instanceof TimeoutError) { return createTimeoutErrorResponse(this.name, error.duration, 10000 // Default timeout from timeout-manager ); } const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; return createErrorResponse(this.name, `Failed to get device status: ${errorMessage}`, ErrorType.API_ERROR, { originalError: errorMessage }); } } } //# sourceMappingURL=device.js.map