Skip to main content
Glama

OPNSense MCP Server

client.ts20 kB
// Consolidated OPNsense API Client with enhanced error handling and full functionality import axios, { AxiosInstance, AxiosError } from 'axios'; import https from 'https'; import { APICall } from '../macro/types.js'; /** * Custom error class for OPNsense API errors */ export class OPNSenseAPIError extends Error { constructor( message: string, public statusCode: number, public apiResponse?: any ) { super(message); this.name = 'OPNSenseAPIError'; } } /** * OPNsense API Client with comprehensive functionality and error handling */ export class OPNSenseAPIClient { private axios: AxiosInstance; private debugMode: boolean; private recorder?: (call: Omit<APICall, 'id' | 'timestamp'>) => void; constructor(private config: { host: string; apiKey: string; apiSecret: string; verifySsl?: boolean; debugMode?: boolean; timeout?: number; }) { this.debugMode = config.debugMode || false; // Create axios instance with proper configuration this.axios = axios.create({ baseURL: `${this.config.host}/api`, auth: { username: this.config.apiKey, password: this.config.apiSecret }, httpsAgent: new https.Agent({ rejectUnauthorized: this.config.verifySsl !== false }), timeout: this.config.timeout || 30000, // Don't set default headers - we'll add them per request type validateStatus: () => true // Handle all status codes }); // Add request interceptor for debugging this.axios.interceptors.request.use( (config) => { if (this.debugMode) { console.log('[API Request]', { method: config.method?.toUpperCase(), url: config.url, headers: config.headers, data: config.data }); } return config; }, (error) => { if (this.debugMode) { console.error('[API Request Error]', error); } return Promise.reject(error); } ); // Add response interceptor for debugging and error handling this.axios.interceptors.response.use( (response) => { if (this.debugMode) { console.log('[API Response]', { status: response.status, statusText: response.statusText, data: response.data }); } return response; }, (error: AxiosError) => { if (this.debugMode) { console.error('[API Response Error]', { status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data }); } return Promise.reject(error); } ); } /** * Extract error message from various API response formats */ private extractErrorMessage(data: any): string | null { if (typeof data === 'string') { return data; } if (data?.message) { return data.message; } if (data?.errorMessage) { return data.errorMessage; } if (data?.validations) { if (typeof data.validations === 'string') { return data.validations; } if (typeof data.validations === 'object') { return Object.entries(data.validations) .map(([key, value]) => `${key}: ${value}`) .join(', '); } } return null; } /** * Set a recorder function to capture API calls */ setRecorder(recorder: (call: Omit<APICall, 'id' | 'timestamp'>) => void): void { this.recorder = recorder; } /** * Remove the recorder */ removeRecorder(): void { this.recorder = undefined; } /** * Record an API call if a recorder is set */ private recordCall( method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, params?: Record<string, any>, payload?: any, response?: any, duration?: number, error?: { code: string; message: string } ): void { if (!this.recorder) return; const call: Omit<APICall, 'id' | 'timestamp'> = { method, path, params, payload, duration }; if (response) { call.response = { status: response.status, data: response.data, headers: response.headers }; } if (error) { call.error = error; } this.recorder(call); } /** * GET request - NO Content-Type header for OPNsense compatibility */ async get<T = any>(path: string): Promise<T> { const startTime = Date.now(); const response = await this.axios.get(path, { headers: { 'Accept': 'application/json' // NO Content-Type header for GET requests! } }); const duration = Date.now() - startTime; if (response.status === 404) { this.recordCall('GET', path, undefined, undefined, response, duration, { code: 'NOT_FOUND', message: `Endpoint not found: ${path}` }); throw new OPNSenseAPIError(`Endpoint not found: ${path}`, 404); } // Handle API errors returned with 200 status if (response.data && response.data.result === 'failed') { const errorMessage = this.extractErrorMessage(response.data) || 'API operation failed'; this.recordCall('GET', path, undefined, undefined, response, duration, { code: 'API_FAILED', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } if (response.status >= 200 && response.status < 300) { this.recordCall('GET', path, undefined, undefined, response, duration); return response.data; } const errorMessage = this.extractErrorMessage(response.data) || `API error: ${response.status} ${response.statusText}`; this.recordCall('GET', path, undefined, undefined, response, duration, { code: 'API_ERROR', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } /** * POST request - WITH Content-Type header */ async post<T = any>(path: string, data: any = {}): Promise<T> { const startTime = Date.now(); const response = await this.axios.post(path, data, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); const duration = Date.now() - startTime; if (response.status === 404) { this.recordCall('POST', path, undefined, data, response, duration, { code: 'NOT_FOUND', message: `Endpoint not found: ${path}` }); throw new OPNSenseAPIError(`Endpoint not found: ${path}`, 404); } // Handle API errors returned with 200 status if (response.data && response.data.result === 'failed') { const errorMessage = this.extractErrorMessage(response.data) || 'API operation failed'; this.recordCall('POST', path, undefined, data, response, duration, { code: 'API_FAILED', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } if (response.status >= 200 && response.status < 300) { this.recordCall('POST', path, undefined, data, response, duration); return response.data; } const errorMessage = this.extractErrorMessage(response.data) || `API error: ${response.status} ${response.statusText}`; this.recordCall('POST', path, undefined, data, response, duration, { code: 'API_ERROR', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } /** * PUT request - WITH Content-Type header */ async put<T = any>(path: string, data: any = {}): Promise<T> { const startTime = Date.now(); const response = await this.axios.put(path, data, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); const duration = Date.now() - startTime; if (response.status === 404) { this.recordCall('PUT', path, undefined, data, response, duration, { code: 'NOT_FOUND', message: `Endpoint not found: ${path}` }); throw new OPNSenseAPIError(`Endpoint not found: ${path}`, 404); } if (response.status >= 200 && response.status < 300) { this.recordCall('PUT', path, undefined, data, response, duration); return response.data; } const errorMessage = this.extractErrorMessage(response.data) || `API error: ${response.status} ${response.statusText}`; this.recordCall('PUT', path, undefined, data, response, duration, { code: 'API_ERROR', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } /** * DELETE request */ async delete<T = any>(path: string): Promise<T> { const startTime = Date.now(); const response = await this.axios.delete(path, { headers: { 'Accept': 'application/json' } }); const duration = Date.now() - startTime; if (response.status === 404) { this.recordCall('DELETE', path, undefined, undefined, response, duration, { code: 'NOT_FOUND', message: `Endpoint not found: ${path}` }); throw new OPNSenseAPIError(`Endpoint not found: ${path}`, 404); } if (response.status >= 200 && response.status < 300) { this.recordCall('DELETE', path, undefined, undefined, response, duration); return response.data; } const errorMessage = this.extractErrorMessage(response.data) || `API error: ${response.status} ${response.statusText}`; this.recordCall('DELETE', path, undefined, undefined, response, duration, { code: 'API_ERROR', message: errorMessage }); throw new OPNSenseAPIError(errorMessage, response.status, response.data); } // ===== VLAN METHODS ===== /** * Search for VLANs using the working endpoint */ async searchVlans(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 100, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; // Use the working endpoint return this.post('/interfaces/vlan_settings/searchItem', searchParams); } /** * Get all VLAN settings */ async getVlanSettings(): Promise<any> { return this.get('/interfaces/vlan_settings/get'); } /** * Add a new VLAN */ async addVlan(vlanData: any): Promise<any> { return this.post('/interfaces/vlan_settings/addItem', { vlan: vlanData }); } /** * Update a VLAN */ async setVlan(uuid: string, vlanData: any): Promise<any> { return this.post(`/interfaces/vlan_settings/setItem/${uuid}`, { vlan: vlanData }); } /** * Delete a VLAN */ async delVlan(uuid: string): Promise<any> { return this.post(`/interfaces/vlan_settings/delItem/${uuid}`); } /** * Apply VLAN changes */ async applyVlanChanges(): Promise<any> { return this.post('/interfaces/vlan_settings/reconfigure'); } // ===== FIREWALL RULE METHODS ===== /** * Search firewall rules */ async searchFirewallRules(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 1000, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; return this.post('/firewall/filter/searchRule', searchParams); } /** * Get firewall rule by UUID */ async getFirewallRule(uuid: string): Promise<any> { return this.get(`/firewall/filter/getRule/${uuid}`); } /** * Get firewall rule options (for dropdowns) */ async getFirewallRuleOptions(): Promise<any> { return this.get('/firewall/filter/getRule'); } /** * Add firewall rule */ async addFirewallRule(ruleData: any): Promise<any> { return this.post('/firewall/filter/addRule', { rule: ruleData }); } /** * Update firewall rule */ async setFirewallRule(uuid: string, ruleData: any): Promise<any> { return this.post(`/firewall/filter/setRule/${uuid}`, { rule: ruleData }); } /** * Delete firewall rule */ async delFirewallRule(uuid: string): Promise<any> { return this.post(`/firewall/filter/delRule/${uuid}`); } /** * Apply firewall changes */ async applyFirewallChanges(): Promise<any> { return this.post('/firewall/filter/apply'); } /** * Reconfigure firewall filter */ async reconfigureFirewall(): Promise<any> { return this.post('/firewall/filter/reconfigure'); } /** * Get all firewall filter settings */ async getFirewallFilterSettings(): Promise<any> { return this.get('/firewall/filter/get'); } /** * Save firewall configuration */ async saveFirewallConfig(): Promise<any> { return this.post('/firewall/filter/savepoint'); } // ===== BACKUP METHODS ===== /** * Download current configuration */ async downloadConfig(): Promise<any> { return this.get('/core/backup/download/this'); } /** * List available backups */ async listBackups(): Promise<any> { return this.get('/core/backup/list'); } /** * Upload/restore configuration */ async restoreConfig(configData: any): Promise<any> { return this.post('/core/backup/restore', configData); } /** * Get system information for backups */ async getSystemInfo(): Promise<any> { return this.get('/core/system/status'); } // ===== DHCP METHODS ===== /** * Search DHCP leases */ async searchDhcpLeases(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 1000, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; return this.post('/dhcpv4/leases/searchLease', searchParams); } /** * Get DHCP settings */ async getDhcpSettings(): Promise<any> { return this.get('/dhcpv4/settings/get'); } /** * Search static mappings */ async searchStaticMappings(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 1000, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; return this.post('/dhcpv4/settings/searchStaticMap', searchParams); } /** * Add static mapping */ async addStaticMapping(mappingData: any): Promise<any> { return this.post('/dhcpv4/settings/addStaticMap', { staticmap: mappingData }); } /** * Update static mapping */ async setStaticMapping(uuid: string, mappingData: any): Promise<any> { return this.post(`/dhcpv4/settings/setStaticMap/${uuid}`, { staticmap: mappingData }); } /** * Delete static mapping */ async delStaticMapping(uuid: string): Promise<any> { return this.post(`/dhcpv4/settings/delStaticMap/${uuid}`); } /** * Apply DHCP changes */ async applyDhcpChanges(): Promise<any> { return this.post('/dhcpv4/service/reconfigure'); } // ===== DNS/UNBOUND METHODS ===== /** * Get Unbound DNS settings */ async getUnboundSettings(): Promise<any> { return this.get('/unbound/settings/get'); } /** * Get specific Unbound host override */ async getUnboundHost(uuid: string): Promise<any> { return this.get(`/unbound/settings/getHostOverride/${uuid}`); } /** * Add Unbound host override (for DNS blocking) */ async addUnboundHost(hostData: any): Promise<any> { return this.post('/unbound/settings/addHostOverride', { host: hostData }); } /** * Update Unbound host override */ async setUnboundHost(uuid: string, hostData: any): Promise<any> { return this.post(`/unbound/settings/setHostOverride/${uuid}`, { host: hostData }); } /** * Delete Unbound host override */ async delUnboundHost(uuid: string): Promise<any> { return this.post(`/unbound/settings/delHostOverride/${uuid}`); } /** * Search Unbound host overrides */ async searchUnboundHosts(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 1000, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; return this.post('/unbound/settings/searchHostOverride', searchParams); } /** * Apply Unbound configuration changes */ async applyUnboundChanges(): Promise<any> { return this.post('/unbound/service/reconfigure'); } /** * Get Unbound access lists (for interface-specific DNS) */ async getUnboundAccessLists(): Promise<any> { return this.get('/unbound/settings/get'); } /** * Add Unbound access list */ async addUnboundAccessList(aclData: any): Promise<any> { return this.post('/unbound/settings/addAcl', { acl: aclData }); } /** * Update Unbound access list */ async setUnboundAccessList(uuid: string, aclData: any): Promise<any> { return this.post(`/unbound/settings/setAcl/${uuid}`, { acl: aclData }); } /** * Delete Unbound access list */ async delUnboundAccessList(uuid: string): Promise<any> { return this.post(`/unbound/settings/delAcl/${uuid}`); } // ===== ARP TABLE METHODS ===== /** * Get ARP table entries */ async getArpTable(): Promise<any> { return this.get('/diagnostics/interface/getArp'); } /** * Get network interfaces */ async getInterfaces(): Promise<any> { return this.get('/interfaces/overview/list'); } /** * Search ARP table entries */ async searchArpTable(params: any = {}): Promise<any> { const searchParams = { current: params.current || 1, rowCount: params.rowCount || 10000, sort: params.sort || {}, searchPhrase: params.searchPhrase || '' }; return this.post('/diagnostics/interface/searchArp', searchParams); } /** * Clear/flush ARP entry */ async flushArpEntry(data: { address: string; interface?: string }): Promise<any> { return this.post('/diagnostics/interface/flushArp', data); } /** * Add static ARP entry */ async setArpEntry(data: { address: string; mac: string; interface: string }): Promise<any> { return this.post('/diagnostics/interface/setArp', data); } // ===== COMMON UTILITY METHODS ===== /** * Test connection */ async testConnection(): Promise<{ success: boolean; version?: string; product?: string; error?: string }> { try { const result = await this.get<any>('/core/firmware/info'); return { success: true, version: result.product_version, product: result.product_name }; } catch (error: any) { return { success: false, error: error.message }; } } /** * Get service status */ async getServiceStatus(serviceName: string): Promise<{ status: 'running' | 'stopped'; pid?: number }> { return this.post('/core/service/status', { name: serviceName }); } /** * Control service (start/stop/restart) */ async controlService(serviceName: string, action: 'start' | 'stop' | 'restart'): Promise<{ response: string }> { return this.post('/core/service/' + action, { name: serviceName }); } } // Type definitions for common API patterns export interface SearchParams { current?: number; rowCount?: number; sort?: Record<string, string>; searchPhrase?: string; } export interface SearchResult<T> { total: number; rowCount: number; current: number; rows: T[]; } export interface AddItemResult { result: string; uuid?: string; validations?: Record<string, string>; } export interface SetItemResult { result: string; validations?: Record<string, string>; } export interface DeleteResult { result: string; message?: string; } export interface ApplyResult { status: string; } export interface ReconfigureResult { status: string; } export interface ToggleResult { result: string; } // Export for both TypeScript and JavaScript usage export default OPNSenseAPIClient;

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