Skip to main content
Glama
drewrad8

Firewalla MCP Server

by drewrad8
server.ts52.8 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; // ============================================================================ // Structured Logger // ============================================================================ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; interface LogEntry { timestamp: string; level: LogLevel; message: string; context?: Record<string, unknown>; } const LOG_LEVELS: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3, }; export const ENV_LOG_LEVEL = 'LOG_LEVEL'; class Logger { private level: LogLevel; private readonly serviceName: string; constructor(serviceName: string = 'firewalla-mcp') { this.serviceName = serviceName; this.level = this.parseLogLevel(process.env[ENV_LOG_LEVEL]); } private parseLogLevel(envValue?: string): LogLevel { if (envValue && envValue.toLowerCase() in LOG_LEVELS) { return envValue.toLowerCase() as LogLevel; } return 'info'; // default } private shouldLog(level: LogLevel): boolean { return LOG_LEVELS[level] >= LOG_LEVELS[this.level]; } private formatEntry( level: LogLevel, message: string, context?: Record<string, unknown> ): LogEntry { return { timestamp: new Date().toISOString(), level, message, ...(context && Object.keys(context).length > 0 ? { context } : {}), }; } private output(entry: LogEntry): void { // Use stderr for logging (stdout is reserved for MCP communication) const json = JSON.stringify({ service: this.serviceName, ...entry }); process.stderr.write(json + '\n'); } debug(message: string, context?: Record<string, unknown>): void { if (this.shouldLog('debug')) { this.output(this.formatEntry('debug', message, context)); } } info(message: string, context?: Record<string, unknown>): void { if (this.shouldLog('info')) { this.output(this.formatEntry('info', message, context)); } } warn(message: string, context?: Record<string, unknown>): void { if (this.shouldLog('warn')) { this.output(this.formatEntry('warn', message, context)); } } error(message: string, context?: Record<string, unknown>): void { if (this.shouldLog('error')) { this.output(this.formatEntry('error', message, context)); } } setLevel(level: LogLevel): void { this.level = level; } getLevel(): LogLevel { return this.level; } } // Global logger instance export const logger = new Logger(); // ============================================================================ // Types based on your Firewalla API responses // ============================================================================ export interface FirewallaConfig { baseUrl: string; bearerToken: string; firewallId: string; } export interface FlowRecord { ts: number; fd: 'in' | 'out'; count: number; duration?: number; intf: string; dTags?: string[]; oIntf?: string; protocol: 'tcp' | 'udp'; port: number; devicePort: number; ip: string; deviceIP: string; upload?: number; download?: number; device: string; host?: string; country: string; deviceName?: string; macVendor?: string; tags: string[]; tagIds: string[]; networkName: string; onWan: boolean; blocked?: boolean; blockType?: string; blockPid?: string; category?: string; app?: string; apid?: number; flowTags?: string[]; intfInfo?: { name: string; type: string; uuid: string; }; portInfo?: { description?: string; name?: string; port: number; protocol: string; }; devicePortInfo?: { description?: string; name?: string; port: number; protocol: string; }; type?: string; total?: number | null; } interface DeviceInterface { uuid: string; name: string; } interface DevicePolicy { family?: boolean; acl?: boolean; doh?: { state: boolean }; safeSearch?: { state: boolean }; unbound?: { state: boolean }; device_service_scan?: boolean; weak_password_scan?: { state: boolean }; deviceOffline?: boolean; monitor?: boolean; adblock?: boolean; ntp_redirect?: { state: boolean }; devicePresence?: boolean; qos?: boolean; ipAllocation?: { allocations: Record< string, { type: 'static' | 'dynamic'; ipv4?: string; } >; }; deviceTags: string[]; userTags: string[]; tags: string[]; ssidTags: string[]; } interface NetworkGroup { meta: { type: string; name: string; uuid: string; }; enabled: boolean; intf?: string; ipv4?: string; uuid: string; name: string; gid: string; devices: Array<{ ip: string; mac: string; name: string; lastActive: number; deviceType?: string; }>; dhcp?: boolean; extra?: Record<string, any>; listenPort?: number; peers?: Array<{ allowedIPs: string[]; publicKey: string; }>; privateKey?: string; vpn?: boolean; } interface CloudRule { id: string; type: string; name: string; source: string; scope: string; beta: boolean; disabled: boolean; last_updated: number; rules: Array<{ type: string; target: string[]; action: string; remotePort: string; protocol: string; dnsmasq_only: boolean; }>; notes: string; count: number; dnsmasq_only: boolean; } interface DeviceRule { cronTime?: string; duration?: string; expire?: string; action: 'allow' | 'block' | 'deny'; notes?: string; type: string; remotePort?: string; targetList?: string; target: string; dnsmasq_only: boolean; direction: 'inbound' | 'outbound'; scope: string[]; timestamp?: number; activatedTime?: number; pid?: string; autoDeleteWhenExpires?: number; tag?: string[]; } interface RuleUpdateRequest { rules: DeviceRule[]; gids: string[]; } interface TrendData { upload: Record<string, number>; download: Record<string, number>; totalUpload: number; totalDownload: number; block: Record< string, { percent: number; total: number; blocked: number; } >; totalConn: number; totalIpB: number; totalDnsB: number; } interface FirewallaBox { name: string; model: string; gid: string; eid: string; status: boolean; activeTs: number; syncTs: number; lokiEnabled: boolean; } interface NetworkTargets { devices: Device[]; deviceTags: DeviceTag[]; networkGroups: NetworkGroup[]; } interface DeviceTag { uid: string; name: string; createTs: number; policy: Record<string, any>; gid: string; devices: Array<{ ip: string; mac: string; name: string; lastActive: number; deviceType: string; }>; } export interface Device { ip: string; mac: string; lastActive: number; firstFound: number; macVendor: string; bname: string; names: string[]; policy: DevicePolicy; name: string; deviceType: string; intf: DeviceInterface; reserved?: boolean; totalUpload: number; totalDownload: number; gid: string; online: boolean; tags?: DeviceTag[]; firewallas?: Array<any>; isFirewalla?: boolean; publicKey?: string; // For WireGuard devices allowedIPs?: string[]; // For WireGuard devices uid?: string; // For WireGuard devices } interface FlowQueryParams { start: number; end: number; filters?: any[]; focus?: boolean; } // Custom error classes for better error handling export class FirewallaAPIError extends Error { constructor( message: string, public readonly statusCode?: number, public readonly endpoint?: string, public readonly retryable: boolean = false ) { super(message); this.name = 'FirewallaAPIError'; } } export class FirewallaNetworkError extends FirewallaAPIError { constructor(message: string, endpoint?: string) { super(message, undefined, endpoint, true); this.name = 'FirewallaNetworkError'; } } export class FirewallaAuthError extends FirewallaAPIError { constructor(message: string, endpoint?: string) { super(message, 401, endpoint, false); this.name = 'FirewallaAuthError'; } } export class FirewallaRateLimitError extends FirewallaAPIError { constructor( message: string, public readonly retryAfter?: number, endpoint?: string ) { super(message, 429, endpoint, true); this.name = 'FirewallaRateLimitError'; } } // Retry configuration interface RetryConfig { maxRetries: number; baseDelayMs: number; maxDelayMs: number; timeoutMs: number; } const DEFAULT_RETRY_CONFIG: RetryConfig = { maxRetries: 3, baseDelayMs: 1000, maxDelayMs: 10000, timeoutMs: 30000, }; export class FirewallaAPI { private config: FirewallaConfig; private retryConfig: RetryConfig; constructor(config: FirewallaConfig, retryConfig: Partial<RetryConfig> = {}) { this.config = config; this.retryConfig = { ...DEFAULT_RETRY_CONFIG, ...retryConfig }; } /** * Determines if an error is retryable */ private isRetryableError(error: unknown, statusCode?: number): boolean { // Network errors are retryable if (error instanceof TypeError && error.message.includes('fetch')) { return true; } // Retry on server errors (5xx) and rate limits (429) if (statusCode) { return statusCode === 429 || (statusCode >= 500 && statusCode < 600); } return false; } /** * Calculate delay for exponential backoff with jitter */ private calculateBackoffDelay(attempt: number): number { const exponentialDelay = this.retryConfig.baseDelayMs * Math.pow(2, attempt); const jitter = Math.random() * 0.3 * exponentialDelay; // Add up to 30% jitter return Math.min(exponentialDelay + jitter, this.retryConfig.maxDelayMs); } /** * Sleep for a specified duration */ private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Create an AbortController with timeout */ private createTimeoutController(): { controller: AbortController; timeoutId: NodeJS.Timeout } { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.retryConfig.timeoutMs); return { controller, timeoutId }; } private async makeRequest(endpoint: string, options: RequestInit = {}): Promise<any> { const url = `${this.config.baseUrl}${endpoint}`; let lastError: Error | null = null; for (let attempt = 0; attempt <= this.retryConfig.maxRetries; attempt++) { const { controller, timeoutId } = this.createTimeoutController(); try { const response = await fetch(url, { ...options, signal: controller.signal, headers: { 'Content-Type': 'application/json; charset=utf-8', Authorization: `Bearer ${this.config.bearerToken}`, 'X-Firewalla-ID': this.config.firewallId, 'Cache-Control': 'no-cache', ...options.headers, }, }); clearTimeout(timeoutId); if (response.ok) { return response.json(); } // Handle specific error codes const statusCode = response.status; const statusText = response.statusText; if (statusCode === 401 || statusCode === 403) { throw new FirewallaAuthError( `Authentication failed: ${statusCode} ${statusText}`, endpoint ); } if (statusCode === 429) { const retryAfter = parseInt(response.headers.get('Retry-After') || '60', 10); if (attempt < this.retryConfig.maxRetries) { logger.warn('Rate limited, waiting before retry', { endpoint, retryAfterSeconds: retryAfter, }); await this.sleep(retryAfter * 1000); continue; } throw new FirewallaRateLimitError( `Rate limited: ${statusCode} ${statusText}`, retryAfter, endpoint ); } // Check if error is retryable if (this.isRetryableError(null, statusCode) && attempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoffDelay(attempt); logger.warn('Request failed, retrying', { endpoint, statusCode, delayMs: Math.round(delay), attempt: attempt + 1, maxRetries: this.retryConfig.maxRetries, }); await this.sleep(delay); continue; } throw new FirewallaAPIError( `Firewalla API error: ${statusCode} ${statusText}`, statusCode, endpoint, false ); } catch (error) { clearTimeout(timeoutId); // Don't retry auth errors if (error instanceof FirewallaAuthError) { throw error; } // Handle abort/timeout if (error instanceof Error && error.name === 'AbortError') { lastError = new FirewallaNetworkError( `Request to ${endpoint} timed out after ${this.retryConfig.timeoutMs}ms`, endpoint ); if (attempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoffDelay(attempt); logger.warn('Request timed out, retrying', { endpoint, timeoutMs: this.retryConfig.timeoutMs, delayMs: Math.round(delay), attempt: attempt + 1, maxRetries: this.retryConfig.maxRetries, }); await this.sleep(delay); continue; } throw lastError; } // Handle network errors (fetch failures) if (error instanceof TypeError) { lastError = new FirewallaNetworkError( `Network error connecting to ${endpoint}: ${error.message}`, endpoint ); if (attempt < this.retryConfig.maxRetries) { const delay = this.calculateBackoffDelay(attempt); logger.warn('Network error, retrying', { endpoint, error: error.message, delayMs: Math.round(delay), attempt: attempt + 1, maxRetries: this.retryConfig.maxRetries, }); await this.sleep(delay); continue; } throw lastError; } // Re-throw known API errors if (error instanceof FirewallaAPIError) { throw error; } // Unknown error throw new FirewallaAPIError( `Unexpected error calling ${endpoint}: ${error instanceof Error ? error.message : String(error)}`, undefined, endpoint, false ); } } // Should not reach here, but just in case throw ( lastError || new FirewallaAPIError('Request failed after all retries', undefined, endpoint, false) ); } async queryFlows(params: FlowQueryParams): Promise<FlowRecord[]> { return this.makeRequest('/v1/flows/query', { method: 'POST', body: JSON.stringify(params), }); } async listDevices(): Promise<Device[]> { return this.makeRequest('/v1/device/list'); } async getNetworkTargets(): Promise<NetworkTargets> { return this.makeRequest('/v1/rule/targets'); } async getCloudRules(): Promise<Record<string, CloudRule>> { return this.makeRequest('/v1/rule/cloud/list'); } async getTrendData(type: '24h' | '7d' | '30d' = '24h'): Promise<TrendData> { return this.makeRequest(`/v1/dashboard/trend?type=${type}`); } async getFirewallaBoxes(): Promise<FirewallaBox[]> { return this.makeRequest('/v1/box/list'); } async getDevice(mac: string): Promise<Device> { const devices = await this.listDevices(); const device = devices.find((d) => d.mac === mac); if (!device) { throw new Error(`Device with MAC ${mac} not found`); } return device; } async updateRules(rules: DeviceRule[], gids: string[]): Promise<any> { const requestBody: RuleUpdateRequest = { rules, gids, }; return this.makeRequest('/v1/rule/batchUpdate', { method: 'POST', body: JSON.stringify(requestBody), }); } } // Environment variable names for configuration export const ENV_FIREWALLA_URL = 'FIREWALLA_URL'; export const ENV_FIREWALLA_TOKEN = 'FIREWALLA_TOKEN'; export const ENV_FIREWALLA_ID = 'FIREWALLA_ID'; export class FirewallaMCPServer { private server: Server; private api: FirewallaAPI | null = null; private configSource: 'none' | 'env' | 'tool' = 'none'; constructor() { this.server = new Server( { name: 'firewalla-server', version: '0.1.0', }, { capabilities: { tools: {}, }, } ); this.initializeFromEnv(); this.setupToolHandlers(); } /** * Initialize the API client from environment variables if available. * Required env vars: FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID */ private initializeFromEnv(): void { const baseUrl = process.env[ENV_FIREWALLA_URL]; const bearerToken = process.env[ENV_FIREWALLA_TOKEN]; const firewallId = process.env[ENV_FIREWALLA_ID]; if (baseUrl && bearerToken && firewallId) { this.api = new FirewallaAPI({ baseUrl, bearerToken, firewallId, }); this.configSource = 'env'; logger.info('Firewalla API configured from environment variables', { baseUrl }); } else { const missing: string[] = []; if (!baseUrl) missing.push(ENV_FIREWALLA_URL); if (!bearerToken) missing.push(ENV_FIREWALLA_TOKEN); if (!firewallId) missing.push(ENV_FIREWALLA_ID); if (missing.length > 0 && missing.length < 3) { // Some env vars set but not all - warn user logger.warn('Partial Firewalla config detected', { missing }); } // If no env vars set, user must use configure_firewalla tool } } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'configure_firewalla', description: 'Configure Firewalla API connection. Can also be configured via environment variables: FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID', inputSchema: { type: 'object', properties: { baseUrl: { type: 'string', description: 'Base URL for Firewalla API (e.g., https://my.firewalla.com)', }, bearerToken: { type: 'string', description: 'Bearer token for authentication', }, firewallId: { type: 'string', description: 'Firewalla device ID (X-Firewalla-ID header)', }, }, required: ['baseUrl', 'bearerToken', 'firewallId'], }, }, { name: 'get_config_status', description: 'Check if Firewalla API is configured and show configuration source', inputSchema: { type: 'object', properties: {}, }, }, { name: 'query_network_flows', description: 'Query network flows with time range and optional filters', inputSchema: { type: 'object', properties: { startTime: { type: 'number', description: 'Start time as Unix timestamp', }, endTime: { type: 'number', description: 'End time as Unix timestamp', }, filters: { type: 'array', description: 'Optional filters for the query', items: { type: 'object' }, }, focus: { type: 'boolean', description: 'Whether to focus the query', default: false, }, }, required: ['startTime', 'endTime'], }, }, { name: 'list_devices', description: 'List all devices on the network', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_device_details', description: 'Get detailed information about a specific device by MAC address', inputSchema: { type: 'object', properties: { mac: { type: 'string', description: 'MAC address of the device', }, }, required: ['mac'], }, }, { name: 'analyze_network_traffic', description: 'Analyze network traffic patterns for a time period', inputSchema: { type: 'object', properties: { startTime: { type: 'number', description: 'Start time as Unix timestamp', }, endTime: { type: 'number', description: 'End time as Unix timestamp', }, analysisType: { type: 'string', enum: ['summary', 'top_talkers', 'blocked_traffic', 'security_events'], description: 'Type of analysis to perform', }, }, required: ['startTime', 'endTime', 'analysisType'], }, }, { name: 'get_device_traffic', description: 'Get traffic information for a specific device', inputSchema: { type: 'object', properties: { mac: { type: 'string', description: 'MAC address of the device', }, startTime: { type: 'number', description: 'Start time as Unix timestamp', }, endTime: { type: 'number', description: 'End time as Unix timestamp', }, }, required: ['mac', 'startTime', 'endTime'], }, }, { name: 'get_network_overview', description: 'Get comprehensive network overview including devices, groups, and policies', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_cloud_rules', description: 'Get active cloud security rules and policies', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_traffic_trends', description: 'Get network traffic trends over time', inputSchema: { type: 'object', properties: { period: { type: 'string', enum: ['24h', '7d', '30d'], description: 'Time period for trend analysis', default: '24h', }, }, }, }, { name: 'get_firewalla_status', description: 'Get Firewalla device status and information', inputSchema: { type: 'object', properties: {}, }, }, { name: 'search_devices', description: 'Search devices by name, IP, MAC, or device type', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (name, IP, MAC, or device type)', }, deviceType: { type: 'string', description: 'Filter by specific device type', }, online: { type: 'boolean', description: 'Filter by online status', }, }, required: ['query'], }, }, { name: 'update_device_rules', description: 'Update security rules for devices (allow/block/deny categories, apps, or specific targets)', inputSchema: { type: 'object', properties: { deviceMac: { type: 'string', description: 'MAC address of the device to apply rules to', }, action: { type: 'string', enum: ['allow', 'block', 'deny'], description: 'Action to take (allow, block, or deny)', }, target: { type: 'string', description: 'Target to apply rule to (e.g., "av" for All Video Sites, specific domain, IP, etc.)', }, type: { type: 'string', description: 'Type of rule (e.g., "category", "domain", "ip")', default: 'category', }, direction: { type: 'string', enum: ['inbound', 'outbound'], description: 'Traffic direction', default: 'outbound', }, dnsmasqOnly: { type: 'boolean', description: 'Whether to use DNS-only blocking', default: false, }, notes: { type: 'string', description: 'Optional notes for the rule', }, }, required: ['deviceMac', 'action', 'target'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'configure_firewalla': return this.configureFirewalla(args as any); case 'get_config_status': return this.getConfigStatus(); case 'query_network_flows': return this.queryNetworkFlows(args as any); case 'list_devices': return this.listDevices(); case 'get_device_details': return this.getDeviceDetails(args as any); case 'analyze_network_traffic': return this.analyzeNetworkTraffic(args as any); case 'get_device_traffic': return this.getDeviceTraffic(args as any); case 'get_network_overview': return this.getNetworkOverview(); case 'get_cloud_rules': return this.getCloudRules(); case 'get_traffic_trends': return this.getTrafficTrends(args as any); case 'get_firewalla_status': return this.getFirewallaStatus(); case 'search_devices': return this.searchDevices(args as any); case 'update_device_rules': return this.updateDeviceRules(args as any); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError( ErrorCode.InternalError, `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async configureFirewalla(args: { baseUrl: string; bearerToken: string; firewallId: string; }) { const wasConfigured = this.api !== null; const previousSource = this.configSource; this.api = new FirewallaAPI({ baseUrl: args.baseUrl, bearerToken: args.bearerToken, firewallId: args.firewallId, }); this.configSource = 'tool'; let message = 'Firewalla API configured successfully. You can now use other tools to interact with your Firewalla device.'; if (wasConfigured && previousSource === 'env') { message = 'Firewalla API reconfigured (overriding environment variable configuration). You can now use other tools to interact with your Firewalla device.'; } return { content: [ { type: 'text', text: message, }, ], }; } private async getConfigStatus() { const envVars = { FIREWALLA_URL: process.env[ENV_FIREWALLA_URL] ? 'set' : 'not set', FIREWALLA_TOKEN: process.env[ENV_FIREWALLA_TOKEN] ? 'set (hidden)' : 'not set', FIREWALLA_ID: process.env[ENV_FIREWALLA_ID] ? 'set' : 'not set', }; const status = { configured: this.api !== null, source: this.configSource, environmentVariables: envVars, }; let message: string; if (this.api) { message = `Firewalla API is configured (source: ${this.configSource === 'env' ? 'environment variables' : 'configure_firewalla tool'})`; } else { message = 'Firewalla API is not configured. Set environment variables or use configure_firewalla tool.'; } return { content: [ { type: 'text', text: message, }, { type: 'text', text: JSON.stringify(status, null, 2), }, ], }; } private async queryNetworkFlows(args: { startTime: number; endTime: number; filters?: any[]; focus?: boolean; }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } const flows = await this.api.queryFlows({ start: args.startTime, end: args.endTime, filters: args.filters || [], focus: args.focus || false, }); return { content: [ { type: 'text', text: `Found ${flows.length} network flows between ${new Date(args.startTime * 1000).toISOString()} and ${new Date(args.endTime * 1000).toISOString()}.`, }, { type: 'text', text: JSON.stringify(flows, null, 2), }, ], }; } private async listDevices() { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } const devices = await this.api.listDevices(); const summary = devices.map((device) => ({ name: device.name, ip: device.ip, mac: device.mac, deviceType: device.deviceType, online: device.online, totalUpload: device.totalUpload, totalDownload: device.totalDownload, lastActive: new Date(device.lastActive * 1000).toISOString(), })); return { content: [ { type: 'text', text: `Found ${devices.length} devices on the network:`, }, { type: 'text', text: JSON.stringify(summary, null, 2), }, ], }; } private async getDeviceDetails(args: { mac: string }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } const device = await this.api.getDevice(args.mac); return { content: [ { type: 'text', text: `Device details for ${device.name} (${device.mac}):`, }, { type: 'text', text: JSON.stringify(device, null, 2), }, ], }; } private async analyzeNetworkTraffic(args: { startTime: number; endTime: number; analysisType: 'summary' | 'top_talkers' | 'blocked_traffic' | 'security_events'; }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } const flows = await this.api.queryFlows({ start: args.startTime, end: args.endTime, filters: [], focus: false, }); let analysis: any = {}; switch (args.analysisType) { case 'summary': analysis = this.generateTrafficSummary(flows); break; case 'top_talkers': analysis = this.getTopTalkers(flows); break; case 'blocked_traffic': analysis = this.getBlockedTraffic(flows); break; case 'security_events': analysis = this.getSecurityEvents(flows); break; } return { content: [ { type: 'text', text: `Network traffic analysis (${args.analysisType}) for period ${new Date(args.startTime * 1000).toISOString()} to ${new Date(args.endTime * 1000).toISOString()}:`, }, { type: 'text', text: JSON.stringify(analysis, null, 2), }, ], }; } private async getDeviceTraffic(args: { mac: string; startTime: number; endTime: number }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const [device, flows] = await Promise.all([ this.api.getDevice(args.mac), this.api.queryFlows({ start: args.startTime, end: args.endTime, filters: [], focus: false, }), ]); const deviceFlows = flows.filter((flow) => flow.device === args.mac); const traffic = { device: { name: device.name, ip: device.ip, mac: device.mac, deviceType: device.deviceType, online: device.online, }, period: { start: new Date(args.startTime * 1000).toISOString(), end: new Date(args.endTime * 1000).toISOString(), }, totalFlows: deviceFlows.length, totalUpload: deviceFlows.reduce((sum, flow) => sum + (flow.upload || 0), 0), totalDownload: deviceFlows.reduce((sum, flow) => sum + (flow.download || 0), 0), protocols: this.groupFlowsByProtocol(deviceFlows), topDestinations: this.getTopDestinations(deviceFlows), blockedConnections: deviceFlows.filter((flow) => flow.blocked).length, securityEvents: deviceFlows.filter((flow) => flow.category === 'intel' || flow.blocked) .length, }; return { content: [ { type: 'text', text: `Traffic analysis for device ${device.name} (${device.mac}):`, }, { type: 'text', text: JSON.stringify(traffic, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to get device traffic: ${error instanceof Error ? error.message : String(error)}` ); } } private async getNetworkOverview() { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const targets = await this.api.getNetworkTargets(); const overview = { summary: { totalDevices: targets.devices.length, onlineDevices: targets.devices.filter((d) => d.online).length, totalNetworkGroups: targets.networkGroups.length, totalDeviceTags: targets.deviceTags.length, }, networkGroups: targets.networkGroups.map((group) => ({ name: group.name, type: group.meta.type, enabled: group.enabled, deviceCount: group.devices.length, subnet: group.ipv4, })), devicesByType: this.groupDevicesByType(targets.devices), deviceTags: targets.deviceTags.map((tag) => ({ name: tag.name, deviceCount: tag.devices.length, created: new Date(tag.createTs * 1000).toISOString(), })), }; return { content: [ { type: 'text', text: 'Network Overview:', }, { type: 'text', text: JSON.stringify(overview, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to get network overview: ${error instanceof Error ? error.message : String(error)}` ); } } private async getCloudRules() { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const rules = await this.api.getCloudRules(); const rulesSummary = Object.values(rules).map((rule) => ({ id: rule.id, name: rule.name, type: rule.type, enabled: !rule.disabled, ruleCount: rule.count, lastUpdated: new Date(rule.last_updated * 1000).toISOString(), scope: rule.scope, })); return { content: [ { type: 'text', text: `Found ${rulesSummary.length} cloud security rules:`, }, { type: 'text', text: JSON.stringify(rulesSummary, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to get cloud rules: ${error instanceof Error ? error.message : String(error)}` ); } } private async getTrafficTrends(args: { period?: '24h' | '7d' | '30d' }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const period = args.period || '24h'; const trendData = await this.api.getTrendData(period); const analysis = { period, summary: { totalUpload: trendData.totalUpload, totalDownload: trendData.totalDownload, totalTraffic: trendData.totalUpload + trendData.totalDownload, totalConnections: trendData.totalConn, blockedIPs: trendData.totalIpB, blockedDNS: trendData.totalDnsB, }, peaks: this.findTrafficPeaks(trendData), blockingEfficiency: this.calculateBlockingEfficiency(trendData), }; return { content: [ { type: 'text', text: `Traffic trends analysis for ${period}:`, }, { type: 'text', text: JSON.stringify(analysis, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to get traffic trends: ${error instanceof Error ? error.message : String(error)}` ); } } private async getFirewallaStatus() { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const boxes = await this.api.getFirewallaBoxes(); const status = boxes.map((box) => ({ name: box.name, model: box.model, status: box.status ? 'Online' : 'Offline', lastActive: new Date(box.activeTs * 1000).toISOString(), lastSync: new Date(box.syncTs * 1000).toISOString(), lokiEnabled: box.lokiEnabled, })); return { content: [ { type: 'text', text: `Firewalla device status (${boxes.length} device${boxes.length === 1 ? '' : 's'}):`, }, { type: 'text', text: JSON.stringify(status, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to get Firewalla status: ${error instanceof Error ? error.message : String(error)}` ); } } private async searchDevices(args: { query: string; deviceType?: string; online?: boolean }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { const devices = await this.api.listDevices(); const query = args.query.toLowerCase(); let filtered = devices.filter( (device) => device.name.toLowerCase().includes(query) || device.ip.includes(query) || device.mac.toLowerCase().includes(query) || (device.deviceType && device.deviceType.toLowerCase().includes(query)) ); if (args.deviceType) { filtered = filtered.filter((device) => device.deviceType === args.deviceType); } if (args.online !== undefined) { filtered = filtered.filter((device) => device.online === args.online); } const results = filtered.map((device) => ({ name: device.name, ip: device.ip, mac: device.mac, deviceType: device.deviceType, online: device.online, lastActive: new Date(device.lastActive * 1000).toISOString(), totalUpload: device.totalUpload, totalDownload: device.totalDownload, })); return { content: [ { type: 'text', text: `Found ${results.length} device${results.length === 1 ? '' : 's'} matching "${args.query}":`, }, { type: 'text', text: JSON.stringify(results, null, 2), }, ], }; } catch (error) { throw new Error( `Failed to search devices: ${error instanceof Error ? error.message : String(error)}` ); } } private async updateDeviceRules(args: { deviceMac: string; action: 'allow' | 'block' | 'deny'; target: string; type?: string; direction?: 'inbound' | 'outbound'; dnsmasqOnly?: boolean; notes?: string; }) { if (!this.api) { throw new Error( 'Firewalla API not configured. Set environment variables (FIREWALLA_URL, FIREWALLA_TOKEN, FIREWALLA_ID) or use configure_firewalla tool.' ); } try { // Validate device exists const device = await this.api.getDevice(args.deviceMac); // Create the rule const rule: DeviceRule = { action: args.action, type: args.type || 'category', target: args.target, dnsmasq_only: args.dnsmasqOnly || false, direction: args.direction || 'outbound', scope: [args.deviceMac], timestamp: Date.now() / 1000, activatedTime: Date.now() / 1000, notes: args.notes || '', autoDeleteWhenExpires: 0, tag: [], }; // Get Firewalla device ID for gids const firewallBoxes = await this.api.getFirewallaBoxes(); if (firewallBoxes.length === 0) { throw new Error('No Firewalla devices found'); } const gids = firewallBoxes.map((box) => box.gid); // Update the rules const result = await this.api.updateRules([rule], gids); return { content: [ { type: 'text', text: `Successfully ${args.action === 'allow' ? 'allowed' : 'blocked'} ${args.target} for device "${device.name}" (${args.deviceMac})`, }, { type: 'text', text: `Rule details:\n- Action: ${args.action}\n- Target: ${args.target}\n- Type: ${args.type || 'category'}\n- Direction: ${args.direction || 'outbound'}\n- DNS-only: ${args.dnsmasqOnly || false}`, }, { type: 'text', text: `Response: ${JSON.stringify(result, null, 2)}`, }, ], }; } catch (error) { throw new Error( `Failed to update device rules: ${error instanceof Error ? error.message : String(error)}` ); } } // Helper methods for traffic analysis private generateTrafficSummary(flows: FlowRecord[]) { const totalUpload = flows.reduce((sum, flow) => sum + (flow.upload || 0), 0); const totalDownload = flows.reduce((sum, flow) => sum + (flow.download || 0), 0); const uniqueDevices = new Set(flows.map((flow) => flow.device)).size; const blockedFlows = flows.filter((flow) => flow.blocked).length; return { totalFlows: flows.length, totalUpload, totalDownload, totalTraffic: totalUpload + totalDownload, uniqueDevices, blockedFlows, protocolDistribution: this.groupFlowsByProtocol(flows), }; } private getTopTalkers(flows: FlowRecord[], limit = 10) { const deviceTraffic = new Map<string, { upload: number; download: number; name: string }>(); flows.forEach((flow) => { const existing = deviceTraffic.get(flow.device) || { upload: 0, download: 0, name: flow.deviceName || flow.device, }; existing.upload += flow.upload || 0; existing.download += flow.download || 0; deviceTraffic.set(flow.device, existing); }); return Array.from(deviceTraffic.entries()) .map(([mac, traffic]) => ({ mac, ...traffic, total: traffic.upload + traffic.download })) .sort((a, b) => b.total - a.total) .slice(0, limit); } private getBlockedTraffic(flows: FlowRecord[]) { const blocked = flows.filter((flow) => flow.blocked); const blockedByType = new Map<string, number>(); const blockedByDestination = new Map<string, number>(); blocked.forEach((flow) => { const type = flow.blockType || 'unknown'; blockedByType.set(type, (blockedByType.get(type) || 0) + 1); const dest = flow.host || flow.ip; blockedByDestination.set(dest, (blockedByDestination.get(dest) || 0) + 1); }); return { totalBlocked: blocked.length, byType: Object.fromEntries(blockedByType), topBlockedDestinations: Array.from(blockedByDestination.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([dest, count]) => ({ destination: dest, count })), }; } private getSecurityEvents(flows: FlowRecord[]) { const securityFlows = flows.filter( (flow) => flow.blocked || flow.category === 'intel' || flow.flowTags?.includes('security') || flow.tags?.some((tag) => tag.toLowerCase().includes('security')) ); return { totalEvents: securityFlows.length, blockedConnections: securityFlows.filter((flow) => flow.blocked).length, threatIntelEvents: securityFlows.filter((flow) => flow.category === 'intel').length, eventsByCountry: this.groupFlowsByCountry(securityFlows), suspiciousPorts: this.getSuspiciousPorts(securityFlows), }; } private groupFlowsByProtocol(flows: FlowRecord[]) { const protocols = new Map<string, number>(); flows.forEach((flow) => { protocols.set(flow.protocol, (protocols.get(flow.protocol) || 0) + 1); }); return Object.fromEntries(protocols); } private groupFlowsByCountry(flows: FlowRecord[]) { const countries = new Map<string, number>(); flows.forEach((flow) => { countries.set(flow.country, (countries.get(flow.country) || 0) + 1); }); return Object.fromEntries(countries); } private getTopDestinations(flows: FlowRecord[], limit = 10) { const destinations = new Map<string, { count: number; upload: number; download: number }>(); flows.forEach((flow) => { const dest = flow.host || flow.ip; const existing = destinations.get(dest) || { count: 0, upload: 0, download: 0 }; existing.count++; existing.upload += flow.upload || 0; existing.download += flow.download || 0; destinations.set(dest, existing); }); return Array.from(destinations.entries()) .map(([dest, stats]) => ({ destination: dest, ...stats })) .sort((a, b) => b.count - a.count) .slice(0, limit); } private getSuspiciousPorts(flows: FlowRecord[]) { const suspiciousPorts = [22, 23, 445, 3389, 1433, 3306, 5432]; // Common attack targets const portActivity = new Map<number, number>(); flows.forEach((flow) => { if (suspiciousPorts.includes(flow.port)) { portActivity.set(flow.port, (portActivity.get(flow.port) || 0) + 1); } }); return Object.fromEntries(portActivity); } private groupDevicesByType(devices: Device[]) { const deviceTypes = new Map<string, number>(); devices.forEach((device) => { const type = device.deviceType || 'unknown'; deviceTypes.set(type, (deviceTypes.get(type) || 0) + 1); }); return Object.fromEntries(deviceTypes); } private findTrafficPeaks(trendData: TrendData) { const uploads = Object.entries(trendData.upload); const downloads = Object.entries(trendData.download); const peakUpload = uploads.reduce( (max, [time, bytes]) => bytes > max.bytes ? { time: new Date(parseInt(time) * 1000).toISOString(), bytes } : max, { time: '', bytes: 0 } ); const peakDownload = downloads.reduce( (max, [time, bytes]) => bytes > max.bytes ? { time: new Date(parseInt(time) * 1000).toISOString(), bytes } : max, { time: '', bytes: 0 } ); return { peakUpload, peakDownload }; } private calculateBlockingEfficiency(trendData: TrendData) { const blockData = Object.values(trendData.block); const totalConnections = blockData.reduce((sum, block) => sum + block.total, 0); const totalBlocked = blockData.reduce((sum, block) => sum + block.blocked, 0); return { totalConnections, totalBlocked, efficiencyPercent: totalConnections > 0 ? ((totalBlocked / totalConnections) * 100).toFixed(2) : '0.00', averageBlockRate: blockData.length > 0 ? (blockData.reduce((sum, block) => sum + block.percent, 0) / blockData.length).toFixed(2) : '0.00', }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info('Firewalla MCP server started', { transport: 'stdio' }); // Set up graceful shutdown handlers this.setupShutdownHandlers(); } private setupShutdownHandlers(): void { let isShuttingDown = false; const shutdown = async (signal: string) => { if (isShuttingDown) { logger.warn('Received signal again during shutdown, forcing exit', { signal }); process.exit(1); } isShuttingDown = true; logger.info('Received shutdown signal, shutting down gracefully', { signal }); try { // Close the MCP server connection await this.server.close(); logger.info('MCP server closed successfully'); process.exit(0); } catch (error) { logger.error('Error during shutdown', { error: error instanceof Error ? error.message : String(error), }); process.exit(1); } }; // Handle termination signals process.on('SIGINT', () => shutdown('SIGINT')); process.on('SIGTERM', () => shutdown('SIGTERM')); // Handle uncaught errors gracefully process.on('uncaughtException', (error) => { logger.error('Uncaught exception', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined, }); shutdown('uncaughtException'); }); process.on('unhandledRejection', (reason) => { logger.error('Unhandled rejection', { reason: reason instanceof Error ? reason.message : String(reason), }); shutdown('unhandledRejection'); }); } } // Only run the server when this file is executed directly (not imported for testing) const isMainModule = import.meta.url === `file://${process.argv[1]}`; if (isMainModule) { const server = new FirewallaMCPServer(); server.run().catch((error) => { logger.error('Failed to start server', { error: error instanceof Error ? error.message : String(error), }); process.exit(1); }); }

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/drewrad8/mcps'

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