Noizu.MCP.Transport.StreamableHTTP.Plug (Noizu MCP v0.1.6)

Copy Markdown View Source

Streamable HTTP server transport (MCP 2025-11-25): a single MCP endpoint handling POST/GET/DELETE, mountable in Phoenix or any Plug stack:

# Phoenix router
forward "/mcp", Noizu.MCP.Transport.StreamableHTTP.Plug, server: MyApp.MCP

# standalone with Bandit
{Bandit, plug: {Noizu.MCP.Transport.StreamableHTTP.Plug, server: MyApp.MCP}, port: 4040}

Behavior per spec: initialize POSTs create a session and return Mcp-Session-Id; requests answer as application/json when the handler produces only a response, upgrading to an SSE stream when progress, logging, or server-initiated requests flow first; GET opens the general SSE stream (with Last-Event-ID resumability backed by Noizu.MCP.Server.EventStore); DELETE terminates the session.

Options

  • :server (required) — the use Noizu.MCP.Server module
  • :origins:localhost (default; allows non-browser clients and localhost origins), :mcp_clients (localhost plus the known browser MCP hosts, see mcp_client_origins/0), :any, or an explicit allowlist of origins. Origin validation guards against DNS-rebinding attacks.
  • :idle_timeout — session idle expiry in ms (default 30 minutes)
  • :request_timeout — max time to wait for a handler response (default 300_000)
  • :keepalive — SSE keepalive comment interval in ms (default 25_000)
  • :context{module, function} invoked as fun.(conn) returning a map merged into session assigns at initialize (how plug-level auth reaches handlers)
  • :auth — resource-server enforcement, see below
  • :corsfalse (default) or true/keyword to answer browser preflights, see below

Authorization (:auth)

  • :verifier (required) — Noizu.MCP.Auth.TokenVerifier module or {module, opts}
  • :resource_metadata — the RFC 9728 URL advertised in the 401 challenge. A binary, {module, function} (called with the conn), {module, function, args}, a 1-arity fun, or :derive (built from the request and the forward's mount path).
  • :scope — scope advertised in the challenge, so a client that has never seen a token knows what to ask for

CORS (:cors)

A browser MCP client (claude.ai) cannot read the 401 challenge — and so cannot start OAuth — unless WWW-Authenticate is exposed, and cannot continue a session unless Mcp-Session-Id is. Enabling :cors answers OPTIONS with 204 and adds, to every response:

access-control-allow-origin: <origin>
access-control-expose-headers: WWW-Authenticate, Mcp-Session-Id, Mcp-Protocol-Version

Only origins that pass :origins are answered, so CORS never widens the DNS-rebinding guard. Options: allow_headers:, max_age:.

Summary

Functions

The origins allowed by origins: :mcp_clients — the browser MCP hosts, which :mcp_clients allows in addition to localhost.

Functions

mcp_client_origins()

@spec mcp_client_origins() :: [String.t()]

The origins allowed by origins: :mcp_clients — the browser MCP hosts, which :mcp_clients allows in addition to localhost.

Extend it rather than replacing it when a host needs one more origin:

origins: ["https://mcp.internal" | Noizu.MCP.Transport.StreamableHTTP.Plug.mcp_client_origins()]