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

unifi-protect-tools

Skill294 starsChanged 18 days agoApache-2.0View on GitHub

Use when adding, changing or testing a UniFi Protect MCP tool (any src/tools/protect_*.py module) - covers the Protect client and paths, v7 models, the confirm/dry_run write pattern, response shapes, test fixtures and known Protect API gaps.

We split this skill into the rules it gives an agent

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

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    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 in the module is a tool (register_module_tools() in src/tool_registry.py).

    line 36
  2. Very specific:

    Use ProtectClient from src/api/protect_client.py, not UniFiClient.

    line 43
  3. Very specific:

    Call await client.authenticate() first.

    line 45
  4. Very specific:

    only then open ProtectClient

    line 73
  5. Very specific:

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

    line 75
  6. Very specific:

    After you add or rename a tool, run UPDATE_TOOL_TIERS=1 pytest tests/unit/fleet/test_tiers.py.

    line 94

≈ “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 Protect Tools

Protect tools talk to the Protect integration API on the local console. Read this before you add or change a tool in any src/tools/protect_*.py module. For the general add-a-tool workflow, see unifi-mcp-tool-builder.

Module map

15 modules, 72 functions, 71 registered tool names.

ModuleToolsExamples
src/tools/protect_alarm.py8list_protect_arm_profiles, set_current_protect_arm_profile, disable_protect_alarm
src/tools/protect_alarm_hubs.py4list_protect_alarm_hubs, update_protect_alarm_hub
src/tools/protect_bridges.py3get_protect_bridge, update_protect_bridge
src/tools/protect_cameras.py2list_protect_cameras, get_protect_camera
src/tools/protect_devices.py13list_protect_devices, update_protect_light, get_protect_chime
src/tools/protect_events.py7list_protect_events, send_protect_alarm_webhook
src/tools/protect_fobs.py3update_protect_fob
src/tools/protect_link_stations.py3update_protect_link_station
src/tools/protect_nvr.py2list_protect_nvrs, get_protect_nvr
src/tools/protect_pos.py1ingest_pos_transaction
src/tools/protect_relays.py4update_protect_relay, activate_protect_relay_output
src/tools/protect_sirens.py6play_protect_siren, stop_protect_siren
src/tools/protect_speakers.py4update_protect_speaker
src/tools/protect_users.py4list_protect_users, list_protect_ulp_users
src/tools/protect_views.py8get_protect_meta_info, create_protect_live_view

MCP resources for Protect are in src/resources/protect.py. The raw connector_protect_* proxy tools are in src/tools/connector.py and belong to the cloud tool set.

Registration

  • Every public async def in the module is a tool (register_module_tools() in src/tool_registry.py). Name helpers with a leading _.
  • Protect modules are in _LOCAL_TOOL_MODULES only (src/main.py). They do not exist in cloud modes.
  • A new module goes into three places in src/main.py: _LOCAL_TOOL_MODULES, the protect profile and the system profile.
  • Protect reads are not cached. Protect modules are not in CACHED_MODULES (src/cache.py).

HTTP

  • Use ProtectClient from src/api/protect_client.py, not UniFiClient.
  • Build every path with settings.get_protect_integration_path("<endpoint>"). In local mode that gives /proxy/protect/integration/v1/<endpoint>.
  • Call await client.authenticate() first; it checks access with a GET to cameras.
  • Send bodies with json_data=, not json=.
  • A status of 400 or more raises APIError.
async with ProtectClient(settings) as client:
    await client.authenticate()
    response = await client.patch(
        settings.get_protect_integration_path(f"relays/{relay_id}"),
        json_data=payload,
    )

Models

  • v7 models are in src/models/protect_v7.py and inherit _ProtectV7Base, which sets populate_by_name=True and extra="allow". Unknown fields from newer firmware must not break parsing.
  • Fields are snake_case with a camelCase alias=. Dump with model_dump(by_alias=True).
  • Older models (src/models/protect_camera.py, src/models/protect_nvr.py and others) do not allow extra fields. Do not copy them for new work.
  • Export new models from src/models/__init__.py.

Write tools

Copy update_protect_relay in src/tools/protect_relays.py. The order is fixed:

  1. validate_confirmation(confirm, "<operation>", dry_run)
  2. validate IDs and arguments, and raise ValidationError on bad input
  3. build the payload from the arguments that are not None; raise ValidationError if it is empty
  4. if coerce_bool(dry_run): return {"dry_run": True, "operation": "<tool name>", ..., "payload": payload}
  5. only then open ProtectClient

Do not write audit code; the registry wrapper audits every mutating call. A tool with a risky word in its name (disable, reset and others) must be in DESTRUCTIVE_TOOLS (src/tool_registry.py) or in RISKY_NAME_EXEMPTIONS (tests/unit/fleet/test_tiers.py).

Response shapes

KindReturn
List{"offset", "limit", "count", "totalCount", "data": [...]}; use validate_limit_offset() (default 100, range 1 to 1000)
Get or updateModel.model_validate(_extract_item(response)).model_dump(by_alias=True)
Action{"success": True, <ids>, "response": ...}

_extract_collection and _extract_item accept a bare value or a {"data": ...} wrapper. Each module has its own copy. Log through sanitize_log_message().

Tests

  • Files: tests/unit/tools/test_protect_<module>.py. Representative: tests/unit/tools/test_protect_relays.py.
  • Patch the client: patch("src.tools.protect_relays.ProtectClient", return_value=mock_client), where mock_client is an AsyncMock with __aenter__ returning itself and __aexit__ returning False.
  • Assert the exact path and body: mock_client.patch.assert_awaited_once_with(path, json_data=...).
  • For every write, test refusal without confirm (and with confirm="false"), that the client is never created on refusal, and that a dry run makes no call.
  • tests/unit/tools/test_protect_write_gates.py checks every Protect write for confirm and dry_run.
  • After you add or rename a tool, run UPDATE_TOOL_TIERS=1 pytest tests/unit/fleet/test_tiers.py.

Gotchas

  • The spec baseline is Protect v7.3.68 (scripts/protect-api-spec-v7.3.68.json). Check an endpoint there before you call it.
  • The Phase 5a write paths are tested against mocks only; nobody has verified them on hardware. Say so when you change one.
  • get_protect_arm_profile calls a GET path that is not in the v7.3.68 spec, so it may return 404.
  • docs/UNIFI_API.md still calls Protect v7 "planned" near its start, which contradicts its later Phase 5a section.

Known defects (not fixed)

  • list_protect_device_updates is defined once, in src/tools/protect_devices.py. src/tools/protect_events.py imports it for its two compatibility wrappers; register_module_tools skips imported functions, so it registers once. TestToolModuleSets in tests/unit/test_main_runtime.py fails if a tool name is defined in two modules.
  • MUTATING_TOOLS_WITHOUT_GATE in src/tool_registry.py still lists Protect tools that now have gates. It is harmless.

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.