agentleFS
enunoenuno/unifi-mcp-server/.agents/skills/unifi-network-config-tools/SKILL.md

unifi-network-config-tools

Skill294 starsChanged 18 days agoApache-2.0View on GitHub

Use when adding, changing or testing a UniFi Network configuration MCP tool (networks, WiFi, WAN, DHCP, DNS, QoS, port profiles, switching, VPN) - covers URL spaces and site ID translation, read-modify-write updates, the write pattern, caching, test fixtures and firmware gotchas.

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 3 are distinctive, and 6 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 6

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Every public async def is a tool (register_module_tools() in src/tool_registry.py).

    line 28
  2. Very specific:

    All 11 modules are in _LOCAL_TOOL_MODULES (src/main.py).

    line 29
  3. Distinctive:

    Add a new module to the right set.

    line 40
  4. Distinctive:

    No module checks the Network version in code.

    line 41
  5. Very specific:

    Do not hardcode /proxy/network/api/s/....

    line 53
  6. Very specific:

    Only WANConnection and TrafficRoute set extra="allow".

    line 59
  7. Very specific:

    Call validate_confirmation(confirm, "<operation>", dry_run) first.

    line 65
  8. Distinctive:

    Do not add audit code; the registry wrapper audits every mutating call.

    line 66
  9. Very specific:

    When a tool checks the API mode, set settings.api_type = APIType.LOCAL on the settings fixture.

    line 98

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

UniFi Network Configuration Tools

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

ModuleToolsExamples
src/tools/networks.py4get_network_details, list_vlans, get_subnet_info
src/tools/network_config.py3create_network, update_network, delete_network
src/tools/wifi.py5list_wlans, update_wlan, get_wlan_statistics
src/tools/wans.py6list_wan_connections, create_dynamic_dns
src/tools/dhcp_reservations.py5create_dhcp_reservation, remove_dhcp_reservation
src/tools/dns_management.py4list_wan_dns, update_dns_filter
src/tools/qos.py3list_traffic_routes, configure_smart_queue
src/tools/port_profiles.py8update_port_profile, set_device_port_overrides
src/tools/switching.py6list_switch_stacks, get_lag_details (reads only)
src/tools/vpn.py2list_vpn_tunnels, list_vpn_servers
src/tools/site_vpn.py3get_site_to_site_vpn, update_site_to_site_vpn

Registration and modes

  • Every public async def is a tool (register_module_tools() in src/tool_registry.py). port_profiles.get_device_by_mac is 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_PROFILE sets in src/main.py:

    ProfileModules
    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

SpaceHow to build the pathSite ID
LegacyWrite /ea/sites/{site_id}/<resource>. UniFiClient rewrites it to /proxy/network/api/s/{site_name}/<resource> in local modeUUID or name; the client maps a UUID to the site name
v2settings.get_v2_api_path(site_id)the client maps a UUID to the site name
Integrationsettings.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 id with alias="_id" and populate_by_name=True. Integration models use camelCase aliases.
  • Only WANConnection and TrafficRoute set extra="allow". The rest drop unknown keys.
  • The legacy API calls the VLAN field vlan. Network aliases vlan_id to vlan, but network_config returns raw controller records, which contain vlan. 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) requires dry_run on 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_network
  • update_wlan
  • update_port_profile
  • set_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 as False overwrites 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_profiles re-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 the dns_filters list in update_dns_filter replaces 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

  • UniFiClient returns {"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 raise AuthenticationError, and other errors raise APIError.
  • The Redis read cache covers only the networks and wifi modules (CACHED_MODULES in src/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.py passes 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 in tests/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 a MagicMock client with AsyncMock methods, and make __aexit__ return False, 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.LOCAL on 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 in docs/UNIFI_API.md or a spec snapshot in scripts/, 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. Setting fixed_ip without use_fixedip does nothing.
  • A network's purpose cannot change after creation.
  • The integration API WAN list returns only id and name.

Known defects (not fixed)

  • src/tools/site_vpn.py sends /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_vpn returns {"error": ...} where other modules raise, and the DHCP and DNS modules raise ValueError where others raise ValidationError.
  • Dry-run replies differ between modules: {"dry_run": True, "would_create": ...} in network_config and {"status": "dry_run", ...} in dhcp_reservations.
  • qos.py maps site UUIDs to names by hand through client._site_uuid_to_name, which the client now does for v2 paths anyway.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.