HTTP plug serving MCP clients that speak the 2025-06-18 or 2025-11-25
protocol, on top of the stateless 2026-07-28 core.
This is a migration shim, not a second implementation. A 2025 request is
translated at this boundary into the same per-request contract
GenMCP.Transport.StreamableHTTP drives, so GenMCP.Suite, your tools, and
every other provider stay written once against the 2026-07-28 vocabulary and
never learn that a 2025 client exists.
Mount it alongside the 2026 transport, on its own path:
scope "/mcp" do
forward "/", GenMCP.Transport.StreamableHTTP,
server_name: "My App",
server_version: "1.0.0",
tools: [MyApp.AddTool]
forward "/2025", GenMCP.Transport.StreamableHTTP.V2511,
server_name: "My App",
server_version: "1.0.0",
tools: [MyApp.AddTool]
endServed methods
The surface is what a migrating client needs to keep calling tools, not feature parity with the 2025 spec:
initializeandnotifications/initialized— the handshake.ping.tools/listandtools/call, including progress and log notifications on the POST's own SSE response.GET— the server-to-client notification stream, served by the Suite's subscription handler (see below).
Any other method is answered with a JSON-RPC -32601.
Sessions
The 2025 protocol is stateful: initialize returns an Mcp-Session-Id that
every later request must carry. The default GenMCP.SessionController.Token
seals the session into the id itself, so no server-side state is introduced.
Pass :session_controller to store sessions yourself — see
GenMCP.SessionController.
A request with no Mcp-Session-Id after the handshake is answered 400, and
one whose id is unknown or expired is answered 404, which is the 2025
signal for the client to start a new session.
The GET stream
A 2025 client opens a long-lived GET to receive server-initiated
notifications. That maps onto the 2026 subscriptions/listen request, so the
stream is served by the Suite's configured :subscription_handler and the
2026 subscription vocabulary is translated away on the wire.
forward "/2025", GenMCP.Transport.StreamableHTTP.V2511,
server_name: "My App",
server_version: "1.0.0",
tools: [MyApp.AddTool],
subscription_handler: MyApp.ToolChangesWithout a subscription handler, GET is answered 405 Method Not Allowed,
which the 2025 spec permits for a server that offers no server-initiated
stream.
Options
:assigns(map/0) - A map of assigns to define to the channel passed to tools. The default value is%{}.:copy_assigns(list ofatom/0) - A list of assigns keys that will be copied from the conn to the channel. Those will overwrite the assigns from the:assignsoption above. The default value is[].:allowed_origins- Origin allowlist for DNS-rebinding protection. A request carrying anOriginheader not in the list is rejected with 403 Forbidden. Requests without anOriginheader (non-browser clients) are always accepted. Use:anyto disable the check. The default value is[].:session_controller- TheGenMCP.SessionControllerimplementation that mints and reads back session ids. The default seals the session into the id itself and stores nothing server-side. The default value isGenMCP.SessionController.Token.
Every other option is forwarded to the server implementation, exactly as for
GenMCP.Transport.StreamableHTTP.
Summary
Functions
Callback implementation for Plug.call/2.
Defines a named plug module delegating to this transport.
Initializes the plug, returning the prepared transport configuration.
Functions
Callback implementation for Plug.call/2.
Defines a named plug module delegating to this transport.
The 2025 counterpart of GenMCP.Transport.StreamableHTTP.defplug/1, for
routers that allow a module to be forwarded only once.
Initializes the plug, returning the prepared transport configuration.