Wymcp (Wymcp v0.1.1)

View Source

MCP 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 --> JsonRpc

The 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.