Phantom.Plug (phantom_mcp v0.4.4)

Copy Markdown View Source

Main Plug implementation for MCP HTTP transport with SSE support.

This module provides a complete MCP server implementation with:

  • JSON-RPC 2.0 message handling
  • Server-Sent Events (SSE) streaming
  • CORS handling and security features
  • Session management integration
  • Origin validation

Phoenix

defmodule MyAppWeb.Router do
  use MyAppWeb, :router
  # ...

  pipeline :mcp do
    plug :accepts, ["json", "sse"]

    plug Plug.Parsers,
      parsers: [{:json, length: 1_000_000}],
      pass: ["application/json"],
      json_decoder: JSON
  end

  scope "/mcp" do
    pipe_through :mcp

    forward "/", Phantom.Plug,
      router: MyApp.MCPRouter,
      pubsub: MyApp.PubSub
  end
end

Plug.Router

defmodule MyAppWeb.Router do
  use Plug.Router

  plug :match

  plug Plug.Parsers,
      parsers: [{:json, length: 1_000_000}],
      pass: ["application/json"],
      json_decoder: JSON

  plug :dispatch

  forward "/mcp",
      to: Phantom.Plug,
      init_opts: [
        router: MyApp.MCP.Router,
        pubsub: MyApp.PubSub
      ]
end

Here are the defaults:

[
  pubsub: nil,
  origins: ["http://localhost:4000"],
  validate_origin: true,
  session_timeout: 30000,
  max_request_size: 1048576
]

Local testing

Most MCP clients support Streamable HTTP natively. Configure them to connect directly to your Phantom server:

{
  "mcpServers": {
    "my_app": {
      "type": "http",
      "url": "http://localhost:4000/mcp"
    }
  }
}

Avoid mcp-remote and mcp-proxy

Third-party proxies like mcp-remote and mcp-proxy can break MCP features such as elicitation, resource subscriptions, and session management. Connect directly over HTTP when possible.

If your client only supports stdio, use Phantom.Stdio instead of proxying HTTP through a stdio wrapper.

For a direct stdio transport without HTTP, see Phantom.Stdio.

Telemetry

Telemetry is provided with these events:

  • [:phantom, :plug, :request, :connect] with meta: ~w[session router conn opts]a
  • [:phantom, :plug, :request, :disconnect] with meta: ~w[session router conn]a
  • [:phantom, :plug, :request, :terminate] with meta: ~w[session router conn]a
  • [:phantom, :plug, :request, :exception] with meta: ~w[session router conn stacktrace request exception]a

Summary

Functions

Callback implementation for Plug.call/2.

Initializes the plug with the given options.

Construct a WWW-Authenticate header as defined by RFC 9728 from the map.

Types

opts()

@type opts() :: [
  router: module(),
  origins: [String.t()] | :all | mfa(),
  validate_origin: boolean(),
  session_timeout: pos_integer(),
  max_request_size: pos_integer()
]

www_authenticate()

@type www_authenticate() :: %{
  :method => String.t(),
  optional(String.t() | atom()) => atom() | String.t()
}

Functions

call(conn, opts)

Callback implementation for Plug.call/2.

init(opts)

Initializes the plug with the given options.

Options

  • :router - The MCP router module (required)
  • :origins - List of allowed origins or :all (default: localhost)
  • :validate_origin - Whether to validate Origin header (default: true)
  • :session_timeout - Session timeout in milliseconds (default: 30s)
  • :max_request_size - Maximum request size in bytes (default: 1MB)

www_authenticate(info)

@spec www_authenticate(map()) :: String.t()

Construct a WWW-Authenticate header as defined by RFC 9728 from the map.

This requires the map to contain a :method to indicate acceptable authentication methods, typically "Bearer", and then rest of the attributes will be serialized into the header as key=value.

For example,

iex> Phantom.Plug.www_authenticate(%{
...>   method: "Bearer",
...>   resource_metadata: "https://myapp.com/.well-known/oauth-protected-resource",
...>   max_age: 42000
...> })
~s|Bearer max_age="42000", resource_metadata="https://myapp.com/.well-known/oauth-protected-resource"|

https://datatracker.ietf.org/doc/html/rfc9728#name-use-of-www-authenticate-for