Commit 8aae70f
committed
feat(mcp): validate every documented API response at the apiFetch boundary
The stdio CLI read every API response as `payload: any` and picked fields out by
optional-chaining guesswork, so a renamed Worker field degraded silently at runtime.
gen-contract-api-schemas.ts generates @loopover/contract/api-schemas from
src/openapi/schemas.ts, which stays canonical and untouched: moving the schemas was
tried first and rejected because zod-to-openapi's .openapi() exists only on schemas
constructed after extendZodWithOpenApi runs, and both post-hoc naming routes (.openapi()
clones; .meta({id}) propagates through .nullable()/.extend() differently) changed the
published document. Generation with the names stripped keeps the document byte-identical
by construction, with --check in test:ci like the other generators.
The generator also emits CLI_RESPONSE_SCHEMAS, path -> response schema, derived from a
scan of the CLI's own literal api* call sites joined against the published document --
nothing hand-listed. The scanner requires the closing delimiter: the first draft
collected a template path's truncated prefix, which for a documented base path would
have validated the wrong endpoint's schema. apiGet/apiPost are overloaded so a literal
documented path returns the schema's inferred type, and apiFetch safeParses every
documented response under #9519's recorded posture: report and pass the payload through
untouched by default (returning zod's parse would strip fields the document
under-describes), throw under LOOPOVER_VALIDATE_RESPONSES for CI and self-host. The
warning dedupes per path so a polling loop cannot flood stderr, and lives in a hoisted
accessor because the entrypoint awaits runCli before module consts this far down
initialize.
Typing the responses immediately surfaced real drift: /v1/local/branch-analysis returns
predictedGate and dataQuality, which the document omitted entirely while two predict-gate
tools read them; both are now declared (as unknown until #9531 types the verdict). The
skipped-pr-audit call site moves its query out of the path template so the scanner keeps
it validated.
`payload: any` is extinct in the package and pinned by a grep gate test, along with the
table matching the scan exactly and the unvalidated remainder as a shrink-only list. The
posture tests drive the real compiled bin against a local server in both modes.1 parent 423f269 commit 8aae70f
8 files changed
Lines changed: 1551 additions & 26 deletions
File tree
- packages
- loopover-contract
- src
- loopover-mcp/bin
- scripts
- test/unit
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
25 | 25 | | |
26 | 26 | | |
27 | 27 | | |
| 28 | + | |
| 29 | + | |
28 | 30 | | |
29 | 31 | | |
30 | 32 | | |
| |||
124 | 126 | | |
125 | 127 | | |
126 | 128 | | |
127 | | - | |
| 129 | + | |
128 | 130 | | |
129 | 131 | | |
130 | 132 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
58 | 62 | | |
59 | 63 | | |
60 | 64 | | |
| |||
0 commit comments