Wymcp (Wymcp v0.9.1)
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"]
Tool --> ValueGate["Tool.ValueGate"]
ValueGate --> KeySet
ValueGate --> JsonRpc
Actions --> KeySet
Schema --> Actions
Actions --> Tool
Tool --> Context
Tool --> Hint
Router -->|compile| ServerInfo
Router -->|compile| CallbackSurface["Wymcp.CallbackSurface"]
Tool -->|compile| CallbackSurface
Tool -->|compile| ConfigurationError["Wymcp.ConfigurationError"]
Router -->|compile| ConfigurationError
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,
every mount tool's tools/list definition, the input-schema root of
every action (:input_schema_roots), which the value gate reads a
call's data against, and, for each tool with an output schema, the root
tools/call validates its structured content against, 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-shape check and what it
builds from one obtain of a tool's actions — the tools/list
definition, each action's input-schema root (through
Wymcp.Tool.ValueGate) and the output schema's root tools/call
validates against, each refusing a schema JSV cannot build — the
dispatch gates that validate a call's arguments and every value in its
data 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, the answers the client has already given this call,
and the called tool's input-schema roots, which wymcp sets for dispatch
and a tool never reads — 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.ConfigurationError is the exception every configuration-time
check raises when it refuses a tool's declaration or a mount's options —
a configuration refusal, at a tool module's own compile, at the
registration moment, or at an obtaining moment's re-check. It owns the
type and the two facts it carries beside the message: the module refused
and the callback at fault. It owns no check and no message — each check
words its own. It exists so a configuration refusal of the tool being
checked can be told from a consumer callback's own raise, and reported at
the tool's file rather than as a bare exit.
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, the distilled shape every validation failure reaches the
wire as, and the cause-level reading of a JSV error that shape and the
value gate's answer are both built from.
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
configuration 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.Tool.ValueGate owns the value gate's half of dispatch: the one
JSV root per action that a call's data is checked against — built at
the registration moment with format asserted, proving each action's
:defaults as it is built — and the reading of a validation error into
bounded data paths. It owns neither the rule, which Wymcp.Tool states
beside the other dispatch gates, nor the tool-dialect answer, which
dispatch builds. It exists so a value the action's schema refuses never
reaches Wymcp.Tool.run_action/3, and so the schema a caller reads is
the one that refuses it.
Wymcp.CallbackSurface owns the callback-surface invariant and the one
check that holds its required half — run at the registration moment for
every module a mount wires in, :tools and :auth alike, and at a
use Wymcp.Tool module's own compile. It owns no behaviour: Wymcp.Tool
and Wymcp.Auth declare their callbacks, and the check reads them back
through behaviour_info/1. It exists because the invariant claims every
wymcp behaviour, so its home belongs to none of 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.