ConduitMcp.Client.SchemaValidator behaviour (ConduitMCP v0.11.0)

Copy Markdown View Source

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.

Summary

Types

Why a schema was refused before building.

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

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

bounds_error()

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

Why a schema was refused before building.

compiled()

@type compiled() :: term()

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

Callbacks

build(schema, opts)

@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(compiled, data)

@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.

Functions

check_bounds(schema)

@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}