Wymcp (Wymcp v0.4.0)
View SourceMCP server library for Elixir — a Plug-based implementation of the MCP JSON-RPC 2.0 protocol, serving the modern and the legacy era 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; docs/glossary.md holds the vocabulary and says where each term is
defined. Neither is repeated here.
flowchart LR
CA(Consumer App) -->|implements| Tool
CA -->|implements| Auth
CA -->|implements| Server
Router --> Pipeline["Plugs.Pipeline"]
Router --> OriginCheck["Plugs.OriginCheck"]
Router --> AuthPlug["Plugs.Auth"]
Router --> SingletonHeaders["Plugs.SingletonHeaders"]
Pipeline --> OriginCheck
Pipeline --> AuthPlug
Pipeline --> SingletonHeaders
Pipeline --> Classify["Plugs.Classify"]
Pipeline --> Era["Plugs.Era"]
Pipeline --> ProtocolFields["Plugs.ProtocolFields"]
Pipeline --> SessionPlug["Plugs.Session"]
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 --> Session
Methods --> Help
Help --> Schema
Help --> Tool
Help --> Actions
Tool --> Schema["Tool.Schema"]
Tool --> Actions["Tool.Actions"]
Schema --> Actions
Actions --> Tool
Tool --> Context
Tool --> Hint
Context --> Session
Router --> Session
Router -->|compile| ServerInfo
Router --> Stream["Transport.Stream"]
Stream --> Session
Stream --> SSE["Transport.SSE"]
Session --> Telemetry
Session --> Server
Validate --> JsonRpcThe consumer surface
Wymcp.Router is the Plug entry point. It owns the routes — POST, the GET
SSE stream, DELETE, and a fallthrough that runs nothing — 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
that orders the three checks on every one of them. 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 check, 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, nor a session. 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 — session reference, request metadata,
the merged assigns — and the pure builders that turn a tool's return value
into MCP content. It owns no session state itself; it is the read path onto
Wymcp.Session. 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 — notably that tool and
action are strings, matching 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
application'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.
Wymcp.Server is the optional session-lifecycle behaviour:
Wymcp.Server.init/2 when a session becomes ready,
Wymcp.Server.terminate/2 at shutdown. It deliberately owns no
per-request hook — logging, rate limiting and metrics belong in Plug
middleware ahead of the mount, where they compose with the host app.
Wymcp.Session is the GenServer holding one session's state: the negotiated
version, capabilities, the effective tool list, per-session assigns, and
pending server→client requests. It owns session lifetime, the push path to
an open stream, and the ordered log-level vocabulary — the one statement of
it, from which Wymcp.Context derives its rank map at compile time. It is
on this surface because Wymcp.Session.register_tool/2 is a documented
consumer entry point, called from a Wymcp.Server callback.
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:
which revision each era serves, why 2024-11-05 is refused, and the names of
the modern protocol fields. It owns no negotiation policy — the
counter-proposal is built where initialize is answered.
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.
Wymcp.Testing provides helpers for a consuming app's own test suite —
session opts 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
two build-time-compiled protocol schema roots — one per era — and the
distilled shape every validation rejection reaches the wire as.
Wymcp.Response owns sending: one primitive per error dialect, the shared
rejection sender above them, and the rule that a rejection echoes an id only
when the inbound message was a request. Every sender halts the connection,
so no downstream plug runs after an answer is sent.
Wymcp.Modern owns the modern lane's result decoration — the result type
stamp, the serverInfo _meta, and the cache-hint defaults. Framework
defaults with zero consumer surface; era-guarded no-ops on the legacy lane.
Wymcp.ServerInfo owns the server identity map both eras emit — 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 — both are
dispatch's, and the full schemas are surfaced on demand by Wymcp.Help —
which is what keeps the tools/list payload compact. 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.Transport.Stream owns the SSE connection for one session: the receive
loop run by the GET request process that owns the socket, the mutual monitor
with the session, and the one-active-stream-per-session rule.
Wymcp.Transport.SSE owns event framing and nothing else — no process state,
no side effects, just the wire format for an already-encoded payload.
The plug lane
Ten plugs are one story, not ten: a POST's order of checks. In that order —
Wymcp.Plugs.OriginCheck, Wymcp.Plugs.Classify, Wymcp.Plugs.Auth,
Wymcp.Plugs.SingletonHeaders, Wymcp.Plugs.Era,
Wymcp.Plugs.ProtocolFields, Wymcp.Plugs.Session, Wymcp.Plugs.Validate,
Wymcp.Plugs.Dispatch — assembled by Wymcp.Plugs.Pipeline, which owns the
order itself and the inline body parsing that sits between the first check
and the rest. The three wire checks also run in Wymcp.Router's GET and
DELETE bodies, where there is no pipeline. Each plug's own moduledoc carries
its rules; the order, and why it is that order, lives in
Wymcp.Plugs.Pipeline.