Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-2227Done

11.6.1 Decision — overturn the recorded no-`outputSchema` choice, pin where the shared schemas live, the zod v3→v4 boundary into `lib/mcp`, the totality mechanism, and the exemption list

Deliverable: an amendment to docs/decisions/public-api-conventions.md (this changes the schema-ownership split Amendments 2 and 5 pinned, so it belongs there rather than in a new file).

⚠️ Q1 — the shipped code carries a RECORDED DECISION AGAINST this story, and its reason has gone stale

lib/mcp/toolResult.ts says, in a header comment, exactly what this story proposes to undo:

"We deliberately do NOT declare an outputSchema on the tools, so structuredContent is free-form DTO JSON — the route layer ships these exact DTOs already; re-deriving a zod mirror of every DTO would be duplicate surface for no gain."

That was correct when Story 7.8 wrote it: the only routes were the internal cookie-authenticated ones, which do pass DTOs through. It is no longer correct. ADR Amendment 2 pinned that "a v1 response is a schema's output, never a service DTO passed through", and 11.2/11.3 shipped exactly that — so the premise "the route layer ships these exact DTOs" is now false, and the conclusion "duplicate surface for no gain" has lost its argument. There is now a second consumer of the shape, and it is versioned.

This card must therefore overturn that comment EXPLICITLY, in the ADR and in the file, not quietly contradict it. Decide and record:

  • Whether MCP tools declare an outputSchema (the SDK affordance the comment rejected) or merely derive structuredContent internally from the shared schema. These differ in a user-visible way: an outputSchema is published in tools/list, so agents see the shape — which is arguably a feature, and is arguably the prompt-engineering surface criterion 3 of this story promises to leave free. Take a position and say which.
  • Rewrite the toolResult.ts comment to state the NEW reasoning, with a dated note that the old one was true under 7.8's premises. A stale comment that outranks the code in a reader's mind is worse than no comment.

Q2 — WHERE do the shared schemas live? (lib/api/v1/** is about to stop being v1-only)

Amendments 2 and 5 pin that each resource story owns its zod schemas under lib/api/v1/**. The moment lib/mcp imports them, that directory is a shared kernel with a version-specific name — and it is the exact mirror of an oddity already shipped in the other direction: lib/mcp/scopes.ts owns TOKEN_SCOPES, which lib/api/v1/route.ts and lib/api/v1/openapi/security.ts already import.

Decide one of: (a) both surfaces import from lib/api/v1/** and the naming is documented as historical; (b) the shared response schemas hoist to a neutral home and lib/api/v1 re-exports; (c) hoist both the schemas AND TOKEN_SCOPES and fix the asymmetry once. This is cheap now and expensive after every tool imports the path — which is why it is a Q here rather than a discovery in 11.6.3.

Q3 — the zod v3 → v4 boundary, which this story pushes into lib/mcp for the first time

Verified: lib/mcp/** is 100% classic zod today (25 import sites, zero zod/v4). The v1 schema layer is zod/v4 per Amendment 4 Q1, and Zod 3 and Zod 4 instances do not interoperate. So any MCP module that COMPOSES a v1 schema must move. Pin the line this story applies (the card's own amendment states it; make it operational):

  • Composes a v1 schema — wraps, .extends, .pick/.omits, or declares a type against it ⇒ imports zod/v4, and never both entrypoints in one file.
  • Only calls .parse / .safeParse on it ⇒ unchanged, stays on classic zod.
  • The tools' INPUT schemas stay classic zod unless they compose a response schema — they are prompt-engineering surface this story leaves free.

Enumerate the files that actually cross, by grep, rather than asserting a count.

Q4 — what makes coverage TOTAL rather than a list someone maintains

lib/mcp/toolResult.ts's toolOk(text, structuredContent: Record<string, unknown>) is a single chokepoint — 33 of the 37 tools return through it (the four that do not are listSprints.ts plus three non-tool helpers: readyFilters, sprintRef, workItemRef). That is the seam where totality can be made structural rather than aspirational: type toolOk so a tool whose output resource has a declared schema CANNOT pass an unvalidated object, mirroring what lib/mcp/scopes.ts already does for scopes (an ungated tool is a compile error).

Pin: the exact typing, what happens to listSprints.ts's divergent return, and how the "resource exposed by BOTH surfaces" set is derived from lib/api/v1/openapi/registry.ts rather than hand-listed — the property this story's first acceptance criterion requires.

Q5 — the exemption list, named and reasoned

A tool with no v1 counterpart is exempt, and the exemption must be explicit per this story's criteria. Enumerate it with a reason each — validate_work_item, validate_sprint, get_project_state, claim_next_ready are the known set (11.7's boundary records why they have no REST form) — and state how a NEW tool joins the list deliberately rather than by nobody noticing.

Scope BOUNDARY

Ends at the ADR amendment plus the corrected toolResult.ts comment. It changes no tool, no schema, no route and no test. It does NOT decide anything about /api/v1's own shapes — those are settled. It does NOT re-open Amendment 4's zod migration; it applies that decision's boundary rule to a directory the amendment did not reach.

Acceptance criteria

  • The ADR carries a new amendment answering Q1–Q5, each with one chosen option and one line per rejected alternative.
  • Q1's answer QUOTES the toolResult.ts comment, states which of its premises became false and when, and records the position on outputSchema either way — an overturn that does not name what it overturns is how the next reader re-adopts the old reasoning.
  • lib/mcp/toolResult.ts's comment is rewritten to the new decision, dated, noting the old reasoning was sound under Story 7.8's premises.
  • Q2's answer names the directory every shared response schema is imported FROM, and says whether TOKEN_SCOPES moves with it or the asymmetry is accepted and documented.
  • Q3's answer lists, by grep against origin/main, the lib/mcp files that will compose a v1 schema and therefore cross to zod/v4, and confirms no file ends up importing both entrypoints.
  • Q4's answer pins the toolOk typing and states how the shared-resource set is derived from the operation registry, not enumerated.
  • Q5's answer is a table of exempt tool → reason, and the rule for adding to it.
  • No code changes beyond the one comment: this card ships a document.

Context refs

  • lib/mcp/toolResult.ts — the recorded decision Q1 overturns (the header comment above toolOk), and toolOk itself, the chokepoint Q4 types.
  • lib/mcp/scopes.ts — the totality pattern Q4 copies, and the shared-kernel asymmetry Q2 weighs.
  • lib/api/v1/openapi/registry.ts — where the "exposed by both surfaces" set is derived from.
  • lib/api/v1/workItems/schema.ts · ready/schema.ts · sprints/schema.ts · projects/schema.ts · identity/schema.ts — the shared schemas, and Q2's subject.
  • docs/decisions/public-api-conventions.md — Amendment 2 (a response is a schema's output), Amendment 4 Q1 (the zod/v4 migration + its enumerated boundary), Amendment 5 (the ownership split is total).
  • tests/helpers/v1RouteAudit.ts — the imports-mcp-tools rule, which constrains the direction of any hoist Q2 chooses.
  • Story: 11.6.