OPNsense MCP fixes: O1,O2,O4,O6 - narrow except, health check, register host, VLAN tools
This commit is contained in:
@@ -10,6 +10,7 @@ from opnsense_mcp.tools import (
|
|||||||
dns,
|
dns,
|
||||||
firewall,
|
firewall,
|
||||||
firmware,
|
firmware,
|
||||||
|
hosts,
|
||||||
interfaces,
|
interfaces,
|
||||||
logs,
|
logs,
|
||||||
nat,
|
nat,
|
||||||
@@ -21,6 +22,7 @@ from opnsense_mcp.tools import (
|
|||||||
|
|
||||||
def register_all(mcp: FastMCP) -> None:
|
def register_all(mcp: FastMCP) -> None:
|
||||||
system.register(mcp)
|
system.register(mcp)
|
||||||
|
hosts.register(mcp)
|
||||||
interfaces.register(mcp)
|
interfaces.register(mcp)
|
||||||
firewall.register(mcp)
|
firewall.register(mcp)
|
||||||
aliases.register(mcp)
|
aliases.register(mcp)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import json
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from opnsense_mcp.audit import audit_log
|
from opnsense_mcp.audit import audit_log
|
||||||
from opnsense_mcp.client import get_client
|
from opnsense_mcp.client import OpnsenseAPIError, get_client
|
||||||
from opnsense_mcp.config import settings
|
from opnsense_mcp.config import settings
|
||||||
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ def register(mcp: FastMCP) -> None:
|
|||||||
result = client.search_get(
|
result = client.search_get(
|
||||||
"dhcpv4", "leases", "searchLease", search_phrase, row_count
|
"dhcpv4", "leases", "searchLease", search_phrase, row_count
|
||||||
)
|
)
|
||||||
except Exception:
|
except OpnsenseAPIError:
|
||||||
result = client.search_grid(
|
result = client.search_grid(
|
||||||
"dhcpv4", "leases", "searchLease", search_phrase, row_count
|
"dhcpv4", "leases", "searchLease", search_phrase, row_count
|
||||||
)
|
)
|
||||||
@@ -65,7 +65,7 @@ def register(mcp: FastMCP) -> None:
|
|||||||
result = client.search_grid(
|
result = client.search_grid(
|
||||||
"kea", "dhcpv4", "searchReservation", search_phrase, row_count
|
"kea", "dhcpv4", "searchReservation", search_phrase, row_count
|
||||||
)
|
)
|
||||||
except Exception:
|
except OpnsenseAPIError:
|
||||||
result = client.search_grid(
|
result = client.search_grid(
|
||||||
"dhcpv4", "settings", "searchReservation", search_phrase, row_count
|
"dhcpv4", "settings", "searchReservation", search_phrase, row_count
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from opnsense_mcp.audit import audit_log
|
from opnsense_mcp.audit import audit_log
|
||||||
from opnsense_mcp.client import get_client
|
from opnsense_mcp.client import OpnsenseAPIError, get_client
|
||||||
from opnsense_mcp.config import settings
|
from opnsense_mcp.config import settings
|
||||||
from opnsense_mcp.guards import (
|
from opnsense_mcp.guards import (
|
||||||
GuardError,
|
GuardError,
|
||||||
@@ -38,7 +38,7 @@ def register(mcp: FastMCP) -> None:
|
|||||||
result = client.search_get(
|
result = client.search_get(
|
||||||
"firewall", "filter", "searchRule", search_phrase, row_count, **extra
|
"firewall", "filter", "searchRule", search_phrase, row_count, **extra
|
||||||
)
|
)
|
||||||
except Exception:
|
except OpnsenseAPIError:
|
||||||
result = client.search_grid(
|
result = client.search_grid(
|
||||||
"firewall", "filter", "searchRule", search_phrase, row_count, **extra
|
"firewall", "filter", "searchRule", search_phrase, row_count, **extra
|
||||||
)
|
)
|
||||||
|
|||||||
125
src/opnsense_mcp/tools/hosts.py
Normal file
125
src/opnsense_mcp/tools/hosts.py
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
"""Host registration composite tool: DHCP reservation + DNS override."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
|
from opnsense_mcp.audit import audit_log
|
||||||
|
from opnsense_mcp.client import OpnsenseAPIError, get_client
|
||||||
|
from opnsense_mcp.config import settings
|
||||||
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
||||||
|
|
||||||
|
|
||||||
|
def register(mcp: FastMCP) -> None:
|
||||||
|
@mcp.tool()
|
||||||
|
def opnsense_register_host(
|
||||||
|
mac: str,
|
||||||
|
ip: str,
|
||||||
|
hostname: str,
|
||||||
|
domain: str = "",
|
||||||
|
description: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Register a host by creating a DHCP static mapping and DNS host override, then reconfigure both services.
|
||||||
|
|
||||||
|
Returns a combined result with dhcp_result, dns_result, dhcp_reconfigure, dns_reconfigure.
|
||||||
|
"""
|
||||||
|
payload = {
|
||||||
|
"mac": mac,
|
||||||
|
"ip": ip,
|
||||||
|
"hostname": hostname,
|
||||||
|
"domain": domain,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.dry_run:
|
||||||
|
return json.dumps(
|
||||||
|
is_dry_run_response("register_host", payload), indent=2
|
||||||
|
)
|
||||||
|
|
||||||
|
require_writes("register_host", payload)
|
||||||
|
client = get_client()
|
||||||
|
|
||||||
|
# ── 1. Add DHCP static mapping (try Kea, fall back to ISC) ──
|
||||||
|
dhcp_payload = {
|
||||||
|
"reservation": {
|
||||||
|
"hw_address": mac,
|
||||||
|
"ip_address": ip,
|
||||||
|
"hostname": hostname,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dhcp_result = {}
|
||||||
|
try:
|
||||||
|
dhcp_result = client.post(
|
||||||
|
"kea", "dhcpv4", "addReservation", data=dhcp_payload
|
||||||
|
)
|
||||||
|
except OpnsenseAPIError:
|
||||||
|
try:
|
||||||
|
dhcp_result = client.post(
|
||||||
|
"dhcpv4", "settings", "addReservation", data=dhcp_payload
|
||||||
|
)
|
||||||
|
except OpnsenseAPIError as exc:
|
||||||
|
dhcp_result = {
|
||||||
|
"error": str(exc),
|
||||||
|
"status_code": exc.status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 2. Add DNS host override ──
|
||||||
|
dns_host = {
|
||||||
|
"enabled": "1",
|
||||||
|
"hostname": hostname,
|
||||||
|
"domain": domain,
|
||||||
|
"rr": "A",
|
||||||
|
"server": ip,
|
||||||
|
"description": description,
|
||||||
|
}
|
||||||
|
dns_payload = {"host": dns_host}
|
||||||
|
dns_result = {}
|
||||||
|
try:
|
||||||
|
dns_result = client.post(
|
||||||
|
"unbound", "settings", "addHostOverride", data=dns_payload
|
||||||
|
)
|
||||||
|
except OpnsenseAPIError as exc:
|
||||||
|
dns_result = {
|
||||||
|
"error": str(exc),
|
||||||
|
"status_code": exc.status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 3. Reconfigure DHCP service ──
|
||||||
|
dhcp_reconf = {}
|
||||||
|
try:
|
||||||
|
dhcp_reconf = client.post("dhcpv4", "service", "reconfigure")
|
||||||
|
except OpnsenseAPIError as exc:
|
||||||
|
dhcp_reconf = {
|
||||||
|
"error": str(exc),
|
||||||
|
"status_code": exc.status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── 4. Reconfigure Unbound DNS ──
|
||||||
|
dns_reconf = {}
|
||||||
|
try:
|
||||||
|
dns_reconf = client.post("unbound", "service", "reconfigure")
|
||||||
|
except OpnsenseAPIError as exc:
|
||||||
|
dns_reconf = {
|
||||||
|
"error": str(exc),
|
||||||
|
"status_code": exc.status_code,
|
||||||
|
}
|
||||||
|
|
||||||
|
audit_log("register_host", payload | {
|
||||||
|
"dhcp_result": dhcp_result,
|
||||||
|
"dns_result": dns_result,
|
||||||
|
})
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"host": {"mac": mac, "ip": ip, "hostname": hostname, "domain": domain},
|
||||||
|
"dhcp_result": dhcp_result,
|
||||||
|
"dns_result": dns_result,
|
||||||
|
"dhcp_reconfigure": dhcp_reconf,
|
||||||
|
"dns_reconfigure": dns_reconf,
|
||||||
|
},
|
||||||
|
indent=2,
|
||||||
|
default=str,
|
||||||
|
)
|
||||||
@@ -59,3 +59,45 @@ def register(mcp: FastMCP) -> None:
|
|||||||
client = get_client()
|
client = get_client()
|
||||||
result = client.get("diagnostics", "interface", "getVipStatus")
|
result = client.get("diagnostics", "interface", "getVipStatus")
|
||||||
return json.dumps(result, indent=2, default=str)
|
return json.dumps(result, indent=2, default=str)
|
||||||
|
@mcp.tool()
|
||||||
|
def opnsense_add_vlan(
|
||||||
|
interface: str = "opt1",
|
||||||
|
vlan_tag: int = 0,
|
||||||
|
description: str = "",
|
||||||
|
) -> str:
|
||||||
|
"""Add a new VLAN interface on a parent interface (e.g. opt1). Returns the new UUID."""
|
||||||
|
from opnsense_mcp.audit import audit_log
|
||||||
|
from opnsense_mcp.config import settings
|
||||||
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
||||||
|
|
||||||
|
vlan = {
|
||||||
|
"vlanif": interface,
|
||||||
|
"tag": str(vlan_tag),
|
||||||
|
"descr": description,
|
||||||
|
}
|
||||||
|
payload = {"vlan": vlan}
|
||||||
|
if settings.dry_run:
|
||||||
|
return json.dumps(is_dry_run_response("add_vlan", payload), indent=2)
|
||||||
|
require_writes("add_vlan", payload)
|
||||||
|
client = get_client()
|
||||||
|
result = client.post(
|
||||||
|
"interfaces", "vlan_settings", "addItem", data=payload
|
||||||
|
)
|
||||||
|
audit_log("add_vlan", {"vlan": vlan, "result": result})
|
||||||
|
return json.dumps(result, indent=2, default=str)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def opnsense_delete_vlan(uuid: str) -> str:
|
||||||
|
"""Delete a VLAN interface configuration by UUID."""
|
||||||
|
from opnsense_mcp.audit import audit_log
|
||||||
|
from opnsense_mcp.config import settings
|
||||||
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
||||||
|
|
||||||
|
payload = {"uuid": uuid}
|
||||||
|
if settings.dry_run:
|
||||||
|
return json.dumps(is_dry_run_response("delete_vlan", payload), indent=2)
|
||||||
|
require_writes("delete_vlan", payload)
|
||||||
|
client = get_client()
|
||||||
|
result = client.post("interfaces", "vlan_settings", "delItem", uuid)
|
||||||
|
audit_log("delete_vlan", payload | {"result": result})
|
||||||
|
return json.dumps(result, indent=2, default=str)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import Any
|
|||||||
from mcp.server.fastmcp import FastMCP
|
from mcp.server.fastmcp import FastMCP
|
||||||
|
|
||||||
from opnsense_mcp.audit import audit_log
|
from opnsense_mcp.audit import audit_log
|
||||||
from opnsense_mcp.client import get_client
|
from opnsense_mcp.client import OpnsenseAPIError, get_client
|
||||||
from opnsense_mcp.config import settings
|
from opnsense_mcp.config import settings
|
||||||
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
from opnsense_mcp.guards import is_dry_run_response, require_writes
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ def _search_nat(nat_type: str, search_phrase: str, row_count: int) -> str:
|
|||||||
client = get_client()
|
client = get_client()
|
||||||
try:
|
try:
|
||||||
result = client.search_get("firewall", nat_type, "searchRule", search_phrase, row_count)
|
result = client.search_get("firewall", nat_type, "searchRule", search_phrase, row_count)
|
||||||
except Exception:
|
except OpnsenseAPIError:
|
||||||
result = client.search_grid("firewall", nat_type, "searchRule", search_phrase, row_count)
|
result = client.search_grid("firewall", nat_type, "searchRule", search_phrase, row_count)
|
||||||
return json.dumps(result, indent=2, default=str)
|
return json.dumps(result, indent=2, default=str)
|
||||||
|
|
||||||
|
|||||||
@@ -67,3 +67,39 @@ def register(mcp: FastMCP) -> None:
|
|||||||
resources = client.get("diagnostics", "system", "systemResources")
|
resources = client.get("diagnostics", "system", "systemResources")
|
||||||
memory = client.get("diagnostics", "system", "memory")
|
memory = client.get("diagnostics", "system", "memory")
|
||||||
return json.dumps({"resources": resources, "memory": memory}, indent=2, default=str)
|
return json.dumps({"resources": resources, "memory": memory}, indent=2, default=str)
|
||||||
|
@mcp.tool()
|
||||||
|
def opnsense_health_check() -> str:
|
||||||
|
"""Comprehensive health snapshot: gateways, interfaces, services, system resources, firmware, WireGuard status."""
|
||||||
|
client = get_client()
|
||||||
|
try:
|
||||||
|
gateways = client.get("routes", "gateway", "status")
|
||||||
|
except Exception:
|
||||||
|
gateways = {"error": "gateway check failed"}
|
||||||
|
try:
|
||||||
|
interfaces = client.get("interfaces", "overview", "interfacesInfo")
|
||||||
|
except Exception:
|
||||||
|
interfaces = {"error": "interface check failed"}
|
||||||
|
try:
|
||||||
|
services = client.search_grid("core", "service", "search", "", 50)
|
||||||
|
except Exception:
|
||||||
|
services = {"error": "service check failed"}
|
||||||
|
try:
|
||||||
|
resources = client.get("diagnostics", "system", "systemResources")
|
||||||
|
except Exception:
|
||||||
|
resources = {"error": "resource check failed"}
|
||||||
|
try:
|
||||||
|
firmware = client.get("core", "firmware", "status")
|
||||||
|
except Exception:
|
||||||
|
firmware = {"error": "firmware check failed"}
|
||||||
|
try:
|
||||||
|
wg_status = client.get("wireguard", "service", "show")
|
||||||
|
except Exception:
|
||||||
|
wg_status = {"error": "WireGuard check failed or not installed"}
|
||||||
|
return json.dumps({
|
||||||
|
"gateways": gateways,
|
||||||
|
"interfaces": interfaces,
|
||||||
|
"services": services,
|
||||||
|
"resources": resources,
|
||||||
|
"firmware": firmware,
|
||||||
|
"wireguard": wg_status,
|
||||||
|
}, indent=2, default=str)
|
||||||
|
|||||||
Reference in New Issue
Block a user