Skip to content

AjvJsonSchemaValidator: allow ajv options (e.g. useDefaults) without giving up the per-dialect engine choice #3017

Open

Description

@marcusschiesser

AjvJsonSchemaValidator (@modelcontextprotocol/server/validators/ajv, 2.3.1) picks an ajv engine by each schema's $schema: Ajv2020 for 2020-12 or none, Ajv2019 for 2019-09, and the classic Ajv for draft-07 and draft-06. It creates each engine lazily, with strict: false, validateFormats: true, validateSchema: false, allErrors: true and ajv-formats. That is exactly what a server needs when it registers tools from raw JSON Schema with fromJsonSchema.

The only way to change an engine's options is to pass a whole ajv instance (new AjvJsonSchemaValidator(ajv)). As documented, that instance is then used for every schema and the $schema dispatch is skipped. So a server that needs one more option loses the dialect handling and has to rebuild it: one instance per draft, with formats registered on each.

Use case. We serve tools whose input schemas were captured from other MCP servers, some of them not written in TypeScript, through registerTool(name, { inputSchema: fromJsonSchema(schema, validator) }, cb). A real server applies the defaults its schema states before its handler runs. With the SDK's validator a call with {} reaches the callback as {}. To match the real server we need ajv's useDefaults: true. Today that means either keeping our own copy of the dispatch, or filling defaults after validation ourselves (top-level only).

Proposal. Let the constructor take ajv options that are applied to every engine it creates, keeping the dispatch:

new AjvJsonSchemaValidator({ options: { useDefaults: true } })

The existing new AjvJsonSchemaValidator(ajv) form would stay as it is. An alternative of the same size: an engine factory, new AjvJsonSchemaValidator({ engine: (dialect) => … }), called once per dialect.

Notes

  • useDefaults changes the data ajv validates. The validator already returns data and the SDK passes it to the callback, so defaults would reach handlers with no other change. We tested this by passing an instance with useDefaults: nested defaults arrived filled too.
  • We're happy to send a PR if this direction is acceptable.

Activity

  1. Ben-Home commented on Oct 14, 2026

    @Ben-Home

    The dispatch-preserving options direction is the right one. Two decisions worth pinning down before implementation:

    1. Merge semantics and precedence. State it as: internal defaults, user options override, shallow merge per engine. Then name the keys users cannot replace. formats is the visible one: ajv-formats is registered internally today, so a user-supplied formats list either appends or suppresses the internal registration. Either rule works, but it should be documented rather than emergent.
    2. Engines are created lazily per dialect and cached. Constructor options apply to every engine, which covers useDefaults and the common case, but it rules out per-dialect options later. Keeping the factory alternative in mind, the constructor form can be a convenience that delegates to it, so the two cannot diverge.

    On useDefaults itself: it mutates the data being validated, and since the SDK returns data and passes it to the callback, schema defaults reach handlers with no other change. Worth documenting the mutation explicitly (caller-visible objects change in place) and testing both useDefaults: true and 'empty' plus nested defaults, since their fill semantics differ.

    One compatibility note: passing a full ajv instance today silently skips dialect dispatch, so whichever merged form you pick, keep the old form detectable (instance check vs plain options object) and fail clearly if the argument is ambiguous. Test matrix: the same fromJsonSchema registration across the three dialects with useDefaults on, asserting defaults arrive filled and 2019-09 schemas still route to the 2019 engine.

  2. added
    v2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes
    on Oct 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementRequest for a new feature that's not currently supportedv2Ideas, requests and plans for v2 of the SDK which will incorporate major changes and fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions