// unifi's tools — its own code (novox/hq ADR 0039), importing unifi's own client. They return // structured data; the mesh serves them through the sdk's tool harness. unifi is tools-only (no // events entrypoint): the controller does not push lifecycle events the mesh consumes, so this // module reads its resources — port forwards, networks, devices, clients — and exposes them, and stops there. import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"; import { UnifiApiClient, type UnifiNetwork, type UnifiPortForward } from "../client.js"; function summarizeNetwork(n: UnifiNetwork): Record { const dns = [n.dhcpd_dns_1, n.dhcpd_dns_2, n.dhcpd_dns_3, n.dhcpd_dns_4].filter((s): s is string => !!s); return { id: n._id, name: n.name, purpose: n.purpose, subnet: n.ip_subnet ?? null, dhcp: n.dhcpd_enabled ?? null, // What DHCP hands out as DNS: the listed servers when set, otherwise the gateway itself. dhcp_dns: n.dhcpd_dns_enabled ? dns : "the gateway", }; } function summarizePortForward(r: UnifiPortForward): Record { return { id: r._id, name: r.name, enabled: r.enabled, src: r.src || "any", dst_port: r.dst_port, forward: `${r.fwd}:${r.fwd_port}`, proto: r.proto, }; } export function getUnifiTools(unifi: UnifiApiClient): ToolDefinition[] { return [ { name: "unifi_reachable", description: "Health probe: whether the UniFi controller answers and can be logged into. Never fails.", input: {}, run: async () => unifi.reachable(), }, { name: "unifi_list_port_forwards", description: "List all port-forwarding rules on the UniFi gateway.", input: {}, run: async () => { const rules = await unifi.listPortForwards(); return { count: rules.length, rules: rules.map(summarizePortForward) }; }, }, { name: "unifi_create_port_forward", description: "Create a port-forwarding rule on the UniFi gateway.", input: { name: { type: "string", description: "rule name (e.g. 'Redis')" }, dst_port: { type: "string", description: "external/WAN port (e.g. '6379')" }, fwd: { type: "string", description: "forward to LAN IP (e.g. '192.0.2.10')" }, fwd_port: { type: "string", description: "forward to port (e.g. '6379')" }, proto: { type: "string", description: "protocol: tcp, udp or tcp_udp (default tcp)" }, src: { type: "string", description: "source IP/CIDR restriction (omitted = any)" }, enabled: { type: "boolean", description: "enable the rule (default true)" }, }, run: async (args) => { const rule = await unifi.createPortForward({ name: String(args.name), dst_port: String(args.dst_port), fwd: String(args.fwd), fwd_port: String(args.fwd_port), proto: args.proto ? String(args.proto) : "tcp", src: args.src ? String(args.src) : "any", enabled: args.enabled === undefined ? true : Boolean(args.enabled), pfwd_interface: "wan", log: false, }); return { created: summarizePortForward(rule) }; }, }, { name: "unifi_toggle_port_forward", description: "Enable or disable a port-forwarding rule by id (see unifi_list_port_forwards).", input: { id: { type: "string", description: "the port-forward rule id" }, enabled: { type: "boolean", description: "true to enable, false to disable" }, }, run: async (args) => { const enabled = Boolean(args.enabled); const rule = await unifi.updatePortForward(String(args.id), { enabled }); return { updated: summarizePortForward(rule) }; }, }, { name: "unifi_delete_port_forward", description: "Delete a port-forwarding rule by id. Requires confirm: true.", input: { id: { type: "string", description: "the port-forward rule id" }, confirm: { type: "boolean", description: "must be true to confirm deletion" }, }, run: async (args) => { if (!args.confirm) return { deleted: false, reason: "set confirm: true to delete" }; await unifi.deletePortForward(String(args.id)); return { deleted: true, id: String(args.id) }; }, }, { name: "unifi_list_networks", description: "List the networks the UniFi controller manages, with the DNS servers each one's DHCP hands out.", input: {}, run: async () => { const nets = await unifi.listNetworks(); return { count: nets.length, networks: nets.map(summarizeNetwork) }; }, }, { name: "unifi_set_network_dns", description: "Set the DNS servers a network's DHCP hands out to its devices (see unifi_list_networks for ids). " + "Up to four addresses, comma-separated; \"gateway\" hands out the gateway itself. Devices pick it up when they renew.", input: { id: { type: "string", description: "the network id" }, dns: { type: "string", description: "comma-separated DNS server addresses, or \"gateway\"" }, }, run: async (args) => { const asked = String(args.dns ?? "").trim(); if (!asked) return { updated: false, reason: "say dns: addresses, or \"gateway\"" }; const servers = asked === "gateway" ? [] : asked.split(",").map((s) => s.trim()).filter(Boolean); if (servers.length > 4) return { updated: false, reason: "DHCP hands out at most four DNS servers" }; const fields: Partial = { dhcpd_dns_enabled: servers.length > 0, dhcpd_dns_1: servers[0] ?? "", dhcpd_dns_2: servers[1] ?? "", dhcpd_dns_3: servers[2] ?? "", dhcpd_dns_4: servers[3] ?? "", }; const net = await unifi.updateNetwork(String(args.id), fields); return { updated: summarizeNetwork(net) }; }, }, { name: "unifi_list_devices", description: "List the network devices (APs, switches, gateways) the UniFi controller manages.", input: {}, run: async () => { const devices = await unifi.listDevices(); return { count: devices.length, devices: devices.map((d) => ({ name: d.name || d.mac, model: d.model, ip: d.ip, state: d.state === 1 ? "online" : "offline", adopted: d.adopted, version: d.version, uptimeHours: d.uptime ? Math.floor(d.uptime / 3600) : 0, })), }; }, }, { name: "unifi_list_clients", description: "List the connected network clients — wired and wireless.", input: {}, run: async () => { const clients = await unifi.listClients(); return { count: clients.length, clients: clients .slice() .sort((a, b) => (a.name || a.hostname || a.ip).localeCompare(b.name || b.hostname || b.ip)) .map((c) => ({ name: c.name || c.hostname || c.mac, ip: c.ip, mac: c.mac, type: c.is_wired ? "wired" : "wifi", network: c.network, })), }; }, }, ]; } // The tools exist only when credentials are configured; without them, unifi contributes none rather // than failing the whole runtime. registerModuleTools("unifi", (env) => { try { return getUnifiTools(UnifiApiClient.fromEnv(env)); } catch { return []; } });