Skip to content

build(schema): TypeScript source-of-truth pipeline for server.json + OpenAPI inversion#1380

Draft
tadasant wants to merge 4 commits into
mainfrom
schema/ts-source-of-truth
Draft

build(schema): TypeScript source-of-truth pipeline for server.json + OpenAPI inversion#1380
tadasant wants to merge 4 commits into
mainfrom
schema/ts-source-of-truth

Conversation

@tadasant

@tadasant tadasant commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Adopts the schema-definition infrastructure used by the experimental Server Card repo for the registry's server.json schema, and makes that TypeScript file the single source of truth:

  1. A TypeScript source of truth (schema.ts) generates the committed draft/server.schema.json, plus generate / check / validate scripts and example fixtures.
  2. OpenAPI inversion: docs/reference/api/openapi.yaml no longer duplicates the 15 server.json definitions — its components/schemas now references the generated schema via thin external $ref stubs. The old Go drift-gate (tools/extract-server-schema) and the "edit both files" burden are gone.

This PR is a strict no-op for the schema content. It changes how server.schema.json is produced and validated, not what constraints it contains. None of the Server Card proposal's actual schema changes are adopted here. The only textual change to server.schema.json is its one-line $comment provenance string (now points at schema.ts); every validation constraint is byte-identical.

How it's a no-op (and proof)

The generator (scripts/generate-schema.ts) reproduces Go's encoding/json MarshalIndent(v, "", " ") output exactly: object keys sorted ascending, array order preserved, HTML escaping of < > & U+2028 U+2029 (Go's SetEscapeHTML default), two-space indent + trailing newline.

Proof the inversion preserves content: running origin/main's openapi.yaml through the old extract-server-schema transform reproduces the previously-committed server.schema.json byte-for-byte (verified with a faithful reimplementation of the Go extractor: 15 definitions extracted, output === committed file). The old inline OpenAPI definitions therefore defined exactly the schema content that the external $refs now resolve to. The git diff on server.schema.json is a single line:

-  "$comment": "This file is auto-generated from docs/reference/api/openapi.yaml. ...",
+  "$comment": "This file is auto-generated from docs/reference/server-json/schema.ts. ...",

Why an object literal (not typescript-json-schema interfaces)

Server Card authors its schema as TS interfaces + JSDoc and runs typescript-json-schema. That idiom cannot reproduce this schema as a no-op — several constructs have no faithful interface representation: Package.version uses not: { const: "latest" }; PositionalArgument uses anyOf: [{ required: ["valueHint"] }, { required: ["value"] }]; plus allOf + $ref composition and heavy example annotations. So schema.ts is a typed object literal holding the full JSON Schema document. Two examples/invalid/ fixtures (package-version-latest.json, positional-argument-missing-value.json) deliberately exercise the not / anyOf constraints to lock in that these are preserved.

The OpenAPI inversion

Previously server.schema.json was extracted from openapi.yaml and a Go tool gated drift — making openapi.yaml a second source you had to keep in sync. Now:

  • openapi.yaml's components/schemas exposes each of the 15 server.json types as {$ref: '../server-json/draft/server.schema.json#/definitions/<Name>'}. The 4 REST-only schemas (ServerList, ServerResponse, StatusUpdateRequest, AllVersionsStatusResponse) stay inline; their internal refs resolve to the stub aliases.
  • tools/extract-server-schema is deleted.
  • A small TS check (scripts/check-openapi.ts) asserts every schema.ts definition has a matching stub and every stub targets a real definition — catching dangling refs and added/removed definitions. Wired into npm run check (and thus CI's make validate).

Tradeoff (documented in CONTRIBUTING.md): the reference openapi.yaml is no longer self-contained — its relative $refs need the repo checkout to resolve. This is reference-doc-only: the spec subregistries consume is the runtime one at registry.modelcontextprotocol.io/openapi.yaml, generated independently from the Go types by huma, and is unaffected. The pre-existing OAS-3.1-doc → draft-07-file dialect mix is likewise inert (nothing dereferences the cross-dialect ref at runtime).

Changes

  • docs/reference/server-json/schema.ts — object-literal source of truth ($commentschema.ts)
  • docs/reference/server-json/scripts/generate-schema.ts — generator + --check drift mode
  • docs/reference/server-json/scripts/check-openapi.tsnew openapi $ref-stub consistency check
  • docs/reference/server-json/scripts/validate-examples.ts — Ajv (draft-07 + formats) example validation
  • docs/reference/server-json/examples/{valid,invalid}/ — fixtures
  • docs/reference/api/openapi.yaml — 15 inline server.json schemas → external $ref stubs
  • tools/extract-server-schema/deleted (openapi is no longer a source)
  • package.json / package-lock.json / tsconfig.json / .gitignore — isolated Node project (tsx, typescript, ajv, ajv-formats, yaml)
  • Makefilegenerate-schema / check-schema on the TS pipeline; Go drift gate removed
  • .github/workflows/ci.yml — Node setup so make validate runs the TS pipeline
  • docs/reference/server-json/CONTRIBUTING.md — single-source workflow

Verification

  • Content no-op proven: a faithful reimplementation of the old Go extract-server-schema transform, run on origin/main's openapi.yaml, reproduces the previously-committed server.schema.json byte-for-byte (15 defs). The only diff in this PR's server.schema.json is the one-line $comment provenance string; all constraints are byte-identical.
  • Generate / check / validate pass locally (Node 22, clean npm ci):
    • npm run generate → regenerates byte-identically
    • npm run check✓ schema up to date + ✓ openapi.yaml references all 15 definitions via external $ref + tsc --noEmit clean
    • npm run validate → all 8 examples pass (3 valid clean, 5 invalid rejected; not/anyOf confirmed exercised)
  • check-openapi.ts catches drift — empirically tested in review: missing stub → exit 1; dangling ref → exit 1; inline-shape drift → exit 1.
  • No Go breakage from deleting the tool — no .go/Makefile/CI/shell references remain; gopkg.in/yaml.v3 is still used by internal/api/openapi_compliance_test.go (go.mod stays tidy); that compliance test compares only paths (untouched), so it is unaffected.
  • Self-review done — independent in-process fresh-eyes review: zero critical/should-fix-blocking findings, confirmed the relative $ref path, stub↔definition completeness (all 15), REST-only schemas intact, drift-detection, and CI wiring. One should-fix doc note applied (openapi.yaml now needs the repo checkout to resolve).
  • CI green — all checks pass on the latest commit (95b78db): Build/Lint/Validate (make validate runs the new openapi check), Tests, Analyze (go), Analyze (actions), CodeQL.

🤖 Generated with Claude Code

Agent Orchestrator and others added 4 commits June 19, 2026 00:33
…son schema

Introduce the schema-definition layout used by the experimental Server Card
repo: a TypeScript source of truth (schema.ts) that generates the committed
draft/server.schema.json, plus generate/check/validate npm scripts and
example fixtures.

This is a strict no-op for the schema content. The generator reproduces Go's
encoding/json MarshalIndent output byte-for-byte (recursive key sort, HTML
escaping of <, >, &, U+2028/U+2029, two-space indent, trailing newline), so
draft/server.schema.json is regenerated identically -- there is no change to
that file in this diff.

- docs/reference/server-json/schema.ts: object-literal source of truth
- scripts/generate-schema.ts: generator with --check drift mode
- scripts/validate-examples.ts: Ajv (draft-07) validation of examples/
- examples/{valid,invalid}: fixtures, incl. cases exercising the not/anyOf
  constraints that the typescript-json-schema idiom cannot express
- Makefile: repoint generate-schema/check-schema to the TS pipeline while
  retaining the Go extract-server-schema -check as an openapi.yaml drift gate
- ci.yml: add Node setup so `make validate` can run the TS pipeline
- CONTRIBUTING.md: document the new schema.ts workflow

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Portability nit from self-review (Windows-safe vs file.split("/")).
Display-only; no behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Complete the source-of-truth inversion: schema.ts is now the single source.
openapi.yaml no longer duplicates the server.json definitions -- its
components/schemas exposes each of the 15 server.json types as a thin external
$ref into the generated draft/server.schema.json. The hand-authored REST-only
schemas (ServerList, ServerResponse, StatusUpdateRequest,
AllVersionsStatusResponse) stay inline.

This removes the previous dual-source arrangement (the Go extract-server-schema
drift gate and the "edit both files" burden). The Go tool is deleted; a small
TS check (scripts/check-openapi.ts) asserts every definition has a stub and
every stub targets a real definition.

Semantic no-op for the schema content: running origin/main's openapi.yaml
through the old extract transform reproduces the committed server.schema.json
byte-for-byte, so the external $refs now resolve to identical content. The only
textual change to server.schema.json is the $comment provenance line (now points
at schema.ts); all schema constraints are byte-identical.

- openapi.yaml: 15 inline server.json schemas -> external $ref stubs (-346 lines)
- tools/extract-server-schema: removed (openapi is no longer the source)
- scripts/check-openapi.ts: new stub/definition consistency check (uses yaml)
- Makefile check-schema: drop the Go drift gate (now covered by the TS check)
- schema.ts/server.schema.json: $comment provenance -> schema.ts
- CONTRIBUTING.md: document the single-source workflow

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ckout

The external $ref stubs are relative-path refs into draft/server.schema.json, so
the reference openapi.yaml is no longer self-contained for standalone tooling.
Clarify that the self-contained, runtime-served spec is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tadasant tadasant changed the title build(schema): adopt TypeScript source-of-truth pipeline for server.json (no-op) build(schema): TypeScript source-of-truth pipeline for server.json + OpenAPI inversion Jun 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant