Wymcp (Wymcp v0.8.7)
View SourceMCP server library for Elixir — a Plug-based implementation of the MCP JSON-RPC 2.0 protocol, speaking the 2026-07-28 revision on one endpoint. Tools are the only MCP primitive wymcp implements; resources and prompts are not served.
This module is the map: every published module, what it owns, and why it exists. README narrates why the project exists and how to mount a first server; the glossary holds the vocabulary and says where each term is defined. Neither is repeated here.
flowchart LR
CA(Consumer App) -->|implements| Tool
CA -->|implements| Auth
Router --> Pipeline["Plugs.Pipeline"]
Pipeline --> OriginCheck["Plugs.OriginCheck"]
Pipeline --> AuthPlug["Plugs.Auth"]
Pipeline --> SingletonHeaders["Plugs.SingletonHeaders"]
Pipeline --> Classify["Plugs.Classify"]
Pipeline --> ProtocolFields["Plugs.ProtocolFields"]
Pipeline --> HeaderBinding["Plugs.HeaderBinding"]
HeaderBinding --> Schema
AuthPlug --> Auth
Pipeline --> Validate["Plugs.Validate"]
Pipeline --> Dispatch["Plugs.Dispatch"]
Dispatch --> Methods["Methods.*"]
Dispatch --> Discover["Methods.Discover"]
Discover --> Modern["Wymcp.Modern"]
Methods --> Modern
Modern --> ServerInfo["Wymcp.ServerInfo"]
Methods --> Tool
Methods --> Help
Methods --> Telemetry
Help --> Schema
Help --> Tool
Help --> Actions
Help --> KeySet
Tool --> Schema["Tool.Schema"]
Tool --> Actions["Tool.Actions"]
Tool --> KeySet["Tool.KeySet"]
Actions --> KeySet
Schema --> Actions
Actions --> Tool
Tool --> Context
Tool --> Hint
Router -->|compile| ServerInfo
Validate --> JsonRpcThe consumer surface
Wymcp.Router is the Plug entry point. It owns the routes — POST, and a
fallthrough that answers every other verb 405, a POST at a path it does
not serve 404, and marks its answer either way — the
options a consuming app declares in its mount module together
with the router-option invariant that closes and validates that set, and
the wire-check invariant: the ordered wire-check list every served route
runs and the served-verb list itself, each stated once in that module and
held to the routes and to the POST chain by Wymcp.WireCheckInvariantTest.
It also owns the mount's build artifacts: Wymcp.ServerInfo's partial, and
every mount tool's tools/list definition, assembled at the registration
moment from the callbacks the validation chain has just read. It owns no
method's answer — Wymcp.Methods.ToolsList decides which tools a listing
carries and shapes the response around the definitions it looks up — and
not the POST chain's interior. It exists so a consuming app mounts MCP with
a use and a forward, and never touches the pipeline.
Wymcp.Tool is the behaviour a consuming app implements to expose
capabilities. It owns the action-dispatched pattern — one tool name
multiplexing many actions — the action-schema vocabulary and the format
catalogue a consumer writes against, the callback-surface and callback-shape checks, the
dispatch gates that validate a call before the handler runs, and the
contract governing the text a consumer writes. It owns neither the
validation of the schemas it describes — that is Wymcp.Tool.Actions — nor
a wire envelope. It exists because a tool author should describe actions,
not JSON-RPC.
Wymcp.Context is the struct every Wymcp.Tool.run_action/3 receives. It
owns the per-call view of the world — request identity and metadata, the
per-request assigns, and the answers the client has already given this
call — and the pure builders that turn a tool's return value into MCP
content. It owns no state that outlives the call. It exists so a tool
never reaches for a Plug.Conn.
Wymcp.Hint is the struct for follow-up action suggestions. It owns the
hint's shape and its construction-time validation in Wymcp.Hint.new/1 —
every field checked against the type it publishes for the wire. It owns no
decision about when a hint is emitted; that is Wymcp.Tool's
Wymcp.Tool.hints/2.
Wymcp.Auth is the consumer contract for request authentication. It owns
the Wymcp.Auth.authenticate/1 callback and the rule that authentication
reads connection data, never a request body. It owns neither the 401 nor its
challenge — the auth check does. It exists so credentials stay the host's
business.
Wymcp.Auth.Noop is the :auth default: it accepts every request. It exists
because a consumer meets it without choosing it — mount without :auth and
this is what runs — so it is named here rather than left to be discovered.
There is deliberately no per-request or lifecycle hook for a consumer to
implement: observability is telemetry's — Wymcp.Telemetry's events,
Wymcp.Telemetry.Logger's lines, and the rejection mark
Wymcp.Response.rejection/1 reads — while the cross-cutting concerns that
are not observability, rate limiting and per-route auth among them,
compose as host plugs ahead of the mount.
Wymcp.Help is the framework-owned introspection tool, injected into every
server under the reserved name help. It owns the three answer levels and
the rule that unknown targets error naming the valid ones. It owns no
content of its own — it renders what tools declare, from the same source the
tools/list description builder uses, so the two cannot drift. What it will
not do is render around a gap; the guarantee is not its own, but the one
every reader inherits from obtaining a schema (the read-side corollary,
Wymcp.Tool.Actions).
Wymcp.ProtocolVersion is the single source of truth for version support:
the one revision wymcp serves and the names of the protocol fields. It
owns no answer to any other revision — Wymcp.Plugs.ProtocolFields gives
that.
Wymcp.Telemetry is the catalogue of :telemetry events wymcp emits, with
their measurements and metadata. It owns the event contract, not the
emission: each event is emitted where it happens. It exists so a consuming
app can attach handlers against a documented surface — the library emits, a
handler renders, the consumer chooses.
Wymcp.Telemetry.Logger is the handler wymcp ships against that surface: it
renders the catalogue's events as structured Logger lines under one
policy — its line table says which — and it is attached at boot unless the
consuming app turns it off
with one key. It owns the lines and nothing else — no event, no metadata —
so a consumer wanting other levels or keys attaches a handler of its own
rather than living with wymcp's.
Wymcp.Testing provides helpers for a consuming app's own test suite —
building a context, running a tool the way the wire runs it, building a
request body the router serves and the mirrored headers it implies, and
response unwrapping. It owns test conveniences only, and ships in the
package because a consumer's tests need it.
Internals
These modules are wymcp's own machinery. A consuming app does not call them; they are catalogued because the map is complete or it is not a map.
Wymcp.JsonRpc owns the JSON-RPC envelope shapes, the error-code table, the
contract every error envelope keeps, the build-time-compiled protocol
schema root, and the distilled shape every validation failure reaches the
wire as.
Wymcp.Response owns sending, and the record of every rejection: the
envelope sender and the acceptance sender beside it — the 202 with no
body every accepted notification answers — the shared rejection sender
above them, the mark-and-emit helper beneath both — which writes the
rejection mark, emits the rejection event, and accepts only a rejecter
and reason its declared table names — the two readers of that mark, and
the rule that an error envelope echoes an id only when the inbound message
was a request. The rejection invariant is its moduledoc's to state. Every
sender halts the connection, so no downstream plug runs after an answer
is sent.
Wymcp.Modern owns the 2026-07-28 result shape — the completed result,
and the input-required one a call answers when it still needs input from
the user — with the result-type stamp, the serverInfo _meta, the
cache-hint defaults, and the requestState codec that carries answers
between rounds. Framework defaults with zero consumer surface.
Wymcp.ServerInfo owns the server identity map every result carries — the
serverInfo partial precomputed from the :server_info option at the
mount module's compile (an unknown key refuses the compile), plus the
per-request merge of name and version from application config.
Wymcp.Tool.Actions owns everything the framework asks of the actions a
tool module declares: the validator chain that runs at each action-schema
validation moment, the action-schema invariant those moments enforce, and
the obtaining accessors through which every reader — dispatch,
Wymcp.Help, Wymcp.Tool.Schema — gets a schema whose mandatory keys are
already checked. It owns neither the vocabulary nor the format catalogue:
both stay with the behaviour in Wymcp.Tool, and the chain reads the key
list back through that module's accessor rather than restating it.
Wymcp.Tool.Schema owns the inputSchema a tool publishes: an action enum
whose description carries the action summaries, a bare data object, and
the closed key set the root declares as additionalProperties: false. It
obtains that tool's action schemas itself rather than being handed them;
that guarantee is not its own, but the one every reader inherits from
obtaining a schema (the read-side corollary, Wymcp.Tool.Actions). It
deliberately owns no per-action constraint, and no enforcement of a
call — both are dispatch's, and the full schemas are surfaced on demand
by Wymcp.Help — which is what keeps the tools/list payload compact. It
does own one enforcement of a schema: validate_header_annotations!/2
holds a hand-written input_schema/0's header annotations to the spec's
constraints at both registration moments, beside header_annotations/1,
the reader the header-binding check builds its Mcp-Param-* rows from —
reader and validator in one module; the schema build/1 generates carries
no header annotation. Obtaining points this
module at Wymcp.Tool.Actions, which points at nothing here in return — the
diagram above carries that single edge. It closes a three-module cycle that
exists only at run time, Wymcp.Tool → Wymcp.Tool.Schema →
Wymcp.Tool.Actions → Wymcp.Tool, whose last leg reads the key list
rather than building anything.
Wymcp.Tool.KeySet owns the walk over an action's :properties for the
object schemas whose key set is closed, in both directions: dispatch's
unknown-key gate reads a call's data against them, and Wymcp.Help and
the gate's input_schema digest publish them closed. Wymcp.Tool.Actions
asks the same walks about the author's own maps at validation — where a
schema cannot be read, and which keys of :defaults the closure refuses —
so a schema that registers is one both directions read alike. It owns
neither the rule — Wymcp.Tool states it beside the other dispatch gates
— nor the tool-dialect answer, which dispatch builds, nor the refusal
validation raises, which Wymcp.Tool.Actions words. It exists so the
closure a caller reads and the closure dispatch enforces are walked side
by side, over one reach, and held to agreement by a test that compares
them.
Wymcp.Bound owns wymcp's one bound — the number, read in each value's own
unit — and the total renderer that applies it, in three forms:
Wymcp.Bound.render/1, always a string, for a fault diagnostic and a
string interpolation; Wymcp.Bound.value/1, which keeps a scalar as
itself, for a log line's metadata; and Wymcp.Bound.echo/1, for a value a
refusal quotes from the request. It owns one decision about which values
are bounded — that every refusal quotes the request through the echo form
— and leaves every other to its caller. Its moduledoc is where
bounded and
totality are defined.
The plug chain
The plugs are one story, not several: a POST's order of checks. In that
order — Wymcp.Plugs.OriginCheck, Wymcp.Plugs.Classify,
Wymcp.Plugs.Auth, Wymcp.Plugs.SingletonHeaders,
Wymcp.Plugs.ProtocolFields, Wymcp.Plugs.HeaderBinding,
Wymcp.Plugs.Validate, Wymcp.Plugs.Dispatch — assembled by
Wymcp.Plugs.Pipeline, which owns the order itself, exposed as its chain
and held to the router's list by Wymcp.WireCheckInvariantTest; the
declaration of which plugs ahead of Wymcp.Plugs.Validate are body-bound,
and why; and the inline parse step that sits between the origin check and
the rest. Which of them are wire checks, and the order those run in, is
Wymcp.Router's wire-check invariant to state. Each plug's own moduledoc
carries its rules; the order, and why it is that order, lives in
Wymcp.Plugs.Pipeline.