Internal compiler for the bounded capability JSON Schema profile.
The accepted profile is a strict subset of JSON Schema 2020-12 containing
type, title, description, default, properties, required,
additionalProperties, propertyNames, items, enum, const, minimum,
maximum, minLength, maxLength, minItems, maxItems, maxProperties,
and the single bounded sha256 string format. General type unions are
unsupported, roots are objects, and a missing additionalProperties on an
object is normalized to false. A non-root node may use one scalar type or
the bounded nullable form [<non-null type>, "null"]; source order is
canonicalized and other type unions remain unsupported.
$schema selects the schema dialect; absence means the MCP default
(2020-12). Because the accepted profile is a common subset of the
allowlisted dialects, a supported root $schema URI (2020-12, or draft-07
as a deliberate compatibility translation) is accepted and removed, while
unknown, malformed, and nested dialect markers are rejected. Vendor x-…
extension keys and the standard non-validating default annotation are
discarded from every level as a deliberate client policy — mainstream MCP
SDKs emit them by default. They do not reach normalized output, encodings,
hashes, or runtime argument construction. All other unknown keywords remain
rejected.
Each normalized schema is at most 64 KiB with maximum depth 16, 128
properties per object, and 256 enum members. Host construction compiles with
compile/1. Request-scoped schemas compile with compile_bounded/3 in the
same time- and heap-bounded worker used for runtime validation. Proven schema
invalidity is distinct from compiler or validator timeout, heap exhaustion,
crashes, and malformed worker results. Runtime validation delegates to the
compiled JSV root. Input rejection may retain a small explanation containing
only schema-declared paths, keywords, and bounds; submitted values and
undeclared property names never enter that explanation. Validation and
explanation projection run together in one bounded worker. valid?/2 remains
a host-side predicate for construction, tests, and the MCP structured-result
check inside the already-bounded provider process. Dispatcher output
admission uses validate/5.
Rejection reports the first proven fault as a closed rule atom plus the
segments locating it inside the submitted schema document. Every segment is
a key or index the submitted document actually carries, so the location
always resolves in the file the author opens. The profile is deliberately
closed, and its edges are not guessable from a bare refusal: an unsupported
keyword, a misspelled type, and an unsatisfiable bound are three different
authoring mistakes and must not report identically.
Summary
Functions
Compiles a request-scoped schema in a caller-cancelled bounded worker.
Returns every rule a rejection can carry, most specific first.
Validates a value and returns only bounded, schema-authored rejection facts.
Types
@type compiled() :: JSV.Root.t()
@type segment() :: {:property, binary()} | {:index, non_neg_integer()}
Functions
@spec compile_bounded(term(), pos_integer(), pos_integer()) :: {:ok, map(), compiled()} | {:error, {:invalid_schema, rejection()}} | {:unavailable, atom()}
Compiles a request-scoped schema in a caller-cancelled bounded worker.
Proven profile rejections stay {:error, {:invalid_schema, rejection}}.
Worker timeout, heap exhaustion, cancellation, crashes, and malformed worker
results are {:unavailable, cause} and must not be treated as an invalid
document.
@spec rules() :: [atom()]
Returns every rule a rejection can carry, most specific first.
The set is closed so a diagnostic boundary can render each rule from a fixed literal instead of forwarding compiler prose.
@spec validate(compiled(), map(), term(), pos_integer(), pos_integer()) :: :ok | {:invalid, [violation()]} | {:unavailable, atom()}
Validates a value and returns only bounded, schema-authored rejection facts.