Skip to main content
Glama

OPNSense MCP Server

mcp-integration.ts14.3 kB
// MCP Integration for Natural Language Network Queries import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { NaturalLanguageQueryProcessor } from './processor'; import { DeviceFingerprintingService } from './fingerprinting'; import { OPNSenseAPIClient } from '../../api/client'; import { drizzle } from 'drizzle-orm/node-postgres'; import { Pool } from 'pg'; import { devices, dhcpLeases, trafficStats, activeConnections, networkInterfaces } from './schema'; import { eq, and, sql } from 'drizzle-orm'; export class NetworkQueryMCP { private server: Server; private queryProcessor: NaturalLanguageQueryProcessor; private fingerprinting: DeviceFingerprintingService; private opnsenseClient?: OPNSenseAPIClient; private db: ReturnType<typeof drizzle>; private syncInterval?: NodeJS.Timeout; constructor() { // Initialize database connection const connectionString = `postgresql://${process.env.POSTGRES_USER}:${process.env.POSTGRES_PASSWORD}@${process.env.POSTGRES_HOST || '10.0.0.2'}:${process.env.POSTGRES_PORT || '5432'}/${process.env.POSTGRES_DB}`; const pool = new Pool({ connectionString }); this.db = drizzle(pool); // Initialize services this.queryProcessor = new NaturalLanguageQueryProcessor(connectionString); this.fingerprinting = new DeviceFingerprintingService(connectionString); // Initialize MCP server this.server = new Server( { name: 'opnsense-network-query', version: '1.0.0', }, { capabilities: { tools: { 'network_query': { description: 'Query network devices using natural language', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query about network devices' } }, required: ['query'] } }, 'sync_network_data': { description: 'Sync network data from OPNsense', inputSchema: { type: 'object', properties: {} } }, 'update_device_name': { description: 'Update friendly name for a device', inputSchema: { type: 'object', properties: { macAddress: { type: 'string', description: 'MAC address of the device' }, friendlyName: { type: 'string', description: 'New friendly name for the device' } }, required: ['macAddress', 'friendlyName'] } }, 'group_devices': { description: 'Group devices together (e.g., all devices belonging to one person)', inputSchema: { type: 'object', properties: { groupName: { type: 'string', description: 'Name of the group' }, macAddresses: { type: 'array', items: { type: 'string' }, description: 'MAC addresses to group together' } }, required: ['groupName', 'macAddresses'] } } } } } ); this.setupHandlers(); } private setupHandlers(): void { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'network_query', description: 'Query network devices using natural language', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Natural language query about network devices' } }, required: ['query'] } }, { name: 'sync_network_data', description: 'Sync network data from OPNsense', inputSchema: { type: 'object', properties: {} } }, { name: 'update_device_name', description: 'Update friendly name for a device', inputSchema: { type: 'object', properties: { macAddress: { type: 'string', description: 'MAC address of the device' }, friendlyName: { type: 'string', description: 'New friendly name for the device' } }, required: ['macAddress', 'friendlyName'] } }, { name: 'group_devices', description: 'Group devices together (e.g., all devices belonging to one person)', inputSchema: { type: 'object', properties: { groupName: { type: 'string', description: 'Name of the group' }, macAddresses: { type: 'array', items: { type: 'string' }, description: 'MAC addresses to group together' } }, required: ['groupName', 'macAddresses'] } } ] })); // Set up request handlers like main server this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; switch (name) { case 'network_query': if (!args || typeof args !== 'object' || !('query' in args)) { throw new Error('Query parameter is required'); } return await this.handleNetworkQuery(args.query as string); case 'sync_network_data': return await this.syncNetworkData(); case 'update_device_name': if (!args || typeof args !== 'object' || !('macAddress' in args) || !('friendlyName' in args)) { throw new Error('macAddress and friendlyName parameters are required'); } return await this.updateDeviceName(args.macAddress as string, args.friendlyName as string); case 'group_devices': if (!args || typeof args !== 'object' || !('groupName' in args) || !('macAddresses' in args)) { throw new Error('groupName and macAddresses parameters are required'); } return await this.groupDevices(args.groupName as string, args.macAddresses as string[]); default: throw new Error(`Unknown tool: ${name}`); } }); } // Handle natural language network queries private async handleNetworkQuery(query: string): Promise<any> { try { const result = await this.queryProcessor.processQuery(query); // Format results for display const formattedResults = this.formatQueryResults(result.results, result.intent); return { content: { type: 'text', text: JSON.stringify({ query: query, intent: result.intent, confidence: result.confidence, executionTime: `${result.executionTime}ms`, resultCount: result.results.length, results: formattedResults }, null, 2) } }; } catch (error) { return { content: { type: 'text', text: `Error processing query: ${error instanceof Error ? error.message : 'Unknown error'}` } }; } } // Sync data from OPNsense private async syncNetworkData(): Promise<any> { try { // Initialize OPNsense client if not already done if (!this.opnsenseClient) { this.opnsenseClient = new OPNSenseAPIClient({ host: process.env.OPNSENSE_HOST!, apiKey: process.env.OPNSENSE_API_KEY!, apiSecret: process.env.OPNSENSE_API_SECRET! }); } // 1. Sync DHCP leases const leasesResponse = await this.opnsenseClient.searchDhcpLeases(); const leases = leasesResponse.rows || []; let processedLeases = 0; for (const lease of leases) { await this.fingerprinting.processNewLease({ macAddress: lease.mac, ipAddress: lease.address, hostname: lease.hostname, interfaceName: lease.if || 'unknown' }); processedLeases++; } // 2. Sync network interfaces const interfaces = await this.opnsenseClient.getInterfaces(); let processedInterfaces = 0; for (const [name, info] of Object.entries(interfaces)) { // Skip if not an object with interface info if (typeof info !== 'object' || !info) continue; const interfaceInfo = info as any; await this.db.insert(networkInterfaces) .values({ interfaceName: name, description: interfaceInfo.description || name, ipAddress: interfaceInfo.ipaddr || null, subnet: interfaceInfo.subnet || null, vlanId: interfaceInfo.vlan ? parseInt(interfaceInfo.vlan) : null }) .onConflictDoUpdate({ target: networkInterfaces.interfaceName, set: { description: interfaceInfo.description || name, ipAddress: interfaceInfo.ipaddr || null, subnet: interfaceInfo.subnet || null, updatedAt: new Date() } }); processedInterfaces++; } // 3. Update device summary view await this.queryProcessor.updateDeviceSummaryView(); return { content: { type: 'text', text: `Successfully synced network data:\n- Processed ${processedLeases} DHCP leases\n- Synced ${processedInterfaces} network interfaces\n- Refreshed device summary view` } }; } catch (error) { return { content: { type: 'text', text: `Error syncing network data: ${error instanceof Error ? error.message : 'Unknown error'}` } }; } } // Update device friendly name private async updateDeviceName(macAddress: string, friendlyName: string): Promise<any> { try { await this.db.update(devices) .set({ friendlyName: friendlyName, updatedAt: new Date() }) .where(eq(devices.macAddress, macAddress)); return { content: { type: 'text', text: `Successfully updated device name for ${macAddress} to "${friendlyName}"` } }; } catch (error) { return { content: { type: 'text', text: `Error updating device name: ${error instanceof Error ? error.message : 'Unknown error'}` } }; } } // Group devices together private async groupDevices(groupName: string, macAddresses: string[]): Promise<any> { try { // Implementation would create device groups // This is a placeholder for the actual implementation return { content: { type: 'text', text: `Successfully created group "${groupName}" with ${macAddresses.length} devices` } }; } catch (error) { return { content: { type: 'text', text: `Error creating device group: ${error instanceof Error ? error.message : 'Unknown error'}` } }; } } // Format query results based on intent private formatQueryResults(results: any[], intent: string): any[] { switch (intent) { case 'device_online_check': return results.map(r => ({ device: r.friendlyName || r.macAddress, type: r.deviceType, status: r.isOnline ? 'Online' : 'Offline', lastSeen: r.lastSeen, network: r.interfaceName, vlan: r.vlanId })); case 'device_data_usage': return results.map(r => ({ device: r.friendlyName || 'Unknown', type: r.deviceType, dataUsed: this.formatBytes(r.totalBytes), rawBytes: r.totalBytes })); case 'gaming_consoles': return results.map(r => ({ device: r.friendlyName || r.macAddress, manufacturer: r.manufacturer, status: r.isOnline ? 'Online' : 'Offline', lastSeen: r.lastSeen, ip: r.currentIp, network: r.interfaceName })); default: return results; } } // Helper to format bytes private formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; } // Start periodic sync async startPeriodicSync(intervalMinutes: number = 5): Promise<void> { // Initial sync await this.syncNetworkData(); // Set up periodic sync this.syncInterval = setInterval(async () => { await this.syncNetworkData(); }, intervalMinutes * 60 * 1000); } // Stop periodic sync stopPeriodicSync(): void { if (this.syncInterval) { clearInterval(this.syncInterval); this.syncInterval = undefined; } } // Start the MCP server async start(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); // Initialize fingerprint database await this.fingerprinting.initializeFingerprints(); // Start periodic sync await this.startPeriodicSync(); console.error('Network Query MCP server started'); } } // Start the server if (require.main === module) { const server = new NetworkQueryMCP(); server.start().catch(console.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