Skip to main content
Glama
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.

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
[![GitHub release](https://img.shields.io/github/v/release/philipvanlewis/unifi-mcp-server)](https://github.com/philipvanlewis/unifi-mcp-server/releases)
[![MCP Tools](https://img.shields.io/badge/MCP_Tools-19-3b82f6)](https://modelcontextprotocol.io/)
[![TypeScript](https://img.shields.io/badge/TypeScript-ES2022-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Node.js](https://img.shields.io/badge/Node.js-%3E%3D20-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org/)
[![UniFi](https://img.shields.io/badge/UniFi-Compatible-22c55e)](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