(motir-marketing) motir.co/docs/mcp/tools hand-copies motir-core's tool registry with NO guard — 24 of 55 tools in five invented groups, the one outcome MOTIR-4046's own criterion named unacceptable
Opened by Zhu Yue ·
Type · implementation defect · motir-marketing · ONE PR (see Fix direction — the producer half, if chosen, is a separate motir-core card)
Parent · MOTIR-3932 — the finding story, and the edge test says YES on its equivalently limb: this story's own acceptance criterion is the one being falsified ("whatever is chosen carries a guard that fails when this repository's copy drifts from motir-core's registries. A copied spec with no guard is not an acceptable outcome."). The story is todo, not done-category, so it qualifies under log-bug.md step 3. Containment therefore states something true: MOTIR-3932 genuinely is not finished while this stands.
Discovered in · motir run MOTIR-4046, 2026-09-02. That card was already done and merged (f3fd965, PR #25); the run stopped rather than rebuild it, and verifying its acceptance criteria against origin/main is what surfaced this.
Root cause / fix
Verified against motir-marketing origin/main @ ba0263b and motir-core origin/main.
1. MOTIR-4046 named TWO registries and guarded ONE
The card's scope item 3 and its acceptance criterion both name the two sources /docs is generated from:
"
lib/apiDocs/referencereads the OpenAPI spec file,lib/apiDocs/mcpreads the tool catalogue … Whatever mechanism is chosen carries a guard that fails when this repo's copy drifts from motir-core's registries — a copied spec with no guard is not an acceptable outcome."
The OpenAPI half was done exactly right. lib/docs.ts:fetchOpenApiSpec fetches ${APP_ORIGIN}/api/openapi/v1.json fresh at request time (app/docs/api/page.tsx, export const dynamic = 'force-dynamic'), so there is no copy to drift; tests/docs/docs.test.ts asserts the fetch URL is the published endpoint and that no spec artifact is committed under content/docs/.
The MCP-catalogue half is the shape the criterion forbade. app/docs/mcp/tools/page.tsx:11-51 is a hard-coded const GROUPS array. Its own header says so:
* The MCP tool catalogue (MOTIR-4046) — committed, reproduced from motir-core's published docs.
2. Nothing in this repository checks it — measured, not assumed
git grep -l -E "mark_integrated|dispatch_prompt|tools/list" origin/main -- tests lib app returns exactly one path: app/docs/mcp/tools/page.tsx itself. tests/docs/docs.test.ts is the only docs guard and both of its drift limbs read lib/docs.ts — neither ever loads the tools page. So the enumeration has no reader and no checker.
3. It has ALREADY diverged structurally — the grouping is invented
This is not a latent risk; the copy is wrong today in a way a guard would have caught at authoring time.
motir-core lib/apiDocs/mcp.ts | motir-marketing app/docs/mcp/tools/page.tsx | |
|---|---|---|
| tools | 55 (TOOL_SUMMARIES keys = TOOL_PERMISSIONS keys = 55, held equal by a typecheck totality chain) | 24 |
| grouping | DERIVED — a tool's group IS its TOOL_PERMISSIONS permission; labels derived from permissions.* copy since MOTIR-2581; only the ORDER is authored | five authored strings — Work items · Planning · Dispatch & runs · Sprints · Lessons & designs — corresponding to no permission |
| truth gate | a fingerprint per summary, recomputed from a live tools/list (now itself broken — MOTIR-4165) | none |
Where it is NOT yet wrong, stated so the card is not read as claiming more than it proves: all 24 names still exist. Diffing the page's list against TOOL_PERMISSIONS' 55 keys (comm -23) returns empty, and the 55 keys match this session's live MCP palette exactly. The defect is the absent guard, not an observed stale name — 31 tools are simply unnamed, with no stated selection rule beyond "the tools you will reach for most", and nothing would report the first rename or retirement.
4. Why the guard's absence is load-bearing rather than tidy
motir-core's catalogue cannot go stale by construction: a tool added to MCP_TOOL_NAMES without a permission fails typecheck in toolPermissions.ts, which widens keyof typeof TOOL_PERMISSIONS, which makes TOOL_SUMMARIES incomplete, which fails typecheck in mcp.ts — "a tool cannot reach the server undocumented." Copying a subset of that registry into a second repository is precisely the act that breaks the chain, and it is why MOTIR-4046 called the mechanism a decision rather than a detail. Since MOTIR-3951 deleted app/(public) from motir-core, motir.co/docs/mcp/tools is the only rendered tool catalogue Motir publishes — so the unguarded copy is not a mirror of a guarded original, it is the original.
Reproduce: git -C <marketing> show origin/main:app/docs/mcp/tools/page.tsx (the literal array), then git -C <marketing> grep -l 'tools/list' origin/main -- tests (empty), then rename any tool in motir-core's TOOL_PERMISSIONS and run pnpm test in motir-marketing — green.
Fix direction
Recommended — GENERATE it, symmetric with the half that was done right. motir-core publishes the catalogue at a public, anonymous, versioned endpoint the way it already publishes /api/openapi/v1.json; this page renders it through a lib/docs.ts reader exactly as /docs/api does, and the copy ceases to exist. That is TWO cards in TWO repositories (ONE SUBTASK = ONE REPO = ONE PR): a motir-core producer, and this card as the motir-marketing consumer blocked_by it. ⚠️ The producer card does not exist yet — it is not filed here because a run creates no work item except a bug, and because MOTIR-4165's fix-direction item 4 already names the prior question ("if the catalogue has no reader left in motir-core it may belong in motir-marketing, and then the gate goes with it … a decision for the epic that moved the page") as MOTIR-3875's. Answer that question before scheduling this card, and note this bug supplies the fact it was missing: the reader DOES still exist, it is here, and it is unguarded.
Fallback, single-repo and fully sufficient for the criterion — DELETE the enumeration. The page already tells the reader the authoritative list is the live tools/list; drop the 24 names and the invented groups, keep the prose and the link. A copy that does not exist cannot drift. Cheaper and honest, but it removes the only rendered catalogue Motir has, so it is second choice.
Rejected — pin the copy against a committed fixture in this repository. That guards the copy against a second copy and proves the parse, not the agreement — the exact failure MOTIR-4139 was filed and fixed for on the subprocessor seam one repository over. Do not repeat it here.
Acceptance criteria
(Ordering: every criterion is true at PR-open in motir-marketing; none reads main, a merge or a deploy. Repo: motir-marketing alone — if the recommended shape is taken, the motir-core producer is its own card and this one is blocked_by it.)
/docs/mcp/toolsno longer carries an unguarded copy ofmotir-core's tool registry — the enumeration is either GENERATED from a published artifact, or removed in favour of prose plus the live-surface link. The disposition is recorded in the PR body, naming which of the two shapes was taken and why.- If any enumerated form survives, a guard under
tests/docs/fails when a name the page renders is absent from the cataloguemotir-coreactually serves, naming the tool — proved to FIRE by perturbing one name inside the test itself, never proved to pass. - That guard's source of truth is
motir-core's published surface, never a second committed copy in this repository; when the surface is unreachable it FAILS rather than skips. - The invented five-group structure is gone: either the derived permission grouping arrives with the generated data, or no grouping is asserted at all.
tests/docs/docs.test.ts's existing OpenAPI limb is untouched and still passes — this widens MOTIR-4046's drift guard, it does not replace it.lib/docs.ts's header no longer implies the drift guard covers both registries unless it does; the comment inapp/docs/mcp/tools/page.tsxdescribing the page as "committed, reproduced from motir-core's published docs" is true of what ships or is deleted with the copy.pnpm lint · format:check · typecheck · build · testpass; AA contrast holds in both themes for anything the page's structure changes.
Advisory dispositions
validate_work_item at filing time: valid: true, blockers: [], ONE advisory. Disposed of here, at CREATE time, per run.md guard #4's create-time limb.
-
reference→ MOTIR-4165 (plainadvisory, referenced statustodo): NOT consumed — noblocked_byis owed, and wiring one would break gate 7. Two independent reasons, and both have to hold for the answer to be no edge:- It is not a precondition. MOTIR-4165 restores
motir-core's OWN fingerprint gate overlib/apiDocs/mcp.tsand re-pinsadd_plan_items. This card reads neither that gate nor those pins; every one of its criteria is dischargeable with MOTIR-4165 untouched. The reference is provenance and context — how the sibling guard came off in the same week, and which card already flagged the catalogue's HOME as an open question — which is exactly whatrelates_tocarries and what this card has. - It is in another container. MOTIR-4165 hangs under epic MOTIR-2200; this card hangs under story MOTIR-3932, under epic MOTIR-3875. A leaf-to-leaf
blocked_byacross parents is what gate 7 forbids, so following the wire the edge instruction literally here would satisfy one rule by breaking another.
What IS owed and is not an edge: the answer to MOTIR-4165's fix-direction item 4 — does the catalogue still belong in
motir-core? — is a prior question for MOTIR-3875, and this card must not be scheduled ahead of it (said in Fix direction above, and repeated here because this is the section a dispatch guard reads). - It is not a precondition. MOTIR-4165 restores
Resolution
(open — filled by the subtask that closes this)
Discussion
No comments yet.
Adding to this discussion signs you in on app.motir.co and brings you back to this request.