# `ConduitMcp.Client.SchemaValidator`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/schema_validator.ex#L1)

JSON Schema validation of tool arguments and `structuredContent`, behind a
behaviour so the validator can be swapped.

Validation is opt-in, per client:

    {ConduitMcp.Client,
     name: :docs,
     transport: {...},
     validate: [arguments: true, structured_content: true]}

  * `arguments: true` - a call's final arguments (after pre-hooks) are
    validated against the tool's `inputSchema` before policy runs; an
    invalid call is never sent (`{:error, %Error{kind: :protocol, reason:
    :invalid_arguments}}`, the validator's errors in `data`).
  * `structured_content: true` - a successful result's `structuredContent`
    is validated against the tool's `outputSchema`, when it has one;
    a mismatch (or a missing `structuredContent`) is `{:error,
    %Error{kind: :protocol, reason: :invalid_structured_content}}`.

The default validator is `ConduitMcp.Client.SchemaValidator.JSV`, which
needs the optional `:jsv` dependency (JSON Schema 2020-12 and draft 7).
Asking for validation without it raises
`ConduitMcp.OptionalDependencyError` when the client's options are
validated, never per request. Another validator is set with
`validate: [validator: MyValidator]`.

## Bounds (HK-6)

Tool schemas come from the server, so a schema is checked before any
validator sees it. A schema is refused when:

  * it is not an object or a boolean (`:invalid_schema`);
  * it nests JSON objects and arrays more than 64 deep, or subschemas more
    than 32 deep (`:too_deep`);
  * it has more than 1 000 subschemas (`:too_many_subschemas`);
  * its JSON encoding is over 256 KiB (`:too_large`);
  * a `$ref` or `$dynamicRef` is anything but a local `#...` pointer
    (`:remote_ref`): references are never dereferenced over the network
    or the file system.

A tool whose schema is refused (or fails to build) is left out of the
client's tool lists while that validation is on, and a call to it fails
with `{:error, %Error{kind: :protocol, reason: {:unsafe_schema, :input |
:output, reason}}}` before anything is sent.

## Caching

Built schemas are kept in the client's governance table (written by its
owner process, read directly by callers), keyed by the tool's
`ConduitMcp.Client.Fingerprint` and the validator. A changed definition has
a new fingerprint, so a refresh that changes a tool never reuses the old
schema; the cache is cleared when it holds more than 4 096 entries.
Refusals are cached the same way.

# `bounds_error`

```elixir
@type bounds_error() ::
  :invalid_schema | :too_deep | :too_many_subschemas | :too_large | :remote_ref
```

Why a schema was refused before building.

# `compiled`

```elixir
@type compiled() :: term()
```

A validator's built schema; opaque to the client.

# `build`

```elixir
@callback build(schema :: map() | boolean(), opts :: keyword()) ::
  {:ok, compiled()} | {:error, term()}
```

Builds `schema` (a bounds-checked, string-keyed JSON Schema object or a
boolean) for repeated validation. `opts` is reserved and currently `[]`.

# `validate`

```elixir
@callback validate(compiled(), data :: term()) :: :ok | {:error, term()}
```

Validates `data` (JSON-shaped: string keys) against a built schema.
Errors are returned to the caller in the `data` of the client error.

# `check_bounds`

```elixir
@spec check_bounds(term()) :: :ok | {:error, bounds_error()}
```

Checks a server-supplied schema against the bounds in the moduledoc.
Returns `:ok` or `{:error, reason}`; never raises.

    iex> ConduitMcp.Client.SchemaValidator.check_bounds(%{"type" => "object"})
    :ok

    iex> ConduitMcp.Client.SchemaValidator.check_bounds(%{"$ref" => "http://169.254.169.254/"})
    {:error, :remote_ref}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
