Classifies a decoded JSON-RPC request into a protocol era and builds its
ConduitMcp.Ctx.
Runs on the decoded body, so on HTTP it belongs after Plug.Parsers.
Truth table
pv is params._meta["io.modelcontextprotocol/protocolVersion"], caps
is params._meta["io.modelcontextprotocol/clientCapabilities"].
_meta carries | method | result |
|---|---|---|
pv and caps | any (including initialize) | :modern |
pv only, or caps only | any | -32602, HTTP 400 |
| neither | server/discover | :modern (era-exempt; version: nil) |
| neither | anything else | :legacy |
The Mcp-Session-Id header never changes the era. A legacy request keeps
the session the transport resolved (Ctx.session). A modern request that
carries the header anyway (a "mixed" request) ignores the session — its
context has no session and its conn drops the transport's session entries,
so cancellation scoping falls back to the principal and then the client IP
(ConduitMcp.Cancellation.scope/1) — except for one check: if the header
names a live session that recorded a principal, the request's principal
must be that principal, otherwise -32600 (HTTP 400).
Modern _meta validation
A modern request is rejected with -32602 (HTTP 400) when pv is not a
string, caps is not an object, io.modelcontextprotocol/clientInfo is
present but not an object, or io.modelcontextprotocol/logLevel is present
but not an MCP logging level. Error messages name the key, never its value.
Version support is not checked here: on HTTP the header/body version
comparison (-32020) must come first, so check_version/1 is a separate
step run by ConduitMcp.Plugs.HeaderValidation and again by the modern
handler.
Every error from classify/2 is a request-metadata rejection and is
answered with HTTP 400.
Trace context
traceparent, tracestate and baggage are read from _meta in either
era. An invalid value is dropped, never rejected: traceparent must be a
W3C version-00-style value of exactly 55 characters with non-zero ids;
tracestate and baggage must be at most 512 bytes of printable ASCII.
Summary
Types
A rejection: the neutral error of ConduitMcp.Handler.result/0.
Functions
Checks that a modern context's version is one this server implements.
Classifies request (a decoded JSON-RPC message) received on conn.
value when it is a well-formed W3C traceparent (version-00 style, 55
characters, lowercase hex, non-zero ids, version not ff), else nil.
The server reads _meta.traceparent through it, and the client checks
what it propagates with it.
Types
A rejection: the neutral error of ConduitMcp.Handler.result/0.
Functions
@spec check_version(ConduitMcp.Ctx.t()) :: :ok | error()
Checks that a modern context's version is one this server implements.
:ok for a legacy context and for an era-exempt server/discover sent
without a version; otherwise -32022 with data %{"supported" => ..., "requested" => ...} (HTTP 400).
@spec classify(map(), Plug.Conn.t() | map()) :: {:ok, ConduitMcp.Ctx.t()} | error()
Classifies request (a decoded JSON-RPC message) received on conn.
Returns the request's context — with conn set to the conn the handler
should use from here on — or a rejection (see the moduledoc).
value when it is a well-formed W3C traceparent (version-00 style, 55
characters, lowercase hex, non-zero ids, version not ff), else nil.
The server reads _meta.traceparent through it, and the client checks
what it propagates with it.
iex> ConduitMcp.Era.traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
iex> ConduitMcp.Era.traceparent("00-00000000000000000000000000000000-00f067aa0ba902b7-01")
nil