GenMCP.Transport.StreamableHTTP (gen_mcp v2.0.0)

Copy Markdown View Source

HTTP plug implementing the MCP Streamable HTTP transport for the 2026-07-28 protocol.

This is the entry point of an MCP server over HTTP. It is a Plug.Router, so you mount it in a Plug or Phoenix router and it answers Model Context Protocol requests at that path. A client POSTs a JSON-RPC message, and the transport replies with either a single JSON response or a Server-Sent Events stream, depending on what the server returns.

The transport is stateless: each request is validated, run by a fresh server process, and answered on its own. The server that handles the decoded request is GenMCP.Suite by default, so the smallest useful mount needs no :server option and just lists what to serve:

forward "/mcp", GenMCP.Transport.StreamableHTTP,
  server_name: "My App",
  server_version: "1.0.0",
  tools: [MyApp.AddTool]

:server_name and :server_version are what the Suite reports to clients and are required; every example below carries them for that reason.

To run a custom GenMCP implementation instead of the Suite, pass it as :server:

forward "/mcp", GenMCP.Transport.StreamableHTTP, server: MyApp.Server

Options

Options are read at three layers. The transport keeps the HTTP connection options for itself and passes everything else to the server, which takes its own wrapper options and forwards the rest to the server implementation.

HTTP connection options

  • :assigns (map/0) - A map of assigns to define to the channel passed to tools, resources, etc. 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 (e.g. behind a gateway that already validates origins). The default value is [].

MCP server options

:server selects the GenMCP implementation that handles the decoded request. See GenMCP for the {module, arg} form.

  • :server - The GenMCP behaviour server implemetation that will handle MCP messages. If a simple atom, it will receive all other options given to the session. The default value is GenMCP.Suite.

Server implementation options

Every option not listed above is forwarded to the server implementation. The default server, GenMCP.Suite, accepts:

  • :server_name (String.t/0) - Required.

  • :server_version (String.t/0) - Required.

  • :server_title - The default value is nil.

  • :tools - The list of GenMCP.Suite.Tool implementations that will be available in the server. List items can be either module names, {module, arg} tuples or descriptor maps. The default value is [].

  • :resources - The list of GenMCP.Suite.ResourceRepo implementations to serve resources from. List items can be either module names, {module, arg} tuples or descriptor maps. The default value is [].

  • :prompts - A list of GenMCP.Suite.PromptRepo implementations to generate prompts with. List items can be either module names, {module, arg} tuples or descriptor maps. The default value is [].

  • :extensions - A list Extension implementations to add more tools, resource repositories and prompt repositories. List items can be either module names, {module, arg} tuples or descriptor maps. The default value is [].

  • :subscription_handler - A handler for subscription requests. Either a module name, a {module, arg} tuple or a descriptor map. The default value is nil.

  • :send_server_info (boolean/0) - Includes the server's io.modelcontextprotocol/serverInfo metadata (built from :server_name, :server_version, and :server_title) in the _meta of every result, as the spec recommends. The default value is true.

Passing request data to handlers

Handlers read per-request context from the GenMCP.Mux.Channel they are given. Use :assigns and :copy_assigns to put your own data there. :assigns holds static values set on every channel. :copy_assigns lists conn assign keys to copy from the connection onto the channel, which is how an upstream authentication plug hands the authenticated identity to your tools: the plug puts :current_user on the conn, and you copy it across.

pipeline :mcp_auth do
  plug MyAppWeb.AuthPlug
end

scope "/mcp" do
  pipe_through :mcp_auth

  forward "/", GenMCP.Transport.StreamableHTTP,
    server_name: "My App",
    server_version: "1.0.0",
    tools: [MyApp.AddTool],
    copy_assigns: [:current_user]
end

A copied conn assign overwrites a static :assigns entry of the same key.

DNS-rebinding protection

A browser-based client sends an Origin header. The transport rejects a request whose Origin is not in :allowed_origins with 403 Forbidden, which stops a malicious page from rebinding DNS to reach a local MCP server. A request with no Origin header (a non-browser client) is always accepted. Set allowed_origins: :any to disable the check, for example behind a gateway that already validates the origin.

forward "/mcp", GenMCP.Transport.StreamableHTTP,
  server_name: "My App",
  server_version: "1.0.0",
  tools: [MyApp.AddTool],
  allowed_origins: ["https://app.example.com"]

Multiple endpoints in one router

On Phoenix 1.8 and later you can mount the transport directly as many times as you need. Each forward keeps its own options, so different endpoints can serve different tools, resources, or origins:

scope "/mcp" do
  forward "/files", GenMCP.Transport.StreamableHTTP,
    server_name: "My App (files)",
    server_version: "1.0.0",
    tools: [MyApp.FileTool]

  forward "/admin", GenMCP.Transport.StreamableHTTP,
    server_name: "My App (admin)",
    server_version: "1.0.0",
    tools: [MyApp.AdminTool]
end

Phoenix resolves a forwarded plug to a path by module, so reverse route lookup (path helpers and ~p verified routes) for a module mounted at several paths returns only the first one. This affects URL generation, not request dispatch. If you generate URLs to these endpoints, give each its own module with defplug/1 so each resolves to its own path.

Phoenix before 1.8

Older Phoenix routers refuse to forward the same module more than once and raise at compile time:

** (ArgumentError) GenMCP.Transport.StreamableHTTP has already been
forwarded to. A module can only be forwarded a single time.

Give each endpoint its own module with defplug/1, then forward to those modules instead of to the transport:

require GenMCP.Transport.StreamableHTTP, as: StreamableHTTP

StreamableHTTP.defplug(MyAppWeb.FilesMcp)
StreamableHTTP.defplug(MyAppWeb.AdminMcp)

scope "/mcp" do
  forward "/files", MyAppWeb.FilesMcp,
    server_name: "My App (files)",
    server_version: "1.0.0",
    tools: [MyApp.FileTools]

  forward "/admin", MyAppWeb.AdminMcp,
    server_name: "My App (admin)",
    server_version: "1.0.0",
    tools: [MyApp.AdminTools]
end

Summary

Functions

Callback implementation for Plug.call/2.

Defines a named plug module that delegates to GenMCP.Transport.StreamableHTTP.

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 that delegates to GenMCP.Transport.StreamableHTTP.

Use this to mount more than one MCP endpoint on a router that allows a module to be forwarded only once. Phoenix before 1.8 is the common case: forwarding GenMCP.Transport.StreamableHTTP at two paths raises ArgumentError with the message "has already been forwarded to. A module can only be forwarded a single time". The generated module is a distinct plug that delegates both init/1 and call/2 to the transport, so you forward to it exactly like the transport itself and each endpoint is its own module. On Phoenix 1.8 and later you can forward to GenMCP.Transport.StreamableHTTP directly instead, even for several endpoints.

This is a macro, so require (or alias and require) the transport before calling it, and give a literal module name:

require GenMCP.Transport.StreamableHTTP, as: StreamableHTTP

StreamableHTTP.defplug(MyAppWeb.McpPlug)

# then, in the router, mount the generated module
forward "/mcp", MyAppWeb.McpPlug,
  server_name: "My App",
  server_version: "1.0.0",
  tools: [MyApp.AddTool]

init(opts)

Initializes the plug, returning the prepared transport configuration.

This is the Plug.init/1 callback. It validates the transport's own options (:allowed_origins, :assigns, :copy_assigns) and keeps every other option aside as the server options handed to the server on each request. The returned value is the opaque configuration later passed to call/2; you do not build or read it yourself.