UniFi MCP Server
README.md
# UniFi MCP Server
> MCP server providing Claude Code with full UniFi network management capabilities -- devices, clients, ports, bandwidth auditing, firewall policies, and traffic rules -- all through natural language.
[](https://opensource.org/licenses/MIT)
[](https://github.com/philipvanlewis/unifi-mcp-server/releases)
[](https://modelcontextprotocol.io/)
[](https://www.typescriptlang.org/)
[](https://nodejs.org/)
[](https://ui.com/)
---
## Overview
The UniFi MCP Server connects Claude Code to one or more UniFi network controllers via the UDM REST API. It exposes **19 tools** that let you query, audit, and configure your network infrastructure conversationally.
**Key capabilities:**
- **Self-healing authentication** -- automatic re-auth on expiry or 401, with proactive refresh and mutex protection against concurrent auth storms
- **Automated TOTP** -- generates MFA codes from a stored seed, no manual authenticator app interaction
- **19 management tools** covering devices, clients, ports, port profiles, networks, bandwidth auditing, traffic routes, and firewall policies
- **Encrypted session persistence** using AES-256-GCM
- **Resilient API client** with 429 throttle handling, 5xx exponential backoff, and 401 transparent re-auth
- **Dual-controller support** -- cloud-hosted controllers (two-step MFA) and UDM Pro Max (single-step MFA)
### Architecture
```mermaid
graph LR
CC[Claude Code] <-->|stdio| MCP[MCP Server<br/>StdioServerTransport]
MCP --> Tools[19 Tools<br/>Zod-validated inputs]
Tools --> UC[UniFi Client<br/>Axios + retry interceptors]
UC --> SM[Session Manager<br/>Self-healing auth]
SM --> SC[Session Cache<br/>AES-256-GCM]
SM --> TOTP[TOTP Generator<br/>RFC 6238]
UC -->|HTTPS| API[UniFi Controller<br/>REST API]
style CC fill:#4A90D9,color:#fff
style MCP fill:#6C5CE7,color:#fff
style Tools fill:#00B894,color:#fff
style UC fill:#FDCB6E,color:#000
style SM fill:#E17055,color:#fff
style API fill:#2D3436,color:#fff
```
---
## Features
### Self-Healing Authentication
The server never requires manual re-authentication during operation. The session manager implements a multi-layered strategy:
| Layer | Trigger | Behavior |
|-------|---------|----------|
| **Proactive refresh** | Session < 15 min remaining | Fire-and-forget re-auth in the background; current request uses existing session |
| **Expired session** | Session TTL = 0 | Blocking re-auth before the request proceeds |
| **401 retry** | Controller returns HTTP 401 | Interceptor triggers re-auth, then transparently retries the original request |
| **Mutex protection** | Concurrent requests during auth | All callers await the same in-flight auth promise (no auth storms) |
| **Failure circuit breaker** | 3 consecutive auth failures | Auth disabled until server restart (prevents credential lockout) |
### Two-Step MFA with Automated TOTP
Cloud-hosted UniFi controllers require two-step MFA:
```mermaid
sequenceDiagram
participant S as MCP Server
participant C as UniFi Controller
participant T as TOTP Generator
S->>C: POST /api/auth/login {username, password}
C-->>S: 200 MFA_AUTH_REQUIRED + UBIC_2FA cookie
S->>T: Generate TOTP from base32 seed
T-->>S: 6-digit code
S->>C: POST /api/auth/login {username, password, token}<br/>Cookie: UBIC_2FA=...
C-->>S: 200 + Set-Cookie: TOKEN=jwt...
S->>S: Parse JWT for CSRF token + expiry
S->>S: Encrypt and persist session to disk
```
For UDM Pro Max controllers, authentication completes in a single step (username + password + TOTP in one request).
### Resilient API Client
Every API call passes through axios interceptors that handle transient failures:
- **429 Too Many Requests** -- waits for `Retry-After` header (or 5s default), then retries
- **5xx Server Error** -- exponential backoff (1s, 2s, 4s) up to 3 retries
- **401 Unauthorized** -- triggers self-healing re-auth, refreshes headers, retries the request once
---
## Tools Reference
### Device Tools (3)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-device-list` | List all devices (switches, APs, gateways) with name, model, IP, state, firmware | `type`: filter by `usw`, `uap`, `ugw`, `udm`, or `all` |
| `unifi-device-detail` | Full detail for a single device by MAC address, including port count and uptime | `mac`: device MAC address |
| `unifi-device-ports` | All ports on a switch with status, speed, PoE, connected MACs, and assigned port profile | `mac`: switch MAC address |
### Client Tools (3)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-client-list` | List active clients connected to the network | `filter`: `all`, `wired`, or `wireless`; `limit`: max results (1-500) |
| `unifi-client-detail` | Detailed info for a specific client by MAC | `mac`: client MAC address |
| `unifi-client-history` | All known clients including offline (historical database) | `limit`: max results (1-500) |
### Port Profile Tools (6 -- 2 read, 4 write)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-port-profiles` | List all port profiles (bandwidth/VLAN configs) | -- |
| `unifi-port-profile-detail` | Full raw detail for a port profile by ID | `profileId` |
| `unifi-port-profile-create` | Create a new port profile with bandwidth limits | `name`, `forward`, `nativeNetworkId`, `egressRateKbps`, `ingressRateKbps`, `confirm` |
| `unifi-port-profile-update` | Update an existing profile (fetch-then-merge) | `profileId`, + any field to change, `confirm` |
| `unifi-port-profile-delete` | Delete a port profile (fails if ports still assigned) | `profileId`, `confirm` |
| `unifi-port-set-profile` | Assign a port profile to a specific switch port | `deviceMac`, `portIdx`, `profileId`, `portName`, `confirm` |
All write tools require `confirm: true` to execute. Set `confirm: false` for a dry-run preview of what would change.
### Network Tools (2)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-network-list` | List all configured networks (VLANs, corporate, guest) | -- |
| `unifi-network-detail` | Full configuration for a specific network | `networkId` |
### Audit Tools (1)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-bandwidth-audit` | Per-room bandwidth audit across all switches. Shows port name, bandwidth limits, active status, connected device count. Designed for cross-referencing with billing systems. | `switchMac` (optional), `includeUnnamed` |
### Route & Firewall Tools (2)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-traffic-routes` | List traffic routes/rules with bandwidth shaping data (raw response) | -- |
| `unifi-firewall-policies` | List firewall policies from v2 API including traffic rules (raw response) | -- |
### Utility Tools (2)
| Tool | Description | Key Parameters |
|------|-------------|----------------|
| `unifi-auth-status` | Check authentication status, session expiry, and controller connectivity | -- |
| `unifi-api` | Generic API escape hatch -- make any UniFi API call not covered by other tools | `method`, `path`, `useAbsolutePath`, `params`, `body` |
---
## Setup
### Prerequisites
- Node.js >= 20.0.0
- Access to a UniFi controller (cloud-hosted or UDM Pro / UDM Pro Max)
- TOTP seed from your authenticator app setup (for automated MFA)
- **WireGuard VPN** (if targeting a UDM Pro / UDM Pro Max on a private LAN)
### VPN Setup (WireGuard)
If your UniFi controller is a UDM Pro / UDM Pro Max on a private network, you need a WireGuard VPN tunnel to reach it. Cloud-hosted controllers are publicly accessible and do not require VPN.
**Required routed subnets:**
Your WireGuard `AllowedIPs` must include every subnet the MCP server and your tools need to reach:
| Subnet | Purpose |
|--------|---------|
| UDM management subnet | Controller API access (required) |
| Switch management subnet | SSH access for device operations (if switches are on a separate subnet) |
| Tenant/client subnets | Client data and VLAN networks (if querying client tools) |
Example WireGuard peer config:
```ini
[Peer]
PublicKey = <udm-public-key>
AllowedIPs = <udm-subnet>/24, <switch-subnet>/24, <tenant-supernet>/8
Endpoint = <udm-wan-ip>:51820
```
**Verifying connectivity:**
```bash
# Check the tunnel routes all required subnets
wg show <tunnel-name> allowed-ips
# Verify controller reachable
ping <your-udm-ip>
# Verify switches reachable (if on a separate management subnet)
ping <switch-ip>
```
**Troubleshooting: devices unreachable but controller responds**
The WireGuard Windows app stores tunnel configs internally. Editing the `.conf` file on disk does **not** update the running tunnel. If your config file has the correct `AllowedIPs` but `wg show` is missing a subnet:
1. Open the WireGuard app
2. Remove the tunnel
3. **Import tunnel from file** (re-import the updated `.conf`)
4. Activate the tunnel
Verify with `wg show <tunnel-name> allowed-ips`.
### Installation
```bash
# Clone the repository
git clone <repo-url>
cd mcp-servers/unifi-mcp-server
# Install dependencies
npm install
# Configure environment
cp .env.example .env
# Edit .env with your controller details (see Configuration section)
# Build
npm run build
# Authenticate (first time)
npm run auth # Interactive mode
npm run auth -- --auto # Non-interactive (requires UNIFI_USERNAME, UNIFI_PASSWORD, UNIFI_TOTP_SEED)
# Start the server
npm run start
```
### Claude Code Configuration
Add the server to your Claude Code MCP settings (`~/.claude/settings.json` or project-level `.mcp.json`):
```json
{
"mcpServers": {
"unifi": {
"command": "node",
"args": ["/path/to/unifi-mcp-server/dist/index.js"],
"env": {
"UNIFI_HOST": "your-controller-ip",
"UNIFI_SITE": "default"
}
}
}
}
```
The server communicates over **stdin/stdout** using the MCP `StdioServerTransport`.
---
## Configuration
All configuration is loaded from `.env` at the project root via Zod-validated schemas.
### Core Settings
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `UNIFI_HOST` | Yes | -- | Hostname or IP of the UniFi controller |
| `UNIFI_PORT` | No | `443` | HTTPS port |
| `UNIFI_SITE` | No | `default` | UniFi site ID (visible in the controller URL) |
| `UNIFI_VERIFY_SSL` | No | `false` | Set `true` only if the controller has a valid TLS certificate |
### Authentication
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `UNIFI_USERNAME` | For self-healing | -- | UniFi SSO or local admin username |
| `UNIFI_PASSWORD` | For self-healing | -- | UniFi SSO or local admin password |
| `UNIFI_TOTP_SEED` | For self-healing | -- | Base32-encoded TOTP secret from authenticator setup |
### Session Storage
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `TOKEN_CACHE_PATH` | No | `./data/unifi-session.encrypted.json` | Path to the encrypted session cache file |
| `TOKEN_ENCRYPTION_KEY` | No | -- | 64-character hex key for AES-256-GCM encryption. Generate with: `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`. Falls back to base64 encoding if unset. |
### Logging
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `LOG_LEVEL` | No | `info` | Log verbosity: `debug`, `info`, `warn`, `error` |
### Webhook Push (scripts/webhook-push.ts only)
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `WEBHOOK_URL` | For webhook | -- | Make.com or other webhook URL for monthly bandwidth audit |
| `VPN_TUNNEL_NAME` | No | -- | WireGuard tunnel name for automatic VPN connect/disconnect |
| `VPN_GATEWAY` | No | -- | IP to ping for VPN health check (e.g., your gateway behind the tunnel) |
| `MAX_RETRIES` | No | `3` | Number of retry attempts for webhook push |
| `RETRY_DELAY_MS` | No | `60000` | Initial delay between retries in ms (linear backoff: 1x, 2x, 3x) |
---
## Authentication
### How It Works
UniFi controllers use cookie-based authentication with JWT tokens. The server supports two authentication flows depending on the controller type:
**Cloud-hosted controller (two-step MFA):**
1. POST username + password to `/api/auth/login` -- returns `UBIC_2FA` cookie
2. Generate TOTP code from the stored base32 seed (RFC 6238, HMAC-SHA1, 30-second period, 6 digits)
3. POST username + password + TOTP code with the `UBIC_2FA` cookie -- returns `TOKEN` in `Set-Cookie`
4. Parse the JWT payload for the `csrfToken` and `exp` (expiry) claims
5. Encrypt and persist the session to disk
**UDM Pro / UDM Pro Max (single-step):**
1. POST username + password + TOTP code in a single request -- returns `TOKEN` in `Set-Cookie`
2. Parse and persist as above
### Initial Authentication
Run the auth script to establish the first session:
```bash
# Interactive -- prompts for username and password
npm run auth
# Non-interactive -- reads credentials from environment
npm run auth -- --auto
```
The `--auto` flag is used for scheduled tasks and CI environments. It requires `UNIFI_USERNAME`, `UNIFI_PASSWORD`, and `UNIFI_TOTP_SEED` to be set in `.env`.
### Self-Healing Auth
When `UNIFI_USERNAME`, `UNIFI_PASSWORD`, and `UNIFI_TOTP_SEED` are all present in the environment, the server re-authenticates automatically without any manual intervention:
- **Proactive refresh:** When the session has fewer than 15 minutes remaining, a background refresh is triggered (fire-and-forget; the current request continues with the existing session)
- **Expired re-auth:** When the session has fully expired, re-auth blocks until complete before the request proceeds
- **401 retry:** If the controller returns 401, the axios interceptor triggers re-auth, refreshes the Cookie and X-CSRF-Token headers, and retries the original request
- **Mutex:** A promise-based mutex ensures concurrent requests share a single auth attempt
- **Circuit breaker:** After 3 consecutive authentication failures, auth is disabled until the server is restarted (prevents credential lockout loops)
---
## Scripts
### webhook-push.ts
Automated monthly bandwidth audit push to a Make.com webhook. Designed to run as a scheduled task on the 1st of each month.
```bash
npx tsx scripts/webhook-push.ts
```
**What it does:**
1. Connects WireGuard VPN (if `VPN_TUNNEL_NAME` is set)
2. Authenticates to the UniFi controller with automated TOTP
3. Pulls all switch device data and builds a per-room bandwidth payload
4. POSTs the payload to the configured `WEBHOOK_URL`
5. Disconnects VPN
6. Retries up to `MAX_RETRIES` times with linear backoff on failure
### initial-auth.ts
Interactive or automated initial login to establish a session cache.
```bash
npm run auth # Interactive
npm run auth -- --auto # Non-interactive
```
### auto-reauth.ts
Non-interactive re-authentication for use in scheduled tasks and CI. Generates TOTP automatically, saves the session to the encrypted cache.
```bash
npx tsx scripts/auto-reauth.ts # re-auth + save session
npx tsx scripts/auto-reauth.ts --quiet # suppress progress logs
npx tsx scripts/auto-reauth.ts --json # output token JSON to stdout for script chaining
```
---
## Development
### Commands
| Command | Description |
|---------|-------------|
| `npm run build` | Compile TypeScript to `dist/` |
| `npm run dev` | Watch mode (`tsc --watch`) |
| `npm run start` | Run the compiled server (`node dist/index.js`) |
| `npm run auth` | Run the initial auth script (`npx tsx scripts/initial-auth.ts`) |
### Project Structure
```
unifi-mcp-server/
├── src/
│ ├── index.ts # Server bootstrap (McpServer + StdioServerTransport)
│ ├── config/
│ │ └── index.ts # Zod-validated env config from .env
│ ├── auth/
│ │ ├── sessionManager.ts # Self-healing auth with proactive refresh + mutex
│ │ ├── sessionCache.ts # AES-256-GCM encrypted session persistence
│ │ └── totp.ts # RFC 6238 TOTP generator (zero external dependencies)
│ ├── unifi/
│ │ ├── unifiClient.ts # Axios client with 401/429/5xx retry interceptors
│ │ └── types.ts # TypeScript interfaces for UniFi API entities
│ ├── tools/
│ │ ├── index.ts # registerTools() — wires all tool modules
│ │ ├── devices.ts # 3 tools: device-list, device-detail, device-ports
│ │ ├── clients.ts # 3 tools: client-list, client-detail, client-history
│ │ ├── ports.ts # 6 tools: profiles CRUD + port-set-profile
│ │ ├── networks.ts # 2 tools: network-list, network-detail
│ │ ├── audit.ts # 1 tool: bandwidth-audit
│ │ ├── routes.ts # 2 tools: traffic-routes, firewall-policies
│ │ └── utility.ts # 2 tools: auth-status, generic api escape hatch
│ └── utils/
│ ├── errors.ts # Error hierarchy: UniFiError → Auth / Api / Config
│ ├── logger.ts # stderr logger with sensitive key redaction
│ └── formatters.ts # Entity formatters for clean LLM output
├── scripts/
│ ├── initial-auth.ts # Interactive/auto auth script
│ ├── auto-reauth.ts # Non-interactive re-auth for scheduled tasks
│ └── webhook-push.ts # Monthly bandwidth audit → webhook
├── data/ # Session cache (gitignored)
├── dist/ # Compiled output (gitignored)
├── .env # Environment variables (gitignored)
├── .env.example # Template with all variables documented
├── .gitignore
├── package.json
└── tsconfig.json
```
### Adding a New Tool
1. Create or edit a file in `src/tools/` (group by entity type).
2. Export a `register*Tools(server: McpServer)` function:
```typescript
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { unifiClient } from '../unifi/unifiClient.js';
import { formatErrorForMcp } from '../utils/errors.js';
export function registerMyTools(server: McpServer): void {
server.tool(
'unifi-my-tool',
'Description of what this tool does.',
{
param: z.string().describe('What this parameter controls'),
},
async ({ param }) => {
try {
const data = await unifiClient.get('/rest/endpoint');
return {
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
};
} catch (error) {
return {
content: [{ type: 'text', text: formatErrorForMcp(error) }],
isError: true,
};
}
}
);
}
```
3. Wire it into `src/tools/index.ts`:
```typescript
import { registerMyTools } from './mytools.js';
// ...
export function registerTools(server: McpServer): void {
// ... existing registrations
registerMyTools(server);
}
```
4. Build and test: `npm run build && npm run start`
### TypeScript Conventions
- **Target:** ES2022 with NodeNext module resolution
- **ESM:** `"type": "module"` in package.json -- all imports must use `.js` extensions
- **Strict mode:** enabled (`"strict": true` in tsconfig)
- **Output:** `dist/` with declaration files and source maps
---
## Security
### Token Encryption
Session tokens are persisted to disk for survival across server restarts. When `TOKEN_ENCRYPTION_KEY` is set (64-character hex string = 32 bytes), the session cache is encrypted using **AES-256-GCM** with:
- Random 16-byte IV per write
- GCM authentication tag for tamper detection
- Format: `iv_hex:auth_tag_hex:ciphertext_hex`
If the encryption key is not set, sessions fall back to base64 encoding (a warning is logged on every save).
Generate a key:
```bash
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
```
### Credential Handling
- Credentials (`UNIFI_USERNAME`, `UNIFI_PASSWORD`, `UNIFI_TOTP_SEED`) are read from environment variables only -- never stored in code or committed to version control.
- The logger automatically redacts any log context key containing `token`, `secret`, `password`, `credential`, `authorization`, `bearer`, `apikey`, `api_key`, or `cookie`.
- All logs are written to **stderr** (stdout is reserved for MCP protocol communication).
### Self-Signed SSL
UniFi controllers typically use self-signed certificates. By default, `UNIFI_VERIFY_SSL=false` disables certificate verification via `https.Agent({ rejectUnauthorized: false })`. Set to `true` only if your controller has a valid certificate from a trusted CA.
---
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `@modelcontextprotocol/sdk` | ^1.12.0 | MCP server framework (McpServer, StdioServerTransport) |
| `axios` | ^1.7.9 | HTTP client with interceptor support for retry logic |
| `dotenv` | ^16.4.7 | Load `.env` files into `process.env` |
| `zod` | ^3.24.1 | Runtime schema validation for config and tool inputs |
**Dev dependencies:** `typescript` ^5.7.2, `tsx` ^4.19.2, `@types/node` ^22.10.2
---
## Built by Element Zero
This project is built and maintained by **[Element Zero](https://ezero.ai)** -- an AI-driven network automation company specializing in multi-tenant building infrastructure.
We use this MCP server daily to manage commercial building networks with 60+ tenant VLANs, automated bandwidth auditing, and billing integration. It's battle-tested in production.
**If you manage UniFi networks for multi-tenant buildings, hotels, or coworking spaces** -- we'd love to hear from you. Reach out at [phil@ezero.ai](mailto:phil@ezero.ai).
---
## License
MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessSyncing