# `ConduitMcp.Era`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/era.ex#L1)

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.

# `error`

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

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

# `check_version`

```elixir
@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`

```elixir
@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`

```elixir
@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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
