myco:api-endpoint-serializer-authoring
Skill979 starsChanged 19 days agoMITView on GitHub
Apply this skill when adding an endpoint to apps/api/, authoring a mutation-ack serializer, wiring ManagerFactory, resolving Phase 5A routing collisions, or diagnosing the validate_manifest or test_resource_route_coverage CI gates. Covers: resource vs. action classification (409 vs. 200+envelope); Cursor + paginate() pagination in services/pagination.py; ManagerFactory concurrency scoping; apps/api → unifi-core-only dependency rule; Phase 5A patterns (capability-aware dispatch on product_kinds, dual-kind stats, wrapper-dict unwrap, path disambiguation, TOOL_ROUTE_OVERRIDES in test_resource_route_coverage.py); @register_serializer(tools={...}) decorator; seven RenderKind values; field-curation discipline; _reset_registry_for_tests() isolation; and the two CI gates. Post-Phase 6: read tools use Strawberry types (graphql-api-extension skill); serializers cover mutation acks only. Activate even without explicit serializer mention — any apps/api/ PR may need CI-gate alignment.
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 4 are distinctive, and 8 are 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.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 48
Use Cursor and paginate() from apps/api/src/unifi_api/services/pagination.py:
- Very specific: line 60
Default limit 50, max 200.
- Very specific: line 85
When two products expose identically-named endpoint families (e.g., both Network and Protect expose /events), dispatch at request time by …
- Distinctive: line 91
Add per-product prefixes on ambiguous routes:
- Distinctive: line 103
When a UniFi API returns a wrapper dict (e.g., switch-ports, port-stats, lldp-neighbors):
- Distinctive: line 109
Do NOT push list logic into the DETAIL serializer.
- Very specific: line 112
When a route function name doesn't follow the default convention (tool name minus product prefix), add an entry to TOOL_ROUTE_OVERRIDES in:
- Distinctive: line 205
Every read tool must have a registered GET resource route.
- Very specific: line 208
If the name doesn't follow the default convention, add to TOOL_ROUTE_OVERRIDES in that test file.
- Very specific: line 213
Import _reset_registry_for_tests from unifi_api.serializers._registry.
- Very specific: line 236
product_kindsis comma-separated in the DB: Always split with .split(",") and filter empty strings. - Very specific: line 239
TOOL_ROUTE_OVERRIDES is test-only: Does not register routes. You still need a real FastAPI route function registered in the app.
≈ “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.
Files
API Endpoint and Serializer Authoring (apps/api)
A. Adding a Resource or Action Endpoint
1. Classify the endpoint type
- Resource endpoint (
GET /v1/sites/{id}/cameras): implies the resource exists; return HTTP 409 when the required product is absent. - Action endpoint: advisory; always return HTTP 200 with a
capability_not_availableenvelope when the capability is missing — never 404 or 409 for action endpoints.
2. Wire cursor-based pagination
Use Cursor and paginate() from
apps/api/src/unifi_api/services/pagination.py:
from unifi_api.services.pagination import Cursor, InvalidCursor, paginate
cursor = Cursor.decode(cursor_str) if cursor_str else None
page, next_cursor = paginate(items, cursor=cursor, limit=limit, key_fn=key_fn)- Cursor encodes
{last_id, last_ts}as Base64 — survives new inserts; offset/page-number pagination does not. - Default limit 50, max 200. Expose
cursoras an opaque query-string param.
3. Apply the dependency rule
apps/api/ MUST only import from unifi-core. Never import unifi-mcp-shared
inside apps/api/ — it couples the REST server to MCP protocol concerns.
4. Wire ManagerFactory
ManagerFactory lives in apps/api/src/unifi_api/services/managers.py. It
caches one manager per (controller_id, product) pair behind asyncio locks:
manager = await factory.get_connection_manager(session, controller_id, product)product_kindsin the DB row is a comma-separated string. The factory splits it and raisesUnknownProductif the product isn't listed — so setproduct_kindsaccurately when registering a controller.- Each concurrency scope (request) shares the cached manager instance; no per-request teardown.
B. Phase 5A Advanced Routing Patterns
Capability-aware dispatch
When two products expose identically-named endpoint families (e.g., both
Network and Protect expose /events), dispatch at request time by consulting
controller.product_kinds. Route to the correct manager based on which
products are active — avoids collisions without duplicate routes.
Path disambiguation for cross-product collisions
Add per-product prefixes on ambiguous routes:
/access/events— not/events/protect/health— not/health
Without prefixes, three product packages collide in the route namespace.
Dual-kind stats
Accept a kind=timeseries|detail query parameter on the same endpoint; select
the appropriate RenderKind at the route layer. Avoids duplicating tools for
two render modes of the same data.
Wrapper-dict unwrap pattern
When a UniFi API returns a wrapper dict (e.g., switch-ports, port-stats, lldp-neighbors):
- Register the serializer as
RenderKind.DETAIL. - At the route layer, call the tool inline and override
render_hintto emitLIST.
Do NOT push list logic into the DETAIL serializer.
TOOL_ROUTE_OVERRIDES
When a route function name doesn't follow the default convention (tool name
minus product prefix), add an entry to TOOL_ROUTE_OVERRIDES in:
apps/api/tests/test_resource_route_coverage.pyThis is a test-time mapping only — it is NOT applied at server startup. Adding an override here does NOT register a route; you still need a real FastAPI route function. The dict currently has 17 entries (Task 22 audit).
C. Authoring a Serializer (Post-Phase 6)
Phase 6 shift: Read tools (
unifi_list_*,unifi_get_*,protect_*,access_list_*) now project via Strawberry types — see thegraphql-api-extensionskill. The serializer layer covers mutation acks only (create/update/delete/toggle).validate_manifestaccepts either a serializer or a Strawberry type registration as valid coverage.
1. Module placement
apps/api/src/unifi_api/serializers/<domain>/<resource>.pyExample: apps/api/src/unifi_api/serializers/network/dns.py
2. Decorator syntax — use the tools= dict form
The tool_name= / kind= keyword form does NOT exist. Use:
from unifi_api.serializers._base import RenderKind, Serializer, register_serializer
@register_serializer(
tools={
"unifi_create_dns_record": {"kind": RenderKind.DETAIL},
"unifi_update_dns_record": {"kind": RenderKind.DETAIL},
"unifi_delete_dns_record": {"kind": RenderKind.DETAIL},
},
)
class DnsMutationAckSerializer(Serializer):
@staticmethod
def serialize(obj) -> dict:
if isinstance(obj, bool):
return {"success": obj}
if isinstance(obj, dict):
return obj
return {"result": str(obj)}For a single tool using the class-level kind:
@register_serializer(tools=["unifi_create_foo"])3. RenderKind taxonomy
Seven values in apps/api/src/unifi_api/serializers/_base.py:
| Value | Use |
LIST | Collection of items |
DETAIL | Single resource or mutation ack |
DIFF | Before/after comparison |
TIMESERIES | Time-ordered data points |
EVENT_LOG | Event stream |
EMPTY | Action with no meaningful return |
STREAM | Streaming response |
4. Field curation discipline
Declare exactly which fields appear for each kind. Serializers are curated,
not pass-through. Do not add a "return everything" serializer — the cumulative
curation effort across 235+ tools is intentional.
5. Serializer layering rule
Serializers live in apps/api/ only. Managers and MCP tool functions MUST NOT
reference render hints or catalog metadata. This keeps MCP servers free of
serialization coupling (decision-a63cb266).
D. CI Gates
validate_manifest
Runs at server startup via discover_serializers() in
apps/api/src/unifi_api/serializers/_registry.py. Raises
SerializerRegistryError if any manifest tool lacks both a serializer
registration and a Strawberry type registration. After Phase 6, you need
at least one — not necessarily both.
test_resource_route_coverage
File: apps/api/tests/test_resource_route_coverage.py
Every read tool must have a registered GET resource route. When adding a read tool:
- Register a route function in the FastAPI app.
- If the name doesn't follow the default convention, add to
TOOL_ROUTE_OVERRIDESin that test file. - Run
pytest apps/api/tests/test_resource_route_coverage.pylocally.
Test isolation for serializer tests
Import _reset_registry_for_tests from unifi_api.serializers._registry.
It clears module-level registries AND evicts serializer submodules from
sys.modules, forcing discover_serializers to re-import and re-run all
decorators. Call in both setup and teardown.
from unifi_api.serializers._registry import _reset_registry_for_tests
def setup_function():
_reset_registry_for_tests()
def teardown_function():
_reset_registry_for_tests()Cross-Cutting Gotchas
ManagerFactory, notControllerFactory: class lives inapps/api/src/unifi_api/services/managers.py. Wrong name →ImportError.product_kindsis comma-separated in the DB: Always split with.split(",")and filter empty strings. Inaccurateproduct_kindscausesUnknownProductat request time.- TOOL_ROUTE_OVERRIDES is test-only: Does not register routes. You still need a real FastAPI route function registered in the app.
- Live smoke required: Mock CI cannot detect UniFi API contract violations.
Six confirmed violation classes only surfaced via live hardware testing.
See
live-smoke-testingskill before cutting a PR. apps/apidependency boundary is enforced: Importingunifi_mcp_sharedanywhere inapps/api/is a build-time error.
Other versions1 project uses a version of this.
Word for word · 1
- sirkirby/unifi-mcp★ 978100% same
Edited · 0
None found.
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository16 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- docs/llms.txtllms.txt
- add-integration-api-toolSkill
- community-pr-batch-reviewSkill
- myco:community-pr-reviewSkill
- myco:extend-unifi-apiSkill
- myco:live-smoke-testingSkill
- myco:mcp-response-redaction-governanceSkill
- myco:monorepo-release-pipelineSkill
- myco:runtime-bootstrap-and-test-isolationSkill
- myco:skill-reference-and-manifest-drift-maintenanceSkill
- test-pruningSkill
- myco:unifi-client-data-correctnessSkill
- code-reviewSkill
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
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.

