Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3c067c4
fix(core-internal): make zod toJSONSchema conversion wire-truthful fo…
claude Jul 27, 2026
034ccc1
fix(core-internal): address review findings on zod conversion options
claude Jul 27, 2026
d11d6fc
fix(core-internal): harden the missing-key probe and cover enum-keyed…
claude Jul 27, 2026
665a056
docs(core-internal): scope wire-truthfulness claims to exclude pipe/c…
claude Jul 27, 2026
802ac99
docs(core-internal): note zod <4.3.0 override skip on reused cloned s…
claude Jul 27, 2026
d28811d
fix(core-internal): close async-default, .catch(), and unrepresentabl…
claude Jul 27, 2026
b2bd59d
fix(core-internal): root-safe .catch() degrade, x-* annotations, full…
claude Jul 27, 2026
a1484e8
fix(core-internal): protect root-composition catch members and unwrap…
claude Jul 27, 2026
0cee901
fix(core-internal): position-independent catch degrade; unwrap pipe/p…
claude Jul 27, 2026
16fab72
fix(core-internal): composition-aware root guard, symbol/function req…
claude Jul 27, 2026
568ebcf
fix(core-internal): correct composition member classification; object…
claude Jul 27, 2026
591c054
fix(core-internal): classify date members and non-finite literals; un…
claude Jul 27, 2026
4a92acc
fix(core-internal): skeletonize oneOf as anyOf; accept mixed represen…
claude Jul 28, 2026
6018eae
fix(core-internal): broaden structural tolerance walk; loud/quiet roo…
claude Jul 28, 2026
8034645
fix(core-internal): preprocess-wrapped roots; optional/void/intersect…
claude Jul 28, 2026
98a3379
fix(core-internal): loosen parent oneOf when a conversion degraded no…
claude Jul 28, 2026
faef197
Merge remote-tracking branch 'origin/main' into fix/2464-tojsonschema…
claude Aug 5, 2026
19c5b62
fix(core-internal): null-tolerant array/tuple elements; quiet never v…
claude Aug 5, 2026
41e50b7
fix(core-internal): output-root loudness parity; file verdicts; posit…
claude Aug 5, 2026
36657a3
fix(core-internal): schema-position keyword classification; strip reg…
claude Aug 5, 2026
8270f46
fix(core-internal): io-aware pipe loudness; quiet symbol literals; an…
claude Aug 5, 2026
f105c75
fix(core-internal): conjunction-preserving oneOf rewrite; skip negate…
claude Aug 5, 2026
6814683
fix(core-internal): all-key object proof; contains skip; io-aware big…
claude Aug 5, 2026
3b72567
fix(core-internal): restore every()-proof for untouched allOf; extend…
claude Aug 5, 2026
eaa15f2
fix(core-internal): complete reference-target keep; loud output date …
claude Aug 5, 2026
8fdacf4
fix(core-internal): first-present-key proof; pointer rewrite for move…
claude Aug 5, 2026
8494566
fix(core-internal): override-time pointer integrity; dangling-ref neu…
claude Aug 5, 2026
66559b9
fix(core-internal): anchor/base-addressed dangle repair; polarity-awa…
claude Aug 5, 2026
8007798
fix(core-internal): array/tuple representable verdicts; per-resource …
claude Aug 5, 2026
21ae868
refactor(core-internal): replace ref-repair machinery with a referenc…
claude Aug 5, 2026
36c5ea3
fix(core-internal): genuine object proof for the allOf-push stamp; po…
claude Aug 5, 2026
31b6746
fix(core-internal): copy-on-wrap tuple prefixItems; legacy-draft guar…
claude Aug 5, 2026
c105ae7
fix(core-internal): wrap stamp reads the strict pre-loosen snapshot; …
claude Aug 5, 2026
37d8eb4
fix(core-internal): nonoptional probe deferral; deferred id strip on …
claude Aug 5, 2026
74ab2bd
fix(core-internal): pipe tolerance requires bare-transform OUT; unifo…
claude Aug 5, 2026
a0a0252
fix(core-internal): prefault/intersection/promise tolerance deferral;…
claude Aug 5, 2026
53f2be5
fix(core-internal): checks-aware tolerance; plain-object fill gate; r…
claude Aug 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .changeset/zod-tojsonschema-wire-truthful.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
'@modelcontextprotocol/core-internal': patch
'@modelcontextprotocol/server': patch
---

Make zod-to-JSON-Schema conversion wire-truthful for tool schemas. A `z.date()` (or another
unrepresentable type such as `z.bigint()`) in a registered tool's schema no longer throws
during conversion and fails the entire `tools/list` response — dates are advertised as
`{type: 'string', format: 'date-time'}` (the shape `JSON.stringify` actually produces), and
other unrepresentable types degrade to an unconstrained schema. (BigInt values embedded as
defaults or metadata, e.g. `.default(0n)`, still fail conversion — JSON cannot carry them —
and so do dynamic catch values, `.catch(ctx => …)`; the `.catch()` degrade covers static
fallback values only. And a misregistered non-object ROOT — `z.bigint()` or `z.map()` as
the whole `inputSchema`/`outputSchema` — still fails `tools/list` loudly by design,
preserving the pre-fix error instead of listing a permanently-broken tool.)
Comment thread
claude[bot] marked this conversation as resolved.
Registry metadata (`.meta({id: 'X'})`) no longer emits the draft-04 `id` keyword on either
io path — Ajv v8 hard-rejects it at compile time ('NOT SUPPORTED: keyword "id"'), so the
SDK's own client could never validate such advertisements. The key is kept only when the
document carries a hand-authored ref beyond zod's registry shapes: those may resolve
through the `id` base-URI on the `@cfworker/json-schema` engine (URI-form refs, and
fragment pointers inside an `id` resource), so stripping would break them — such documents
ship with `id` intact, exactly as pre-fix.
Output schemas no longer advertise constraints the server doesn't enforce on the raw
`structuredContent` it ships: fields that may be legitimately absent (`.default()`,
undefined-accepting types) are dropped from `required` — on objects and enum-keyed records —
and `additionalProperties: false` is dropped for plain `z.object()` (kept for
`z.strictObject()`), so validating clients no longer reject legitimate tool results for
these schema shapes. Once any such loosening applies, exactly-one `oneOf` compositions —
including zod's discriminated-union emissions over plain objects, i.e. most of them — are
advertised as `anyOf` (the loosened members may overlap; discriminator consts keep them
distinguishable), and the serialized wire forms of tolerant values are additionally
accepted: tolerant array/tuple elements also allow `null` (what `JSON.stringify` makes of
an undefined element), `z.file()` fields also allow `{}` (a `File` has no JSON form), and
non-finite number literals also allow `null`. (Output schemas containing
`.transform()`/`.pipe()`/`z.coerce` still
advertise the post-transform shape while the server ships the raw pre-transform value — a
pre-existing gap this change does not address. And on zod 4.0–4.2.x, `toJSONSchema` skips
the sanitization hook on a schema reused both bare and via a `.describe()`/`.meta()` clone
in the same conversion; full per-node sanitization requires zod >=4.3.0. And the
`z.date()` advertisement assumes a serializing transport: `InMemoryTransport` passes the
raw `Date` by reference, so a validating client rejects it over that testing transport.
On the input side, a required tool/prompt argument of a type JSON cannot carry makes the
tool listed yet uncallable: `z.date()` is advertised as `string`/`date-time` and other
unrepresentable types (`z.bigint()`, `z.map()`, `z.set()`, `z.symbol()`) as an
unconstrained `{}`, but input validation still runs the raw zod schema, which rejects
every JSON payload — use a JSON-representable type such as `z.iso.date()`/
`z.iso.datetime()`, `z.number()`, `z.record(...)`, or `z.array(...)`, or make the field
optional. The same holds for required OUTPUT fields of such types: bigint results fail
JSON-RPC serialization and Map/Set values serialize as `{}`. And a degraded
object-`.catch()` node keeps `type: 'object'` for the 2025-era wrap proof even though
catch-validation does not enforce it on the raw value.
Hand-authored reference keywords disable the loosening: a `.meta()`/registry-injected
`$ref`/`$dynamicRef` beyond zod's own registry shapes (`#`, `#/$defs/<name>`), ANY ref —
registry-shaped included — consumed under a `not`/`if`/`contains` keyword, any
`$anchor`/`$dynamicAnchor` or `$id`, any `$recursiveRef`/`$recursiveAnchor`, a non-root
`$defs`, or any hand-authored `unevaluatedProperties`/`unevaluatedItems` makes the
conversion ship the strict pre-fix-shaped emission instead — such constructs would observe
the loosening's rewrites as dangling pointers, stale anchors, polarity-inverted negations,
or stripped evaluation annotations, so those schemas keep pre-fix strictness, compilable
and working by construction.) Elicitation is unaffected:
`inputRequired.elicit()` keeps throwing on schemas its restricted form grammar cannot
round-trip, including `z.date()`.
Comment thread
claude[bot] marked this conversation as resolved.
7 changes: 6 additions & 1 deletion packages/core-internal/src/shared/elicitation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@ function isJsonObject(value: unknown): value is Record<string, unknown> {

function convertStandardElicitationSchema(schema: StandardSchemaWithJSON): Record<string, unknown> {
try {
return standardSchemaToJsonSchema(schema, 'input');
// `unrepresentable: 'throw'`: the restricted form grammar must reject shapes it
// cannot round-trip. A `z.date()` rewritten to `string`/`date-time` would pass the
// wire checks, but the accepted response (a JSON string) could never satisfy the
// same `z.date()` schema on handler re-entry — keep the documented loud failure
// (`z.iso.date()`/`z.iso.datetime()` are the supported ways to elicit dates).
return standardSchemaToJsonSchema(schema, 'input', { unrepresentable: 'throw' });
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new ProtocolError(
Expand Down
Loading
Loading