# `GenMCP.SessionController`
[🔗](https://github.com/lud/gen_mcp/blob/main/lib/gen_mcp/session_controller.ex#L1)

Behaviour for the session store of the 2025 compatibility transport.

The `2026-07-28` core is stateless and has no sessions. The 2025 protocol
requires one: the client handshakes with `initialize`, the server answers with
an `Mcp-Session-Id`, and every later request carries that id back. A session
controller is what bridges the two — it is asked to `c:create/3` a session id
at the end of the handshake, and to `c:fetch/3` the session data back on each
subsequent request.

The session is deliberately tiny. It holds only what the server actually reads
later: the negotiated protocol version and the client's name and version,
which the transport writes into the `_meta` of the `V2607` request it builds,
so `GenMCP.Mux.Channel` carries them to handlers exactly as it would for a
2026 request. It is written once at `initialize` and only read afterwards.

The default controller is `GenMCP.SessionController.Token`, which stores
nothing server-side. Configure another one on the compat transport with the
`:session_controller` option, as a module or a `{module, arg}` tuple:

    forward "/mcp-2025", GenMCP.Transport.StreamableHTTP.V2511,
      server_name: "My App",
      server_version: "1.0.0",
      tools: [MyApp.AddTool],
      session_controller: {MyApp.RedisSessions, pool: :sessions}

Implement one when the deployment needs the full `clientInfo` and capabilities
kept server-side, or needs real revocation, which a self-contained session id
cannot offer.

# `arg`

```elixir
@type arg() :: term()
```

The value configured alongside the module as `{module, arg}`.

# `controller`

```elixir
@type controller() :: module() | {module(), arg()} | %{mod: module(), arg: arg()}
```

# `session`

```elixir
@type session() :: %{
  protocol_version: binary(),
  client_name: binary() | nil,
  client_version: binary() | nil
}
```

The client data a session carries across requests.

# `session_id`

```elixir
@type session_id() :: binary()
```

# `create`

```elixir
@callback create(session(), Plug.Conn.t(), arg()) ::
  {:ok, session_id()} | {:error, term()}
```

Creates a session and returns the id the client will send back.

Called once, while answering `initialize`, with the `session` built from the
handshake. The returned id becomes the response's `Mcp-Session-Id` header. It
must be usable as an HTTP header value, and small enough to travel on every
later request.

Returning `{:error, reason}` fails the handshake with that reason.

# `delete`
*optional* 

```elixir
@callback delete(session_id(), Plug.Conn.t(), arg()) :: term()
```

Drops the session, on a client `DELETE`. Optional.

A controller that keeps no server-side state has nothing to do here, so the
callback is optional and the `DELETE` is accepted either way.

# `fetch`

```elixir
@callback fetch(session_id(), Plug.Conn.t(), arg()) :: {:ok, session()} | {:error, term()}
```

Reads back the session named by `session_id`.

Called on every request after the handshake. Return `{:error, :invalid}` for
an id this server did not issue or can no longer honor, and
`{:error, :expired}` for one that has aged out; both are answered to the
client as a `404 Not Found`, which the 2025 spec defines as the signal for the
client to start a new session.

# `expand`

```elixir
@spec expand(controller()) :: %{mod: module(), arg: arg()}
```

Normalizes the `:session_controller` option into a `%{mod: _, arg: _}` descriptor.

---

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