The per-request context: which protocol era a request belongs to and what it declared about itself.
ConduitMcp.Era.classify/2 builds it once per request; the handler stores
it in conn.private[:mcp_ctx] before any callback runs, so callbacks keep
receiving a Plug.Conn and read the context with from_conn/1:
def handle_call_tool(conn, "whoami", _params) do
case ConduitMcp.Ctx.from_conn(conn) do
%ConduitMcp.Ctx{era: :modern, client_info: info} -> text(inspect(info))
_ -> text("legacy client")
end
endFields
:era—:modern(2026-07-28, per-request_meta) or:legacy(initialize-negotiated).:version— the modern request's_metaprotocol version; for a legacy request, the version negotiated into its session, if any.nilfor an era-exemptserver/discoversent without_meta.:client_info— the modernio.modelcontextprotocol/clientInfo(self-reported: never base a security decision on it).:client_capabilities— the modern per-requestio.modelcontextprotocol/clientCapabilities;%{}for legacy.:extensions—client_capabilities["extensions"], or%{}.:log_level— the modernio.modelcontextprotocol/logLevel. Without it the server must not sendnotifications/messagefor the request; seelog?/2.:principal—ConduitMcp.Principal.get/1of the request.:session— legacy only:%{id: session_id, data: session_data}when the transport resolved anMcp-Session-Id; alwaysnilfor modern.:conn— the request'sPlug.Conn.:request_id— the JSON-RPC id (nilfor a notification).:server_info— the%{"name" => ..., "version" => ...}this server reports (from the transport's:server_name/:server_version, an Endpoint's:name/:version, or the library defaults).:traceparent,:tracestate,:baggage— the W3C trace-context keys from a modern request's_meta, validated byConduitMcp.Era:traceparentmust be a well-formed 55-character value,tracestateandbaggageat most 512 bytes of printable ASCII. Anything else is dropped (nil) rather than rejected.
Never log or emit it whole
Inspect omits :conn, :principal and :session, which carry
credentials and identities. Telemetry metadata never contains a Ctx; the
request event copies individual, already-validated fields instead.
Summary
Functions
The request's context, with :conn set to conn, or nil when no
context has been stored on it.
Whether a notifications/message at level may be sent for this request.
The valid MCP logging levels, least severe first.
Stores ctx in conn.private[:mcp_ctx] and returns the conn.
The server identity reported to clients: %{"name" => ..., "version" => ...}.
Types
@type era() :: :modern | :legacy
@type t() :: %ConduitMcp.Ctx{ baggage: String.t() | nil, client_capabilities: map(), client_info: map() | nil, conn: Plug.Conn.t() | map() | nil, era: era(), extensions: map(), log_level: String.t() | nil, principal: ConduitMcp.Principal.t() | nil, request_id: term(), server_info: %{required(String.t()) => String.t()} | nil, session: %{id: String.t(), data: map()} | nil, traceparent: String.t() | nil, tracestate: String.t() | nil, version: String.t() | nil }
Functions
@spec from_conn(Plug.Conn.t() | map()) :: t() | nil
The request's context, with :conn set to conn, or nil when no
context has been stored on it.
Whether a notifications/message at level may be sent for this request.
Modern era: only when the request carried
io.modelcontextprotocol/logLevel and level is at least that severe —
the specification forbids log notifications for a request without it.
Legacy era: always true; the legacy era sets its level once, through
logging/setLevel, which the server's own callback handles.
Examples
iex> ctx = %ConduitMcp.Ctx{era: :modern, log_level: "warning"}
iex> ConduitMcp.Ctx.log?(ctx, "error")
true
iex> ConduitMcp.Ctx.log?(ctx, "info")
false
iex> ConduitMcp.Ctx.log?(%ConduitMcp.Ctx{era: :modern}, "emergency")
false
@spec log_levels() :: [String.t()]
The valid MCP logging levels, least severe first.
@spec put(Plug.Conn.t() | map(), t()) :: Plug.Conn.t() | map()
Stores ctx in conn.private[:mcp_ctx] and returns the conn.
The stored copy has :conn cleared, so a conn never nests older versions of
itself; from_conn/1 puts the current conn back.
@spec server_info(Plug.Conn.t() | map()) :: %{required(String.t()) => String.t()}
The server identity reported to clients: %{"name" => ..., "version" => ...}.
Read from the transport-resolved conn.private[:server_name] /
[:server_version], defaulting to "conduit-mcp" and the library version.