# `ConduitMcp.Transport.Shared`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/transport/shared.ex#L1)

The single implementation of everything `ConduitMcp.Transport.StreamableHTTP`
and `ConduitMcp.Transport.SSE` have in common.

Keeping one implementation is what keeps the transports from diverging: a
feature added to one router (the `:oauth` auth strategy,
`/.well-known/oauth-protected-resource`, the `mcp-protocol-version` header)
exists on the other by construction.

`use ConduitMcp.Transport.Shared` therefore generates, in both routers:

  * the `Plug.Router` pipeline up to the transport-specific plugs,
  * `init/1` and `call/2`,
  * the shared plug functions (`add_cors_headers`, `authenticate`,
    `rate_limit`, `classify_era`, `message_rate_limit`),

and `shared_routes/0` generates the shared routes (`options _`,
`get "/health"`, `get "/.well-known/oauth-protected-resource"`, `match _`).
No function body exists in both transports.

## Protocol eras

`classify_era` runs on POST and DELETE after `authenticate` and
`rate_limit`, and before `message_rate_limit`, so a request it rejects is
never counted against the client. It runs
`ConduitMcp.Plugs.HeaderValidation`, which classifies the body
(`ConduitMcp.Era.classify/2`): a modern request has its
`ConduitMcp.Ctx` stored and its request-metadata headers validated (any
rejection is HTTP 400); a legacy request whose `MCP-Protocol-Version`
header names an unsupported version is answered 400 (`initialize`
excepted). `dispatch_post/2` then dispatches each era its own way.

## Plugs are resolved once

`init/1` resolves `:auth`, `:rate_limit` and `:message_rate_limit` into
`{module, initialised_opts}` pairs, so `call/2` is `mod.call(conn, opts)`
and no plug's `init/1` runs per request. It also validates the shape of
`:cors_origin` and `:allowed_origins`, so a misconfiguration raises
`ArgumentError` at boot rather than failing on every request.
`ConduitMcp.Plugs.OAuth`'s `init/1` in particular is where an unusable OAuth
configuration is reported as a configuration error rather than a 401.

## Callbacks in transport options must be remote captures

`Plug.Router.forward/2` runs the transport's `init/1` at compile time and
escapes the result into the router module. An anonymous function
(`fn conn -> ... end`) or a local capture in the options — for example a
rate limiter's `:key_func` — cannot be escaped, so the router fails to
compile. Use a remote capture such as `&MyApp.RateKeys.by_user/1`.

## Deliberate asymmetries

Anything the two transports genuinely do differently is listed in their own
moduledocs with a reason, so that no asymmetry is accidental.

# `resolved_plug`

```elixir
@type resolved_plug() :: {module(), term()} | nil
```

A plug resolved at `init/1` time: the module and its initialised options.

# `__using__`
*macro* 

Generates the shared `Plug.Router` pipeline, `init/1`, `call/2` and the
shared plug functions.

Options:

  * `:extra_plugs` — plug names to insert between `:message_rate_limit` and
    `:dispatch` (StreamableHTTP uses this for `:validate_session`).

# `dispatch_post`

```elixir
@spec dispatch_post(Plug.Conn.t(), (Plug.Conn.t(), map() -&gt;
                                {:ok, Plug.Conn.t()} | {:error, Plug.Conn.t()})) ::
  Plug.Conn.t()
```

The shared JSON-RPC POST body dispatch.

A notification or a JSON-RPC response is answered `202 Accepted` with no
body. A request is answered with its JSON-RPC response and an
`mcp-protocol-version` header (see `put_protocol_version_header/2`).

A **legacy** request's response is sent at HTTP 200, except that under
`strategy: :oauth` an insufficient-scope error is answered 403 with an
`insufficient_scope` Bearer challenge (`ConduitMcp.Plugs.OAuth.forbidden/4`),
so the client can step up.

A **modern** request (one the `:classify_era` plug stored a modern
`ConduitMcp.Ctx` for) is dispatched with `ConduitMcp.Handler.dispatch/3`.
Its response is sent at `ConduitMcp.Protocol.Modern.http_status/1` (400
for `-32020`/`-32021`/`-32022`, 404 for an unknown method, else 200), with
the same 403 challenge. A `{:stream, spec}` dispatch of kind `:response` —
a `tools/call` asking for progress or log notifications — is answered as a
Server-Sent Events stream when the request's `Accept` includes
`text/event-stream` (see `ConduitMcp.StreamSpec`), otherwise as plain JSON
with its notifications dropped. One of kind `:listen` — a
`subscriptions/listen` — is served by `ConduitMcp.Subscriptions` as a
long-lived stream; without `text/event-stream` in `Accept` it is answered
`406` with a `-32600` error, since it has no JSON form. `prepare` never
runs for a modern request: the modern era has no sessions to mint.

`prepare` runs after a successful legacy response map is produced and
before it is sent, so a transport can attach transport-specific headers or
state. It returns `{:ok, conn}` to continue, or `{:error, conn}` when it
has already sent a response of its own.

# `endpoint_config`

```elixir
@spec endpoint_config(module() | nil) :: keyword()
```

Reads a `use ConduitMcp.Endpoint` server's compile-time config, which both
transports use as defaults for options not passed explicitly.

# `init`

```elixir
@spec init(keyword(), module()) :: keyword()
```

Validates and resolves transport options once, at `init/1`.

Returns the options with `:auth_config`, `:auth_plug`, `:rate_limit_plug`,
`:message_rate_limit_plug` and `:shared_private` added. `:shared_private`
also carries the transport's own `__transport_private__/1` entries, so an
invalid transport-specific option raises here too.

# `put_protocol_version_header`

```elixir
@spec put_protocol_version_header(Plug.Conn.t(), map()) :: Plug.Conn.t()
```

Sets `mcp-protocol-version` on the response to request `response_map`.

A modern request is answered with the version its `_meta` named (already
validated by `ConduitMcp.Plugs.HeaderValidation`). Otherwise the value is
the version this connection negotiated, most specific source first: an
`initialize` result's own `protocolVersion`; then the version recorded on
the request's session; then the request's `mcp-protocol-version` header;
then `ConduitMcp.Protocol.protocol_version/0`. A session or header value is
used only when it is in `ConduitMcp.Protocol.supported_versions/0`, so
client input is never echoed unvalidated.

# `resolve_auth_plug`

```elixir
@spec resolve_auth_plug(keyword() | nil) :: resolved_plug()
```

Resolves an auth configuration into `{module, initialised_opts}`.

`strategy: :oauth` needs `ConduitMcp.Plugs.OAuth`, which is compiled only
when `:joken` was available when `:conduit_mcp` itself was built. When it is
missing this raises `ConduitMcp.OptionalDependencyError` naming both the
dependency and `mix deps.compile conduit_mcp --force`, rather than letting
the request fall through to `ConduitMcp.Plugs.Auth`'s catch-all and 401.

Returns `nil` when no auth is configured.

# `shared_routes`
*macro* 

Generates the routes both transports serve: the CORS preflight catch-all,
`GET /health`, the RFC 9728 protected-resource metadata endpoint, and the
404 catch-all.

Call this **last** in the router — `match _` must be the final route.

# `warn_if_origins_unset`

```elixir
@spec warn_if_origins_unset(keyword(), module()) :: :ok
```

Warns once per boot (from `init/1`) when no Origin policy was chosen at all.

Pass `allowed_origins: "*"` to opt out explicitly.

---

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