Wymcp (Wymcp v0.1.1)
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
Tool --> Schema["Tool.Schema"]
Tool --> Context
Tool --> Hint
Context --> Session
Router --> Session
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 passes at forward, and the wire-check invariant that orders
the three checks on every one of them. It owns no method's answer and not
the POST chain's interior. It exists so a consuming app mounts MCP with one
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 dispatch gates that validate a call before
the handler runs, and the contract governing the text a consumer writes. It
owns no wire envelope and no 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 forward, 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 and the push path to
an open stream. 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.
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, assembled
from application config and the router's :server_info, with icon keys
whitelisted rather than passed through.
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
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.
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.