fix(validators): honor declared draft-07/06 JSON Schema dialects instead of rejecting them (#2534)

This commit is contained in:
Felix Weinberger 2026-07-27 16:11:06 +01:00 • committed by GitHub
commit f130e1ac5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -707,10 +707,9 @@ the host side and register the result with `fromJsonSchema()`: zod-4 input via z
own `z.toJSONSchema(z.object(shape), { io: 'input', target: 'draft-2020-12' })` (the
conversion is runtime-structural, so a zod ≥4.2 in the host handles schemas built by a
different zod-4 copy), zod-3 input via the
[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Strip
the `$schema` member from the converted output before passing it to `fromJsonSchema()`
— `zod-to-json-schema` stamps a draft-07 `$schema` by default, and the default
validator [accepts 2020-12 only](#json-schema-2020-12-posture-sep-1613-sep-2106).
[`zod-to-json-schema`](https://www.npmjs.com/package/zod-to-json-schema) package. Its
default draft-07 `$schema` stamp is fine as-is — the default validator
[honors declared draft-07/06 dialects](#json-schema-2020-12-posture-sep-1613-sep-2106).
How a too-old zod surfaces depends on which entry point your code imports. With
main-entry `import { z } from 'zod'` on a zod-3 range, the project **typechecks cleanly
@ -1455,9 +1454,19 @@ classes — import it from one package consistently within a process.
#### JSON Schema 2020-12 posture (SEP-1613, SEP-2106)
The default validator supports **JSON Schema 2020-12 only**. On Node it is now `Ajv2020`
instead of draft-07 `Ajv`; the Cloudflare Workers default was already 2020-12. Schemas
declaring a different `$schema` are rejected with `Error("…unsupported dialect…")`.
The default validator dispatches on the schema's declared `$schema`: absent or 2020-12
validates as **JSON Schema 2020-12** — on Node via `Ajv2020` instead of v1's draft-07
`Ajv` (the Cloudflare Workers default was already 2020-12) — a declared 2019-09
`$schema` validates with 2019-09 semantics (`Ajv2019`), and a declared draft-07 or
draft-06 `$schema` validates with draft-07 semantics. Schemas declaring any other
`$schema` are rejected with `Error("…unsupported dialect…")`. Two known draft-07 engine
differences: the Node engine (classic Ajv, same as v1's default) evaluates keywords
adjacent to `$ref`, stricter than draft-07's ignore-siblings rule, while the
browser/Workers engine ignores them per spec; and the browser/Workers engine does not
resolve a `$ref` inside a `dependencies` entry whose key collides with a JSON Schema
keyword (`type`, `default`, `format`, …) — validation throws `Unresolved $ref` when
that dependency triggers (surfaced as the SDK's typed validation error), while the
Node engine handles the same schema correctly.
`CallToolResult.structuredContent` is widened from `{ [k: string]: unknown }` to
`unknown` (SEP-2106 lifts the `type:"object"` root restriction). The presence check is
@ -1465,13 +1474,13 @@ declaring a different `$schema` are rejected with `Error("…unsupported dialect
`$ref` is not dereferenced (unchanged from v1; Ajv throws `MissingRefError` at compile,
surfaced per-tool on `callTool`).
| v1 pattern | Mechanical fix |
| ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `result.structuredContent.<key>` / `result.structuredContent?.<k>` | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }` |
| `if (!result.structuredContent)` | `if (result.structuredContent === undefined)` |
| relying on default `Ajv` being draft-07 | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |
| draft-07 idioms via `fromJsonSchema(schema)` | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas |
| `outputSchema` / `inputSchema` with absolute-URI `$ref` | inline under `$defs` and reference with `#/$defs/Name` |
| v1 pattern | Mechanical fix |
| ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `result.structuredContent.<key>` / `result.structuredContent?.<k>` | narrow first: `const sc = result.structuredContent; if (typeof sc === 'object' && sc !== null && '<k>' in sc) { sc.<k> }` |
| `if (!result.structuredContent)` | `if (result.structuredContent === undefined)` |
| draft-07 idioms **without** a declared `$schema` (a declared draft-07/06 `$schema` dispatches automatically) | `new AjvJsonSchemaValidator(new Ajv({ strict: false, validateFormats: true, validateSchema: false, allErrors: true }))` (import `Ajv`, `addFormats`, `AjvJsonSchemaValidator` from `…/validators/ajv`) |
| undeclared draft-07 idioms via `fromJsonSchema(schema)` | `fromJsonSchema(schema, new AjvJsonSchemaValidator(ajv))` — the `McpServer`/`Client` `jsonSchemaValidator` option does **not** reach `fromJsonSchema`-authored schemas |
| `outputSchema` / `inputSchema` with absolute-URI `$ref` | inline under `$defs` and reference with `#/$defs/Name` |
A tool may now register an `outputSchema` whose root is `type:"array"`, `type:"string"`,
etc.; toward 2025-era clients the codec wraps it in a `{result:…}` envelope, and toward