Wymcp. JsonRpc
(Wymcp v0.9.1)
View Source
JSON-RPC 2.0 envelopes and MCP protocol schema validation — the wire-format floor every answer stands on.
success_response/2 and error_response/3 build the envelope every answer
is sent as; Wymcp.Response does the sending. The error map is the one home
for wymcp's JSON-RPC error codes: each error_type/0 atom pairs with its
{code, message} tuple, so a call site names the condition and never the
number. This module owns the envelope's shape; it owns no decision about
which condition applies — that is each plug's and each method module's.
The error envelope's contract
One rule, four clauses, over every JSON-RPC error this module builds — the 400s and 404s that are rejections and the 200-status method errors alike:
codecomes from the error-code table; a site names the condition, never the number.messageis the site's one-line cause. The table's string is what a site sends when it has nothing more to say —-32601Method not found.datais absent, or a map whose keys are drawn from a closed vocabulary of structured detail:errors(a validation failure's locations),header,expected,received(the header-binding check),supported,requested(the protocol version). No key carries a prose sentence, and no key carries the request or any part of it beyond the single value a condition is about, quoted under the ruleWymcp.Boundstates in "What a refusal quotes" — as is anything amessagequotes from the request.- A new structured key joins in one place — the
data_keyslist this module keeps beside its error-code table, whichWymcp.ErrorEnvelopeInvariantTestreads to hold every envelope to this vocabulary — the same discipline the rejection table keeps, a gate rather than a catalogue. Clause 3 names the keys for a reader; that list is what the gate is checked against.
The sentence belongs in message because that is the slot the MCP schemas
define for it — "A short description of the error … a concise single
sentence" — and the slot a client renders: a tools/call refused for a
wrong-typed argument showed the calling model Invalid params and nothing
else, measured 2026-09-10, while the cause sat under data unread.
Message and output-schema validation
The self-contained MCP protocol schema — priv/schema-2026-07-28.json,
JSON Schema 2020-12 — is compiled to a JSV.Root at build time through
a module attribute, so an inbound request is validated against the official
protocol definition with no runtime schema parsing. Its entry point is
JSONRPCMessage, which validate_mcp_request/1 validates against.
@external_resource on the path makes an edit to the schema file recompile
this module.
A validation failure reaches the wire as a distilled error — the
envelope's message, a one-line summary, plus data.errors, one entry per
instance location, locations sorted, each location's problems deduplicated
— never JSV.normalize_error/1's full tree. A distilled error is a shape,
not one function's return value: the producers below return the two halves
as one %{message:, errors:} map, and the site that answers splits them
across the envelope's two slots. It carries only problems that state why
the data was rejected — never one that only points at a deeper problem, or
restates one its parent already reports. Where JSV produces the error, the
producer meets this by keeping only the problems JSV rates at its cause
level. This module
distills it on two paths: validate_mcp_request/1 on the -32600
message path, whose reader is the human debugging a client, and
validate_schema/2 on a tool's output-schema failure, which is logged and
never reaches the wire. That path validates against a JSV.Root built
ahead of the call, as the protocol schema's is: Wymcp.Router.init/1
builds it from the tool's output schema at the
registration moment, never per
tools/call. Wymcp.Methods.ToolsCall builds a conforming distilled error
by hand for the -32602 it answers a malformed requestState or
inputResponses with. The value gate (Wymcp.Tool.ValueGate) reads JSV's
cause-level problems through this module as well, but answers in the tool
dialect rather than as a distilled error.
The full tree is JSON-encodable and was what previously reached the wire;
it is simply too verbose for either reader — 2353 bytes against 435 for one
garbage message, measured 2026-08-15.
Summary
Types
The atom naming one row of the error-code table: the JSON-RPC errors
wymcp answers, each pairing its atom with a {code, message} tuple.
Both error_response arities accept exactly these atoms.
Distinct from telemetry's error_kind, which classifies a tool
error's origin, not a JSON-RPC error.
Functions
The error envelope carrying the error-code table's own message and no
data — the form for a site whose condition has one wording and nothing
to locate.
The error envelope — the JSON-RPC error object wymcp answers with: code
from the error-code table row error_type names, message the site's
one-line cause, the caller's request_id as id, and data absent or a
map of structured detail from the closed vocabulary this module's contract
states — never a prose key, never the request.
Validates data against a built JSV.Root — for a tool's structured
content, the root Wymcp.Router.init/1 stored for its output schema —
returning :ok, or {:error, distilled} in the distilled shape above.
Types
@type error_type() ::
:parse_error
| :invalid_request
| :method_not_found
| :invalid_params
| :internal_error
| :unsupported_protocol_version
| :header_mismatch
The atom naming one row of the error-code table: the JSON-RPC errors
wymcp answers, each pairing its atom with a {code, message} tuple.
Both error_response arities accept exactly these atoms.
Distinct from telemetry's error_kind, which classifies a tool
error's origin, not a JSON-RPC error.
Functions
The error envelope carrying the error-code table's own message and no
data — the form for a site whose condition has one wording and nothing
to locate.
The error envelope — the JSON-RPC error object wymcp answers with: code
from the error-code table row error_type names, message the site's
one-line cause, the caller's request_id as id, and data absent or a
map of structured detail from the closed vocabulary this module's contract
states — never a prose key, never the request.
opts carries :message and :data, both optional; without :message
the table's own string is sent. Any other key raises: a misspelled
:message would otherwise ship the table's generic string in place of the
site's cause, silently, which is the answer this contract exists to end.
A :message term that is not a binary is rendered rather than refused.
Wymcp.Plugs.Auth hands on a consumer's Wymcp.Auth.authenticate/1
reason as given, term() by contract, while the MCP schema types this
field as a string — so the rendering happens here, once, rather than at
each site that can receive one.
Validates data against a built JSV.Root — for a tool's structured
content, the root Wymcp.Router.init/1 stored for its output schema —
returning :ok, or {:error, distilled} in the distilled shape above.