Skip to main content
Glama
service.py14.5 kB
"""Technitium DNS Server service implementation.""" import logging from typing import Any from fastmcp import FastMCP from ...core.client import HTTPClient from ...core.health import HealthStatus, ServiceHealth from ..base import ServiceBase logger = logging.getLogger(__name__) class TechnitiumConfig: """Technitium DNS configuration.""" def __init__( self, enabled: bool = False, url: str = "", api_token: str = "", ): self.enabled = enabled self.url = url self.api_token = api_token class TechnitiumService(ServiceBase): """Technitium DNS Server service for authoritative and recursive DNS.""" name = "technitium" def __init__(self, config: TechnitiumConfig) -> None: """Initialize Technitium service.""" super().__init__(config) self.config: TechnitiumConfig = config def _create_client(self) -> HTTPClient: """Create HTTP client for Technitium API.""" return HTTPClient( base_url=f"{self.config.url}/api", timeout=30.0, verify_ssl=False, ) async def _api_call(self, endpoint: str, params: dict[str, Any] | None = None) -> dict[str, Any]: """Make authenticated API call to Technitium. Args: endpoint: API endpoint params: Query parameters Returns: API response data """ if params is None: params = {} params["token"] = self.config.api_token response = await self.client.get(endpoint, params=params) response.raise_for_status() data = response.json() if data.get("status") == "error": raise Exception(data.get("errorMessage", "Unknown error")) return data.get("response", data) async def health_check(self) -> ServiceHealth: """Check Technitium service health.""" try: data = await self._api_call("/dashboard/stats/get") return ServiceHealth( name=self.name, status=HealthStatus.HEALTHY, message="Technitium DNS running", details={ "total_queries": data.get("stats", {}).get("totalQueries", 0), "zones": data.get("stats", {}).get("zones", 0), "cached_entries": data.get("stats", {}).get("cachedEntries", 0), }, ) except Exception as e: logger.error(f"Technitium health check failed: {e}") return ServiceHealth( name=self.name, status=HealthStatus.UNHEALTHY, message=str(e), ) def register_tools(self, mcp: FastMCP) -> None: """Register Technitium tools with MCP.""" @mcp.tool() async def technitium_get_stats() -> dict[str, Any]: """Get Technitium DNS server statistics. Returns: Server statistics including queries, cache, zones """ data = await self._api_call("/dashboard/stats/get") stats = data.get("stats", {}) return { "total_queries": stats.get("totalQueries", 0), "total_no_error": stats.get("totalNoError", 0), "total_server_failure": stats.get("totalServerFailure", 0), "total_nx_domain": stats.get("totalNxDomain", 0), "total_refused": stats.get("totalRefused", 0), "total_blocked": stats.get("totalBlocked", 0), "total_cached": stats.get("totalCached", 0), "zones": stats.get("zones", 0), "cached_entries": stats.get("cachedEntries", 0), "allowed_zones": stats.get("allowedZones", 0), "blocked_zones": stats.get("blockedZones", 0), } @mcp.tool() async def technitium_list_zones() -> list[dict[str, Any]]: """List all DNS zones. Returns: List of configured DNS zones """ data = await self._api_call("/zones/list") zones = [] for zone in data.get("zones", []): zones.append({ "name": zone.get("name"), "type": zone.get("type"), "disabled": zone.get("disabled", False), "internal": zone.get("internal", False), }) return zones @mcp.tool() async def technitium_get_zone_records(zone: str) -> list[dict[str, Any]]: """Get all records in a DNS zone. Args: zone: Zone name (e.g., 'example.com') Returns: List of DNS records in the zone """ data = await self._api_call("/zones/records/get", {"domain": zone, "zone": zone}) records = [] for record in data.get("records", []): records.append({ "name": record.get("name"), "type": record.get("type"), "ttl": record.get("ttl"), "rdata": record.get("rData"), "disabled": record.get("disabled", False), }) return records @mcp.tool() async def technitium_create_zone( zone: str, zone_type: str = "Primary" ) -> dict[str, Any]: """Create a new DNS zone. Args: zone: Zone name (e.g., 'example.com') zone_type: Zone type (Primary, Secondary, Stub, Forwarder) Returns: Result of the operation """ data = await self._api_call("/zones/create", { "zone": zone, "type": zone_type, }) return {"success": True, "zone": zone, "type": zone_type} @mcp.tool() async def technitium_add_record( zone: str, name: str, record_type: str, value: str, ttl: int = 3600 ) -> dict[str, Any]: """Add a DNS record to a zone. Args: zone: Zone name name: Record name (e.g., 'www' or '@' for apex) record_type: Record type (A, AAAA, CNAME, MX, TXT, etc.) value: Record value ttl: Time to live in seconds Returns: Result of the operation """ # Build the full domain name if name == "@": domain = zone else: domain = f"{name}.{zone}" params = { "zone": zone, "domain": domain, "type": record_type, "ttl": ttl, } # Different record types have different value parameters if record_type == "A": params["ipAddress"] = value elif record_type == "AAAA": params["ipAddress"] = value elif record_type == "CNAME": params["cname"] = value elif record_type == "MX": params["exchange"] = value params["preference"] = 10 elif record_type == "TXT": params["text"] = value elif record_type == "NS": params["nameServer"] = value elif record_type == "PTR": params["ptrName"] = value else: params["value"] = value await self._api_call("/zones/records/add", params) return {"success": True, "zone": zone, "name": name, "type": record_type, "value": value} @mcp.tool() async def technitium_delete_record( zone: str, name: str, record_type: str, value: str ) -> dict[str, Any]: """Delete a DNS record from a zone. Args: zone: Zone name name: Record name record_type: Record type value: Record value to delete Returns: Result of the operation """ if name == "@": domain = zone else: domain = f"{name}.{zone}" params = { "zone": zone, "domain": domain, "type": record_type, } if record_type == "A" or record_type == "AAAA": params["ipAddress"] = value elif record_type == "CNAME": params["cname"] = value elif record_type == "MX": params["exchange"] = value elif record_type == "TXT": params["text"] = value else: params["value"] = value await self._api_call("/zones/records/delete", params) return {"success": True, "zone": zone, "name": name, "type": record_type, "deleted": value} @mcp.tool() async def technitium_delete_zone(zone: str) -> dict[str, Any]: """Delete a DNS zone. Args: zone: Zone name to delete Returns: Result of the operation """ await self._api_call("/zones/delete", {"zone": zone}) return {"success": True, "zone": zone, "deleted": True} @mcp.tool() async def technitium_enable_zone(zone: str) -> dict[str, Any]: """Enable a disabled DNS zone. Args: zone: Zone name Returns: Result of the operation """ await self._api_call("/zones/enable", {"zone": zone}) return {"success": True, "zone": zone, "enabled": True} @mcp.tool() async def technitium_disable_zone(zone: str) -> dict[str, Any]: """Disable a DNS zone. Args: zone: Zone name Returns: Result of the operation """ await self._api_call("/zones/disable", {"zone": zone}) return {"success": True, "zone": zone, "enabled": False} @mcp.tool() async def technitium_get_cache_stats() -> dict[str, Any]: """Get DNS cache statistics. Returns: Cache statistics """ data = await self._api_call("/cache/list") return { "total_entries": len(data.get("entries", [])), "entries": data.get("entries", [])[:50], # Limit to 50 } @mcp.tool() async def technitium_flush_cache() -> dict[str, Any]: """Flush the DNS cache. Returns: Result of the operation """ await self._api_call("/cache/flush") return {"success": True, "message": "DNS cache flushed"} @mcp.tool() async def technitium_get_query_logs( page: int = 1, entries_per_page: int = 100 ) -> dict[str, Any]: """Get DNS query logs. Args: page: Page number entries_per_page: Number of entries per page Returns: Query log entries """ data = await self._api_call("/logs/query", { "pageNumber": page, "entriesPerPage": entries_per_page, }) return { "page": page, "total_pages": data.get("totalPages", 1), "total_entries": data.get("totalEntries", 0), "entries": data.get("entries", []), } @mcp.tool() async def technitium_get_blocked_list() -> list[dict[str, Any]]: """Get list of blocked zones/domains. Returns: List of blocked zones """ data = await self._api_call("/zones/list") blocked = [] for zone in data.get("zones", []): if zone.get("type") == "Block": blocked.append({ "name": zone.get("name"), "disabled": zone.get("disabled", False), }) return blocked @mcp.tool() async def technitium_block_domain(domain: str) -> dict[str, Any]: """Block a domain. Args: domain: Domain to block Returns: Result of the operation """ await self._api_call("/zones/create", { "zone": domain, "type": "Block", }) return {"success": True, "domain": domain, "action": "blocked"} @mcp.tool() async def technitium_get_forwarders() -> list[dict[str, Any]]: """Get configured DNS forwarders. Returns: List of DNS forwarders """ data = await self._api_call("/settings/get") forwarders = data.get("forwarders", []) return [{"address": f} for f in forwarders] @mcp.tool() async def technitium_set_forwarders(forwarders: list[str]) -> dict[str, Any]: """Set DNS forwarders. Args: forwarders: List of forwarder addresses (e.g., ['8.8.8.8', '1.1.1.1']) Returns: Result of the operation """ await self._api_call("/settings/set", { "forwarders": ",".join(forwarders), }) return {"success": True, "forwarders": forwarders} logger.info("Technitium DNS tools registered")

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/HavartiBard/homelab-mcp'

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