Skip to main content
Glama
mikrotik.ts16.4 kB
/** * @file MikroTik API Routes * @description REST endpoints for MikroTik Command Builder * * Endpoints: * GET /mikrotik/:deviceId/facts - Get device facts * POST /mikrotik/plan - Generate Plan JSON from intent (AI) * POST /mikrotik/compile - Compile Plan JSON to commands * POST /mikrotik/validate - Validate Plan JSON * POST /mikrotik/apply - Apply commands (dry-run or execute) */ import { Router, Request, Response } from 'express'; import { z } from 'zod'; import { logger } from '../../logging/logger.js'; import { compilePlan } from '../../mikrotik/compiler.js'; import { validatePlan } from '../../mikrotik/validation/policy.js'; import type { MikrotikPlan, DeviceFacts } from '../../mikrotik/types.js'; import { routeRequest } from '../../routing/router.js'; import type { LLMRequest, RoutingContext } from '../../mcp/types.js'; const router = Router(); /** * Request schemas */ const PlanRequestSchema = z.object({ intent: z.string(), deviceId: z.string(), facts: z.any(), // DeviceFacts constraints: z.any().optional(), selectedModules: z.array(z.string()).optional(), }); const CompileRequestSchema = z.object({ plan: z.any(), // MikrotikPlan facts: z.any(), // DeviceFacts }); const ValidateRequestSchema = z.object({ plan: z.any(), // MikrotikPlan facts: z.any(), // DeviceFacts }); const ApplyRequestSchema = z.object({ changeId: z.string(), commands: z.array(z.string()), executionMode: z.enum(['dryRun', 'apply']), deviceId: z.string(), }); /** * GET /mikrotik/:deviceId/facts * Get device facts (mock for now, in production would query RouterOS API) */ router.get('/:deviceId/facts', (req: Request, res: Response) => { try { const { deviceId } = req.params; // Mock facts (in production, fetch from RouterOS API) const facts: DeviceFacts = { deviceId, routeros: '7.16', model: 'CCR2116', interfaces: [ { name: 'ether1', type: 'ether', disabled: false, comment: 'WAN1' }, { name: 'ether2', type: 'ether', disabled: false, comment: 'WAN2' }, { name: 'ether3', type: 'ether', disabled: false }, { name: 'br-lan', type: 'bridge', disabled: false, comment: 'LAN Bridge' }, ], bridges: [ { name: 'br-lan', vlanFiltering: false, ports: ['ether3', 'ether4', 'ether5'] }, ], vlans: [], ipAddresses: [ { address: '192.168.1.1/24', interface: 'br-lan', disabled: false }, ], services: [ { name: 'winbox', port: 8291, disabled: false, address: '' }, { name: 'ssh', port: 22, disabled: false, address: '' }, { name: 'www', port: 80, disabled: true }, ], routes: [], systemIdentity: 'MikroTik', dnsServers: ['8.8.8.8', '8.8.4.4'], }; res.json({ deviceId, facts, timestamp: new Date().toISOString(), }); } catch (error) { logger.error('[MikroTikAPI] Get facts failed', { error: error instanceof Error ? error.message : String(error), }); res.status(500).json({ error: 'Failed to retrieve device facts', details: error instanceof Error ? error.message : String(error), }); } }); /** * POST /mikrotik/plan * Generate Plan JSON from user intent (AI-powered) */ router.post('/plan', async (req: Request, res: Response) => { try { const { intent, deviceId, facts, constraints, selectedModules } = PlanRequestSchema.parse(req.body); logger.info('[MikroTikAPI] Generating plan', { deviceId, intent: intent.substring(0, 100), }); // Build comprehensive system prompt with schema and safety rules const systemPrompt = `You are a MikroTik RouterOS configuration expert. Your task is to generate a complete MikroTik Plan JSON based on user requirements. CRITICAL INSTRUCTIONS: 1. This is your FINAL output - no intermediate steps or reasoning text 2. Output ONLY valid JSON matching the MikrotikPlan schema below 3. NEVER generate raw RouterOS CLI commands - only structured Plan JSON 4. Each step must specify module, action, and params according to schema 5. Set appropriate risk levels: low/medium/high based on operation impact 6. Include realistic assumptions about the network environment 7. Consider safety: noLockout policy, management access, VLAN filtering risks 8. Be comprehensive - include ALL necessary steps for the configuration Available modules: ${selectedModules?.join(', ') || 'system, services, firewall, nat, dhcp-server, dhcp-client, ip, dns, routing'} MikrotikPlan JSON Schema (THIS IS YOUR OUTPUT FORMAT): { "changeId": "string (generate unique ID like: change-TIMESTAMP-RANDOM)", "createdAt": "ISO timestamp (use current time)", "target": { "deviceId": "${deviceId}", "routeros": "${facts.routeros}", "model": "${facts.model}" }, "description": "Brief clear description of what this configuration does", "assumptions": [ "List key assumptions, e.g.:", "- ether1 is WAN1 interface", "- ether2 is WAN2 interface", "- br-lan is LAN bridge for internal network", "- Management subnet is 192.168.1.0/24" ], "steps": [ { "id": "step-001 (sequential, unique)", "title": "Clear human-readable title", "module": "system|services|firewall|nat|dhcp-server|dhcp-client|ip|dns|routing", "action": "set|add|remove|enable|disable|configure", "params": { // Module-specific parameters - follow types.ts definitions // For system: { identity, note, timezone, ntpServers, ntpEnabled } // For services: { services: [{ name, disabled, port, address }] } // For firewall: { preset, wanInterfaces, lanInterfaces, mgmtSubnets, enableFastTrack } // For nat: { wanInterface, masquerade, portForwards, hairpinNat } // For dhcp-server: { servers: [{ name, interface, poolName, poolRange, network, gateway }] } }, "risk": "low|medium|high", "precheck": [ { "type": "interface_exists", "description": "Check interface exists", "params": { "interface": "ether1" } } ] } ], "policy": { "noLockout": true, "requireSnapshot": true, "mgmtSubnets": ["192.168.1.0/24"], "allowVlanFiltering": false, "allowMgmtIpChange": false }, "metadata": { "intent": "${intent}", "selectedModules": ${JSON.stringify(selectedModules || [])} } } Current Device Facts: - Device ID: ${deviceId} - Model: ${facts.model} - RouterOS: ${facts.routeros} - Existing Interfaces: ${JSON.stringify(facts.interfaces?.map((i: any) => `${i.name} (${i.type})`) || [])} - Existing Bridges: ${JSON.stringify(facts.bridges?.map((b: any) => b.name) || [])} - Existing IPs: ${JSON.stringify(facts.ipAddresses?.map((ip: any) => `${ip.address} on ${ip.interface}`) || [])} OUTPUT ONLY THE JSON - NO MARKDOWN, NO EXPLANATIONS, NO REASONING TEXT. Just the complete JSON object starting with { and ending with }.`; // Build user prompt const userPrompt = `Generate a complete MikroTik configuration Plan JSON for the following request: ${intent} Selected modules to configure: ${selectedModules?.join(', ') || 'all necessary modules'} Remember: Output ONLY the final JSON - this is not a conversation, just return the structured Plan JSON.`; // Routing context: Use L0 with no limits for this specialized task const routingContext: RoutingContext = { taskType: 'general', // Could be 'reasoning' for complex logic complexity: 'high', // MikroTik config is complex quality: 'high', // Need accurate structured output preferredLayer: 'L0', // Start with L0 as requested enableAutoEscalate: false, // Don't auto-escalate, L0 is fine for JSON generation enableCrossCheck: false, // Not needed for structured output budget: 0, // L0 is free }; // Call orchestrator/router logger.info('[MikroTikAPI] Calling router for plan generation', { deviceId, intentLength: intent.length, selectedModules, routingContext, }); const llmRequest: LLMRequest = { messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: userPrompt }, ], maxTokens: undefined, // No limit for L0 temperature: undefined, // Use model default }; // Route request via orchestrator const llmResponse = await routeRequest(llmRequest, routingContext); logger.info('[MikroTikAPI] Received response from router', { modelId: llmResponse.modelId, provider: llmResponse.provider, layer: llmResponse.layer, contentLength: llmResponse.content?.length || 0, routingSummary: llmResponse.routingSummary, }); // Parse JSON from response (handle potential markdown wrapping or reasoning text) let planJson: any; try { if (!llmResponse.content) { throw new Error('LLM response content is empty'); } let jsonContent = llmResponse.content.trim(); // Try to extract JSON from various formats // 1. Remove markdown code blocks if present if (jsonContent.includes('```')) { const jsonMatch = jsonContent.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); if (jsonMatch) { jsonContent = jsonMatch[1].trim(); } } // 2. If there's text before JSON, try to extract JSON object if (!jsonContent.startsWith('{')) { const jsonObjectMatch = jsonContent.match(/\{[\s\S]*\}/); if (jsonObjectMatch) { jsonContent = jsonObjectMatch[0]; } } // 3. Parse the cleaned JSON planJson = JSON.parse(jsonContent); logger.info('[MikroTikAPI] Successfully parsed Plan JSON', { stepsCount: planJson.steps?.length || 0, hasDescription: !!planJson.description, hasPolicy: !!planJson.policy, }); } catch (parseError) { logger.error('[MikroTikAPI] Failed to parse LLM response as JSON', { error: parseError instanceof Error ? parseError.message : String(parseError), responsePreview: llmResponse.content?.substring(0, 800) || 'N/A', responseLength: llmResponse.content?.length || 0, }); // Return error with the actual LLM response for debugging res.status(400).json({ error: 'LLM returned invalid JSON', details: parseError instanceof Error ? parseError.message : String(parseError), llmResponse: llmResponse.content?.substring(0, 1000) || 'No content', hint: 'The LLM may have returned reasoning text instead of pure JSON. Check system prompt.', }); return; } // Build final plan with all metadata const plan: MikrotikPlan = { changeId: planJson.changeId || `change-${Date.now()}-${Math.random().toString(36).substring(7)}`, createdAt: planJson.createdAt || new Date().toISOString(), target: { deviceId, routeros: facts.routeros || '7.16', model: facts.model || 'CCR2116', ...planJson.target, }, description: planJson.description || 'AI-generated MikroTik configuration plan', assumptions: planJson.assumptions || [], steps: planJson.steps || [], policy: { noLockout: true, requireSnapshot: true, mgmtSubnets: ['192.168.1.0/24'], allowVlanFiltering: false, allowMgmtIpChange: false, ...planJson.policy, }, metadata: { intent, selectedModules: selectedModules || [], llmModel: llmResponse.modelId, llmProvider: llmResponse.provider, llmCost: llmResponse.cost, routingSummary: llmResponse.routingSummary, ...planJson.metadata, }, }; logger.info('[MikroTikAPI] Plan generated successfully', { changeId: plan.changeId, stepsCount: plan.steps.length, }); res.json(plan); } catch (error) { logger.error('[MikroTikAPI] Plan generation failed', { error: error instanceof Error ? error.message : String(error), }); res.status(400).json({ error: 'Failed to generate plan', details: error instanceof Error ? error.message : String(error), }); } }); /** * POST /mikrotik/compile * Compile Plan JSON to RouterOS commands */ router.post('/compile', async (req: Request, res: Response) => { try { const { plan, facts } = CompileRequestSchema.parse(req.body); logger.info('[MikroTikAPI] Compiling plan', { changeId: plan.changeId, steps: plan.steps.length, }); const result = await compilePlan(plan, facts); res.json({ success: true, result, }); } catch (error) { logger.error('[MikroTikAPI] Compilation failed', { error: error instanceof Error ? error.message : String(error), }); res.status(400).json({ error: 'Failed to compile plan', details: error instanceof Error ? error.message : String(error), }); } }); /** * POST /mikrotik/validate * Validate Plan JSON against policy and facts */ router.post('/validate', (req: Request, res: Response) => { try { const { plan, facts } = ValidateRequestSchema.parse(req.body); logger.info('[MikroTikAPI] Validating plan', { changeId: plan.changeId, }); const result = validatePlan(plan, facts); res.json({ validation: result, }); } catch (error) { logger.error('[MikroTikAPI] Validation failed', { error: error instanceof Error ? error.message : String(error), }); res.status(400).json({ error: 'Failed to validate plan', details: error instanceof Error ? error.message : String(error), }); } }); /** * POST /mikrotik/apply * Apply compiled commands (dry-run or execute) */ router.post('/apply', async (req: Request, res: Response) => { try { const { changeId, commands, executionMode, deviceId } = ApplyRequestSchema.parse(req.body); logger.info('[MikroTikAPI] Applying commands', { changeId, executionMode, commandCount: commands.length, }); if (executionMode === 'dryRun') { // Dry run: just validate syntax res.json({ changeId, executionMode: 'dryRun', message: 'Dry run successful. Commands validated.', commandCount: commands.length, }); } else { // TODO: Execute commands via RouterOS API // For now, mock success res.json({ changeId, executionMode: 'apply', message: 'Commands applied successfully (mock)', totalSteps: commands.length, successSteps: commands.length, failedSteps: 0, }); } } catch (error) { logger.error('[MikroTikAPI] Apply failed', { error: error instanceof Error ? error.message : String(error), }); res.status(400).json({ error: 'Failed to apply commands', details: error instanceof Error ? error.message : String(error), }); } }); export default router;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

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