ConduitMcp.Era (ConduitMCP v0.11.0)

Copy Markdown View Source

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 carriesmethodresult
pv and capsany (including initialize):modern
pv only, or caps onlyany-32602, HTTP 400
neitherserver/discover:modern (era-exempt; version: nil)
neitheranything 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

error()

@type error() :: {:error, integer(), String.t(), map() | nil}

A rejection: the neutral error of ConduitMcp.Handler.result/0.

Functions

check_version(ctx)

@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).

classify(request, conn)

@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).

traceparent(value)

@spec traceparent(term()) :: String.t() | nil

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