unifi-mcp
README.md
# unifi-mcp
Read-only remote MCP server exposing the Example Corp / examplecorp **UniFi
fleet** as a Claude custom connector, via the official **UniFi Site Manager
API** (`api.ui.com`).
Sibling of `odoo-ticket-mcp` and the TRMM MCP servers — same architecture:
```
Claude (claude.ai / Claude Code)
│ OAuth 2.0 bearer token (Microsoft Entra ID)
▼
nginx — TLS, Anthropic egress-IP allowlist (160.79.104.0/21), 403 otherwise
│ http://127.0.0.1:8097
▼
unifi-mcp container (Portainer stack, FastMCP 3.x, Streamable HTTP at /mcp)
│ X-API-KEY (Site Manager API key, read-only usage)
▼
api.ui.com — Site Manager API + connector proxy into each console's Network API
```
- **Client → server auth:** Microsoft Entra ID, resource-server mode. The
server validates audience-bound bearer tokens; it never issues them. Access
is controlled by Entra group membership on the app registration.
- **Server → UniFi auth:** one fixed Site Manager API key (created at
unifi.ui.com → API Keys with Site Manager + Network application scopes, All
Sites). Caller identity is deliberately NOT forwarded to UniFi in v1.
- **Network path:** public DNS → nginx (TLS + Anthropic IP allowlist) →
container. Defense in depth: the allowlist is in addition to OAuth.
## Tools (all read-only)
**Site Manager (fleet-wide):**
| Tool | What it returns |
|------|-----------------|
| `unifi_list_hosts` | Every console (UDM Pro / UNVR / Cloud Key): id, name, model, IP, state, version |
| `unifi_get_host` | One console's detail (optional full raw record) |
| `unifi_list_sites` | All Network sites across consoles, with device/client counts |
| `unifi_list_devices` | Adopted devices fleet-wide (APs, switches, gateways), optional per-console filter |
| `unifi_isp_metrics` | WAN/ISP health (latency, loss, downtime, throughput) for all sites, 5m or 1h granularity |
| `unifi_query_isp_metrics` | Same, but for specific (host, site) pairs and windows |
| `unifi_list_sdwan_configs` / `unifi_sdwan_config_status` | SD-WAN topologies + deployment state |
**Connector proxy (per-console drill-down, no VPN needed):**
| Tool | What it returns |
|------|-----------------|
| `unifi_network_sites` | Console-local site list (local ids differ from Site Manager ids) |
| `unifi_network_devices` | A site's devices from the console's own Network API |
| `unifi_network_device` | Full device detail + latest live stats (CPU, memory, uplink, radios) |
| `unifi_network_clients` | Live connected clients: name, MAC, IP, connection type, uplink |
Typical flow: `unifi_list_hosts` → pick console → `unifi_network_sites` →
`unifi_network_clients` / `unifi_network_device`.
## Local development
```powershell
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
copy .env.example .env # fill in UNIFI_API_KEY; set MCP_AUTH_ENABLED=false
.venv\Scripts\python -m unifi_mcp
# → http://127.0.0.1:8000/mcp (test with MCP Inspector)
```
`MCP_AUTH_ENABLED=false` opens the endpoint — local testing only, never in
production.
## Entra ID app registration (one-time)
Follow `TrmmMCPServer/ENTRA_SSO_PLAYBOOK.md` for the annotated version. Summary
— do these **in order**:
1. **App registrations → New registration** — name `UniFi MCP`, single tenant,
no redirect URI yet. Copy the client id + tenant id.
2. **Expose an API → Application ID URI** — set to exactly
`https://unifi-mcp.example.com/mcp` (prevents AADSTS9010010).
3. **Expose an API → Add a scope** — `access_as_user`, admins and users can
consent, enabled.
4. **Certificates & secrets → New client secret** — `claude-connector`,
24 months. Copy the value immediately.
5. **Authentication → Add platform → Web** — redirect URIs
`https://claude.ai/api/mcp/auth_callback` **and**
`https://claude.com/api/mcp/auth_callback`.
6. **Manifest** — set `requestedAccessTokenVersion` to `2` (prevents
issuer-mismatch 401s).
7. **Enterprise applications → UniFi MCP → Properties** — Assignment
required = **Yes**.
8. **Enterprise applications → Users and groups** — add the `Unifi-MCP-Users`
group. Group membership = who can use the connector.
## Deploy
See [PORTAINER_DEPLOY.md](PORTAINER_DEPLOY.md) (Portainer Git stack + webhook
redeploy) and [deploy/nginx-unifi-mcp.conf](deploy/nginx-unifi-mcp.conf)
(TLS + Anthropic IP allowlist).
## Register the connector in claude.ai
Settings → Connectors → Add custom connector:
| Field | Value |
|-------|-------|
| URL | `https://unifi-mcp.example.com/mcp` |
| Client ID | the Entra app's client id |
| Client Secret | the Entra client secret |
| Authorization URL | `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/authorize` |
| Token URL | `https://login.microsoftonline.com/<tenant-id>/oauth2/v2.0/token` |
| Scopes | `https://unifi-mcp.example.com/mcp/access_as_user` |
## Acceptance tests
```bash
# 1. Discovery doc advertises the full resource scope (not api://…)
curl -s https://unifi-mcp.example.com/.well-known/oauth-protected-resource/mcp | jq .scopes_supported
# 2. Unauthenticated /mcp is challenged
curl -i https://unifi-mcp.example.com/mcp # 401 + WWW-Authenticate: Bearer
# 3. Non-Anthropic source IP is blocked at nginx
curl -i https://unifi-mcp.example.com/mcp # from anywhere else: 403
# 4. Health (host-local, bypasses nginx)
curl http://<docker-host>:8097/healthz # {"status":"ok","service":"unifi_mcp"}
# 5. In claude.ai: connect as a Unifi-MCP-Users member → tools appear;
# non-member → AADSTS50105 at sign-in.
```
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues