agentleFS
sirkirbysirkirby/unifi-network-mcp/.agents/skills/add-integration-api-tool/SKILL.md

add-integration-api-tool

Skill979 starsChanged 19 days agoMITView on GitHub

Use this skill when adding any tool that talks to the UniFi public Integration API (X-API-Key auth, /proxy/network/integration/...). The integration API uses a different ID namespace and field schema from the V2 controller API, so these tools form their own "families" with scoped IDs. Covers when to start a new family vs join an existing one, how to scope IDs in tool descriptions, the narrow conditions under which silent ID translation is allowed (rare), how to wire auth, and how to bridge between ID spaces (explicit named bridging tools only). Applies whether you are adding a read tool, a mutation tool, or a paired family of both.

  • Reads credentials

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 4 are distinctive, and 1 is very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈4

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 1

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If your new tool fits cleanly inside an existing family, join it.

    line 58
  2. Very specific:

    MUST include a scoping clause in its description.

    line 64
  3. Distinctive:

    1:1: every V2 zone has exactly one Integration zone and vice versa

    line 122
  4. Distinctive:

    Don't bury translation inside other tools.

    line 151
  5. Distinctive:

    Read these elements before writing your own:

    line 180

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

Adding a Tool That Uses the UniFi Integration API

The UniFi controller exposes two API surfaces. They are not interchangeable. Skipping this distinction creates silent footguns where IDs from one API are accepted by tools backed by the other and silently fail — or worse, succeed against the wrong resource.

The two surfaces

V2 controller APIIntegration API
Authsession cookieX-API-Key header
Path prefix/proxy/network/api/..., /proxy/network/v2/api/.../proxy/network/integration/v1/...
IDsMongo ObjectIDs (674f0...)UUIDs (0998be5a-...)
Field namingsnake_casecamelCase
Action shape"action": "ALLOW" + flat fieldsnested {"action": {"type": "ALLOW", "allowReturnTraffic": true}}
ip_version"BOTH""ipProtocolScope": {"ipVersion": "IPV4_AND_IPV6"}
Coverageeverything the web UI usescurated, expanding subset
Membershipfull controller statefiltered (system policies often excluded)

These differences are not cosmetic. The same logical resource appears in V2 with a Mongo ID and in Integration with a UUID, with no native bridge field connecting them.

Step 1: Identify the family

A tool family is a set of tools whose returned IDs and accepted IDs are interchangeable.

  • Reading + writing to the same API endpoint group? Same family.
  • Reading from V2 and writing to V2? Same family (the existing firewall policy CRUD family).
  • Reading from Integration and writing to Integration? Same family.
  • Mixing? You're inventing a bridging tool — see Step 5.

Current families in this codebase:

FamilyAPIIDsTools
Firewall policy CRUDV2Mongounifi_list_firewall_policies, unifi_get_firewall_policy_details, create / update / delete / toggle
Firewall policy orderingIntegrationUUIDunifi_get_firewall_policy_ordering + unifi_reorder_firewall_policies

If your new tool fits cleanly inside an existing family, join it. If not, start a new family — and document its boundary.

Step 2: Scope IDs in the tool description

Any tool that returns IDs which could be confused with another family's IDs MUST include a scoping clause in its description. This applies to:

  • MCP tool descriptions (@server.tool(description=...))
  • GraphQL type and query descriptions (@strawberry.type(description=...), @strawberry.field(description=...))
  • REST route descriptions (@router.get(description=...))

Standard formula:

"These IDs are scoped to the <family-name> tool family — do not pass them to other <resource> tools."

Canonical example from the ordering family:

@server.tool(
    name="unifi_get_firewall_policy_ordering",
    description=(
        "Get user-defined firewall policy ordering for a source/destination "
        "firewall zone pair. Returns policy IDs from the UniFi public integration "
        "API (UUIDs); these IDs are scoped to the ordering tool family — pass them "
        "ONLY to unifi_reorder_firewall_policies. They do NOT correspond to the "
        "policy IDs returned by unifi_list_firewall_policies or any other "
        "controller-API firewall tool."
    ),
    ...
)

The scoping clause is not optional for integration-API tools.

Step 3: Require the API key, with clear remediation

Integration API endpoints reject session-cookie auth. Without the API key, the tool MUST fail with a remediation message. Put the check in the manager so every tool in the family inherits it:

if self._auth is None or not self._auth.has_api_key:
    raise RuntimeError(
        "<feature> requires a UniFi API key. "
        "Create a Network API token in UniFi Control Plane -> Integrations "
        "and set UNIFI_API_KEY or UNIFI_NETWORK_API_KEY for the MCP."
    )

See firewall_manager.py:_request_integration_api for the canonical implementation.

Step 4: Silent ID translation — when allowed, when banned

Banned by default. Silently translating between V2 and Integration ID spaces inside a tool is forbidden because the underlying mapping is rarely 1:1.

Grandfathered exception: zones. PR #301 translates V2 zone IDs to Integration zone UUIDs internally via _get_integration_firewall_zones. This is allowed only because zones meet all three criteria:

  • 1:1: every V2 zone has exactly one Integration zone and vice versa
  • Stable: changes via either API remain consistent
  • Deterministic: zone names are unique and stable bridge fields

Do not extend the silent-translation pattern to a new resource without verifying these three criteria against live data. The data for policies fails all three (180 V2 vs 25 Integration; 8 duplicate names in V2; no bridge field in Integration's metadata).

Step 5: When you genuinely need to bridge ID namespaces

Rare. But if a workflow requires it: write a named bridging tool. The bridge is visible in the API surface and its failure modes are documented:

@server.tool(
    name="unifi_resolve_policy_id_for_ordering",
    description=(
        "Look up the integration-API ordering UUID for a V2 firewall policy. "
        "Returns None when the policy has no Integration-API representation "
        "(e.g., predefined system policies, IP-group-referenced policies). "
        "This is the only sanctioned bridge between the firewall policy CRUD "
        "family and the firewall policy ordering family."
    ),
    ...
)

Don't bury translation inside other tools. Make the bridge first-class.

Step 6: Tests

Cover the family boundary explicitly:

  • Auth requirement — tool fails fast without an API key, with the remediation message. Anchor: test_firewall_manager.py::test_ordering_requires_api_key.
  • Scoped IDs — for ordering-style tools, the response contains integration UUIDs (not V2 IDs) and downstream tools in the family accept them.
  • Bridge failure modes — if you wrote a bridging tool (Step 5), every failure mode (missing-in-target, name collision, etc.) has a test.

Step 7: Doc surface

In the skill docs for the resource category, group integration-API tools together. They share a contract; the doc should make that obvious. Don't interleave them with V2 tools in the listing.

For the API server (REST + GraphQL), the same scoping rules apply to the operation/route descriptions. After editing those, regenerate the OpenAPI spec and reference docs via make pre-commit.

Reference implementation

apps/network/src/unifi_network_mcp/tools/firewall.py — the ordering family.

Read these elements before writing your own:

  • The two tool descriptions scoping the UUIDs
  • The shared API-key requirement at the manager layer (_request_integration_api)
  • The zone-ID translation (_get_integration_firewall_zones) — the only sanctioned silent translation
  • That the family is exactly two tools with no cross-family entry points

Enforcement note

Family scoping is currently guidance, not runtime/type enforcement. Cross-family ID misuse fails at the controller (400 from the integration API) and surfaces as a tool error. The agent reads the scoping clause in the tool description and routes IDs accordingly.

Stronger enforcement options (typed IDs via NewType, runtime regex validation, a family field in tools_manifest.json) are deliberately deferred until the integration-API surface grows beyond the current single family. If you find yourself adding a second integration-API family, reopen this question.

Related rules

  • AGENTS.md → ### API Surface Boundaries (terse statement of the rule)
  • Myco decision spore architecture-d44806fa (full reasoning + live data)

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.