Wymcp. JsonRpc
(Wymcp v0.7.1)
View Source
JSON-RPC 2.0 envelopes and MCP protocol schema validation — the wire-format floor both eras share.
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, the 200-status method errors, and the 500 a server refusal answers 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,-32001Session terminated.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.- 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
One self-contained MCP protocol schema per era —
priv/schema-2025-11-25.json and 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. Both schemas name the same entry
point, JSONRPCMessage; validate_mcp_request/2 selects the era's root from
the era Wymcp.Plugs.Validate passes it. @external_resource on both paths
makes an edit to either 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. It 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. This module
distills it on two paths: validate_mcp_request/2 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. Wymcp.Methods.ToolsCall builds a conforming one
by hand for the -32602 arguments path, whose reader is the calling LLM.
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.
Types
@type error_type() ::
:parse_error
| :invalid_request
| :method_not_found
| :invalid_params
| :internal_error
| :session_not_found
| :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 and Wymcp.Methods.Initialized a consumer's
Wymcp.Server.init/2 reason, both term() by contract, while the MCP
schemas type this field as a string — so the rendering happens here, once,
rather than at each site that can receive one.