Configuration
All configuration is read from environment variables (and a .env file when present). Pydantic validates values at startup; invalid config fails fast with a clear message.
Mode toggle
Section titled “Mode toggle”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
STUB_MODE |
bool | true |
no | When false, real-mode controller config is required (either legacy env vars or MCP_UNIFI_CONTROLLERS_FILE). |
Legacy single-controller (auto-promoted)
Section titled “Legacy single-controller (auto-promoted)”These env vars cover the single-controller case. When set without MCP_UNIFI_CONTROLLERS_FILE, they auto-promote to a one-entry controller list named default. v0.4.x users keep working unchanged.
| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
UNIFI_HOST |
string | "" |
real mode | Gateway IP or hostname (no scheme). |
UNIFI_API_KEY |
string | "" |
real mode | Local API key from Settings → Control Plane → Integrations. Or use UNIFI_API_KEY_FILE (below). |
UNIFI_PORT |
int (1-65535) | 443 |
no | HTTPS port for the gateway. |
UNIFI_SITE |
string | default |
no | Controller site identifier. Most setups have one site. |
UNIFI_VERIFY_SSL |
bool | false (true with UNIFI_API_KEY_FILE or UNIFI_PINNED_CERT) |
no | Set true once the gateway has a real TLS certificate. Left unset, it is false for an inline key and true for a file-backed or pinned one; an explicit value always wins. |
UNIFI_PINNED_CERT |
path | (unset) | no | PEM certificate recorded by mcp-unifi-pin-cert. When set it is the only trust anchor for the controller; hostname matching is off because the pin is the identity; a mismatch fails every request. UNIFI_VERIFY_SSL=false alongside it is rejected at startup. See Certificate pinning. |
UNIFI_PROTECT_API |
enum (internal, integration) |
internal |
no | Protect API surface for legacy single-controller config. integration uses /proxy/protect/integration/v1 for UniFi OS 5.x API keys; events and recordings are unavailable there. |
Multi-controller
Section titled “Multi-controller”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_UNIFI_CONTROLLERS_FILE |
path | (unset) | no | YAML file listing named controllers. When set, the legacy UNIFI_* vars are ignored. See the Multi-Site Setup guide for the schema. |
MCP_UNIFI_DEFAULT_CONTROLLER |
string | (unset) | no | Which controller a tool call targets when it omits controller and none is literally named default. With exactly one controller configured it is chosen automatically; with several, set this or pass controller= on every call. There is deliberately no “first in the list” fallback. Must name a controller in the file or startup fails. |
In the YAML, each controller may set protect_api: integration independently.
The default internal preserves existing behavior. The mode is validated at
startup; it does not probe/fallback between APIs automatically.
Module dispatcher
Section titled “Module dispatcher”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_UNIFI_MODULES_ENABLED |
CSV string | network |
no | Modules to load. Known values: network, protect, access. Unknown values fail startup with UnknownModuleError. Access requires UNIFI_ACCESS_HOST and UNIFI_ACCESS_API_KEY in real mode. |
Access module (opt-in)
Section titled “Access module (opt-in)”Only required when MCP_UNIFI_MODULES_ENABLED includes access and STUB_MODE=false. See the Access setup guide.
| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
UNIFI_ACCESS_HOST |
string | "" |
access + real mode | Access hub IP or hostname. Often the same as UNIFI_HOST. |
UNIFI_ACCESS_API_KEY |
string | "" |
access + real mode | Access API key. Separate from the Network API key; generated on the Access controller’s developer settings. |
UNIFI_ACCESS_PORT |
int (1-65535) | 12445 |
no | HTTPS port for the Access hub (the direct Access app port). |
Transport
Section titled “Transport”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_TRANSPORT |
enum (stdio, streamable-http) |
streamable-http |
no | stdio for Claude Desktop / uvx / .dxt installs; streamable-http for the long-running container. |
MCP_HOST |
string | 0.0.0.0 |
no | Bind address (Streamable HTTP only). |
MCP_PORT |
int (1-65535) | 3714 |
no | Listen port (Streamable HTTP only). |
HTTP authentication
Section titled “HTTP authentication”The Streamable HTTP transport is secure by default. See the Authentication guide for the full model.
| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_UNIFI_AUTH_REQUIRED |
bool | true |
no | When true, the HTTP transport refuses to start without tokens. Set to false only on a loopback-bound single-host deployment. Stdio transport ignores this. |
MCP_UNIFI_AUTH_TOKENS |
CSV string | "" |
HTTP + auth on | Comma-separated bearer tokens. Each entry is one of: a bare token (auto-assigned client-N), a name:token pair (recommended — name shows up in the audit log), or a name:token:module1|module2 triple to scope a client to specific modules. Pipe-separated because comma is the entry delimiter. Known modules: network, protect, access; * means all. |
File-backed secrets (opt-in)
Section titled “File-backed secrets (opt-in)”Every secret has a _FILE twin for Docker and Kubernetes secret mounts. The value form keeps working; the file form wins when both are set. A missing, empty or unreadable file fails startup with a message naming the field and the path, never the contents. In the controllers YAML the same fields are api_key_file, access_api_key_file and os_password_file per controller.
| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
UNIFI_API_KEY_FILE |
path | (unset) | no | File containing the local API key. Wins over UNIFI_API_KEY. Also turns UNIFI_VERIFY_SSL on by default. |
UNIFI_ACCESS_API_KEY_FILE |
path | (unset) | no | File containing the Access API key. Wins over UNIFI_ACCESS_API_KEY. |
UNIFI_OS_PASSWORD_FILE |
path | (unset) | no | File containing the UniFi OS console password. Wins over UNIFI_OS_PASSWORD. The username is not a secret and has no file form. |
MCP_UNIFI_AUTH_TOKEN_FILE |
path | (unset) | no | File holding either one bare bearer token, named by MCP_UNIFI_CLIENT_ID, or the same comma-separated grammar as MCP_UNIFI_AUTH_TOKENS. Its entries are added alongside MCP_UNIFI_AUTH_TOKENS; the two combine. Ignored on stdio. |
MCP_UNIFI_CLIENT_ID |
string | "" |
no | Client name for a bare token in MCP_UNIFI_AUTH_TOKEN_FILE, as name:token names one inline. Requires the file; rejected when the file already carries names. |
On every boot the server logs one warning per controller still on the value-supplied shape or running with TLS verification off, and one when HTTP bearer tokens come from MCP_UNIFI_AUTH_TOKENS. Nothing is refused. The warning is step one of the dated path in ADR 0007: opt-in, then warn for a release, then flip at a major.
Certificate pinning
Section titled “Certificate pinning”A UniFi console’s self-signed certificate names unifi.local, localhost and the loopback addresses, never the LAN address the server dials, so standard verification cannot succeed against it. Pinning records that certificate and makes it the controller’s only trust anchor: chain verification is required, the system trust store is not consulted, and hostname matching is off because the certificate itself is the identity.
mcp-unifi-pin-cert <host> --out <path>connects without verification, prints the certificate’s SHA-256 fingerprint, and writes it as PEM. It refuses to overwrite an existing file without--force, and with--expect-fingerprint <sha256>it writes only if the presented certificate matches what you read off the console. To read it there, over a channel the pin does not depend on:ssh root@<host> 'openssl x509 -in /data/unifi-core/config/unifi-core.crt -noout -fingerprint -sha256'(the path UniFi OS keeps its serving certificate at). The UniFi web UI does not display a fingerprint. Exit1means refused,2means the console could not be reached; nothing is written on either.- Set
pinned_cert: <path>on the controller (YAML) orUNIFI_PINNED_CERT(env).verify_sslturns on; setting it tofalsealongside a pin is rejected at startup as a contradiction. A pin that is missing, empty, not a certificate, or a bundle of more than one certificate fails startup naming the controller. After every handshake the certificate the console presented is compared byte for byte against the pin, so a pin that happens to be CA-capable cannot vouch for anything it signed. - Rotation: a firmware update can regenerate the console certificate. Every request then fails with a message naming the re-pin command. Check the new fingerprint on the console and re-run step 1 with
--force. Do not lowerverify_sslto get past it: the same failure is what an interception looks like.
The pin covers the Network, Protect and console-session clients on that host. The Access hub is a separate host and keeps the verify_ssl flag. The server never fetches and trusts a certificate on its own; the startup line reports each controller’s pinned_cert path and fingerprint so you can confirm which one loaded.
Read-only mode
Section titled “Read-only mode”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_UNIFI_READONLY |
bool | false |
no | When true, every tool that changes state is hidden from tools/list and refused on tools/call. Applies to both transports. Refusals come back as the standard {"error": ..., "stub_mode": ...} envelope, so callers need no new error handling. See the Security guide. |
Read-only mode is defense in depth on top of a read-only UniFi API key, not a replacement for one. The key decides what the controller will accept; this setting decides what the server is willing to attempt.
Classification is per tool and explicit: each tool declares mutates=True or
mutates=False at registration, and the server refuses to start if any
registered tool has not declared one. It is not inferred from tool names —
twelve mutating tools (confirm_destructive_action, restore_config,
block_client, restart_device, locate_device, trigger_speedtest, and
others) carry no create_/update_/delete_/set_ prefix, so a name-based
gate would leave them callable.
Logging
Section titled “Logging”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
LOG_LEVEL |
enum | INFO |
no | One of DEBUG, INFO, WARNING, ERROR, CRITICAL. |
LOG_FORMAT |
enum (json, text) |
json |
no | json for production, text for local dev. Logs go to stderr (stdout is reserved for stdio JSON-RPC framing). |
Audit log
Section titled “Audit log”| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
MCP_UNIFI_AUDIT_SINK |
enum | file |
no | One of file, stdout, syslog. |
MCP_UNIFI_AUDIT_PATH |
path | audit.jsonl |
no | File path when SINK=file. Relative paths resolve from the process CWD. |
MCP_UNIFI_AUDIT_SYSLOG_ADDRESS |
string | /dev/log |
no | Socket path or host:port when SINK=syslog. |
See the Dry-Run & Audit Log guide for the JSONL record schema and mcp-unifi-replay usage.
IoT defaults
Section titled “IoT defaults”These shape the subnet and DHCP range that create_iot_network synthesizes when callers don’t specify them.
| Variable | Type | Default | Required | Notes |
|---|---|---|---|---|
IOT_SUBNET_TEMPLATE |
string | 10.0.{vlan_id}.0/24 |
no | Subnet template. Must contain the literal {vlan_id} placeholder. |
IOT_DHCP_START_OFFSET |
int (2-254) | 100 |
no | First DHCP lease offset within the IoT /24. |
IOT_DHCP_STOP_OFFSET |
int (2-254) | 200 |
no | Last DHCP lease offset. Must be greater than IOT_DHCP_START_OFFSET. |
Validation behavior
Section titled “Validation behavior”- Pydantic validates types and ranges at startup.
- Invalid values fail with a clear message that names the field.
- Real mode with no controller config (no
MCP_UNIFI_CONTROLLERS_FILE, no legacyUNIFI_HOST+UNIFI_API_KEY) is a hard error. - Duplicate controller names in the YAML are rejected.
IOT_DHCP_STOP_OFFSET <= IOT_DHCP_START_OFFSETis rejected.IOT_SUBNET_TEMPLATEwithout the{vlan_id}placeholder is rejected.
.env file
Section titled “.env file”The server reads .env from the process CWD if present. Values in the actual environment override .env. The repo’s .env.example is a good starting template.
Inspecting the resolved config
Section titled “Inspecting the resolved config”On startup, the server logs a safe_repr() of its resolved config. Per-controller api_key values are never included; each controller gets an api_key_set: true/false boolean instead, plus the path of any *_file secret it loaded (the path, never the contents). Grep startup logs for safe_repr to confirm what the server actually loaded.