GenMCP.Transport.StreamableHTTP.V2511 (gen_mcp v2.0.0)

Copy Markdown View Source

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]
end

Served methods

The surface is what a migrating client needs to keep calling tools, not feature parity with the 2025 spec:

  • initialize and notifications/initialized — the handshake.
  • ping.
  • tools/list and tools/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.ToolChanges

Without 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 of atom/0) - A list of assigns keys that will be copied from the conn to the channel. Those will overwrite the assigns from the :assigns option above. The default value is [].

  • :allowed_origins - Origin allowlist for DNS-rebinding protection. A request carrying an Origin header not in the list is rejected with 403 Forbidden. Requests without an Origin header (non-browser clients) are always accepted. Use :any to disable the check. The default value is [].

  • :session_controller - The GenMCP.SessionController implementation that mints and reads back session ids. The default seals the session into the id itself and stores nothing server-side. The default value is GenMCP.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

call(conn, opts)

Callback implementation for Plug.call/2.

defplug(module)

(macro)

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.

init(opts)

Initializes the plug, returning the prepared transport configuration.