Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 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
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
34 changes: 34 additions & 0 deletions .changeset/zod-tojsonschema-wire-truthful.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
'@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.)
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. (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.) Elicitation is unaffected:
`inputRequired.elicit()` keeps throwing on schemas its restricted form grammar cannot
round-trip, including `z.date()`.
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