Skill Reference & Generated Manifest Drift Maintenance
Each app has a generated tool manifest (e.g.
apps/protect/src/unifi_protect_mcp/tools_manifest.json) and a generated
skill reference doc under the corresponding plugin (e.g.
plugins/unifi-network/skills/unifi-network/references/network-tools.md,
plugins/unifi-access/skills/unifi-access/references/access-tools.md)
that uses <!-- AUTO:tools:<category> --> marker blocks. Both are
generated artifacts — a pure function of the Python source
(docstrings, parameter signatures, tool registrations).
packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py
implements the shared manifest-generation logic that each app's own
generator wrapper script calls (e.g.
apps/network/scripts/generate_tool_manifest.py, invoked via that app's
make manifest target). Nothing about these generated files should ever
be hand-authored. The recurring failure mode in this codebase is not that
regeneration is hard — it's that CI's drift-check only diffs
already-rendered content against disk. It cannot detect a missing
section, a missing marker, or a description that's technically present
but shallower/staler than the source. Those gaps are only caught by a
human applying the checklists below.
Prerequisites
- Locate the generator scripts for this repo before starting — search
scripts/ (e.g. Grep -r "tools_manifest" scripts/ and
Grep -r "generate_tool_manifest\|generate_skill_references" scripts/)
rather than assuming a fixed filename, since generator script names can
move between refactors. Verified examples in this repo:
scripts/generate_skill_references.py, the shared
packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py
module, and the per-app wrapper it documents (e.g.
apps/network/scripts/generate_tool_manifest.py, run via that app's
make manifest target).
- Locate the skill reference doc(s) for the category you're touching by
searching repo-wide for
AUTO:tools: markers (Grep -rl "AUTO:tools:" .)
rather than assuming a fixed path. These live under each plugin's own
skill references directory (plugins/<plugin>/skills/<plugin>/references/*.md)
— not under apps/ — and the exact category set can vary per plugin.
- Regenerate inside a worktree venv synced to the locked dependencies.
Using system Python or a stale venv can produce output that's subtly
different from what CI would generate, defeating the point of
regenerating at all.
make pre-commit does regenerate manifests when it's actually run —
both the root Makefile (pre-commit: format generate lint test check-generated worker-typecheck, where generate includes manifest)
and each app's own Makefile (e.g. apps/network/Makefile:
pre-commit: format manifest server-manifest lint test). But there is
no .pre-commit-config.yaml wiring it into a git hook — it's a manual
make target. Don't assume a branch's manifest is current just because
its author "probably ran pre-commit"; check explicitly (Procedure A)
whenever behavior-relevant source changed.
Procedure A: Regenerate after any behavior/schema-relevant source change
Any change to a meta-tool's documented behavior, or a tool parameter's
type/default, requires regenerating the manifest and manually diffing
the generated text — not just confirming a description field exists.
- Make the source change (docstring, parameter signature, etc.) in the
Python source — e.g. a shared meta-tool module like
packages/unifi-mcp-shared/src/unifi_mcp_shared/meta_tools.py, or an
app's own tool module.
- Run the manifest generator for every affected app (
make manifest at
the app level, or make generate at the repo root for all apps).
- Diff the app's regenerated manifest for content parity, not just
presence: does the new manifest text actually carry the same depth of
detail as the docstring (edge cases, defaults, caps, thresholds), or
did it fall back to abbreviated legacy text?
- If a parameter's default or type changed, confirm the manifest
description string was regenerated after the docstring update — an
ordering bug (regenerate-then-edit-docstring, instead of
edit-docstring-then-regenerate) silently ships mismatched schema even
though runtime behavior stays correct. Example seen in this codebase:
a parameter changed from
list[str] = [] to Optional[list[str]] = None at runtime coalesces identically (value or None), but a
manifest generated before the docstring update still described the
old "empty list" default — confusing for any LLM client reading the
JSON schema directly instead of the Python source.
- Treat "the description field exists" as insufficient review signal.
The CI drift-check only fails on differing rendered content for
fields/markers that already exist — it will pass even when the
content is stale or abbreviated, because presence ≠ parity.
Procedure B: New tool category checklist (skill reference docs)
Adding a new tool category (e.g. a new settings category surfaced in a
plugin's skill reference doc) requires three things, and bumping a tool
count is not one of them:
- ToC entry for the category in the relevant plugin's skill
reference file (
plugins/<plugin>/skills/<plugin>/references/*.md).
- Section block with
<!-- AUTO:tools:<category_name> --> /
closing marker comments (match the exact naming convention used by
neighboring sections in the same file — e.g. the doors, policies,
credentials sections in
plugins/unifi-access/skills/unifi-access/references/access-tools.md,
or the clients, devices, firewall sections in
plugins/unifi-network/skills/unifi-network/references/network-tools.md).
- Run the generator and confirm the section is actually populated
with tool entries — not just that the total tool count changed.
Why this matters: the drift-check flag only fails when re-rendered
content differs from what's on disk for markers that already exist. A
PR that adds tools but skips the ToC/section/markers entirely will pass
CI cleanly while leaving the new tools completely undiscoverable in the
reference doc — the count goes up, but an agent reading the doc body
never finds them. This must be caught in review, not by the pipeline.
Procedure C: Resolve generated-manifest merge conflicts by regeneration
When merging main into a working branch (fork-edit, rebase, or a
long-lived branch catching up), a conflict in the app's generated
manifest file is the expected failure mode whenever both branches
touched any tool in that app — it is not a real content conflict, it's
two independent generation runs colliding.
- Accept the merge with conflict markers present in the manifest file
(don't try to hand-resolve the diff).
- Regenerate the manifest from the now-merged Python source tree, using
the exact same generation command CI uses (the app's
make manifest
target).
- Stage the freshly regenerated file in place of the conflicted one.
- Do this inside a worktree venv synced to locked dependencies so the
output is byte-identical to what CI would produce — a stale venv can
produce a "regenerated" file that still diffs from CI's expectation.
Never resolve a manifest conflict by manually editing either side of the
diff — because the manifest is a deterministic function of source, manual
resolution only looks plausible; regeneration is the only version
guaranteed correct by construction. This class of conflict is enabled by
make pre-commit being a manual target rather than an enforced git hook:
a branch that never re-ran make pre-commit (or make manifest) after
main moved forward will carry a stale manifest that then conflicts on
merge.
Procedure D: Catch dormant drift when an unrelated PR trips the CI gate
The reference-check CI gate (the root Makefile's check-skill-references
/ check-generated targets) only fires when the generated manifest or a
reference file changes in the current PR. That means a category
shipped months earlier with a skipped documentation step (missing ToC
entry, missing AUTO:tools:<category> markers) can sit silently broken
until some unrelated PR happens to touch the same generated files and
trips the gate.
- When the gate fires unexpectedly (or during any manifest/reference
review), check whether the flagged gap actually originates from the
current change or is pre-existing drift from an earlier PR.
- If pre-existing: bundle the drift fix into the PR that tripped the
gate rather than filing a separate fix — it's faster and unblocks the
triggering PR immediately.
- Add the missing section header +
AUTO:tools:<category> markers,
then run scripts/generate_skill_references.py (or the app-level
make skill-references equivalent) to repopulate all sections, and
sanity-check the resulting tool/section counts look right for the
whole file, not just the category you fixed.
- Treat this as confirmation of the Procedure B checklist for whichever
earlier PR introduced the category — the fix strategy is the same
checklist, applied retroactively.
Cross-Cutting Gotchas
- Presence ≠ parity, everywhere in this pipeline. Every drift-check
gate here (
--check flags, manifest diffs, check-skill-references
CI) only validates that already-rendered content matches disk. None of
them validate that a new category, tool, or behavior actually got a
section, marker, or full-depth description — that's a structural gap
in the tooling, not a one-off bug, so don't rely on CI green to mean
"the docs are complete."
make pre-commit regenerates manifests, but only if someone runs it.
Both the root Makefile (pre-commit: format generate lint test check-generated worker-typecheck) and each app's Makefile (e.g.
apps/network/Makefile's pre-commit: format manifest server-manifest lint test) do include manifest regeneration — but there's no
.pre-commit-config.yaml forcing it automatically on commit. This is
the recurring enabler behind stale manifests, both as isolated drift
(Procedure A) and as merge conflicts (Procedure C). If a contributor's
branch is more than a few commits behind main and touches any tool
file, assume the manifest needs a manual regen check before merge —
don't assume make pre-commit was ever run.
- Never hand-edit a generated artifact, even to "quickly fix" a
conflict or a one-line description mismatch. Any manual edit to a
generated manifest or a skill reference file's
AUTO: block will be
silently overwritten or drift again at the next regeneration — fix the
Python source and regenerate instead.
- A "correct behavior, wrong schema text" gap is still worth fixing.
Even when runtime behavior is unaffected (e.g.
value or None
coalescing masks a default-value mismatch), the generated schema is
what LLM clients and downstream integrators actually read — treat
schema/doc drift as a real defect, not cosmetic, even at low severity.
- Out of scope: the runtime tool-index/registration-mode catalog is a
different "manifest" from the ones this skill covers.
docs/tool-index.md
and apps/worker/worker/src/relay-object.ts describe a discovery-time
catalog keyed by UNIFI_TOOL_REGISTRATION_MODE (eager/lazy/meta_only)
— it is hand-authored documentation of runtime behavior, not a generated
artifact from packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py.
A known divergence had meta_only mode's index initially behaving like
lazy mode instead of the meta_only behavior documented in
docs/tool-index.md. Regenerating an app's generated manifest (e.g. the
one under apps/protect/src/unifi_protect_mcp/) or skill-reference docs
does not touch this code path — treat registration-mode divergence as a
separate bug class from the generated-artifact drift this skill addresses.
1---2name: myco-skill-reference-and-manifest-drift-maintenance3description: Use when adding, changing, or reviewing tool categories, tool parameters, or tool docstrings in unifi-mcp apps (apps/access, apps/network, apps/protect) whose behavior surfaces through generated artifacts: each app's generated tool manifest (e.g. apps/protect/src/unifi_protect_mcp/tools_manifest.json) and the plugin skill reference docs with AUTO:tools:<category> marker blocks. Applies even if the user only asks to "add a tool" and doesn't mention manifests or docs — generated artifacts are a deterministic function of Python source and CI's drift-check gate only verifies rendered content matches disk, never that a new category/tool got a section, marker, or full-depth description. Covers: regenerating manifests/skill-references after source changes, the new-tool-category checklist (ToC + AUTO markers + generator run), resolving generated-manifest merge conflicts by regeneration (never by hand), and catching pre-existing drift surfaced when an unrelated PR trips the reference-check CI gate.4---56# Skill Reference & Generated Manifest Drift Maintenance78Each app has a generated tool manifest (e.g.9`apps/protect/src/unifi_protect_mcp/tools_manifest.json`) and a generated10skill reference doc under the corresponding plugin (e.g.11`plugins/unifi-network/skills/unifi-network/references/network-tools.md`,12`plugins/unifi-access/skills/unifi-access/references/access-tools.md`)13that uses `<!-- AUTO:tools:<category> -->` marker blocks. Both are14**generated artifacts** — a pure function of the Python source15(docstrings, parameter signatures, tool registrations).16`packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py`17implements the shared manifest-generation logic that each app's own18generator wrapper script calls (e.g.19`apps/network/scripts/generate_tool_manifest.py`, invoked via that app's20`make manifest` target). Nothing about these generated files should ever21be hand-authored. The recurring failure mode in this codebase is not that22regeneration is hard — it's that CI's drift-check only diffs23*already-rendered* content against disk. It cannot detect a missing24section, a missing marker, or a description that's technically present25but shallower/staler than the source. Those gaps are only caught by a26human applying the checklists below.2728## Prerequisites2930- Locate the generator scripts for this repo before starting — search31 `scripts/` (e.g. `Grep -r "tools_manifest" scripts/` and32 `Grep -r "generate_tool_manifest\|generate_skill_references" scripts/`)33 rather than assuming a fixed filename, since generator script names can34 move between refactors. Verified examples in this repo:35 `scripts/generate_skill_references.py`, the shared36 `packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py`37 module, and the per-app wrapper it documents (e.g.38 `apps/network/scripts/generate_tool_manifest.py`, run via that app's39 `make manifest` target).40- Locate the skill reference doc(s) for the category you're touching by41 searching repo-wide for `AUTO:tools:` markers (`Grep -rl "AUTO:tools:" .`)42 rather than assuming a fixed path. These live under each plugin's own43 skill references directory (`plugins/<plugin>/skills/<plugin>/references/*.md`)44 — not under `apps/` — and the exact category set can vary per plugin.45- Regenerate inside a worktree venv synced to the **locked dependencies**.46 Using system Python or a stale venv can produce output that's subtly47 different from what CI would generate, defeating the point of48 regenerating at all.49- `make pre-commit` **does** regenerate manifests when it's actually run —50 both the root Makefile (`pre-commit: format generate lint test51 check-generated worker-typecheck`, where `generate` includes `manifest`)52 and each app's own Makefile (e.g. `apps/network/Makefile`:53 `pre-commit: format manifest server-manifest lint test`). But there is54 no `.pre-commit-config.yaml` wiring it into a git hook — it's a manual55 `make` target. Don't assume a branch's manifest is current just because56 its author "probably ran pre-commit"; check explicitly (Procedure A)57 whenever behavior-relevant source changed.5859## Procedure A: Regenerate after any behavior/schema-relevant source change6061Any change to a meta-tool's documented behavior, or a tool parameter's62type/default, requires regenerating the manifest *and* manually diffing63the generated text — not just confirming a description field exists.64651. Make the source change (docstring, parameter signature, etc.) in the66 Python source — e.g. a shared meta-tool module like67 `packages/unifi-mcp-shared/src/unifi_mcp_shared/meta_tools.py`, or an68 app's own tool module.692. Run the manifest generator for every affected app (`make manifest` at70 the app level, or `make generate` at the repo root for all apps).713. Diff the app's regenerated manifest for **content parity**, not just72 presence: does the new manifest text actually carry the same depth of73 detail as the docstring (edge cases, defaults, caps, thresholds), or74 did it fall back to abbreviated legacy text?754. If a parameter's default or type changed, confirm the manifest76 description string was regenerated *after* the docstring update — an77 ordering bug (regenerate-then-edit-docstring, instead of78 edit-docstring-then-regenerate) silently ships mismatched schema even79 though runtime behavior stays correct. Example seen in this codebase:80 a parameter changed from `list[str] = []` to `Optional[list[str]] =81 None` at runtime coalesces identically (`value or None`), but a82 manifest generated before the docstring update still described the83 old "empty list" default — confusing for any LLM client reading the84 JSON schema directly instead of the Python source.855. Treat "the description field exists" as insufficient review signal.86 The CI drift-check only fails on differing rendered content for87 fields/markers that already exist — it will pass even when the88 content is stale or abbreviated, because presence ≠ parity.8990## Procedure B: New tool category checklist (skill reference docs)9192Adding a new tool category (e.g. a new settings category surfaced in a93plugin's skill reference doc) requires three things, and bumping a tool94count is not one of them:95961. **ToC entry** for the category in the relevant plugin's skill97 reference file (`plugins/<plugin>/skills/<plugin>/references/*.md`).982. **Section block** with `<!-- AUTO:tools:<category_name> -->` /99 closing marker comments (match the exact naming convention used by100 neighboring sections in the same file — e.g. the `doors`, `policies`,101 `credentials` sections in102 `plugins/unifi-access/skills/unifi-access/references/access-tools.md`,103 or the `clients`, `devices`, `firewall` sections in104 `plugins/unifi-network/skills/unifi-network/references/network-tools.md`).1053. **Run the generator** and confirm the section is actually populated106 with tool entries — not just that the total tool count changed.107108Why this matters: the drift-check flag only fails when re-rendered109content *differs* from what's on disk for markers that already exist. A110PR that adds tools but skips the ToC/section/markers entirely will pass111CI cleanly while leaving the new tools completely undiscoverable in the112reference doc — the count goes up, but an agent reading the doc body113never finds them. This must be caught in review, not by the pipeline.114115## Procedure C: Resolve generated-manifest merge conflicts by regeneration116117When merging `main` into a working branch (fork-edit, rebase, or a118long-lived branch catching up), a conflict in the app's generated119manifest file is the *expected* failure mode whenever both branches120touched any tool in that app — it is not a real content conflict, it's121two independent generation runs colliding.1221231. Accept the merge with conflict markers present in the manifest file124 (don't try to hand-resolve the diff).1252. Regenerate the manifest from the now-merged Python source tree, using126 the exact same generation command CI uses (the app's `make manifest`127 target).1283. Stage the freshly regenerated file in place of the conflicted one.1294. Do this inside a worktree venv synced to locked dependencies so the130 output is byte-identical to what CI would produce — a stale venv can131 produce a "regenerated" file that still diffs from CI's expectation.132133Never resolve a manifest conflict by manually editing either side of the134diff — because the manifest is a deterministic function of source, manual135resolution only *looks* plausible; regeneration is the only version136guaranteed correct by construction. This class of conflict is enabled by137`make pre-commit` being a manual target rather than an enforced git hook:138a branch that never re-ran `make pre-commit` (or `make manifest`) after139`main` moved forward will carry a stale manifest that then conflicts on140merge.141142## Procedure D: Catch dormant drift when an unrelated PR trips the CI gate143144The reference-check CI gate (the root Makefile's `check-skill-references`145/ `check-generated` targets) only fires when the generated manifest or a146reference file *changes* in the current PR. That means a category147shipped months earlier with a skipped documentation step (missing ToC148entry, missing `AUTO:tools:<category>` markers) can sit silently broken149until some unrelated PR happens to touch the same generated files and150trips the gate.1511521. When the gate fires unexpectedly (or during any manifest/reference153 review), check whether the flagged gap actually originates from the154 current change or is pre-existing drift from an earlier PR.1552. If pre-existing: bundle the drift fix into the PR that tripped the156 gate rather than filing a separate fix — it's faster and unblocks the157 triggering PR immediately.1583. Add the missing section header + `AUTO:tools:<category>` markers,159 then run `scripts/generate_skill_references.py` (or the app-level160 `make skill-references` equivalent) to repopulate all sections, and161 sanity-check the resulting tool/section counts look right for the162 whole file, not just the category you fixed.1634. Treat this as confirmation of the Procedure B checklist for whichever164 earlier PR introduced the category — the fix strategy is the same165 checklist, applied retroactively.166167## Cross-Cutting Gotchas168169- **Presence ≠ parity, everywhere in this pipeline.** Every drift-check170 gate here (`--check` flags, manifest diffs, `check-skill-references`171 CI) only validates that already-rendered content matches disk. None of172 them validate that a *new* category, tool, or behavior actually got a173 section, marker, or full-depth description — that's a structural gap174 in the tooling, not a one-off bug, so don't rely on CI green to mean175 "the docs are complete."176- **`make pre-commit` regenerates manifests, but only if someone runs it.**177 Both the root Makefile (`pre-commit: format generate lint test178 check-generated worker-typecheck`) and each app's Makefile (e.g.179 `apps/network/Makefile`'s `pre-commit: format manifest server-manifest180 lint test`) do include manifest regeneration — but there's no181 `.pre-commit-config.yaml` forcing it automatically on commit. This is182 the recurring enabler behind stale manifests, both as isolated drift183 (Procedure A) and as merge conflicts (Procedure C). If a contributor's184 branch is more than a few commits behind `main` and touches any tool185 file, assume the manifest needs a manual regen check before merge —186 don't assume `make pre-commit` was ever run.187- **Never hand-edit a generated artifact**, even to "quickly fix" a188 conflict or a one-line description mismatch. Any manual edit to a189 generated manifest or a skill reference file's `AUTO:` block will be190 silently overwritten or drift again at the next regeneration — fix the191 Python source and regenerate instead.192- **A "correct behavior, wrong schema text" gap is still worth fixing.**193 Even when runtime behavior is unaffected (e.g. `value or None`194 coalescing masks a default-value mismatch), the generated schema is195 what LLM clients and downstream integrators actually read — treat196 schema/doc drift as a real defect, not cosmetic, even at low severity.197- **Out of scope: the runtime tool-index/registration-mode catalog is a198 different "manifest" from the ones this skill covers.** `docs/tool-index.md`199 and `apps/worker/worker/src/relay-object.ts` describe a discovery-time200 catalog keyed by `UNIFI_TOOL_REGISTRATION_MODE` (`eager`/`lazy`/`meta_only`)201 — it is hand-authored documentation of runtime behavior, not a generated202 artifact from `packages/unifi-mcp-shared/src/unifi_mcp_shared/manifest_generator.py`.203 A known divergence had `meta_only` mode's index initially behaving like204 `lazy` mode instead of the `meta_only` behavior documented in205 `docs/tool-index.md`. Regenerating an app's generated manifest (e.g. the206 one under `apps/protect/src/unifi_protect_mcp/`) or skill-reference docs207 does not touch this code path — treat registration-mode divergence as a208 separate bug class from the generated-artifact drift this skill addresses.