ConduitMcp.Ctx (ConduitMCP v0.11.0)

Copy Markdown View Source

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
end

Fields

  • :era — :modern (2026-07-28, per-request _meta) or :legacy (initialize-negotiated).
  • :version — the modern request's _meta protocol version; for a legacy request, the version negotiated into its session, if any. nil for an era-exempt server/discover sent without _meta.
  • :client_info — the modern io.modelcontextprotocol/clientInfo (self-reported: never base a security decision on it).
  • :client_capabilities — the modern per-request io.modelcontextprotocol/clientCapabilities; %{} for legacy.
  • :extensions — client_capabilities["extensions"], or %{}.
  • :log_level — the modern io.modelcontextprotocol/logLevel. Without it the server must not send notifications/message for the request; see log?/2.
  • :principal — ConduitMcp.Principal.get/1 of the request.
  • :session — legacy only: %{id: session_id, data: session_data} when the transport resolved an Mcp-Session-Id; always nil for modern.
  • :conn — the request's Plug.Conn.
  • :request_id — the JSON-RPC id (nil for 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 by ConduitMcp.Era: traceparent must be a well-formed 55-character value, tracestate and baggage at 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

era()

@type era() :: :modern | :legacy

t()

@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

from_conn(conn)

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

log?(ctx, level)

@spec log?(t(), String.t()) :: boolean()

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

log_levels()

@spec log_levels() :: [String.t()]

The valid MCP logging levels, least severe first.

put(conn, ctx)

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

server_info(conn)

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