ExMCP.HttpPlug (ex_mcp v1.0.0-rc.5)

Copy Markdown View Source

HTTP Plug for MCP (Model Context Protocol) requests. Compatible with Phoenix and Cowboy servers.

This plug provides HTTP transport for MCP servers, allowing integration with standard Elixir web applications. It supports both regular POST requests for RPC calls and Server-Sent Events (SSE) for real-time communication.

Handler options

:handler_opts configures the argument passed to a handler module's init/1. It may be a static term, a one-arity function called with the Plug.Conn, a two-arity function called with the Plug.Conn and decoded JSON-RPC request, or an {module, function, extra_args} tuple. MFA handlers are called as apply(module, function, [conn, request | extra_args]).

:handler_call_timeout is the server-side deadline, in milliseconds, for each call from the plug into a Handler process (default: 10_000). It is independent of client request and stream timeouts.

Usage

# With Cowboy
{:ok, _} = Plug.Cowboy.http(ExMCP.HttpPlug, [
  handler: MyApp.MCPServer,
  server_info: %{name: "my-app", version: "1.0.0"}
], port: 4000)

# With Phoenix
plug ExMCP.HttpPlug,
  handler: MyApp.MCPServer,
  server_info: %{name: "my-app", version: "1.0.0"}

OAuth 2.1 Integration

To enable OAuth 2.1 bearer token validation:

plug ExMCP.HttpPlug,
  handler: MyApp.MCPServer,
  server_info: %{name: "my-app"},
  oauth_enabled: true,
  auth_config: %{
    introspection_endpoint: "https://auth.example.com/introspect",
    realm: "my-mcp-server" # Optional, defaults to server_info.name
  }

Security

Origin validation (:validate_origin, :allowed_origins)

With validate_origin: true (the default), any request carrying an Origin header is rejected with 403 unless the origin is listed in :allowed_origins (or :allowed_origins is :any). There is no "same origin as the Host header" fallback: in a DNS rebinding attack the Host header is attacker-controlled, so such a comparison would always pass.

Requests without an Origin header are allowed. Non-browser clients (CLIs, SDKs, server-to-server callers) do not send the header; use :allowed_hosts to protect them against DNS rebinding.

Host validation (:allowed_hosts)

:allowed_hosts is either :any (default, no restriction) or a list of hostnames. When a list is given, requests whose Host header does not match an entry are rejected with 421 before any processing. Ports are ignored and IPv6 hosts match with or without brackets, so allowed_hosts: ["localhost", "127.0.0.1", "[::1]", "::1"] accepts localhost:4000 and [::1]:8080. Servers started via ExMCP.Server.Transport with a localhost bind get this allow-list by default.

Server-Sent Events (:sse_mode)

:sse_mode is :stream (default) or :oneshot. :stream starts an ExMCP.HttpPlug.SSEHandler and holds the request open for the lifetime of the stream; :oneshot writes a single connected event and returns, which suits test harnesses and health checks.

Session ids

Client-supplied mcp-session-id (and legacy x-session-id) header values are validated before use: at most 128 bytes from the character set A-Z a-z 0-9 . _ ~ + / = - (covering UUIDs and base64/base64url tokens). Invalid values are rejected with a 400 JSON-RPC error and are never echoed back.

Summary

Functions

Broadcasts a resource update to each live SSE client subscribed to uri.

Processes HTTP connections for MCP protocol.

Initializes the plug with configuration options.

Functions

broadcast_resource_update(uri)

@spec broadcast_resource_update(String.t()) :: %{
  subscribers: non_neg_integer(),
  delivered: non_neg_integer()
}

Broadcasts a resource update to each live SSE client subscribed to uri.

Subscription lookup is performed directly against ETS, and delivery uses independent tasks so backpressure from one client does not block the rest. Sessions without a live SSE connection remain subscribed for reconnection and are removed by ExMCP.SessionManager when they expire.

call(conn, opts)

Processes HTTP connections for MCP protocol.

Host validation (:allowed_hosts) runs before any routing so that DNS rebinding attempts are rejected before request processing.

init(opts)

Initializes the plug with configuration options.

start_link(opts \\ [])

This function is deprecated. The session table is owned by ExMCP.HttpPlug.SessionRegistry, started with the :ex_mcp application.