You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(builders,derive): lossless 2020-12 schema pipeline on the 2026 path
Two defects destroyed schema fidelity between tool-author types and
tools/list:
1. ToolSchema::from_schemars (turul-mcp-builders/schemars_helpers.rs)
stripped $defs/definitions from the root while passing properties through
VERBATIM on 2026 — every "#/$defs/X" pointer dangled. The 2026 root now
retains $defs/definitions/$schema (where verbatim property pointers
resolve); the 2025 typed lane keeps its inline-resolution + stripping.
2. The derive macros (param arm + output-schema arm in derive/utils.rs)
funneled schemars-generated documents through the typed-enum converter,
silently collapsing data-bearing unions (oneOf + const tags ->
{"type":"object"}) and other 2020-12 compositions.
New lane-aware turul_mcp_builders::schemars_param_schema():
- 2026: inlines local $refs via the cycle-guarded resolve_local_refs()
($ref siblings compose via allOf) and carries the result verbatim through
the new transparent JsonSchema::Raw variant — an untagged escape hatch on
the 2026 typed enum (also the deserialize fallback for subschemas the
structured variants reject). The 2026 typed enum is a framework vehicle,
not a wire binding; the wire bindings already hold arbitrary Values.
- 2025: status-quo typed conversion (frozen wire model).
Documented limitations with rejection tests, not silent loss:
- cyclic $refs cannot be inlined into a property subschema (error names the
cycle; restructure the type or build a root document via from_schemars,
which keeps $defs at the root);
- non-local/network $refs are rejected (spec: MUST NOT auto-dereference).
Tests: 7 builders fidelity tests (nested-$defs inlining with enum/required
intact; tagged-union oneOf survival; composition keywords verbatim;
cycle + non-local rejection; $ref-sibling allOf; root $defs retention) and
2 real-HTTP e2e in schema_fidelity_2026.rs — a derived tool's tagged-union
param and schemars output reach tools/list undamaged with no dangling $ref,
and tools/call structuredContent satisfies the ADVERTISED outputSchema
wrapper field discovered from tools/list. Revert-and-fail: with the 2026 arm
forced through the old converter, the tagged-union test fails showing the
exact loss ("shape":{"type":"object"}) — recorded. One derive unit test
migrated with the contract (asserts the new helper call). No public
macro/builder API shape changed (stop condition not hit).
Docs: review checklist updated — pipeline items plus the seven P1 fidelity
items closed earlier today (incl. the SEP-2577 line codex flagged as report
drift). CI: schema_fidelity_2026 gate in ci.yml + ci-gates.sh.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
37
37
38
38
### Fixed (2026-06-10)
39
39
40
+
- **Builders/derive schema pipeline is lossless on the 2026 path.** Two defects destroyed JSON Schema 2020-12 fidelity between a tool author's types and `tools/list`: (1) `ToolSchema::from_schemars` stripped `$defs`/`definitions` from the root while passing properties through verbatim — every `#/$defs/X` pointer dangled; the 2026 root now RETAINS `$defs`/`definitions`/`$schema` (the 2025 typed lane keeps its inline-resolution and stripping). (2) The derive macros funneled schemars-generated parameter and output schemas through the typed-enum converter, silently collapsing data-bearing unions (`oneOf` + `const` tags → bare `{"type":"object"}`) and other 2020-12 compositions. New lane-aware `turul_mcp_builders::schemars_param_schema`: on 2026 it inlines local `$ref`s (cycle-guarded `resolve_local_refs`; `$ref` siblings compose via `allOf`) and carries the result verbatim via the new transparent `JsonSchema::Raw` variant (untagged escape hatch on the 2026 typed enum — also the deserialize fallback for subschemas the structured variants reject); on 2025 it is the status-quo typed conversion. **Documented limitations with rejection tests** (not silent loss): cyclic `$ref`s cannot be inlined into a property subschema (error names the cycle; restructure the type or use a root `from_schemars` document), and non-local/network `$ref`s are rejected per the spec's no-auto-deref rule. Tests: 7 builders fidelity tests (nested `$defs` inlining with enum/required intact, tagged-union `oneOf` survival, composition-keyword verbatim round-trip, cycle/non-local rejection, `$ref`-sibling `allOf` composition, root `$defs` retention) + 2 real-HTTP e2e (`schema_fidelity_2026.rs`: a derived tool's tagged-union param and schemars output reach `tools/list` undamaged with no dangling `$ref`; `tools/call` `structuredContent` satisfies the ADVERTISED `outputSchema` wrapper field discovered from `tools/list`). Revert-and-fail: with the 2026 arm forced through the old converter, the tagged-union test fails showing the exact loss (`"shape":{"type":"object"}`) — recorded. No public macro/builder API shape changed.
40
41
- **Protocol-fidelity sweep, part 2 — `ttlMs` as a schema `number` + SEP-2577 marker absorption.** (a) `CacheableResult.ttlMs` (and its embeddings in the tools/resources/prompts/discover results) is now `f64` per the schema's `number` type: fractional values are accepted on deserialize and survive round trips, negative/non-finite values reject (`@minimum 0`), and whole values keep the compact integer wire form (byte-stable for the common `ttlMs: 0` case). (b) The re-pinned schema's SEP-2577 deprecations are now fully absorbed as `#[deprecated]` markers: `LoggingLevel` (+ `LogLevel` alias), the per-request `_meta` `logLevel` key and `RequestMetaObject.log_level`/`with_log_level`, `ServerCapabilities.logging`, `ModelHint`/`ModelPreferences`/`ToolChoice`, the `ContentBlock::ToolUse`/`ToolResult` variants and constructors, and the sampling trait surface (`HasCreateMessageRequestParams`/`CreateMessageRequest`/`CreateMessageResult`/`HasLevelParam`). The earlier rustdoc claim that `LoggingLevel`/`logLevel` were "the non-deprecated replacement" was wrong against the re-pin and is corrected — the whole Logging surface (including the per-request opt-in this branch implements) is deprecated-but-normative through the migration window. Framework-internal use sites carry scoped `#[allow(deprecated)]` (the framework intentionally serves the surface through the window); downstream consumers now get compiler nudges.
41
42
- **Protocol-fidelity sweep, part 1 (wire/type drift vs the pinned schema).** (a) `ToolChoice` no longer carries a non-spec `name` field on the wire (the `specific()` constructor is gone) and `mode` is optional per schema (`{}` parses; absent means `"auto"`; `effective_mode()` helper). (b) `PromptReference` is `BaseMetadata`-shaped: gains `title`, drops the non-spec `description`. (c) `Annotations.audience` is the closed `Role[]` union instead of `Vec<String>` — wire-invalid values like `"system"` are now rejected at parse time; the builders' `annotation_audience` takes `Role` (converted to strings on the frozen 2025 lane). (d) The duplicate `Role` binding is gone — `sampling::Role` re-exports the single `prompts::Role`. (e) `LoggingCapabilities`/`CompletionsCapabilities` match the schema's opaque `JSONObject`: the invented `enabled`/`levels` keys are removed from the bindings and from both server builders' capability advertisements (presence of the object is the signal). 5 new wire-shape contract tests in `compliance.rs`; existing tests migrated with the contract (e.g. the empty `ToolChoice` parse fails against the pre-fix required-`mode` binding).
0 commit comments