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 create/3 a session id
at the end of the handshake, and to 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.
Summary
Types
The value configured alongside the module as {module, arg}.
The client data a session carries across requests.
Callbacks
Creates a session and returns the id the client will send back.
Drops the session, on a client DELETE. Optional.
Reads back the session named by session_id.
Functions
Normalizes the :session_controller option into a %{mod: _, arg: _} descriptor.
Types
Callbacks
@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.
@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.
@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.
Functions
@spec expand(controller()) :: %{mod: module(), arg: arg()}
Normalizes the :session_controller option into a %{mod: _, arg: _} descriptor.