These tools read and change network configuration on the local gateway. Most of them use the legacy Network API, and a few use the integration API. Read this before you change one. For the general add-a-tool workflow, see unifi-mcp-tool-builder.
Module map
| Module | Tools | Examples |
|---|---|---|
src/tools/networks.py |
4 | get_network_details, list_vlans, get_subnet_info |
src/tools/network_config.py |
3 | create_network, update_network, delete_network |
src/tools/wifi.py |
5 | list_wlans, update_wlan, get_wlan_statistics |
src/tools/wans.py |
6 | list_wan_connections, create_dynamic_dns |
src/tools/dhcp_reservations.py |
5 | create_dhcp_reservation, remove_dhcp_reservation |
src/tools/dns_management.py |
4 | list_wan_dns, update_dns_filter |
src/tools/qos.py |
3 | list_traffic_routes, configure_smart_queue |
src/tools/port_profiles.py |
8 | update_port_profile, set_device_port_overrides |
src/tools/switching.py |
6 | list_switch_stacks, get_lag_details (reads only) |
src/tools/vpn.py |
2 | list_vpn_tunnels, list_vpn_servers |
src/tools/site_vpn.py |
3 | get_site_to_site_vpn, update_site_to_site_vpn |
Registration and modes
-
Every public
async defis a tool (register_module_tools()insrc/tool_registry.py).port_profiles.get_device_by_macis public, so it is a tool too. Name helpers with a leading_. -
All 11 modules are in
_LOCAL_TOOL_MODULES(src/main.py). None of them exists in cloud modes. -
Some tools also refuse non-local modes at run time with a
_ensure_local_api()helper: DHCP, DNS, Dynamic DNS and traffic routes. -
UNIFI_PROFILEsets insrc/main.py:Profile Modules networkdhcp, dns, network_config, networks, wans, wifi devicesport_profiles, switching securitysite_vpn, vpn systemqos Add a new module to the right set.
-
No module checks the Network version in code. When a feature needs Network 9.0+, say so in the tool docstring.
URL spaces
| Space | How to build the path | Site ID |
|---|---|---|
| Legacy | Write /ea/sites/{site_id}/<resource>. UniFiClient rewrites it to /proxy/network/api/s/{site_name}/<resource> in local mode |
UUID or name; the client maps a UUID to the site name |
| v2 | settings.get_v2_api_path(site_id) |
the client maps a UUID to the site name |
| Integration | settings.get_integration_path(f"sites/{site_id}/...") |
the client maps a name to the UUID; client.resolve_site_id() returns the UUID |
Legacy resources in use: rest/networkconf, rest/wlanconf, rest/portconf, rest/user, rest/dynamicdns, get/setting/ips. The client also rewrites devices to stat/device and sta to stat/sta.
Do not hardcode /proxy/network/api/s/.... The client passes such a path through unchanged, so a site UUID is never turned into a site name (see Known defects).
Models
- Models:
src/models/network.py,src/models/wan.py,src/models/vpn.py,src/models/port_profile.py,src/models/switching.py,src/models/qos_profile.py. - Legacy records use
idwithalias="_id"andpopulate_by_name=True. Integration models use camelCase aliases. - Only
WANConnectionandTrafficRoutesetextra="allow". The rest drop unknown keys. - The legacy API calls the VLAN field
vlan.Networkaliasesvlan_idtovlan, butnetwork_configreturns raw controller records, which containvlan. The two modules return different shapes.
Write tools
- Signature:
confirm: bool | str = False, dry_run: bool | str = False.test_every_mutating_tool_declares_dry_run(tests/unit/test_tool_registry.py) requiresdry_runon every write. - Call
validate_confirmation(confirm, "<operation>", dry_run)first. Return the dry run before you open a client. - Do not add audit code; the registry wrapper audits every mutating call.
Read-modify-write. These tools GET the whole record, merge the changes and PUT the whole record back:
update_networkupdate_wlanupdate_port_profileset_device_port_overrides(PUTs the whole device)update_site_to_site_vpn
Rules for them:
- Optional update arguments must default to
None. A default such asFalseoverwrites the stored value on every update. - Take a record from a list reply with
first_response_item()(src/utils/helpers.py), never with[0]. - The controller rewrites some fields on its own.
port_profilesre-reads the record and reports the differences (_stored_value_warnings). Do the same when you add a read-modify-write tool. set_device_port_overrides(merge=False)replaces every override, and thedns_filterslist inupdate_dns_filterreplaces the whole list. Say so in the docstring of any argument that replaces a list.
Partial updates. update_wan_dns and configure_smart_queue send only the changed fields to rest/networkconf. DHCP sends partial fields to rest/user.
Responses, errors and cache
UniFiClientreturns{"data": ...}for a reply that has a data key, and{}for an empty body. Handle a list or a dict.- 404 raises
ResourceNotFoundError, 401 and 403 raiseAuthenticationError, and other errors raiseAPIError. - The Redis read cache covers only the
networksandwifimodules (CACHED_MODULESinsrc/cache.py). Tool names containing "statistics" are never cached. - Any write that is not a dry run clears every cached read for that controller. A write tool needs no cache code of its own.
src/tools/wifi.pypasses WLAN output through_redact_wlan(). Keep that for any tool that returns WLAN records, because they contain passphrases.
Tests
- Files:
tests/unit/tools/test_<module>_tools.py. Switching tests are intests/unit/test_switching.py. Representative:tests/unit/tools/test_network_config_tools.py. - Patch the module's client name:
patch.object(nc_module, "UniFiClient", ...). Use aMagicMockclient withAsyncMockmethods, and make__aexit__returnFalse, or exceptions inside the tool are swallowed. - Assert the request body:
client.put.call_args[1]["json_data"]. - When a tool checks the API mode, set
settings.api_type = APIType.LOCALon the settings fixture.
Firmware gotchas
- Mock-only tests once passed for endpoints that do not exist (
rest/qosprofile,rest/wanconf). Before you add an endpoint, find it indocs/UNIFI_API.mdor a spec snapshot inscripts/, and say if it is not verified on hardware. - Smart Queue rates are stored in kbps; the UI shows Mbps.
- DNS-over-TLS has no REST field on firmware 10.2.x.
- POSTing a MAC the controller already knows fails with
api.err.MacUsed, so DHCP looks the client up first. Settingfixed_ipwithoutuse_fixedipdoes nothing. - A network's
purposecannot change after creation. - The integration API WAN list returns only
idandname.
Known defects (not fixed)
src/tools/site_vpn.pysends/ea/sites/{site_id}/rest/networkconf, which the client translates to the legacy path with the site name, so a site UUID and a site name both work.- The confirmation checks are not the same everywhere.
site_vpnreturns{"error": ...}where other modules raise, and the DHCP and DNS modules raiseValueErrorwhere others raiseValidationError. - Dry-run replies differ between modules:
{"dry_run": True, "would_create": ...}innetwork_configand{"status": "dry_run", ...}indhcp_reservations. qos.pymaps site UUIDs to names by hand throughclient._site_uuid_to_name, which the client now does for v2 paths anyway.