SSH MCP Server
Runs commands and moves files on remote Linux and Windows hosts over SSH, behind roles, approvals and audit
Query and manage UniFi Access doors, credentials, visitors, policies and events from an MCP client
By Chris Kirby (individual) Python Local · stdio SecurityDevOps and monitoring
57 popularity score ★ 1.0k ↓ 3.6k/mo
Opens the app, which asks before it adds the server. Then replace each <NAME> in its settings with your value.
It needs UNIFI_ACCESS_HOST, UNIFI_ACCESS_USERNAME, UNIFI_ACCESS_PASSWORD. Replace each <NAME> with your value.
claude mcp add unifi-access -e UNIFI_ACCESS_HOST=<UNIFI_ACCESS_HOST> -e UNIFI_ACCESS_USERNAME=<UNIFI_ACCESS_USERNAME> -e UNIFI_ACCESS_PASSWORD=<UNIFI_ACCESS_PASSWORD> -- uvx unifi-access-mcp{
"mcpServers": {
"unifi-access": {
"command": "uvx",
"args": [
"unifi-access-mcp"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}{
"mcpServers": {
"unifi-access": {
"command": "uvx",
"args": [
"unifi-access-mcp"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}{
"servers": {
"unifi-access": {
"type": "stdio",
"command": "uvx",
"args": [
"unifi-access-mcp"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}[mcp_servers.unifi-access]
command = "uvx"
args = ["unifi-access-mcp"]
[mcp_servers.unifi-access.env]
UNIFI_ACCESS_HOST = "<UNIFI_ACCESS_HOST>"
UNIFI_ACCESS_USERNAME = "<UNIFI_ACCESS_USERNAME>"
UNIFI_ACCESS_PASSWORD = "<UNIFI_ACCESS_PASSWORD>"gemini mcp add --scope user -e UNIFI_ACCESS_HOST=<UNIFI_ACCESS_HOST> -e UNIFI_ACCESS_USERNAME=<UNIFI_ACCESS_USERNAME> -e UNIFI_ACCESS_PASSWORD=<UNIFI_ACCESS_PASSWORD> unifi-access uvx -- unifi-access-mcpOpens the app, which asks before it adds the server. Then replace each <NAME> in its settings with your value.
It needs UNIFI_ACCESS_HOST, UNIFI_ACCESS_USERNAME, UNIFI_ACCESS_PASSWORD. Replace each <NAME> with your value.
claude mcp add unifi-access -e UNIFI_ACCESS_HOST=<UNIFI_ACCESS_HOST> -e UNIFI_ACCESS_USERNAME=<UNIFI_ACCESS_USERNAME> -e UNIFI_ACCESS_PASSWORD=<UNIFI_ACCESS_PASSWORD> -- docker run -i --rm -e UNIFI_ACCESS_HOST -e UNIFI_ACCESS_USERNAME -e UNIFI_ACCESS_PASSWORD ghcr.io/sirkirby/unifi-access-mcp:latest{
"mcpServers": {
"unifi-access": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UNIFI_ACCESS_HOST",
"-e",
"UNIFI_ACCESS_USERNAME",
"-e",
"UNIFI_ACCESS_PASSWORD",
"ghcr.io/sirkirby/unifi-access-mcp:latest"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}{
"mcpServers": {
"unifi-access": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UNIFI_ACCESS_HOST",
"-e",
"UNIFI_ACCESS_USERNAME",
"-e",
"UNIFI_ACCESS_PASSWORD",
"ghcr.io/sirkirby/unifi-access-mcp:latest"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}{
"servers": {
"unifi-access": {
"type": "stdio",
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UNIFI_ACCESS_HOST",
"-e",
"UNIFI_ACCESS_USERNAME",
"-e",
"UNIFI_ACCESS_PASSWORD",
"ghcr.io/sirkirby/unifi-access-mcp:latest"
],
"env": {
"UNIFI_ACCESS_HOST": "<UNIFI_ACCESS_HOST>",
"UNIFI_ACCESS_USERNAME": "<UNIFI_ACCESS_USERNAME>",
"UNIFI_ACCESS_PASSWORD": "<UNIFI_ACCESS_PASSWORD>"
}
}
}
}[mcp_servers.unifi-access]
command = "docker"
args = ["run", "-i", "--rm", "-e", "UNIFI_ACCESS_HOST", "-e", "UNIFI_ACCESS_USERNAME", "-e", "UNIFI_ACCESS_PASSWORD", "ghcr.io/sirkirby/unifi-access-mcp:latest"]
[mcp_servers.unifi-access.env]
UNIFI_ACCESS_HOST = "<UNIFI_ACCESS_HOST>"
UNIFI_ACCESS_USERNAME = "<UNIFI_ACCESS_USERNAME>"
UNIFI_ACCESS_PASSWORD = "<UNIFI_ACCESS_PASSWORD>"gemini mcp add --scope user -e UNIFI_ACCESS_HOST=<UNIFI_ACCESS_HOST> -e UNIFI_ACCESS_USERNAME=<UNIFI_ACCESS_USERNAME> -e UNIFI_ACCESS_PASSWORD=<UNIFI_ACCESS_PASSWORD> unifi-access docker -- run -i --rm -e UNIFI_ACCESS_HOST -e UNIFI_ACCESS_USERNAME -e UNIFI_ACCESS_PASSWORD ghcr.io/sirkirby/unifi-access-mcp:latestUNIFI_ACCESS_HOST |
Required: IP or hostname of the UniFi OS console (falls back to UNIFI_HOST). |
|---|---|
UNIFI_ACCESS_USERNAME |
Required: Local admin username for the session path; can be left out if only UNIFI_ACCESS_API_KEY is used, which limits most changes. |
UNIFI_ACCESS_PASSWORD |
Required: Password for that local admin account. |
UNIFI_ACCESS_API_KEY |
Optional: Official UniFi Access API key; needed for visitor passes and usable alone for reads. |
UNIFI_ACCESS_PORT |
Optional: Console HTTPS port, 443 by default. |
UNIFI_ACCESS_VERIFY_SSL |
Optional: Verify the controller's certificate; false by default. |
UNIFI_ACCESS_API_PORT |
Optional: Port of the Access API used with the key, 12445 by default. |
UNIFI_POLICY_ACCESS_DOORS_UPDATE |
Optional: Set true to allow door lock and unlock; other UNIFI_POLICY_ACCESS_<CATEGORY>_<ACTION> gates work the same way. |
UNIFI_ACCESS_TOOL_PERMISSION_MODE |
Optional: confirm (default, preview then confirm) or bypass. |
UNIFI_TOOL_REGISTRATION_MODE |
Optional: lazy (default), eager or meta_only. |
UNIFI_ACCESS_REDACT_SENSITIVE_FIELDS |
Optional: Set false to return raw credential tokens and PINs. |
In a container, a setting goes in with -e NAME=value after docker run.
UniFi Access MCP Server connects an MCP client to a self-hosted Ubiquiti UniFi Access controller. An assistant can use it to list doors and their lock state, search the event history or the live buffer the controller feeds over its websocket, and review policies, schedules, users, hardware and credentials. When changes are permitted it can also unlock or lock doors, issue or revoke NFC, PIN and mobile credentials, create or cancel visitor passes, edit policies, reboot hubs and readers, and change per-device settings such as a reader's greeting.
It lives in the sirkirby/unifi-mcp monorepo next to separate Network and Protect servers, and ships on PyPI as unifi-access-mcp and as a container on GHCR. There are two ways in. An API key reaches the official Access API on port 12445. A local admin login instead opens a session on the UniFi OS console at port 443 and proxies requests through it. Most changes need that session, while visitor passes go through the API key.
The tool manifest in the repository counts 37 tools: 31 for Access and six helpers for discovery, indirect and batch calls, loading tools on demand and building a support bundle. The tool catalog page in its docs leaves out access_get_device_configs, access_update_device_config and access_get_support_bundle. In the default lazy mode only the helpers are registered at start and the Access tools load when asked for. UNIFI_TOOL_REGISTRATION_MODE=eager registers all of them up front.
One point its docs get wrong: permissions.md says Access denies every change by default, but the shared policy gate in unifi_core/policy_gate.py allows a change when no UNIFI_POLICY_* variable is set. What holds a change back by default is the preview step, which wants a second call with confirm=true. To block changes outright, set UNIFI_ACCESS_READ_ONLY=true or a false policy gate.
| Runtime | Python 3.13+Docker |
|---|---|
| Runs on | macOS, Windows, Linux |
| UniFi Access on a UniFi OS console | Your own instanceA local admin account for the session path, an Access API key, or both |
| Also |
|
Run it with uv (for uvx) on Python 3.13 or later, or with Docker. Its docs advise a local admin or service account on the console and warn against a Ubiquiti SSO cloud account. Tracing export under uvx or pip needs the otel extra, installed as unifi-access-mcp[otel]. The Docker image already carries it.
The permissions docs disagree with the code on the default. permissions.md says Access denies all changes until enabled, and configuration.md still prints a YAML block with create, update and delete set to false. The bundled config.yaml carries no such block, and unifi_core/policy_gate.py returns allowed when no UNIFI_POLICY_* gate is set. A fresh install therefore permits changes behind the preview and confirm step only.
Suits administrators of a UniFi Access site who want to audit badge events, check credentials and policies, or hand out visitor passes from an assistant, with each change shown as a preview before it runs. It runs on your own machine against your own controller, so that machine must be on, or routed to, the controller's network. It is a community project, not a Ubiquiti product. Anyone who wants it to read only should set UNIFI_ACCESS_READ_ONLY=true, since changes are open by default. For networking or cameras the same repository ships unifi-network-mcp and unifi-protect-mcp, and services that call over HTTP are pointed to unifi-api-server.
25 only read, 10 change things, 2 can delete or overwrite. A ? marks an optional parameter.
access_list_doorscompact?: booleanaccess_get_doordoor_id: stringaccess_get_door_statusdoor_id: stringaccess_list_door_groupsaccess_unlock_door changesdoor_id: stringduration?: integerconfirm?: booleanaccess_lock_door changesdoor_id: stringconfirm?: booleanaccess_list_eventsstart?: stringend?: stringdoor_id?: stringuser_id?: stringtopic?: stringlimit?: integeraccess_get_eventevent_id: stringaccess_recent_eventsevent_type?: stringdoor_id?: stringlimit?: integeraccess_subscribe_eventsaccess_get_activity_summarydays?: integerdoor_id?: stringaccess_list_policiesaccess_get_policypolicy_id: stringaccess_list_schedulesaccess_update_policy changespolicy_id: stringchanges: objectconfirm?: booleanaccess_list_credentialsaccess_get_credentialcredential_id: stringaccess_create_credential changescredential_type: stringcredential_data: objectconfirm?: booleanaccess_revoke_credential deletescredential_id: stringconfirm?: booleanaccess_list_visitorsaccess_get_visitorvisitor_id: stringaccess_create_visitor changesname: stringfirst_name?: stringlast_name?: stringvalid_from?: stringvalid_until?: stringaccess_start?: stringaccess_end?: stringemail?: stringphone?: stringcompany?: stringvisit_reason?: stringremarks?: stringconfirm?: booleanaccess_delete_visitor deletesvisitor_id: stringconfirm?: booleanaccess_list_devicescompact?: booleanaccess_get_devicedevice_id: stringaccess_get_device_configsdevice_id: stringaccess_update_device_config changesdevice_id: stringupdates: objectis_camera?: booleanconfirm?: booleanaccess_reboot_device changesdevice_id: stringconfirm?: booleanaccess_get_system_infoaccess_get_healthaccess_list_userscompact?: booleanlimit?: integeraccess_tool_indexname?: stringcategory?: stringsearch?: stringinclude_schemas?: booleanaccess_execute changestool: stringarguments?: objectaccess_batch changesoperations: arrayaccess_batch_statusjobId?: stringjobIds?: arrayaccess_load_tools changestools: arrayaccess_get_support_bundleprobe?: enumresource?: stringaccess://events/streamaccess://events/stream/summary