Skip to main content
Glama

OPNSense MCP Server

index.ts172 kB
#!/usr/bin/env node // OPNsense MCP Server - Phase 3 Implementation with Dual Transport Support import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import { logger } from './utils/logger.js'; // Environment variables are provided by Claude Desktop/Code // No need for dotenv - configuration comes from MCP client // Import transport management import { TransportManager } from './transports/TransportManager.js'; import { SSETransportServer } from './transports/SSETransportServer.js'; import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; // Import our API client and resources import { OPNSenseAPIClient } from './api/client.js'; import { VlanResource } from './resources/vlan.js'; import { FirewallRuleResource } from './resources/firewall/rule.js'; import { NATResource } from './resources/firewall/nat.js'; import { BackupManager } from './resources/backup/manager.js'; import { MCPCacheManager } from './cache/manager.js'; import { DhcpLeaseResource } from './resources/services/dhcp/leases.js'; import { DnsBlocklistResource } from './resources/services/dns/blocklist.js'; import { HAProxyResource } from './resources/services/haproxy/index.js'; import { MacroRecorder } from './macro/index.js'; import { ArpTableResource } from './resources/network/arp.js'; import SystemSettingsResource from './resources/system/settings.js'; import InterfaceConfigResource from './resources/network/interfaces.js'; import RoutingDiagnosticsResource from './resources/diagnostics/routing.js'; import CLIExecutorResource from './resources/cli/executor.js'; import SSHExecutor from './resources/ssh/executor.js'; // Import IaC components import { resourceRegistry } from './resources/registry.js'; import { DeploymentPlanner } from './deployment/planner.js'; import { ExecutionEngine } from './execution/engine.js'; import { ResourceStateStore } from './state/store.js'; // Import IaC resource definitions to register them import './resources/network/vlan-iac.js'; import './resources/firewall/rule-iac.js'; // Configuration schema const ConfigSchema = z.object({ host: z.string().url(), apiKey: z.string().min(1), apiSecret: z.string().min(1), verifySsl: z.boolean().default(true) }); class OPNSenseMCPServer { private server: Server; private client: OPNSenseAPIClient | null = null; private vlanResource: VlanResource | null = null; private firewallRuleResource: FirewallRuleResource | null = null; private natResource: NATResource | null = null; private backupManager: BackupManager | null = null; private cacheManager: MCPCacheManager | null = null; private dhcpResource: DhcpLeaseResource | null = null; private dnsBlocklistResource: DnsBlocklistResource | null = null; private haproxyResource: HAProxyResource | null = null; private macroRecorder: MacroRecorder | null = null; private arpResource: ArpTableResource | null = null; private systemSettingsResource: SystemSettingsResource | null = null; private interfaceConfigResource: InterfaceConfigResource | null = null; private routingDiagnosticsResource: RoutingDiagnosticsResource | null = null; private cliExecutor: CLIExecutorResource | null = null; private sshExecutor: SSHExecutor | null = null; // IaC components private planner: DeploymentPlanner | null = null; private engine: ExecutionEngine | null = null; private stateStore: ResourceStateStore | null = null; private iacEnabled: boolean; constructor() { this.iacEnabled = process.env.IAC_ENABLED !== 'false'; this.server = new Server( { name: 'opnsense-mcp', version: '0.8.0', description: 'OPNsense firewall management via MCP with CLI execution, IaC, ARP table, DNS filtering and HAProxy support' }, { capabilities: { resources: {}, tools: {} } } ); this.setupHandlers(); } private formatHostUrl(host: string): string { // If host doesn't start with http:// or https://, add https:// if (!host.startsWith('http://') && !host.startsWith('https://')) { return `https://${host}`; } return host; } private async initialize() { try { // Check if we have environment variables configured if (!process.env.OPNSENSE_HOST || !process.env.OPNSENSE_API_KEY || !process.env.OPNSENSE_API_SECRET) { logger.info('OPNsense environment variables not configured. Server will start but requires configure tool to be called.'); return false; } // Validate configuration const config = ConfigSchema.parse({ host: this.formatHostUrl(process.env.OPNSENSE_HOST), apiKey: process.env.OPNSENSE_API_KEY, apiSecret: process.env.OPNSENSE_API_SECRET, verifySsl: process.env.OPNSENSE_VERIFY_SSL !== 'false' }); // Create API client this.client = new OPNSenseAPIClient(config); // Initialize macro recorder this.macroRecorder = new MacroRecorder(this.client, process.env.MACRO_STORAGE_PATH); // Set up recording in the API client this.client.setRecorder((call) => this.macroRecorder?.recordAPICall(call)); // Test connection const connectionTest = await this.client.testConnection(); if (!connectionTest.success) { throw new Error(`Failed to connect to OPNsense: ${connectionTest.error}`); } logger.info(`Connected to OPNsense ${connectionTest.version}`); // Initialize resources this.vlanResource = new VlanResource(this.client); this.firewallRuleResource = new FirewallRuleResource(this.client); this.natResource = new NATResource(this.client); this.dhcpResource = new DhcpLeaseResource(this.client); this.dnsBlocklistResource = new DnsBlocklistResource(this.client); this.haproxyResource = new HAProxyResource(this.client); this.arpResource = new ArpTableResource(this.client); this.systemSettingsResource = new SystemSettingsResource(this.client); this.interfaceConfigResource = new InterfaceConfigResource(this.client); this.routingDiagnosticsResource = new RoutingDiagnosticsResource(this.client); this.cliExecutor = new CLIExecutorResource(this.client); // Initialize SSH executor for direct CLI access this.sshExecutor = new SSHExecutor(); // Initialize backup manager if (process.env.BACKUP_ENABLED !== 'false') { this.backupManager = new BackupManager(this.client, process.env.BACKUP_PATH); } // Initialize cache manager (optional) if (process.env.ENABLE_CACHE === 'true') { try { this.cacheManager = new MCPCacheManager(this.client, { redisHost: process.env.REDIS_HOST, redisPort: parseInt(process.env.REDIS_PORT || '6379'), postgresHost: process.env.POSTGRES_HOST, postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresDb: process.env.POSTGRES_DB, postgresUser: process.env.POSTGRES_USER, postgresPassword: process.env.POSTGRES_PASSWORD, cacheTTL: parseInt(process.env.CACHE_TTL || '300'), enableCache: true }); logger.info('Cache manager initialized'); } catch (error) { logger.warn('Cache manager disabled:', error instanceof Error ? error.message : 'Unknown error'); } } // Initialize IaC components if enabled if (this.iacEnabled) { logger.info('Initializing IaC components...'); this.stateStore = new ResourceStateStore(); this.planner = new DeploymentPlanner(); this.engine = new ExecutionEngine(this.client); logger.info('IaC components initialized'); } return true; } catch (error) { logger.error('Failed to initialize OPNsense MCP server:', error instanceof Error ? error.message : 'Unknown error'); return false; } } private async ensureInitialized(): Promise<void> { if (!this.client) { // Try to initialize with environment variables const initialized = await this.initialize(); if (!initialized) { throw new McpError( ErrorCode.InternalError, 'OPNsense client not initialized. Use configure tool first.' ); } } } private setupHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ // Configuration tool { name: 'configure', description: 'Configure OPNsense connection', inputSchema: { type: 'object', properties: { host: { type: 'string', description: 'OPNsense hostname or IP' }, apiKey: { type: 'string', description: 'API key' }, apiSecret: { type: 'string', description: 'API secret' }, verifySsl: { type: 'boolean', description: 'Verify SSL certificate', default: true } }, required: ['host', 'apiKey', 'apiSecret'] } }, // VLAN Management Tools { name: 'list_vlans', description: 'List all VLANs', inputSchema: { type: 'object', properties: {} } }, { name: 'get_vlan', description: 'Get VLAN details', inputSchema: { type: 'object', properties: { tag: { type: 'string', description: 'VLAN tag number' } }, required: ['tag'] } }, { name: 'create_vlan', description: 'Create a new VLAN', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Physical interface (e.g., igc3)' }, tag: { type: 'string', description: 'VLAN tag (1-4094)' }, description: { type: 'string', description: 'VLAN description' }, pcp: { type: 'string', description: 'Priority Code Point (0-7)', default: '0' } }, required: ['interface', 'tag'] } }, { name: 'delete_vlan', description: 'Delete a VLAN', inputSchema: { type: 'object', properties: { tag: { type: 'string', description: 'VLAN tag to delete' } }, required: ['tag'] } }, { name: 'update_vlan', description: 'Update VLAN description', inputSchema: { type: 'object', properties: { tag: { type: 'string', description: 'VLAN tag' }, description: { type: 'string', description: 'New description' } }, required: ['tag', 'description'] } }, // Firewall Rule Management Tools { name: 'list_firewall_rules', description: 'List all firewall rules', inputSchema: { type: 'object', properties: {} } }, { name: 'get_firewall_rule', description: 'Get firewall rule details', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Firewall rule UUID' } }, required: ['uuid'] } }, { name: 'create_firewall_rule', description: 'Create a new firewall rule', inputSchema: { type: 'object', properties: { action: { type: 'string', description: 'Rule action (pass/block/reject)', enum: ['pass', 'block', 'reject'] }, interface: { type: 'string', description: 'Interface name' }, direction: { type: 'string', description: 'Traffic direction', enum: ['in', 'out'] }, protocol: { type: 'string', description: 'Protocol (any/tcp/udp/icmp)' }, source: { type: 'string', description: 'Source address/network or "any"' }, sourcePort: { type: 'string', description: 'Source port (for TCP/UDP)' }, destination: { type: 'string', description: 'Destination address/network or "any"' }, destinationPort: { type: 'string', description: 'Destination port (for TCP/UDP)' }, description: { type: 'string', description: 'Rule description' }, enabled: { type: 'boolean', description: 'Enable rule', default: true } }, required: ['action', 'interface', 'direction', 'protocol', 'source', 'destination'] } }, { name: 'create_firewall_preset', description: 'Create a firewall rule from a preset', inputSchema: { type: 'object', properties: { preset: { type: 'string', description: 'Preset name', enum: ['allow-web', 'allow-ssh', 'allow-minecraft', 'block-all'] }, interface: { type: 'string', description: 'Interface name' }, source: { type: 'string', description: 'Source override (optional)' }, destination: { type: 'string', description: 'Destination override (optional)' }, description: { type: 'string', description: 'Description override (optional)' } }, required: ['preset', 'interface'] } }, { name: 'update_firewall_rule', description: 'Update a firewall rule', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Firewall rule UUID' }, enabled: { type: 'boolean', description: 'Enable/disable rule' }, description: { type: 'string', description: 'New description' }, source: { type: 'string', description: 'New source' }, destination: { type: 'string', description: 'New destination' }, sourcePort: { type: 'string', description: 'New source port' }, destinationPort: { type: 'string', description: 'New destination port' } }, required: ['uuid'] } }, { name: 'delete_firewall_rule', description: 'Delete a firewall rule', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Firewall rule UUID' } }, required: ['uuid'] } }, { name: 'toggle_firewall_rule', description: 'Toggle firewall rule enabled/disabled', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Firewall rule UUID' } }, required: ['uuid'] } }, { name: 'find_firewall_rules', description: 'Find firewall rules by description', inputSchema: { type: 'object', properties: { description: { type: 'string', description: 'Description to search for' } }, required: ['description'] } }, // Backup Management Tools { name: 'create_backup', description: 'Create a configuration backup', inputSchema: { type: 'object', properties: { description: { type: 'string', description: 'Backup description' } } } }, { name: 'list_backups', description: 'List available backups', inputSchema: { type: 'object', properties: {} } }, { name: 'restore_backup', description: 'Restore a configuration backup', inputSchema: { type: 'object', properties: { backupId: { type: 'string', description: 'Backup ID to restore' } }, required: ['backupId'] } }, // Utility tools { name: 'test_connection', description: 'Test API connection and authentication', inputSchema: { type: 'object', properties: {} } }, { name: 'get_interfaces', description: 'List available network interfaces', inputSchema: { type: 'object', properties: {} } }, // DHCP Lease Management Tools { name: 'list_dhcp_leases', description: 'List all DHCP leases', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Filter by interface (optional)' } } } }, { name: 'find_device_by_name', description: 'Find devices by hostname pattern', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Hostname pattern to search (case-insensitive)' } }, required: ['pattern'] } }, { name: 'find_device_by_mac', description: 'Find device by MAC address', inputSchema: { type: 'object', properties: { mac: { type: 'string', description: 'MAC address (with or without colons)' } }, required: ['mac'] } }, { name: 'get_guest_devices', description: 'Get all devices on guest network (VLAN 4)', inputSchema: { type: 'object', properties: {} } }, { name: 'get_devices_by_interface', description: 'Group devices by network interface', inputSchema: { type: 'object', properties: {} } }, // ARP Table Management Tools { name: 'list_arp_entries', description: 'List all ARP table entries', inputSchema: { type: 'object', properties: {} } }, { name: 'find_arp_by_ip', description: 'Find ARP entries by IP address or subnet', inputSchema: { type: 'object', properties: { ipPattern: { type: 'string', description: 'IP address, prefix, or subnet (e.g., "10.0.6", "10.0.6.0/24")' } }, required: ['ipPattern'] } }, { name: 'find_arp_by_mac', description: 'Find ARP entries by MAC address', inputSchema: { type: 'object', properties: { macPattern: { type: 'string', description: 'MAC address or partial MAC (with or without colons)' } }, required: ['macPattern'] } }, { name: 'find_arp_by_interface', description: 'Find ARP entries on specific interface', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Interface name (e.g., "igc3_vlan6", "lan")' } }, required: ['interface'] } }, { name: 'find_arp_by_hostname', description: 'Find ARP entries by hostname pattern', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Hostname pattern to search' } }, required: ['pattern'] } }, { name: 'get_arp_stats', description: 'Get ARP table statistics', inputSchema: { type: 'object', properties: {} } }, { name: 'find_devices_on_vlan', description: 'Find devices on specific VLAN', inputSchema: { type: 'object', properties: { vlanTag: { type: 'string', description: 'VLAN tag number (e.g., "6" for DMZ)' } }, required: ['vlanTag'] } }, // DNS Blocklist Management Tools { name: 'list_dns_blocklist', description: 'List all DNS blocklist entries', inputSchema: { type: 'object', properties: {} } }, { name: 'block_domain', description: 'Add a domain to the DNS blocklist', inputSchema: { type: 'object', properties: { domain: { type: 'string', description: 'Domain to block (e.g., pornhub.com)' }, description: { type: 'string', description: 'Optional description for the block' } }, required: ['domain'] } }, { name: 'unblock_domain', description: 'Remove a domain from the DNS blocklist', inputSchema: { type: 'object', properties: { domain: { type: 'string', description: 'Domain to unblock' } }, required: ['domain'] } }, { name: 'block_multiple_domains', description: 'Block multiple domains at once', inputSchema: { type: 'object', properties: { domains: { type: 'array', items: { type: 'string' }, description: 'List of domains to block' }, description: { type: 'string', description: 'Optional description for the blocks' } }, required: ['domains'] } }, { name: 'apply_blocklist_category', description: 'Apply a predefined category of domain blocks', inputSchema: { type: 'object', properties: { category: { type: 'string', enum: ['adult', 'malware', 'ads', 'social'], description: 'Category of domains to block' } }, required: ['category'] } }, { name: 'search_dns_blocklist', description: 'Search DNS blocklist entries', inputSchema: { type: 'object', properties: { pattern: { type: 'string', description: 'Pattern to search for in domains or descriptions' } }, required: ['pattern'] } }, { name: 'toggle_blocklist_entry', description: 'Enable/disable a DNS blocklist entry', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'UUID of the blocklist entry' } }, required: ['uuid'] } }, // HAProxy Service Control { name: 'haproxy_service_control', description: 'Control HAProxy service (start, stop, restart, reload)', inputSchema: { type: 'object', properties: { action: { type: 'string', enum: ['start', 'stop', 'restart', 'reload', 'status'], description: 'Service action to perform' } }, required: ['action'] } }, // System Settings Management { name: 'system_get_settings', description: 'Get system-level firewall and routing settings', inputSchema: { type: 'object', properties: {} } }, { name: 'system_enable_intervlan_routing', description: 'Enable inter-VLAN routing at the system level', inputSchema: { type: 'object', properties: {} } }, { name: 'system_update_firewall_settings', description: 'Update system firewall settings', inputSchema: { type: 'object', properties: { blockprivatenetworks: { type: 'string', description: '0 to allow private networks, 1 to block' }, blockbogons: { type: 'string', description: '0 to allow bogons, 1 to block' }, allowinterlantraffic: { type: 'string', description: '1 to enable inter-LAN traffic' }, bypassstaticroutes: { type: 'string', description: '1 to bypass firewall for static routes' }, disablereplyto: { type: 'string', description: '1 to disable reply-to' } } } }, // Interface Configuration Management { name: 'interface_list_overview', description: 'List all network interfaces with their overview', inputSchema: { type: 'object', properties: {} } }, { name: 'interface_get_config', description: 'Get detailed configuration for a specific interface', inputSchema: { type: 'object', properties: { interfaceName: { type: 'string', description: 'Interface name (e.g., lan, wan, opt8)' } }, required: ['interfaceName'] } }, { name: 'interface_enable_intervlan_routing', description: 'Enable inter-VLAN routing on a specific interface', inputSchema: { type: 'object', properties: { interfaceName: { type: 'string', description: 'Interface name (e.g., opt8 for DMZ)' } }, required: ['interfaceName'] } }, { name: 'interface_enable_intervlan_all', description: 'Enable inter-VLAN routing on all interfaces', inputSchema: { type: 'object', properties: {} } }, { name: 'interface_configure_dmz', description: 'Configure DMZ interface for inter-VLAN routing', inputSchema: { type: 'object', properties: { dmzInterface: { type: 'string', description: 'DMZ interface name', default: 'opt8' } } } }, { name: 'interface_update_config', description: 'Update interface configuration', inputSchema: { type: 'object', properties: { interfaceName: { type: 'string', description: 'Interface name' }, blockpriv: { type: 'string', description: '0 to allow private networks, 1 to block' }, blockbogons: { type: 'string', description: '0 to allow bogons, 1 to block' }, enable: { type: 'string', description: '1 to enable interface, 0 to disable' }, disableftpproxy: { type: 'string', description: '1 to disable FTP proxy' } }, required: ['interfaceName'] } }, // Routing Diagnostics and Fixes { name: 'routing_diagnostics', description: 'Run comprehensive inter-VLAN routing diagnostics', inputSchema: { type: 'object', properties: { sourceNetwork: { type: 'string', description: 'Source network (e.g., 10.0.6.0/24 for DMZ)' }, destNetwork: { type: 'string', description: 'Destination network (e.g., 10.0.0.0/24 for LAN)' } } } }, { name: 'routing_fix_all', description: 'Automatically fix all detected inter-VLAN routing issues', inputSchema: { type: 'object', properties: {} } }, { name: 'routing_fix_dmz', description: 'Quick fix for DMZ to LAN routing (includes NFS rules)', inputSchema: { type: 'object', properties: {} } }, { name: 'routing_create_intervlan_rules', description: 'Create firewall rules for inter-VLAN routing', inputSchema: { type: 'object', properties: { sourceNetwork: { type: 'string', description: 'Source network (e.g., 10.0.6.0/24)' }, destNetwork: { type: 'string', description: 'Destination network (e.g., 10.0.0.0/24)' }, bidirectional: { type: 'boolean', description: 'Create rules in both directions', default: true } }, required: ['sourceNetwork', 'destNetwork'] } }, // CLI Execution Tools { name: 'cli_execute', description: 'Execute a CLI command on OPNsense for advanced configuration', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Command to execute (e.g., configctl, pfctl, netstat)' }, args: { type: 'array', items: { type: 'string' }, description: 'Command arguments' }, timeout: { type: 'number', description: 'Timeout in milliseconds', default: 30000 } }, required: ['command'] } }, { name: 'cli_fix_interface_blocking', description: 'Fix interface blocking settings via CLI (for DMZ routing issues)', inputSchema: { type: 'object', properties: { interfaceName: { type: 'string', description: 'Interface name (e.g., opt8 for DMZ)' } }, required: ['interfaceName'] } }, { name: 'cli_reload_firewall', description: 'Reload firewall rules via CLI', inputSchema: { type: 'object', properties: {} } }, { name: 'cli_show_routing', description: 'Show routing table via CLI', inputSchema: { type: 'object', properties: {} } }, { name: 'cli_fix_dmz_routing', description: 'Comprehensive DMZ routing fix via CLI', inputSchema: { type: 'object', properties: {} } }, { name: 'cli_check_nfs', description: 'Check NFS connectivity from DMZ', inputSchema: { type: 'object', properties: { truenasIP: { type: 'string', description: 'TrueNAS IP address', default: '10.0.0.14' } } } }, { name: 'cli_apply_changes', description: 'Apply all configuration changes via CLI', inputSchema: { type: 'object', properties: {} } }, // HAProxy Backend Management { name: 'haproxy_backend_create', description: 'Create a new HAProxy backend', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Backend name' }, mode: { type: 'string', enum: ['http', 'tcp'], description: 'Backend mode' }, balance: { type: 'string', enum: ['roundrobin', 'leastconn', 'source', 'uri', 'hdr', 'random'], description: 'Load balancing algorithm' }, servers: { type: 'array', description: 'List of backend servers', items: { type: 'object', properties: { name: { type: 'string' }, address: { type: 'string' }, port: { type: 'number' }, ssl: { type: 'boolean' }, verify: { type: 'string', enum: ['none', 'required'] } }, required: ['name', 'address', 'port'] } }, description: { type: 'string' } }, required: ['name', 'mode', 'balance'] } }, { name: 'haproxy_backend_list', description: 'List all HAProxy backends', inputSchema: { type: 'object', properties: {} } }, { name: 'haproxy_backend_delete', description: 'Delete an HAProxy backend', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Backend UUID' } }, required: ['uuid'] } }, // HAProxy Frontend Management { name: 'haproxy_frontend_create', description: 'Create a new HAProxy frontend', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Frontend name' }, bind: { type: 'string', description: 'Bind address (e.g., 0.0.0.0:443)' }, ssl: { type: 'boolean', description: 'Enable SSL' }, certificates: { type: 'array', items: { type: 'string' }, description: 'Certificate UUIDs or names' }, mode: { type: 'string', enum: ['http', 'tcp'], description: 'Frontend mode' }, backend: { type: 'string', description: 'Default backend name' }, acls: { type: 'array', description: 'Access control lists', items: { type: 'object', properties: { name: { type: 'string' }, expression: { type: 'string' } }, required: ['name', 'expression'] } }, description: { type: 'string' } }, required: ['name', 'bind', 'mode', 'backend'] } }, { name: 'haproxy_frontend_list', description: 'List all HAProxy frontends', inputSchema: { type: 'object', properties: {} } }, { name: 'haproxy_frontend_delete', description: 'Delete an HAProxy frontend', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Frontend UUID' } }, required: ['uuid'] } }, // HAProxy Certificate Management { name: 'haproxy_certificate_list', description: 'List available certificates for HAProxy', inputSchema: { type: 'object', properties: {} } }, { name: 'haproxy_certificate_create', description: 'Create a certificate for HAProxy', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Certificate name' }, type: { type: 'string', enum: ['selfsigned', 'import', 'acme'], description: 'Certificate type' }, cn: { type: 'string', description: 'Common name (for self-signed)' }, san: { type: 'array', items: { type: 'string' }, description: 'Subject alternative names' }, certificate: { type: 'string', description: 'Certificate content (for import)' }, key: { type: 'string', description: 'Private key (for import)' }, ca: { type: 'string', description: 'CA certificate (for import)' } }, required: ['name', 'type'] } }, // HAProxy ACL Management { name: 'haproxy_acl_create', description: 'Create an ACL for HAProxy frontend', inputSchema: { type: 'object', properties: { frontend: { type: 'string', description: 'Frontend UUID' }, name: { type: 'string', description: 'ACL name' }, expression: { type: 'string', description: 'ACL expression' } }, required: ['frontend', 'name', 'expression'] } }, // HAProxy Action Management { name: 'haproxy_action_create', description: 'Create an action for HAProxy frontend', inputSchema: { type: 'object', properties: { frontend: { type: 'string', description: 'Frontend UUID' }, type: { type: 'string', enum: ['use_backend', 'redirect', 'add_header', 'set_header', 'del_header'], description: 'Action type' }, backend: { type: 'string', description: 'Backend name (for use_backend)' }, condition: { type: 'string', description: 'ACL condition' }, value: { type: 'string', description: 'Action value' } }, required: ['frontend', 'type'] } }, // HAProxy Stats { name: 'haproxy_stats', description: 'Get HAProxy statistics', inputSchema: { type: 'object', properties: {} } }, { name: 'haproxy_backend_health', description: 'Get health status of a specific backend', inputSchema: { type: 'object', properties: { backend: { type: 'string', description: 'Backend name' } }, required: ['backend'] } }, // Macro Recording Tools { name: 'macro_start_recording', description: 'Start recording API calls to create a macro', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Macro name' }, description: { type: 'string', description: 'Macro description' } }, required: ['name', 'description'] } }, { name: 'macro_stop_recording', description: 'Stop recording and save the macro', inputSchema: { type: 'object', properties: {} } }, { name: 'macro_list', description: 'List all saved macros', inputSchema: { type: 'object', properties: {} } }, { name: 'macro_play', description: 'Play a saved macro', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Macro ID' }, parameters: { type: 'object', description: 'Parameters to substitute in the macro', additionalProperties: true }, dryRun: { type: 'boolean', description: 'Execute in dry-run mode without making actual API calls', default: false } }, required: ['id'] } }, { name: 'macro_delete', description: 'Delete a saved macro', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Macro ID' } }, required: ['id'] } }, { name: 'macro_analyze', description: 'Analyze a macro to detect patterns and parameters', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Macro ID' } }, required: ['id'] } }, { name: 'macro_generate_tool', description: 'Generate an MCP tool definition from a macro', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Macro ID' }, save: { type: 'boolean', description: 'Save the generated tool to a file', default: false } }, required: ['id'] } }, { name: 'macro_export', description: 'Export all macros to a file', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Export file path' } }, required: ['path'] } }, { name: 'macro_import', description: 'Import macros from a file', inputSchema: { type: 'object', properties: { path: { type: 'string', description: 'Import file path' }, overwrite: { type: 'boolean', description: 'Overwrite existing macros', default: false } }, required: ['path'] } }, // IaC Tools (only if enabled) ...(this.iacEnabled ? [ { name: 'iac_plan_deployment', description: 'Plan infrastructure deployment changes', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Deployment name' }, resources: { type: 'array', description: 'Resources to deploy', items: { type: 'object', properties: { type: { type: 'string' }, id: { type: 'string' }, name: { type: 'string' }, properties: { type: 'object' } }, required: ['type', 'id', 'name', 'properties'] } }, dryRun: { type: 'boolean', default: true, description: 'Preview changes without applying' } }, required: ['name', 'resources'] } }, { name: 'iac_apply_deployment', description: 'Apply a deployment plan', inputSchema: { type: 'object', properties: { planId: { type: 'string', description: 'Plan ID from plan_deployment' }, autoApprove: { type: 'boolean', default: false, description: 'Skip confirmation' } }, required: ['planId'] } }, { name: 'iac_destroy_deployment', description: 'Destroy deployed resources', inputSchema: { type: 'object', properties: { deploymentId: { type: 'string', description: 'Deployment to destroy' }, force: { type: 'boolean', default: false, description: 'Force destruction' } }, required: ['deploymentId'] } }, { name: 'iac_list_resource_types', description: 'List available resource types', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'Filter by category (network, firewall, services)' } } } } ] : []), // SSH Execution Tools { name: 'ssh_execute', description: 'Execute arbitrary command via SSH on OPNsense (full CLI access)', inputSchema: { type: 'object', properties: { command: { type: 'string', description: 'Command to execute via SSH' }, timeout: { type: 'number', description: 'Timeout in milliseconds', default: 30000 }, sudo: { type: 'boolean', description: 'Execute command with sudo', default: false } }, required: ['command'] } }, { name: 'ssh_fix_interface_blocking', description: 'Fix interface blocking settings via SSH (resolves DMZ routing issues)', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Interface name (e.g., opt8 for DMZ)' } }, required: ['interface'] } }, { name: 'ssh_fix_dmz_routing', description: 'Apply comprehensive DMZ routing fix via SSH', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_enable_intervlan_routing', description: 'Enable inter-VLAN routing via SSH', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_reload_firewall', description: 'Reload firewall rules via SSH', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_show_routing', description: 'Show routing table via SSH', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_show_pf_rules', description: 'Show packet filter rules via SSH', inputSchema: { type: 'object', properties: { verbose: { type: 'boolean', description: 'Show verbose output', default: false } } } }, { name: 'ssh_backup_config', description: 'Backup OPNsense configuration via SSH', inputSchema: { type: 'object', properties: { backupName: { type: 'string', description: 'Optional backup filename' } } } }, { name: 'ssh_restore_config', description: 'Restore OPNsense configuration via SSH', inputSchema: { type: 'object', properties: { backupPath: { type: 'string', description: 'Path to backup file to restore' } }, required: ['backupPath'] } }, { name: 'ssh_check_nfs_connectivity', description: 'Check NFS connectivity from OPNsense', inputSchema: { type: 'object', properties: { targetIP: { type: 'string', description: 'Target NFS server IP', default: '10.0.0.14' } } } }, { name: 'ssh_system_status', description: 'Get comprehensive system status via SSH', inputSchema: { type: 'object', properties: {} } }, // NAT Management Tools { name: 'nat_list_outbound', description: 'List all outbound NAT rules', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_list_port_forwards', description: 'List all port forward rules', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_get_mode', description: 'Get current NAT mode (automatic, hybrid, manual, disabled)', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_set_mode', description: 'Set NAT mode (automatic, hybrid, manual, disabled)', inputSchema: { type: 'object', properties: { mode: { type: 'string', enum: ['automatic', 'hybrid', 'manual', 'disabled'], description: 'NAT mode to set' } }, required: ['mode'] } }, { name: 'nat_create_outbound_rule', description: 'Create an outbound NAT rule', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Outbound interface (e.g., wan)' }, source_net: { type: 'string', description: 'Source network (e.g., 10.0.6.0/24)' }, destination_net: { type: 'string', description: 'Destination network (e.g., any)' }, target: { type: 'string', description: 'NAT target IP (empty for no NAT)' }, nonat: { type: 'string', description: 'Set to "1" for no NAT (exception rule)' }, description: { type: 'string', description: 'Rule description' }, enabled: { type: 'string', description: '1 to enable, 0 to disable', default: '1' } }, required: ['interface', 'source_net', 'destination_net'] } }, { name: 'nat_delete_outbound_rule', description: 'Delete an outbound NAT rule by description (SSH mode) or UUID (API mode)', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'NAT rule UUID (for API mode)' }, description: { type: 'string', description: 'NAT rule description (for SSH mode)' } } } }, { name: 'nat_create_port_forward', description: 'Create a port forward rule', inputSchema: { type: 'object', properties: { interface: { type: 'string', description: 'Inbound interface (e.g., wan)' }, protocol: { type: 'string', enum: ['tcp', 'udp', 'tcp/udp'], description: 'Protocol' }, destination_port: { type: 'string', description: 'External port(s)' }, target: { type: 'string', description: 'Internal target IP' }, local_port: { type: 'string', description: 'Internal port(s)' }, description: { type: 'string', description: 'Rule description' }, enabled: { type: 'string', description: '1 to enable, 0 to disable', default: '1' } }, required: ['interface', 'protocol', 'destination_port', 'target'] } }, { name: 'nat_delete_port_forward', description: 'Delete a port forward rule', inputSchema: { type: 'object', properties: { uuid: { type: 'string', description: 'Port forward UUID to delete' } }, required: ['uuid'] } }, { name: 'nat_fix_dmz', description: 'Fix DMZ NAT issue - adds no-NAT rules for inter-VLAN traffic', inputSchema: { type: 'object', properties: { dmzNetwork: { type: 'string', description: 'DMZ network', default: '10.0.6.0/24' }, lanNetwork: { type: 'string', description: 'LAN network', default: '10.0.0.0/24' }, otherInternalNetworks: { type: 'array', items: { type: 'string' }, description: 'Other internal networks to exclude from NAT', default: ['10.0.2.0/24', '10.0.4.0/24'] } } } }, { name: 'nat_quick_fix_dmz', description: 'Quick fix for DMZ NAT issue with minimal configuration', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_cleanup_dmz_fix', description: 'Remove all MCP-created NAT fix rules', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_analyze_config', description: 'Analyze NAT configuration for issues', inputSchema: { type: 'object', properties: {} } }, { name: 'nat_apply_changes', description: 'Apply NAT configuration changes', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_test_vlan_connectivity', description: 'Test connectivity between VLANs', inputSchema: { type: 'object', properties: { sourceInterface: { type: 'string', description: 'Source interface name' }, targetIP: { type: 'string', description: 'Target IP to test connectivity to' } }, required: ['sourceInterface', 'targetIP'] } }, { name: 'ssh_quick_dmz_fix', description: 'Apply quick DMZ fix (streamlined version)', inputSchema: { type: 'object', properties: {} } }, { name: 'ssh_batch_execute', description: 'Execute multiple commands in sequence via SSH', inputSchema: { type: 'object', properties: { commands: { type: 'array', items: { type: 'string' }, description: 'Array of commands to execute' }, stopOnError: { type: 'boolean', description: 'Stop execution if a command fails', default: false } }, required: ['commands'] } } ] })); // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'opnsense://vlans', name: 'VLANs', description: 'List of all configured VLANs', mimeType: 'application/json' }, { uri: 'opnsense://firewall/rules', name: 'Firewall Rules', description: 'List of all firewall rules', mimeType: 'application/json' }, { uri: 'opnsense://interfaces', name: 'Network Interfaces', description: 'Available network interfaces', mimeType: 'application/json' }, { uri: 'opnsense://status', name: 'Connection Status', description: 'OPNsense connection status', mimeType: 'application/json' }, { uri: 'opnsense://dhcp/leases', name: 'DHCP Leases', description: 'Current DHCP leases', mimeType: 'application/json' }, { uri: 'opnsense://dns/blocklist', name: 'DNS Blocklist', description: 'DNS blocklist entries', mimeType: 'application/json' }, { uri: 'opnsense://haproxy/backends', name: 'HAProxy Backends', description: 'HAProxy backend configurations', mimeType: 'application/json' }, { uri: 'opnsense://haproxy/frontends', name: 'HAProxy Frontends', description: 'HAProxy frontend configurations', mimeType: 'application/json' }, { uri: 'opnsense://haproxy/stats', name: 'HAProxy Statistics', description: 'HAProxy statistics and health status', mimeType: 'application/json' }, { uri: 'opnsense://macros', name: 'Recorded Macros', description: 'List of recorded API macros', mimeType: 'application/json' }, { uri: 'opnsense://arp', name: 'ARP Table', description: 'ARP table entries showing IP to MAC mappings', mimeType: 'application/json' }, // IaC Resources (only if enabled) ...(this.iacEnabled ? [ { uri: 'opnsense://iac/resources', name: 'IaC Resource Types', description: 'Available infrastructure resource types', mimeType: 'application/json' }, { uri: 'opnsense://iac/deployments', name: 'Deployments', description: 'Current infrastructure deployments', mimeType: 'application/json' }, { uri: 'opnsense://iac/state', name: 'Resource State', description: 'Current state of managed resources', mimeType: 'application/json' } ] : []) ] })); // Read resource this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { await this.ensureInitialized(); const uri = request.params.uri; // Handle IaC resources if (uri.startsWith('opnsense://iac/')) { if (!this.iacEnabled) { throw new McpError( ErrorCode.InvalidRequest, 'IaC features are disabled. Set IAC_ENABLED=true to enable.' ); } return this.handleIaCResourceRead(uri); } switch (uri) { case 'opnsense://vlans': { const vlans = await this.vlanResource!.list(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(vlans, null, 2) }] }; } case 'opnsense://firewall/rules': { const rules = await this.firewallRuleResource!.list(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(rules, null, 2) }] }; } case 'opnsense://interfaces': { const interfaces = await this.vlanResource!.getAvailableInterfaces(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(interfaces, null, 2) }] }; } case 'opnsense://status': { const status = await this.client!.testConnection(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(status, null, 2) }] }; } case 'opnsense://dhcp/leases': { if (!this.dhcpResource) { throw new McpError( ErrorCode.InternalError, 'DHCP resource not initialized' ); } const leases = await this.dhcpResource!.listLeases(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(leases, null, 2) }] }; } case 'opnsense://dns/blocklist': { if (!this.dnsBlocklistResource) { throw new McpError( ErrorCode.InternalError, 'DNS blocklist resource not initialized' ); } const blocklist = await this.dnsBlocklistResource!.list(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(blocklist, null, 2) }] }; } case 'opnsense://haproxy/backends': { if (!this.haproxyResource) { throw new McpError( ErrorCode.InternalError, 'HAProxy resource not initialized' ); } const backends = await this.haproxyResource!.listBackends(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(backends, null, 2) }] }; } case 'opnsense://haproxy/frontends': { if (!this.haproxyResource) { throw new McpError( ErrorCode.InternalError, 'HAProxy resource not initialized' ); } const frontends = await this.haproxyResource!.listFrontends(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(frontends, null, 2) }] }; } case 'opnsense://haproxy/stats': { if (!this.haproxyResource) { throw new McpError( ErrorCode.InternalError, 'HAProxy resource not initialized' ); } const stats = await this.haproxyResource!.getStats(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(stats, null, 2) }] }; } case 'opnsense://macros': { if (!this.macroRecorder) { throw new McpError( ErrorCode.InternalError, 'Macro recorder not initialized' ); } const macros = await this.macroRecorder!.listMacros(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(macros, null, 2) }] }; } case 'opnsense://arp': { if (!this.arpResource) { throw new McpError( ErrorCode.InternalError, 'ARP resource not initialized' ); } const arpEntries = await this.arpResource!.list(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(arpEntries, null, 2) }] }; } default: throw new McpError( ErrorCode.InvalidRequest, `Unknown resource: ${uri}` ); } }); // Handle tool calls this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; // Route IaC tools if (name.startsWith('iac_')) { if (!this.iacEnabled) { throw new McpError( ErrorCode.InvalidRequest, 'IaC features are disabled. Set IAC_ENABLED=true to enable.' ); } return this.handleIaCTool(name, args); } switch (name) { case 'configure': { if (!args) { throw new McpError( ErrorCode.InvalidRequest, 'Configuration parameters are required' ); } try { const config = ConfigSchema.parse(args); this.client = new OPNSenseAPIClient(config); const test = await this.client.testConnection(); if (test.success) { this.vlanResource = new VlanResource(this.client); this.firewallRuleResource = new FirewallRuleResource(this.client); this.natResource = new NATResource(this.client); this.dhcpResource = new DhcpLeaseResource(this.client); this.dnsBlocklistResource = new DnsBlocklistResource(this.client); this.haproxyResource = new HAProxyResource(this.client); this.arpResource = new ArpTableResource(this.client); // Initialize macro recorder this.macroRecorder = new MacroRecorder(this.client, process.env.MACRO_STORAGE_PATH); this.client.setRecorder((call) => this.macroRecorder?.recordAPICall(call)); // Optional backup manager if (process.env.BACKUP_ENABLED !== 'false') { this.backupManager = new BackupManager(this.client, process.env.BACKUP_PATH); } // Optional cache manager if (process.env.ENABLE_CACHE === 'true') { try { this.cacheManager = new MCPCacheManager(this.client, { redisHost: process.env.REDIS_HOST, redisPort: parseInt(process.env.REDIS_PORT || '6379'), postgresHost: process.env.POSTGRES_HOST, postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), postgresDb: process.env.POSTGRES_DB, postgresUser: process.env.POSTGRES_USER, postgresPassword: process.env.POSTGRES_PASSWORD, cacheTTL: parseInt(process.env.CACHE_TTL || '300'), enableCache: true }); } catch (error) { logger.debug('Cache not available'); } } // Re-initialize IaC components if enabled if (this.iacEnabled) { this.stateStore = new ResourceStateStore(); this.planner = new DeploymentPlanner(); this.engine = new ExecutionEngine(this.client); } return { content: [{ type: 'text', text: `Successfully connected to OPNsense ${test.version}` }] }; } else { throw new Error(test.error); } } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, `Configuration failed: ${error.message}` ); } } case 'test_connection': { await this.ensureInitialized(); const status = await this.client!.testConnection(); return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }; } case 'list_vlans': { await this.ensureInitialized(); const vlans = await this.vlanResource!.list(); return { content: [{ type: 'text', text: JSON.stringify(vlans, null, 2) }] }; } case 'get_vlan': { await this.ensureInitialized(); if (!args || !args.tag) { throw new McpError( ErrorCode.InvalidRequest, 'tag parameter is required' ); } const vlan = await this.vlanResource!.findByTag(args.tag as string); if (!vlan) { throw new McpError( ErrorCode.InvalidRequest, `VLAN ${args.tag} not found` ); } return { content: [{ type: 'text', text: JSON.stringify(vlan, null, 2) }] }; } case 'create_vlan': { await this.ensureInitialized(); if (!args || !args.interface || !args.tag) { throw new McpError( ErrorCode.InvalidRequest, 'interface and tag parameters are required' ); } try { const result = await this.vlanResource!.create({ if: args.interface as string, tag: args.tag as string, descr: args.description as string || '', pcp: args.pcp as string || '0' }); return { content: [{ type: 'text', text: `Successfully created VLAN ${args.tag} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'delete_vlan': { await this.ensureInitialized(); if (!args || !args.tag) { throw new McpError( ErrorCode.InvalidRequest, 'tag parameter is required' ); } try { await this.vlanResource!.deleteByTag(args.tag as string); return { content: [{ type: 'text', text: `Successfully deleted VLAN ${args.tag}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'update_vlan': { await this.ensureInitialized(); if (!args || !args.tag || !args.description) { throw new McpError( ErrorCode.InvalidRequest, 'tag and description parameters are required' ); } try { const vlan = await this.vlanResource!.findByTag(args.tag as string); if (!vlan || !vlan.uuid) { throw new Error(`VLAN ${args.tag} not found`); } await this.vlanResource!.update(vlan.uuid, { descr: args.description as string }); return { content: [{ type: 'text', text: `Successfully updated VLAN ${args.tag}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'get_interfaces': { await this.ensureInitialized(); const interfaces = await this.vlanResource!.getAvailableInterfaces(); return { content: [{ type: 'text', text: JSON.stringify(interfaces, null, 2) }] }; } // Firewall Rule Management case 'list_firewall_rules': { await this.ensureInitialized(); const rules = await this.firewallRuleResource!.list(); return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }; } case 'get_firewall_rule': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } const rule = await this.firewallRuleResource!.get(args.uuid as string); if (!rule) { throw new McpError( ErrorCode.InvalidRequest, `Firewall rule ${args.uuid} not found` ); } return { content: [{ type: 'text', text: JSON.stringify(rule, null, 2) }] }; } case 'create_firewall_rule': { await this.ensureInitialized(); if (!args || !args.action || !args.interface || !args.direction || !args.protocol || !args.source || !args.destination) { throw new McpError( ErrorCode.InvalidRequest, 'action, interface, direction, protocol, source, and destination are required' ); } try { const result = await this.firewallRuleResource!.create({ enabled: args.enabled !== false ? '1' : '0', action: args.action as string, interface: args.interface as string, direction: args.direction as string, ipprotocol: 'inet', // Default to IPv4 protocol: args.protocol as string, source_net: args.source as string, source_port: args.sourcePort as string || '', destination_net: args.destination as string, destination_port: args.destinationPort as string || '', description: args.description as string || '' }); return { content: [{ type: 'text', text: `Successfully created firewall rule with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'create_firewall_preset': { await this.ensureInitialized(); if (!args || !args.preset || !args.interface) { throw new McpError( ErrorCode.InvalidRequest, 'preset and interface parameters are required' ); } try { const presetRule = this.firewallRuleResource!.createPreset( args.preset as string, { source: args.source, destination: args.destination, description: args.description } ); const result = await this.firewallRuleResource!.create({ ...presetRule, interface: args.interface as string } as any); return { content: [{ type: 'text', text: `Successfully created ${args.preset} firewall rule with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'update_firewall_rule': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { const updates: any = {}; if (args.enabled !== undefined) { updates.enabled = args.enabled ? '1' : '0'; } if (args.description !== undefined) { updates.description = args.description; } if (args.source !== undefined) { updates.source_net = args.source; } if (args.destination !== undefined) { updates.destination_net = args.destination; } if (args.sourcePort !== undefined) { updates.source_port = args.sourcePort; } if (args.destinationPort !== undefined) { updates.destination_port = args.destinationPort; } await this.firewallRuleResource!.update(args.uuid as string, updates); return { content: [{ type: 'text', text: `Successfully updated firewall rule ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'delete_firewall_rule': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.firewallRuleResource!.delete(args.uuid as string); return { content: [{ type: 'text', text: `Successfully deleted firewall rule ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'toggle_firewall_rule': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.firewallRuleResource!.toggle(args.uuid as string); return { content: [{ type: 'text', text: `Successfully toggled firewall rule ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'find_firewall_rules': { await this.ensureInitialized(); if (!args || !args.description) { throw new McpError( ErrorCode.InvalidRequest, 'description parameter is required' ); } try { const rules = await this.firewallRuleResource!.findByDescription(args.description as string); return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // Backup Management case 'create_backup': { await this.ensureInitialized(); if (!this.backupManager) { throw new McpError( ErrorCode.InternalError, 'Backup manager not enabled.' ); } try { const backup = await this.backupManager.createBackup({ description: args?.description as string || 'Manual backup from Claude' }); return { content: [{ type: 'text', text: `Backup created successfully:\nID: ${backup.id}\nDescription: ${backup.description}\nTimestamp: ${backup.timestamp}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'list_backups': { await this.ensureInitialized(); if (!this.backupManager) { throw new McpError( ErrorCode.InternalError, 'Backup manager not enabled.' ); } const backups = await this.backupManager.listBackups(); return { content: [{ type: 'text', text: JSON.stringify(backups, null, 2) }] }; } case 'restore_backup': { await this.ensureInitialized(); if (!this.backupManager) { throw new McpError( ErrorCode.InternalError, 'Backup manager not enabled.' ); } if (!args || !args.backupId) { throw new McpError( ErrorCode.InvalidRequest, 'backupId parameter is required' ); } try { const success = await this.backupManager.restoreBackup(args.backupId as string); return { content: [{ type: 'text', text: success ? `Backup ${args.backupId} restored successfully` : `Failed to restore backup ${args.backupId}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // DHCP Lease Management case 'list_dhcp_leases': { await this.ensureInitialized(); try { const leases = await this.dhcpResource!.listLeases(); // Filter by interface if specified const filtered = args?.interface ? leases.filter(l => l.if === args.interface) : leases; // Format for display const formatted = filtered.map(lease => ({ hostname: lease.hostname || 'Unknown Device', ip: lease.address, mac: lease.hwaddr, interface: lease.if, state: lease.state, starts: lease.starts, ends: lease.ends, deviceInfo: this.dhcpResource!.getDeviceInfo(lease) })); return { content: [{ type: 'text', text: JSON.stringify(formatted, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list DHCP leases: ${error.message}` ); } } case 'find_device_by_name': { await this.ensureInitialized(); if (!args || !args.pattern) { throw new McpError( ErrorCode.InvalidRequest, 'pattern parameter is required' ); } try { const results = await this.dhcpResource!.findByHostname(args.pattern as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No devices found matching pattern "${args.pattern}"` }] }; } const formatted = results.map(lease => this.dhcpResource!.formatLease(lease) ); return { content: [{ type: 'text', text: `Found ${results.length} device(s):\n\n${formatted.join('\n')}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search devices: ${error.message}` ); } } case 'find_device_by_mac': { await this.ensureInitialized(); if (!args || !args.mac) { throw new McpError( ErrorCode.InvalidRequest, 'mac parameter is required' ); } try { const results = await this.dhcpResource!.findByMac(args.mac as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No device found with MAC address "${args.mac}"` }] }; } const formatted = results.map(lease => this.dhcpResource!.formatLease(lease) ); return { content: [{ type: 'text', text: formatted.join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search by MAC: ${error.message}` ); } } case 'get_guest_devices': { await this.ensureInitialized(); try { const guestDevices = await this.dhcpResource!.getGuestLeases(); if (guestDevices.length === 0) { return { content: [{ type: 'text', text: 'No devices currently connected to guest network' }] }; } const formatted = guestDevices.map(lease => { const info = this.dhcpResource!.getDeviceInfo(lease); return `• ${info}`; }); return { content: [{ type: 'text', text: `${guestDevices.length} device(s) on guest network:\n\n${formatted.join('\n')}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get guest devices: ${error.message}` ); } } case 'get_devices_by_interface': { await this.ensureInitialized(); try { const byInterface = await this.dhcpResource!.getLeasesByInterface(); let output = 'Devices by Interface:\n\n'; for (const [iface, devices] of byInterface) { output += `${iface}: ${devices.length} device(s)\n`; // Show first few devices per interface const preview = devices.slice(0, 3); for (const device of preview) { const hostname = device.hostname || 'Unknown'; output += ` • ${hostname} (${device.address})\n`; } if (devices.length > 3) { output += ` ... and ${devices.length - 3} more\n`; } output += '\n'; } return { content: [{ type: 'text', text: output }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to group devices: ${error.message}` ); } } // ARP Table Management case 'list_arp_entries': { await this.ensureInitialized(); try { const entries = await this.arpResource!.list(); return { content: [{ type: 'text', text: JSON.stringify(entries, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list ARP entries: ${error.message}` ); } } case 'find_arp_by_ip': { await this.ensureInitialized(); if (!args || !args.ipPattern) { throw new McpError( ErrorCode.InvalidRequest, 'ipPattern parameter is required' ); } try { const results = await this.arpResource!.findByIp(args.ipPattern as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No ARP entries found for IP pattern "${args.ipPattern}"` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} ARP entries:\n\n` + results.map(entry => this.arpResource!.formatEntry(entry)).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search ARP by IP: ${error.message}` ); } } case 'find_arp_by_mac': { await this.ensureInitialized(); if (!args || !args.macPattern) { throw new McpError( ErrorCode.InvalidRequest, 'macPattern parameter is required' ); } try { const results = await this.arpResource!.findByMac(args.macPattern as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No ARP entries found for MAC pattern "${args.macPattern}"` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} ARP entries:\n\n` + results.map(entry => this.arpResource!.formatEntry(entry)).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search ARP by MAC: ${error.message}` ); } } case 'find_arp_by_interface': { await this.ensureInitialized(); if (!args || !args.interface) { throw new McpError( ErrorCode.InvalidRequest, 'interface parameter is required' ); } try { const results = await this.arpResource!.findByInterface(args.interface as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No ARP entries found on interface "${args.interface}"` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} ARP entries on ${args.interface}:\n\n` + results.map(entry => this.arpResource!.formatEntry(entry)).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search ARP by interface: ${error.message}` ); } } case 'find_arp_by_hostname': { await this.ensureInitialized(); if (!args || !args.pattern) { throw new McpError( ErrorCode.InvalidRequest, 'pattern parameter is required' ); } try { const results = await this.arpResource!.findByHostname(args.pattern as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No ARP entries found with hostname pattern "${args.pattern}"` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} ARP entries:\n\n` + results.map(entry => this.arpResource!.formatEntry(entry)).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search ARP by hostname: ${error.message}` ); } } case 'get_arp_stats': { await this.ensureInitialized(); try { const stats = await this.arpResource!.getStats(); return { content: [{ type: 'text', text: `ARP Table Statistics:\n\n` + `Total entries: ${stats.totalEntries}\n` + `Dynamic entries: ${stats.dynamicEntries}\n` + `Static entries: ${stats.staticEntries}\n` + `Interfaces: ${stats.interfaces.join(', ')}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get ARP stats: ${error.message}` ); } } case 'find_devices_on_vlan': { await this.ensureInitialized(); if (!args || !args.vlanTag) { throw new McpError( ErrorCode.InvalidRequest, 'vlanTag parameter is required' ); } try { const results = await this.arpResource!.findOnVlan(args.vlanTag as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No devices found on VLAN ${args.vlanTag}` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} devices on VLAN ${args.vlanTag}:\n\n` + results.map(entry => this.arpResource!.formatEntry(entry)).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to find devices on VLAN: ${error.message}` ); } } // DNS Blocklist Management case 'list_dns_blocklist': { await this.ensureInitialized(); try { const blocklist = await this.dnsBlocklistResource!.list(); return { content: [{ type: 'text', text: JSON.stringify(blocklist, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list DNS blocklist: ${error.message}` ); } } case 'block_domain': { await this.ensureInitialized(); if (!args || !args.domain) { throw new McpError( ErrorCode.InvalidRequest, 'domain parameter is required' ); } try { const result = await this.dnsBlocklistResource!.blockDomain( args.domain as string, args.description as string ); return { content: [{ type: 'text', text: `Successfully blocked domain ${args.domain} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'unblock_domain': { await this.ensureInitialized(); if (!args || !args.domain) { throw new McpError( ErrorCode.InvalidRequest, 'domain parameter is required' ); } try { await this.dnsBlocklistResource!.unblockDomain(args.domain as string); return { content: [{ type: 'text', text: `Successfully unblocked domain ${args.domain}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'block_multiple_domains': { await this.ensureInitialized(); if (!args || !args.domains || !Array.isArray(args.domains)) { throw new McpError( ErrorCode.InvalidRequest, 'domains array parameter is required' ); } try { const result = await this.dnsBlocklistResource!.blockMultipleDomains( args.domains as string[], args.description as string ); return { content: [{ type: 'text', text: `Blocked ${result.blocked.length} domains successfully.\n` + (result.failed.length > 0 ? `Failed to block: ${result.failed.join(', ')}` : '') }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'apply_blocklist_category': { await this.ensureInitialized(); if (!args || !args.category) { throw new McpError( ErrorCode.InvalidRequest, 'category parameter is required' ); } try { const result = await this.dnsBlocklistResource!.applyBlocklistCategory( args.category as 'adult' | 'malware' | 'ads' | 'social' ); return { content: [{ type: 'text', text: `Applied ${args.category} blocklist category.\n` + `Blocked ${result.blocked.length} domains successfully.\n` + (result.failed.length > 0 ? `Failed to block: ${result.failed.join(', ')}` : '') }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'search_dns_blocklist': { await this.ensureInitialized(); if (!args || !args.pattern) { throw new McpError( ErrorCode.InvalidRequest, 'pattern parameter is required' ); } try { const results = await this.dnsBlocklistResource!.searchBlocklist(args.pattern as string); if (results.length === 0) { return { content: [{ type: 'text', text: `No blocklist entries found matching pattern "${args.pattern}"` }] }; } return { content: [{ type: 'text', text: `Found ${results.length} blocklist entries:\n\n` + results.map(entry => `• ${entry.host} ${entry.enabled ? '(enabled)' : '(disabled)'}` + (entry.description ? ` - ${entry.description}` : '') ).join('\n') }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to search blocklist: ${error.message}` ); } } case 'toggle_blocklist_entry': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.dnsBlocklistResource!.toggleBlocklistEntry(args.uuid as string); return { content: [{ type: 'text', text: `Successfully toggled blocklist entry ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy Service Control case 'haproxy_service_control': { await this.ensureInitialized(); if (!args || !args.action) { throw new McpError( ErrorCode.InvalidRequest, 'action parameter is required' ); } try { if (args.action === 'status') { const status = await this.haproxyResource!.getServiceStatus(); return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }; } else { const result = await this.haproxyResource!.controlService(args.action as any); return { content: [{ type: 'text', text: `HAProxy service ${args.action} completed: ${result ? 'success' : 'failed'}` }] }; } } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } // System Settings Management case 'system_get_settings': { await this.ensureInitialized(); try { const settings = await this.systemSettingsResource!.getAllSettings(); return { content: [{ type: 'text', text: JSON.stringify(settings, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get system settings: ${error.message}` ); } } case 'system_enable_intervlan_routing': { await this.ensureInitialized(); try { const success = await this.systemSettingsResource!.enableInterVLANRouting(); return { content: [{ type: 'text', text: success ? 'Inter-VLAN routing enabled successfully at system level' : 'Failed to enable inter-VLAN routing at system level' }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to enable inter-VLAN routing: ${error.message}` ); } } case 'system_update_firewall_settings': { await this.ensureInitialized(); try { const settings: any = {}; if (args?.blockprivatenetworks !== undefined) settings.blockprivatenetworks = args.blockprivatenetworks; if (args?.blockbogons !== undefined) settings.blockbogons = args.blockbogons; if (args?.allowinterlantraffic !== undefined) settings.allowinterlantraffic = args.allowinterlantraffic; if (args?.bypassstaticroutes !== undefined) settings.bypassstaticroutes = args.bypassstaticroutes; if (args?.disablereplyto !== undefined) settings.disablereplyto = args.disablereplyto; const success = await this.systemSettingsResource!.updateFirewallSettings(settings); return { content: [{ type: 'text', text: success ? 'Firewall settings updated successfully' : 'Failed to update firewall settings' }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to update firewall settings: ${error.message}` ); } } // Interface Configuration Management case 'interface_list_overview': { await this.ensureInitialized(); try { const interfaces = await this.interfaceConfigResource!.listOverview(); const formatted = interfaces.map(iface => `${iface.name}: ${iface.device} - ${iface.status} (${iface.ipaddr || 'no IP'}/${iface.subnet || ''})` ); return { content: [{ type: 'text', text: `Found ${interfaces.length} interface(s):\n\n${formatted.join('\n')}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list interfaces: ${error.message}` ); } } case 'interface_get_config': { await this.ensureInitialized(); if (!args || !args.interfaceName) { throw new McpError( ErrorCode.InvalidRequest, 'interfaceName parameter is required' ); } try { const config = await this.interfaceConfigResource!.getInterfaceConfig(args.interfaceName as string); if (!config) { return { content: [{ type: 'text', text: `Interface ${args.interfaceName} not found` }] }; } return { content: [{ type: 'text', text: JSON.stringify(config, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get interface config: ${error.message}` ); } } case 'interface_enable_intervlan_routing': { await this.ensureInitialized(); if (!args || !args.interfaceName) { throw new McpError( ErrorCode.InvalidRequest, 'interfaceName parameter is required' ); } try { const success = await this.interfaceConfigResource!.enableInterVLANRoutingOnInterface( args.interfaceName as string ); return { content: [{ type: 'text', text: success ? `Inter-VLAN routing enabled on interface ${args.interfaceName}` : `Failed to enable inter-VLAN routing on interface ${args.interfaceName}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to enable inter-VLAN routing: ${error.message}` ); } } case 'interface_enable_intervlan_all': { await this.ensureInitialized(); try { const result = await this.interfaceConfigResource!.enableInterVLANRoutingOnAllInterfaces(); const summary = result.details.map(d => `${d.interface}: ${d.success ? '✓' : '✗'} ${d.message}` ); return { content: [{ type: 'text', text: `Inter-VLAN routing configuration:\n\n${summary.join('\n')}\n\nOverall: ${result.success ? 'Success' : 'Some interfaces failed'}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to enable inter-VLAN routing on all interfaces: ${error.message}` ); } } case 'interface_configure_dmz': { await this.ensureInitialized(); const dmzInterface = args?.dmzInterface || 'opt8'; try { const success = await this.interfaceConfigResource!.configureDMZInterface(dmzInterface as string); return { content: [{ type: 'text', text: success ? `DMZ interface ${dmzInterface} configured for inter-VLAN routing` : `Failed to configure DMZ interface ${dmzInterface}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to configure DMZ interface: ${error.message}` ); } } case 'interface_update_config': { await this.ensureInitialized(); if (!args || !args.interfaceName) { throw new McpError( ErrorCode.InvalidRequest, 'interfaceName parameter is required' ); } try { const config: any = {}; if (args?.blockpriv !== undefined) config.blockpriv = args.blockpriv; if (args?.blockbogons !== undefined) config.blockbogons = args.blockbogons; if (args?.enable !== undefined) config.enable = args.enable; if (args?.disableftpproxy !== undefined) config.disableftpproxy = args.disableftpproxy; const success = await this.interfaceConfigResource!.updateInterfaceConfig( args.interfaceName as string, config ); return { content: [{ type: 'text', text: success ? `Interface ${args.interfaceName} configuration updated` : `Failed to update interface ${args.interfaceName} configuration` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to update interface config: ${error.message}` ); } } // Routing Diagnostics and Fixes case 'routing_diagnostics': { await this.ensureInitialized(); try { const result = await this.routingDiagnosticsResource!.runDiagnostics( args?.sourceNetwork as string, args?.destNetwork as string ); let output = `Routing Diagnostics Report\n`; output += `==========================\n\n`; output += `Summary: ${result.summary}\n\n`; if (result.issues.length > 0) { output += `Issues Found:\n`; for (const issue of result.issues) { const icon = issue.severity === 'critical' ? '❌' : issue.severity === 'warning' ? '⚠️' : 'ℹ️'; output += `${icon} [${issue.component}] ${issue.issue}\n`; if (issue.fixAvailable) { output += ` Fix available: ${issue.fixCommand}\n`; } } output += `\n`; } if (result.recommendations.length > 0) { output += `Recommendations:\n`; for (const rec of result.recommendations) { output += `• ${rec}\n`; } } if (result.canAutoFix) { output += `\nAuto-fix available: Run 'routing_fix_all' to fix issues automatically`; } return { content: [{ type: 'text', text: output }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Routing diagnostics failed: ${error.message}` ); } } case 'routing_fix_all': { await this.ensureInitialized(); try { const result = await this.routingDiagnosticsResource!.fixAllRoutingIssues(); let output = `Inter-VLAN Routing Fix Results\n`; output += `===============================\n\n`; output += `Status: ${result.success ? '✅ Success' : '⚠️ Partial Success'}\n\n`; if (result.actions.length > 0) { output += `Actions Taken:\n`; for (const action of result.actions) { output += `• ${action}\n`; } } if (result.success) { output += `\nRouting should now be working. Test with:\n`; output += `• ping 10.0.0.14 (from DMZ)\n`; output += `• showmount -e 10.0.0.14 (for NFS)\n`; } return { content: [{ type: 'text', text: output }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Routing fix failed: ${error.message}` ); } } case 'routing_fix_dmz': { await this.ensureInitialized(); try { const success = await this.routingDiagnosticsResource!.fixDMZRouting(); return { content: [{ type: 'text', text: success ? '✅ DMZ routing fixed successfully! Test with: ping 10.0.0.14 from DMZ' : '⚠️ Some DMZ routing fixes failed. Run routing_diagnostics for details.' }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `DMZ routing fix failed: ${error.message}` ); } } case 'routing_create_intervlan_rules': { await this.ensureInitialized(); if (!args || !args.sourceNetwork || !args.destNetwork) { throw new McpError( ErrorCode.InvalidRequest, 'sourceNetwork and destNetwork parameters are required' ); } try { const success = await this.routingDiagnosticsResource!.createInterVLANRules( args.sourceNetwork as string, args.destNetwork as string, args.bidirectional !== false ); return { content: [{ type: 'text', text: success ? `✅ Inter-VLAN rules created between ${args.sourceNetwork} and ${args.destNetwork}` : '❌ Failed to create inter-VLAN rules' }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to create inter-VLAN rules: ${error.message}` ); } } // CLI Execution Tools case 'cli_execute': { await this.ensureInitialized(); if (!args || !args.command) { throw new McpError( ErrorCode.InvalidRequest, 'command parameter is required' ); } try { const result = await this.cliExecutor!.execute({ command: args.command as string, args: args.args as string[] || [], timeout: args.timeout as number || 30000 }); return { content: [{ type: 'text', text: result.success ? `Command executed successfully:\n${result.output || 'No output'}` : `Command failed: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to execute CLI command: ${error.message}` ); } } case 'cli_fix_interface_blocking': { await this.ensureInitialized(); if (!args || !args.interfaceName) { throw new McpError( ErrorCode.InvalidRequest, 'interfaceName parameter is required' ); } try { const result = await this.cliExecutor!.disableInterfaceBlocking( args.interfaceName as string ); return { content: [{ type: 'text', text: result.success ? `✅ Successfully disabled blocking on interface ${args.interfaceName}` : `❌ Failed to fix interface blocking: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to fix interface blocking: ${error.message}` ); } } case 'cli_reload_firewall': { await this.ensureInitialized(); try { const result = await this.cliExecutor!.reloadFirewall(); return { content: [{ type: 'text', text: result.success ? '✅ Firewall rules reloaded successfully' : `❌ Failed to reload firewall: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to reload firewall: ${error.message}` ); } } case 'cli_show_routing': { await this.ensureInitialized(); try { const result = await this.cliExecutor!.getRoutingTable(); return { content: [{ type: 'text', text: result.success ? `Routing Table:\n${result.output}` : `Failed to get routing table: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get routing table: ${error.message}` ); } } case 'cli_fix_dmz_routing': { await this.ensureInitialized(); try { const result = await this.cliExecutor!.runComprehensiveDMZFix(); return { content: [{ type: 'text', text: result.success ? `✅ DMZ Routing Fix Completed\n\n${result.output}` : `❌ DMZ Fix Failed\n\n${result.output}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to fix DMZ routing: ${error.message}` ); } } case 'cli_check_nfs': { await this.ensureInitialized(); try { const result = await this.cliExecutor!.checkNFSConnectivity( args?.truenasIP as string || '10.0.0.14' ); return { content: [{ type: 'text', text: result.success ? `NFS Exports Available:\n${result.output}` : `Failed to check NFS: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to check NFS connectivity: ${error.message}` ); } } case 'cli_apply_changes': { await this.ensureInitialized(); try { const result = await this.cliExecutor!.applyAllChanges(); return { content: [{ type: 'text', text: result.success ? `✅ All configuration changes applied successfully\n${result.output}` : `❌ Failed to apply changes: ${result.error}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to apply changes: ${error.message}` ); } } // SSH Execution Tools case 'ssh_execute': { await this.ensureInitialized(); if (!args || !args.command) { throw new McpError( ErrorCode.InvalidRequest, 'command parameter is required' ); } try { const result = await this.sshExecutor!.execute( args.command as string, { timeout: args.timeout as number, sudo: args.sudo as boolean } ); return { content: [{ type: 'text', text: result.success ? `✅ SSH Command executed successfully (exit code: ${result.exitCode}):\n${result.stdout}\n${result.stderr ? `\nStderr:\n${result.stderr}` : ''}` : `❌ SSH Command failed (exit code: ${result.exitCode}):\n${result.stderr || 'No error output'}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to execute SSH command: ${error.message}` ); } } case 'ssh_fix_interface_blocking': { await this.ensureInitialized(); if (!args || !args.interface) { throw new McpError( ErrorCode.InvalidRequest, 'interface parameter is required' ); } try { const result = await this.sshExecutor!.fixInterfaceBlocking( args.interface as string ); return { content: [{ type: 'text', text: result.success ? `✅ Successfully fixed interface blocking on ${args.interface} via SSH:\n${result.stdout}` : `❌ Failed to fix interface blocking: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to fix interface blocking via SSH: ${error.message}` ); } } case 'ssh_fix_dmz_routing': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.fixDMZRouting(); return { content: [{ type: 'text', text: result.success ? `✅ DMZ Routing Fix Applied Successfully via SSH:\n${result.stdout}` : `❌ DMZ Routing Fix Failed:\n${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to fix DMZ routing via SSH: ${error.message}` ); } } case 'ssh_enable_intervlan_routing': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.enableInterVLANRouting(); return { content: [{ type: 'text', text: result.success ? `✅ Inter-VLAN routing enabled successfully via SSH:\n${result.stdout}` : `❌ Failed to enable inter-VLAN routing:\n${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to enable inter-VLAN routing via SSH: ${error.message}` ); } } case 'ssh_reload_firewall': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.execute('configctl filter reload && pfctl -f /tmp/rules.debug'); return { content: [{ type: 'text', text: result.success ? `✅ Firewall reloaded successfully via SSH` : `❌ Failed to reload firewall: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to reload firewall via SSH: ${error.message}` ); } } case 'ssh_show_routing': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.getRoutingTable(); return { content: [{ type: 'text', text: result.success ? `Routing Table (via SSH):\n${result.stdout}` : `Failed to get routing table: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get routing table via SSH: ${error.message}` ); } } case 'ssh_show_pf_rules': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.showPfRules({ verbose: args?.verbose as boolean }); return { content: [{ type: 'text', text: result.success ? `Packet Filter Rules:\n${result.stdout}` : `Failed to get PF rules: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get PF rules via SSH: ${error.message}` ); } } case 'ssh_backup_config': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.backupConfig( args?.backupName as string ); return { content: [{ type: 'text', text: result.success ? `✅ Configuration backed up successfully:\n${result.stdout}` : `❌ Backup failed: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to backup config via SSH: ${error.message}` ); } } case 'ssh_restore_config': { await this.ensureInitialized(); if (!args || !args.backupPath) { throw new McpError( ErrorCode.InvalidRequest, 'backupPath parameter is required' ); } try { const result = await this.sshExecutor!.restoreConfig( args.backupPath as string ); return { content: [{ type: 'text', text: result.success ? `✅ Configuration restored successfully:\n${result.stdout}` : `❌ Restore failed: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to restore config via SSH: ${error.message}` ); } } case 'ssh_check_nfs_connectivity': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.checkNFSConnectivity( args?.targetIP as string ); return { content: [{ type: 'text', text: result.success ? `NFS Connectivity Check:\n${result.stdout}` : `NFS connectivity check failed: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to check NFS connectivity via SSH: ${error.message}` ); } } case 'ssh_system_status': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.getSystemStatus(); return { content: [{ type: 'text', text: result.success ? `System Status:\n${result.stdout}` : `Failed to get system status: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get system status via SSH: ${error.message}` ); } } case 'ssh_test_vlan_connectivity': { await this.ensureInitialized(); if (!args || !args.sourceInterface || !args.targetIP) { throw new McpError( ErrorCode.InvalidRequest, 'sourceInterface and targetIP parameters are required' ); } try { const result = await this.sshExecutor!.testVLANConnectivity( args.sourceInterface as string, args.targetIP as string ); return { content: [{ type: 'text', text: result.success ? `VLAN Connectivity Test:\n${result.stdout}` : `Connectivity test failed: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to test VLAN connectivity via SSH: ${error.message}` ); } } case 'ssh_quick_dmz_fix': { await this.ensureInitialized(); try { const result = await this.sshExecutor!.quickDMZFix(); return { content: [{ type: 'text', text: result.success ? `✅ Quick DMZ fix applied successfully via SSH` : `❌ Quick DMZ fix failed: ${result.stderr}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to apply quick DMZ fix via SSH: ${error.message}` ); } } case 'ssh_batch_execute': { await this.ensureInitialized(); if (!args || !args.commands || !Array.isArray(args.commands)) { throw new McpError( ErrorCode.InvalidRequest, 'commands array parameter is required' ); } try { const result = await this.sshExecutor!.executeBatch( args.commands as string[], { stopOnError: args.stopOnError as boolean } ); const commands = args.commands as string[]; const output = result.results.map((r, i) => `Command ${i + 1}: ${commands[i]}\n` + `Status: ${r.success ? '✅ Success' : '❌ Failed'}\n` + `Exit Code: ${r.exitCode}\n` + `${r.stdout ? `Output:\n${r.stdout}\n` : ''}` + `${r.stderr ? `Error:\n${r.stderr}\n` : ''}` ).join('\n---\n'); return { content: [{ type: 'text', text: `Batch Execution ${result.success ? 'Completed' : 'Failed'}:\n\n${output}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to execute batch commands via SSH: ${error.message}` ); } } // HAProxy Backend Management case 'haproxy_backend_create': { await this.ensureInitialized(); if (!args || !args.name || !args.mode || !args.balance) { throw new McpError( ErrorCode.InvalidRequest, 'name, mode, and balance parameters are required' ); } try { const result = await this.haproxyResource!.createBackend({ name: args.name as string, mode: args.mode as 'http' | 'tcp', balance: args.balance as any, servers: args.servers as any[] || [], description: args.description as string }); return { content: [{ type: 'text', text: `Successfully created HAProxy backend ${args.name} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'haproxy_backend_list': { await this.ensureInitialized(); try { const backends = await this.haproxyResource!.listBackends(); return { content: [{ type: 'text', text: JSON.stringify(backends, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } case 'haproxy_backend_delete': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.haproxyResource!.deleteBackend(args.uuid as string); return { content: [{ type: 'text', text: `Successfully deleted HAProxy backend ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy Frontend Management case 'haproxy_frontend_create': { await this.ensureInitialized(); if (!args || !args.name || !args.bind || !args.mode || !args.backend) { throw new McpError( ErrorCode.InvalidRequest, 'name, bind, mode, and backend parameters are required' ); } try { const result = await this.haproxyResource!.createFrontend({ name: args.name as string, bind: args.bind as string, mode: args.mode as 'http' | 'tcp', backend: args.backend as string, acls: args.acls as any[] || [], description: args.description as string, bindOptions: { ssl: args.ssl as boolean, certificates: args.certificates as string[] || [] } }); return { content: [{ type: 'text', text: `Successfully created HAProxy frontend ${args.name} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'haproxy_frontend_list': { await this.ensureInitialized(); try { const frontends = await this.haproxyResource!.listFrontends(); return { content: [{ type: 'text', text: JSON.stringify(frontends, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } case 'haproxy_frontend_delete': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.haproxyResource!.deleteFrontend(args.uuid as string); return { content: [{ type: 'text', text: `Successfully deleted HAProxy frontend ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy Certificate Management case 'haproxy_certificate_list': { await this.ensureInitialized(); try { const certificates = await this.haproxyResource!.listCertificates(); return { content: [{ type: 'text', text: JSON.stringify(certificates, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } case 'haproxy_certificate_create': { await this.ensureInitialized(); if (!args || !args.name || !args.type) { throw new McpError( ErrorCode.InvalidRequest, 'name and type parameters are required' ); } try { const result = await this.haproxyResource!.createCertificate({ name: args.name as string, type: args.type as any, cn: args.cn as string, san: args.san as string[], certificate: args.certificate as string, key: args.key as string, ca: args.ca as string }); return { content: [{ type: 'text', text: `Successfully created certificate ${args.name} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy ACL Management case 'haproxy_acl_create': { await this.ensureInitialized(); if (!args || !args.frontend || !args.name || !args.expression) { throw new McpError( ErrorCode.InvalidRequest, 'frontend, name, and expression parameters are required' ); } try { const result = await this.haproxyResource!.addACLToFrontend( args.frontend as string, { name: args.name as string, expression: args.expression as string } ); return { content: [{ type: 'text', text: `Successfully created ACL ${args.name} with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy Action Management case 'haproxy_action_create': { await this.ensureInitialized(); if (!args || !args.frontend || !args.type) { throw new McpError( ErrorCode.InvalidRequest, 'frontend and type parameters are required' ); } try { const result = await this.haproxyResource!.addActionToFrontend( args.frontend as string, { type: args.type as any, backend: args.backend as string, condition: args.condition as string, value: args.value as string } ); return { content: [{ type: 'text', text: `Successfully created action with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // HAProxy Stats case 'haproxy_stats': { await this.ensureInitialized(); try { const stats = await this.haproxyResource!.getStats(); return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } case 'haproxy_backend_health': { await this.ensureInitialized(); if (!args || !args.backend) { throw new McpError( ErrorCode.InvalidRequest, 'backend parameter is required' ); } try { const health = await this.haproxyResource!.getBackendHealth(args.backend as string); return { content: [{ type: 'text', text: JSON.stringify(health, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } // Macro Recording Tools case 'macro_start_recording': { await this.ensureInitialized(); if (!args || !args.name || !args.description) { throw new McpError( ErrorCode.InvalidRequest, 'name and description parameters are required' ); } try { this.macroRecorder!.startRecording(args.name as string, args.description as string); return { content: [{ type: 'text', text: `Started recording macro: ${args.name}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_stop_recording': { await this.ensureInitialized(); try { const recording = this.macroRecorder!.stopRecording(); if (!recording) { throw new Error('No recording in progress'); } await this.macroRecorder!.saveMacro(recording); return { content: [{ type: 'text', text: `Stopped recording and saved macro: ${recording.name}\nID: ${recording.id}\nCalls recorded: ${recording.calls.length}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_list': { await this.ensureInitialized(); try { const macros = await this.macroRecorder!.listMacros(); return { content: [{ type: 'text', text: JSON.stringify(macros.map(m => ({ id: m.id, name: m.name, description: m.description, created: m.created, callCount: m.calls.length, parameters: m.parameters.length })), null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, error.message ); } } case 'macro_play': { await this.ensureInitialized(); if (!args || !args.id) { throw new McpError( ErrorCode.InvalidRequest, 'id parameter is required' ); } try { const results = await this.macroRecorder!.playMacro(args.id as string, { parameters: args.parameters as Record<string, any>, dryRun: args.dryRun as boolean }); const summary = results.map((r, i) => ({ step: i + 1, method: r.call.method, path: r.call.path, success: !r.error, dryRun: r.dryRun, duration: r.duration })); return { content: [{ type: 'text', text: `Macro playback ${args.dryRun ? '(dry run) ' : ''}completed:\n${JSON.stringify(summary, null, 2)}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_delete': { await this.ensureInitialized(); if (!args || !args.id) { throw new McpError( ErrorCode.InvalidRequest, 'id parameter is required' ); } try { await this.macroRecorder!.deleteMacro(args.id as string); return { content: [{ type: 'text', text: `Successfully deleted macro ${args.id}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_analyze': { await this.ensureInitialized(); if (!args || !args.id) { throw new McpError( ErrorCode.InvalidRequest, 'id parameter is required' ); } try { const macro = await this.macroRecorder!.loadMacro(args.id as string); if (!macro) { throw new Error(`Macro ${args.id} not found`); } const analysis = this.macroRecorder!.analyzeMacro(macro); return { content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_generate_tool': { await this.ensureInitialized(); if (!args || !args.id) { throw new McpError( ErrorCode.InvalidRequest, 'id parameter is required' ); } try { const macro = await this.macroRecorder!.loadMacro(args.id as string); if (!macro) { throw new Error(`Macro ${args.id} not found`); } const tool = this.macroRecorder!.generateTool(macro); if (args.save) { // Save to a file const fs = await import('fs/promises'); const path = await import('path'); const filename = path.default.join( process.cwd(), 'generated-tools', `${tool.name}.ts` ); await fs.mkdir(path.default.dirname(filename), { recursive: true }); await fs.writeFile(filename, tool.implementation || ''); return { content: [{ type: 'text', text: `Generated tool saved to: ${filename}\n\nTool Definition:\n${JSON.stringify(tool, null, 2)}` }] }; } return { content: [{ type: 'text', text: JSON.stringify(tool, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_export': { await this.ensureInitialized(); if (!args || !args.path) { throw new McpError( ErrorCode.InvalidRequest, 'path parameter is required' ); } try { const storage = (this.macroRecorder as any).storage; await storage.exportAll(args.path as string); return { content: [{ type: 'text', text: `Successfully exported macros to: ${args.path}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } case 'macro_import': { await this.ensureInitialized(); if (!args || !args.path) { throw new McpError( ErrorCode.InvalidRequest, 'path parameter is required' ); } try { const storage = (this.macroRecorder as any).storage; const imported = await storage.importAll( args.path as string, args.overwrite as boolean ); return { content: [{ type: 'text', text: `Successfully imported ${imported} macro(s) from: ${args.path}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InvalidRequest, error.message ); } } // NAT Management Handlers case 'nat_list_outbound': { await this.ensureInitialized(); try { const rules = await this.natResource!.listOutboundRules(); return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list outbound NAT rules: ${error.message}` ); } } case 'nat_list_port_forwards': { await this.ensureInitialized(); try { const rules = await this.natResource!.listPortForwards(); return { content: [{ type: 'text', text: JSON.stringify(rules, null, 2) }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to list port forward rules: ${error.message}` ); } } case 'nat_get_mode': { await this.ensureInitialized(); try { const mode = await this.natResource!.getOutboundMode(); return { content: [{ type: 'text', text: `Current NAT mode: ${mode}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to get NAT mode: ${error.message}` ); } } case 'nat_set_mode': { await this.ensureInitialized(); if (!args || !args.mode) { throw new McpError( ErrorCode.InvalidRequest, 'mode parameter is required' ); } try { const success = await this.natResource!.setOutboundMode(args.mode as any); return { content: [{ type: 'text', text: success ? `✅ NAT mode set to: ${args.mode}` : `❌ Failed to set NAT mode to: ${args.mode}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to set NAT mode: ${error.message}` ); } } case 'nat_create_outbound_rule': { await this.ensureInitialized(); if (!args || !args.interface || !args.source_net || !args.destination_net) { throw new McpError( ErrorCode.InvalidRequest, 'interface, source_net, and destination_net parameters are required' ); } try { const result = await this.natResource!.createOutboundRule({ interface: args.interface as string, source_net: args.source_net as string, destination_net: args.destination_net as string, target: args.target as string, nonat: args.nonat as string, description: args.description as string, enabled: args.enabled as string || '1' }); return { content: [{ type: 'text', text: `✅ Created outbound NAT rule with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to create outbound NAT rule: ${error.message}` ); } } case 'nat_delete_outbound_rule': { await this.ensureInitialized(); if (!args || (!args.uuid && !args.description)) { throw new McpError( ErrorCode.InvalidRequest, 'Either uuid (for API mode) or description (for SSH mode) parameter is required' ); } try { // Use description for SSH mode, UUID for API mode const identifier = args.description || args.uuid; await this.natResource!.deleteOutboundRule(identifier as string); return { content: [{ type: 'text', text: `✅ Deleted outbound NAT rule: ${identifier}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to delete outbound NAT rule: ${error.message}` ); } } case 'nat_create_port_forward': { await this.ensureInitialized(); if (!args || !args.interface || !args.protocol || !args.destination_port || !args.target) { throw new McpError( ErrorCode.InvalidRequest, 'interface, protocol, destination_port, and target parameters are required' ); } try { const result = await this.natResource!.createPortForward({ interface: args.interface as string, protocol: args.protocol as string, destination_port: args.destination_port as string, target: args.target as string, local_port: args.local_port as string || args.destination_port as string, description: args.description as string, enabled: args.enabled as string || '1' }); return { content: [{ type: 'text', text: `✅ Created port forward rule with UUID: ${result.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to create port forward: ${error.message}` ); } } case 'nat_delete_port_forward': { await this.ensureInitialized(); if (!args || !args.uuid) { throw new McpError( ErrorCode.InvalidRequest, 'uuid parameter is required' ); } try { await this.natResource!.deletePortForward(args.uuid as string); return { content: [{ type: 'text', text: `✅ Deleted port forward rule: ${args.uuid}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to delete port forward: ${error.message}` ); } } case 'nat_fix_dmz': { await this.ensureInitialized(); try { const result = await this.natResource!.fixDMZNAT({ dmzNetwork: args?.dmzNetwork as string, lanNetwork: args?.lanNetwork as string, otherInternalNetworks: args?.otherInternalNetworks as string[] }); return { content: [{ type: 'text', text: result.success ? `✅ ${result.message}\n\nCreated rules:\n${result.rulesCreated.join('\n')}` : `❌ ${result.message}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to fix DMZ NAT: ${error.message}` ); } } case 'nat_quick_fix_dmz': { await this.ensureInitialized(); try { const result = await this.natResource!.quickFixDMZNAT(); return { content: [{ type: 'text', text: result.success ? `✅ ${result.message}` : `❌ ${result.message}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to apply quick DMZ NAT fix: ${error.message}` ); } } case 'nat_cleanup_dmz_fix': { await this.ensureInitialized(); try { const result = await this.natResource!.cleanupDMZNATFix(); return { content: [{ type: 'text', text: `✅ Cleanup complete. Removed ${result.deletedCount} MCP-created NAT rules.` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to cleanup NAT fix rules: ${error.message}` ); } } case 'nat_analyze_config': { await this.ensureInitialized(); try { const analysis = await this.natResource!.analyzeNATConfiguration(); return { content: [{ type: 'text', text: `NAT Configuration Analysis: Mode: ${analysis.mode} Rules: - Outbound: ${analysis.rules.outbound} - Port Forwards: ${analysis.rules.portForwards} - One-to-One: ${analysis.rules.oneToOne} - NPT (IPv6): ${analysis.rules.npt} Issues: ${analysis.issues.length > 0 ? analysis.issues.map(i => `- ${i}`).join('\n') : '- No issues detected'} Recommendations: ${analysis.recommendations.length > 0 ? analysis.recommendations.map(r => `- ${r}`).join('\n') : '- No recommendations'}` }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to analyze NAT configuration: ${error.message}` ); } } case 'nat_apply_changes': { await this.ensureInitialized(); try { await this.natResource!.applyNATChanges(); return { content: [{ type: 'text', text: '✅ NAT configuration changes applied successfully' }] }; } catch (error: any) { throw new McpError( ErrorCode.InternalError, `Failed to apply NAT changes: ${error.message}` ); } } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } }); } async start() { try { // Try to initialize with environment variables await this.initialize(); } catch (error) { logger.warn('Auto-initialization failed. Use configure tool to set up connection.'); } // Get transport configuration const transportType = TransportManager.getTransportType(); const transportOptions = TransportManager.getTransportOptions(); // Create and connect transport const transportOrServer = await TransportManager.createTransport(transportType, transportOptions); if (transportType === 'stdio') { // For STDIO, connect directly await this.server.connect(transportOrServer as Transport); logger.info('OPNsense MCP server running on stdio'); } else if (transportType === 'sse') { // For SSE, set up connection handler const sseServer = transportOrServer as SSETransportServer; sseServer.onConnection(async (transport) => { // Connect each new SSE client await this.server.connect(transport); logger.info('New SSE client connected'); }); logger.info(`OPNsense MCP server running on SSE (port: ${transportOptions.port || 3000})`); logger.info('Waiting for SSE client connections...'); } } // IaC Handler Methods private async handleIaCResourceRead(uri: string) { if (!this.iacEnabled) { throw new McpError( ErrorCode.InvalidRequest, 'IaC features are disabled' ); } switch (uri) { case 'opnsense://iac/resources': { const types = resourceRegistry.listResourceTypes(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify({ types, definitions: resourceRegistry.exportDefinitions() }, null, 2) }] }; } case 'opnsense://iac/deployments': { if (!this.stateStore) { throw new McpError(ErrorCode.InternalError, 'State store not initialized'); } const deployments = await this.stateStore.listDeployments(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(deployments, null, 2) }] }; } case 'opnsense://iac/state': { if (!this.stateStore) { throw new McpError(ErrorCode.InternalError, 'State store not initialized'); } const state = await this.stateStore.getCurrentState(); return { contents: [{ uri, mimeType: 'application/json', text: JSON.stringify(state, null, 2) }] }; } default: throw new McpError( ErrorCode.InvalidRequest, `Unknown IaC resource: ${uri}` ); } } private async handleIaCTool(name: string, args: any) { if (!this.planner || !this.engine || !this.stateStore) { throw new McpError( ErrorCode.InternalError, 'IaC components not initialized' ); } switch (name) { case 'iac_plan_deployment': return this.planDeployment(args); case 'iac_apply_deployment': return this.applyDeployment(args); case 'iac_destroy_deployment': return this.destroyDeployment(args); case 'iac_list_resource_types': return this.listResourceTypes(args); default: throw new McpError( ErrorCode.InvalidRequest, `Unknown IaC tool: ${name}` ); } } private async planDeployment(args: any) { // Create resource instances const resources = args.resources.map((r: any) => resourceRegistry.createResource(r.type, r.id, r.name, r.properties) ); // Get current state const currentState = await this.stateStore!.getDeploymentState(args.name); const currentResourcesArray = currentState?.resources ? Object.values(currentState.resources) : []; // Create plan const plan = await this.planner!.planDeployment( args.name, resources, currentResourcesArray ); // Store plan for later execution await this.stateStore!.storePlan(plan); return { content: [{ type: 'text', text: this.formatPlan(plan) }] }; } private async applyDeployment(args: any) { // Retrieve plan const plan = await this.stateStore!.getPlan(args.planId); if (!plan) { throw new McpError( ErrorCode.InvalidRequest, `Plan not found: ${args.planId}` ); } // Execute plan const result = await this.engine!.execute(plan, { dryRun: false, parallel: true, maxConcurrency: 5 }); // Update state if (result.success) { await this.stateStore!.updateDeploymentState(plan.name, result); } return { content: [{ type: 'text', text: this.formatExecutionResult(result) }] }; } private async destroyDeployment(args: any) { const deployment = await this.stateStore!.getDeploymentState(args.deploymentId); if (!deployment) { throw new McpError( ErrorCode.InvalidRequest, `Deployment not found: ${args.deploymentId}` ); } // Create destruction plan const resourcesArray = Object.values(deployment.resources); const plan = await this.planner!.planDestruction( args.deploymentId, resourcesArray ); // Execute destruction const result = await this.engine!.execute(plan, { dryRun: false, force: args.force }); // Clean up state if successful if (result.success) { await this.stateStore!.deleteDeployment(args.deploymentId); } return { content: [{ type: 'text', text: `Deployment ${args.deploymentId} destroyed successfully` }] }; } private async listResourceTypes(args: any) { const types = resourceRegistry.listResourceTypes(); const filtered = args.category ? types.filter(t => t.startsWith(args.category)) : types; return { content: [{ type: 'text', text: filtered.join('\n') }] }; } private formatPlan(plan: any): string { let output = `Deployment Plan: ${plan.name}\n`; output += `Plan ID: ${plan.id}\n\n`; output += `Summary:\n`; output += ` + Create: ${plan.summary.create}\n`; output += ` ~ Update: ${plan.summary.update}\n`; output += ` - Delete: ${plan.summary.delete}\n`; output += ` [REPLACE] Replace: ${plan.summary.replace}\n\n`; output += `Execution Waves:\n`; plan.executionWaves.forEach((wave: any) => { output += `\nWave ${wave.wave} (${wave.estimatedTime}s):\n`; wave.changes.forEach((change: any) => { const symbolMap: Record<string, string> = { create: '+', update: '~', delete: '-', replace: '[R]' }; const symbol = symbolMap[change.type as keyof typeof symbolMap] || '?'; output += ` ${symbol} ${change.resource.type} "${change.resource.name}"\n`; }); }); if (plan.risks && plan.risks.length > 0) { output += `\nRisks:\n`; plan.risks.forEach((risk: any) => { output += ` [${risk.severity.toUpperCase()}] ${risk.description}\n`; }); } return output; } private formatExecutionResult(result: any): string { let output = `Execution ${result.success ? 'Successful' : 'Failed'}\n`; output += `Duration: ${(result.duration / 1000).toFixed(2)}s\n\n`; output += `Changes Applied:\n`; result.executedChanges.forEach((change: any) => { const status = change.status === 'success' ? '[OK]' : '[FAIL]'; output += ` ${status} ${change.type} ${change.resourceId}\n`; }); if (result.failedChanges?.length > 0) { output += `\nFailed Changes:\n`; result.failedChanges.forEach((failure: any) => { output += ` [FAIL] ${failure.resourceId}: ${failure.error}\n`; }); } if (result.rollbackPerformed) { output += `\n[WARNING] Rollback was performed due to failures\n`; } return output; } } // Start the server const server = new OPNSenseMCPServer(); server.start().catch(error => logger.error('Server startup failed:', error));

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/vespo92/OPNSenseMCP'

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