#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import axios, { AxiosInstance } from "axios";
import https from "https";
// Configuration from environment
const UNIFI_URL = process.env.UNIFI_URL || "https://192.168.1.1";
const UNIFI_USERNAME = process.env.UNIFI_USERNAME || "admin";
const UNIFI_PASSWORD = process.env.UNIFI_PASSWORD || "";
const UNIFI_SITE = process.env.UNIFI_SITE || "default";
// Create axios instance with cookie jar support
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
let unifiClient: AxiosInstance;
let cookies: string[] = [];
// Authentication
async function login(): Promise<void> {
const loginResponse = await axios.post(
`${UNIFI_URL}/api/auth/login`,
{ username: UNIFI_USERNAME, password: UNIFI_PASSWORD },
{ httpsAgent, withCredentials: true }
);
const setCookies = loginResponse.headers["set-cookie"];
if (setCookies) {
cookies = setCookies.map((c: string) => c.split(";")[0]);
}
unifiClient = axios.create({
baseURL: `${UNIFI_URL}/proxy/network/api/s/${UNIFI_SITE}`,
headers: { Cookie: cookies.join("; "), "Content-Type": "application/json" },
httpsAgent,
timeout: 30000,
});
console.error("UniFi login successful");
}
const tools = [
{ name: "unifi_get_site_health", description: "Get overall site health and status", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_list_devices", description: "List all UniFi devices (APs, switches, gateways)", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["uap", "usw", "ugw", "udm"], description: "Filter by device type" } } } },
{ name: "unifi_get_device", description: "Get detailed info about a device", inputSchema: { type: "object", properties: { mac: { type: "string", description: "Device MAC" } }, required: ["mac"] } },
{ name: "unifi_restart_device", description: "Restart a UniFi device", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_list_clients", description: "List all connected clients", inputSchema: { type: "object", properties: { type: { type: "string", enum: ["all", "wireless", "wired"] } } } },
{ name: "unifi_get_client", description: "Get client details", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_block_client", description: "Block a client", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_unblock_client", description: "Unblock a client", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_reconnect_client", description: "Force client reconnect", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_list_networks", description: "List all networks", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_list_wlans", description: "List wireless networks", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_enable_wlan", description: "Enable a WLAN", inputSchema: { type: "object", properties: { wlan_id: { type: "string" } }, required: ["wlan_id"] } },
{ name: "unifi_disable_wlan", description: "Disable a WLAN", inputSchema: { type: "object", properties: { wlan_id: { type: "string" } }, required: ["wlan_id"] } },
{ name: "unifi_set_wlan_password", description: "Change WLAN password", inputSchema: { type: "object", properties: { wlan_id: { type: "string" }, password: { type: "string" } }, required: ["wlan_id", "password"] } },
{ name: "unifi_list_port_profiles", description: "List port profiles", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_get_switch_ports", description: "Get switch port config", inputSchema: { type: "object", properties: { mac: { type: "string" } }, required: ["mac"] } },
{ name: "unifi_list_firewall_rules", description: "List firewall rules", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_get_traffic_stats", description: "Get traffic statistics", inputSchema: { type: "object", properties: { timeframe: { type: "string", enum: ["hourly", "daily", "monthly"] } } } },
{ name: "unifi_get_dpi_stats", description: "Get DPI/app usage stats", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_list_alerts", description: "List recent alerts", inputSchema: { type: "object", properties: { limit: { type: "number" } } } },
{ name: "unifi_list_events", description: "List recent events", inputSchema: { type: "object", properties: { limit: { type: "number" } } } },
{ name: "unifi_list_vouchers", description: "List guest vouchers", inputSchema: { type: "object", properties: {} } },
{ name: "unifi_create_voucher", description: "Create guest vouchers", inputSchema: { type: "object", properties: { count: { type: "number" }, expire_minutes: { type: "number" }, note: { type: "string" } }, required: ["count", "expire_minutes"] } },
{ name: "unifi_create_backup", description: "Create controller backup", inputSchema: { type: "object", properties: {} } },
];
function formatBytes(bytes: number): string {
if (!bytes) return "0 B";
const k = 1024, sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
}
function formatUptime(seconds: number): string {
if (!seconds) return "N/A";
const d = Math.floor(seconds / 86400), h = Math.floor((seconds % 86400) / 3600), m = Math.floor((seconds % 3600) / 60);
return `${d}d ${h}h ${m}m`;
}
async function handleTool(name: string, args: Record<string, unknown>): Promise<string> {
try {
switch (name) {
case "unifi_get_site_health": { const r = await unifiClient.get("/stat/health"); return JSON.stringify(r.data.data, null, 2); }
case "unifi_list_devices": {
const r = await unifiClient.get("/stat/device");
let devices = r.data.data;
if (args.type) devices = devices.filter((d: any) => d.type === args.type);
return JSON.stringify(devices.map((d: any) => ({ name: d.name || d.mac, mac: d.mac, type: d.type, model: d.model, ip: d.ip, state: d.state === 1 ? "connected" : "disconnected", uptime: formatUptime(d.uptime), clients: d.num_sta })), null, 2);
}
case "unifi_get_device": { const r = await unifiClient.get("/stat/device"); const d = r.data.data.find((x: any) => x.mac.toLowerCase() === (args.mac as string).toLowerCase()); if (!d) throw new Error("Device not found"); return JSON.stringify(d, null, 2); }
case "unifi_restart_device": { await unifiClient.post("/cmd/devmgr", { cmd: "restart", mac: args.mac }); return `Device ${args.mac} restart initiated`; }
case "unifi_list_clients": {
const r = await unifiClient.get("/stat/sta");
let clients = r.data.data;
if (args.type === "wireless") clients = clients.filter((c: any) => !c.is_wired);
else if (args.type === "wired") clients = clients.filter((c: any) => c.is_wired);
return JSON.stringify(clients.map((c: any) => ({ name: c.name || c.hostname || c.mac, mac: c.mac, ip: c.ip, type: c.is_wired ? "wired" : "wireless", signal: c.signal, uptime: formatUptime(c.uptime), tx_bytes: formatBytes(c.tx_bytes), rx_bytes: formatBytes(c.rx_bytes) })), null, 2);
}
case "unifi_get_client": { const r = await unifiClient.get("/stat/sta"); const c = r.data.data.find((x: any) => x.mac.toLowerCase() === (args.mac as string).toLowerCase()); if (!c) throw new Error("Client not found"); return JSON.stringify(c, null, 2); }
case "unifi_block_client": { await unifiClient.post("/cmd/stamgr", { cmd: "block-sta", mac: args.mac }); return `Client ${args.mac} blocked`; }
case "unifi_unblock_client": { await unifiClient.post("/cmd/stamgr", { cmd: "unblock-sta", mac: args.mac }); return `Client ${args.mac} unblocked`; }
case "unifi_reconnect_client": { await unifiClient.post("/cmd/stamgr", { cmd: "kick-sta", mac: args.mac }); return `Client ${args.mac} kicked`; }
case "unifi_list_networks": { const r = await unifiClient.get("/rest/networkconf"); return JSON.stringify(r.data.data.map((n: any) => ({ id: n._id, name: n.name, purpose: n.purpose, vlan: n.vlan, subnet: n.ip_subnet })), null, 2); }
case "unifi_list_wlans": { const r = await unifiClient.get("/rest/wlanconf"); return JSON.stringify(r.data.data.map((w: any) => ({ id: w._id, name: w.name, enabled: w.enabled, security: w.security })), null, 2); }
case "unifi_enable_wlan": { await unifiClient.put(`/rest/wlanconf/${args.wlan_id}`, { enabled: true }); return `WLAN enabled`; }
case "unifi_disable_wlan": { await unifiClient.put(`/rest/wlanconf/${args.wlan_id}`, { enabled: false }); return `WLAN disabled`; }
case "unifi_set_wlan_password": { await unifiClient.put(`/rest/wlanconf/${args.wlan_id}`, { x_passphrase: args.password }); return `WLAN password updated`; }
case "unifi_list_port_profiles": { const r = await unifiClient.get("/rest/portconf"); return JSON.stringify(r.data.data, null, 2); }
case "unifi_get_switch_ports": { const r = await unifiClient.get("/stat/device"); const d = r.data.data.find((x: any) => x.mac.toLowerCase() === (args.mac as string).toLowerCase()); if (!d) throw new Error("Switch not found"); return JSON.stringify(d.port_table, null, 2); }
case "unifi_list_firewall_rules": { const r = await unifiClient.get("/rest/firewallrule"); return JSON.stringify(r.data.data, null, 2); }
case "unifi_get_traffic_stats": { const r = await unifiClient.get(`/stat/report/${args.timeframe || "daily"}.site`); return JSON.stringify(r.data.data, null, 2); }
case "unifi_get_dpi_stats": { const r = await unifiClient.get("/stat/dpi"); return JSON.stringify(r.data.data, null, 2); }
case "unifi_list_alerts": { const r = await unifiClient.get("/stat/alarm", { params: { _limit: args.limit || 50 } }); return JSON.stringify(r.data.data, null, 2); }
case "unifi_list_events": { const r = await unifiClient.get("/stat/event", { params: { _limit: args.limit || 50 } }); return JSON.stringify(r.data.data, null, 2); }
case "unifi_list_vouchers": { const r = await unifiClient.get("/stat/voucher"); return JSON.stringify(r.data.data, null, 2); }
case "unifi_create_voucher": { const r = await unifiClient.post("/cmd/hotspot", { cmd: "create-voucher", n: args.count, expire: args.expire_minutes, note: args.note || "" }); return JSON.stringify(r.data.data, null, 2); }
case "unifi_create_backup": { await unifiClient.post("/cmd/backup", { cmd: "backup" }); return "Backup initiated"; }
default: throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
if (error.response) throw new Error(`UniFi API error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
throw error;
}
}
const server = new Server({ name: "unifi-mcp", version: "1.0.0" }, { capabilities: { tools: {} } });
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(name, args as Record<string, unknown>);
return { content: [{ type: "text", text: result }] };
} catch (error: any) {
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
}
});
async function main() {
try {
await login();
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("UniFi MCP server running");
} catch (error: any) {
console.error("Failed to start:", error.message);
process.exit(1);
}
}
main().catch(console.error);