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'sinputSchemabefore policy runs; an invalid call is never sent ({:error, %Error{kind: :protocol, reason: :invalid_arguments}}, the validator's errors indata).structured_content: true- a successful result'sstructuredContentis validated against the tool'soutputSchema, when it has one; a mismatch (or a missingstructuredContent) 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
$refor$dynamicRefis 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.
Summary
Callbacks
Builds schema (a bounds-checked, string-keyed JSON Schema object or a
boolean) for repeated validation. opts is reserved and currently [].
Validates data (JSON-shaped: string keys) against a built schema.
Errors are returned to the caller in the data of the client error.
Functions
Checks a server-supplied schema against the bounds in the moduledoc.
Returns :ok or {:error, reason}; never raises.
Types
@type bounds_error() ::
:invalid_schema | :too_deep | :too_many_subschemas | :too_large | :remote_ref
Why a schema was refused before building.
@type compiled() :: term()
A validator's built schema; opaque to the client.
Callbacks
@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 [].
Validates data (JSON-shaped: string keys) against a built schema.
Errors are returned to the caller in the data of the client error.
Functions
@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}