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

unifi-cloud-fleet-tools

Skill294 starsChanged 18 days agoApache-2.0View on GitHub

Use when adding, changing or testing a cloud or multi-controller MCP tool (Site Manager, the connector proxy, the integration API tools, sites, Mobility, InnerSpace, Carrier Fabric) or the fleet stack (router, registry, pool, read cache, workers, fleet_* and admin tools) - covers API modes, gating, per-call routing, tokens and roles, test fixtures and known gaps.

  • Reads credentials

We split this skill into the rules it gives an agent

5 rules. About 0 are common, 0 are distinctive, and 5 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.

  • 5

    Very specific

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

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

    Every routed tool gets a keyword-only controller argument.

    line 50
  2. Very specific:

    Audit: Every mutating call writes a hash-chained attempt record and outcome record (src/utils/audit.py).

    line 90
  3. Very specific:

    Client tests: use a real httpx client with httpx.MockTransport (tests/unit/api/test_site_manager_client.py).

    line 107
  4. Very specific:

    Redis tests: use fakeredis (tests/unit/fleet/test_read_cache.py).

    line 108
  5. Very specific:

    Postgres tests: start a real server with pgserver, with no Docker.

    line 109

≈ “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 Cloud and Fleet Tools

Cloud tools call https://api.ui.com (Site Manager and the connector proxy). Fleet code lets one server serve many controllers. Read this before you change either. For the general add-a-tool workflow, see unifi-mcp-tool-builder.

Module map

ModuleToolsExamples
src/tools/site_manager.py16list_all_sites_aggregated, get_isp_metrics, list_hosts
src/tools/connector.py10connector_network_get, connector_protect_post
src/tools/integration_api.py14list_integration_sites, list_integration_wans
src/tools/sites.py3list_sites, get_site_details, get_site_statistics
src/tools/mobility.py8list_mobility_workspaces, update_mobility_device
src/tools/innerspace.py6list_innerspace_floor_plans (reads only)
src/tools/carrier.py11create_carrier_subscriber, suspend_carrier_subscriber

Fleet tools are built inside functions, not defined at module level, so the "every public async function is a tool" rule does not apply to them:

SourceToolsRegistered when
register_fleet_tools() in src/fleet/tools.pylist_controllers, select_controller, get_active_controlleralways
register_admin_tools() in src/fleet/admin_tools.py11, such as register_controller, create_api_token, verify_audit_chainDATABASE_URL is set, and not read-only
register_fleet_read_tools() in src/fleet/fleet_tools.pyfleet_health, fleet_sites, fleet_inventory, fleet_fanoutDATABASE_URL is set

API modes and gating

  • APIType (src/config/config.py) is local, cloud-v1 or cloud-ea. The code default is cloud-ea.
  • Cloud modes register only _CLOUD_TOOL_MODULES (src/main.py): sites, site_manager, connector, integration_api, carrier, innerspace and mobility. Local mode registers these too. In cloud mode get_site_statistics is left out, because its endpoints return 404 on the cloud API. That gives 71 tools, including the 4 fleet tools.
  • Local mode registers both lists, so the cloud tools are available there too.
  • Site Manager tools need UNIFI_SITE_MANAGER_ENABLED=true:
    • site_manager tools use the require_site_manager decorator;
    • mobility, innerspace and carrier call their own _require_cloud_api(), which checks site_manager_enabled;
    • the connector tools repeat the check inline.
  • Paths:
    • SiteManagerClient (src/api/site_manager_client.py) uses the base URL https://api.ui.com/v1/ and the X-API-KEY header.
    • Connector calls go to connector/consoles/{console_id}/proxy/{network|protect}/{path}.
    • InnerSpace goes through the connector proxy.
    • settings.get_integration_path() gives /v1/... for cloud-v1 and /integration/v1/... for cloud-ea.
  • Profiles: UNIFI_PROFILE has mobility, innerspace and carrier sets.

Per-call routing

Routing happens in the registry wrapper (_make_tool_wrapper() in src/tool_registry.py), not in tool bodies.

  • Every routed tool gets a keyword-only controller argument.
  • FleetRouter.resolve() (src/fleet/router.py) picks the controller in this order:
    1. the explicit controller argument;
    2. the session choice from select_controller (state key SESSION_KEY);
    3. the registry default.
  • A write must name its target. With two or more controllers registered, a write that would fall back to the default raises ValidationError. Never weaken this.
  • The tool receives per-controller Settings from ControllerProfile.apply_to() (src/fleet/registry.py). Only connection fields can differ; safety switches such as read-only stay process-wide.
  • select_controller is refused in stateless HTTP mode (MCP_STATELESS_HTTP).

Fleet stack

  • Registry. Without DATABASE_URL, EnvRegistry holds one controller named default. With it, PostgresRegistry (src/fleet/postgres_registry.py) serves lookups from memory and refreshes:

    • every UNIFI_FLEET_REGISTRY_REFRESH_SECONDS;
    • on a Redis change event.

    Stored credentials are encrypted with UNIFI_FLEET_CREDENTIAL_KEY (src/fleet/credentials.py).

  • Pool. ClientPool (src/api/pool.py) keeps one HTTP client, one rate limiter and one circuit breaker per controller, up to UNIFI_FLEET_MAX_OPEN_CLIENTS.
  • Rate limit. UNIFI_RATE_LIMIT_REQUESTS per UNIFI_RATE_LIMIT_PERIOD (src/api/rate_limit.py).
    • With Redis, the window is shared across replicas, and cloud calls share one window per API key.
    • Without Redis, each process gets the limit divided by UNIFI_FLEET_EXPECTED_REPLICAS.
  • Read cache. ReadCache (src/cache.py) needs UNIFI_CACHE_ENABLED and an explicit Redis setting.
    • It caches only the modules in CACHED_MODULES, which hold no cloud module.
    • Cached tools gain a fresh argument.
    • Any write that is not a dry run clears the controller's cached reads.
  • Workers. python -m src.worker runs health and sync jobs (src/worker/jobs.py). Controllers are split between workers by rendezvous hashing and Redis leases (src/worker/shards.py).
  • Fan-out. fleet_fanout runs only read-tier tools that take a controller argument, and returns one {ok, data|error} entry per controller. Writes are never fanned out. The limits are UNIFI_FLEET_FANOUT_CONCURRENCY, UNIFI_FLEET_FANOUT_TIMEOUT_SECONDS and UNIFI_FLEET_FANOUT_DEADLINE_SECONDS.
  • Install. The fleet extra in pyproject.toml adds redis, SQLAlchemy, asyncpg and alembic.

Auth, roles and audit

  • Network transports refuse to start without auth (ensure_network_transport_authenticated() in src/main.py).
    • MCP_AUTH_TOKEN holds static bearer tokens.
    • With a database, FleetTokenVerifier checks server-issued tokens (src/fleet/tokens.py). They start with umcp_, are stored only as SHA-256 hashes, and can be limited by role, module and controller label.
  • Roles (ROLE_TIERS in src/access.py):

    • viewer: read
    • operator: read and write
    • admin: adds destructive
    • fleet-admin: everything

    stdio callers and MCP_AUTH_TOKEN callers are fleet-admin, unless UNIFI_STDIO_ROLE lowers the stdio role.

  • Audit. Every mutating call writes a hash-chained attempt record and outcome record (src/utils/audit.py). The call is refused if the write fails while UNIFI_AUDIT_FAIL_CLOSED is on. Verify a log with python -m src.utils.audit_verify.

Write tools

  • Signature: confirm: bool | str = False, dry_run: bool | str = False. Call validate_confirmation(confirm, "<operation>", dry_run) first, and return {"dry_run": True, "operation": ..., "payload": ...} before you open a client (see src/tools/mobility.py).
  • DESTRUCTIVE_TOOLS (src/tool_registry.py) already holds all 8 mutating connector tools and suspend_carrier_subscriber. Carrier writes have the largest blast radius; keep them confirm-gated.
  • A risky word in a tool name needs an entry in DESTRUCTIVE_TOOLS or in RISKY_NAME_EXEMPTIONS (tests/unit/fleet/test_tiers.py). Regenerate the snapshot with UPDATE_TOOL_TIERS=1 pytest tests/unit/fleet/test_tiers.py.

Models and responses

  • Models: src/models/site_manager.py, src/models/mobility.py, src/models/innerspace.py, src/models/carrier.py, src/models/integration_api.py. They use populate_by_name=True, extra="allow" and camelCase aliases.
  • List tools return {"count", "data"}. Integration API lists also return offset, limit and totalCount.
  • Many site_manager tools return raw dicts or lists.

Tests

  • Tool tests patch the client: patch("src.tools.carrier.SiteManagerClient", return_value=cm). Here cm is an AsyncMock context manager, and the settings MagicMock has site_manager_enabled=True. Representative: tests/unit/tools/test_carrier.py. Assert the endpoint string and the gate error.
  • Client tests use a real httpx client with httpx.MockTransport (tests/unit/api/test_site_manager_client.py).
  • Redis tests use fakeredis (tests/unit/fleet/test_read_cache.py).
  • Postgres tests start a real server with pgserver, with no Docker. The fixtures are in tests/unit/fleet/conftest.py.
  • make verify-fleet-scale runs 3 replicas and 2 workers locally.

Gotchas

  • A docs test requires every setting to be listed in API.md and .env.example. Add both when you add a setting.
  • Spec snapshots for Mobility, InnerSpace and Carrier Fabric are in scripts/. Upstream hosts only the current version, so keep old snapshots when you need a diff.
  • MCP resources still read the default controller, not the routed one.
  • Stateful HTTP behind a load balancer needs sticky routing on mcp-session-id.
  • Tool modules are chosen once, from the base UNIFI_API_TYPE. A registered controller with a different API type still gets that tool set.

Known defects (not fixed)

  • SiteManagerClient hardcodes its base URL. It ignores UNIFI_CLOUD_API_URL, and it does not use the pool or the rate limiter.
  • The connector path argument is checked only for empty values and a leading slash.

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.