Skip to content

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.

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).

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.
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.

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.

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).
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).

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.

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.

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.

  1. 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. Exit 1 means refused, 2 means the console could not be reached; nothing is written on either.
  2. Set pinned_cert: <path> on the controller (YAML) or UNIFI_PINNED_CERT (env). verify_ssl turns on; setting it to false alongside 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.
  3. 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 lower verify_ssl to 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.

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.

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).
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.

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.
  • 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 legacy UNIFI_HOST + UNIFI_API_KEY) is a hard error.
  • Duplicate controller names in the YAML are rejected.
  • IOT_DHCP_STOP_OFFSET <= IOT_DHCP_START_OFFSET is rejected.
  • IOT_SUBNET_TEMPLATE without the {vlan_id} placeholder is rejected.

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.

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.