Phoenix Plug for MCP server integration.
Prerequisites
Before using this plug, you must start the Anubis MCP server in your application:
# In your application.ex
children = [
# ... other children ...
{Anubis.Server.Supervisor, {MyApp.MCP, transport: {:streamable_http, start: true}}},
MyAppWeb.Endpoint
]Router Integration
Add to your router:
scope "/mcp" do
pipe_through :api
forward "/", Ectomancer.Plug, server: MyApp.MCP
endActor Extraction
The actor is extracted using the configured actor_from function:
config :ectomancer,
actor_from: fn conn ->
conn
|> Plug.Conn.get_req_header("authorization")
|> List.first()
|> case do
nil -> {:error, :unauthorized}
"Bearer " <> token -> MyApp.Auth.verify_token(token)
_ -> {:error, :unauthorized}
end
endIf no actor_from is configured, the actor defaults to nil.
Options
:server- The MCP server module (required):session_header- Custom header name for session ID (default: "mcp-session-id"):request_timeout- Request timeout in milliseconds (default: 30000)
The actor will be available in tool handlers via frame.assigns[:ectomancer_actor].
Summary
Functions
Handles the MCP request by extracting the actor from the connection and delegating to the Anubis StreamableHTTP transport plug.
Extracts the actor from the connection using the configured actor_from function.
Helper function to extract API key from a custom header.
Helper function to extract a Bearer token from the Authorization header.
Gets the current actor from the connection assigns.
Functions
Handles the MCP request by extracting the actor from the connection and delegating to the Anubis StreamableHTTP transport plug.
Calls extract_actor/1 to resolve the actor, then either rejects with
401 (if {:error, _} returned) or stores the actor in
conn.assigns[:ectomancer_actor] and forwards to Anubis.
@spec extract_actor(Plug.Conn.t()) :: any()
Extracts the actor from the connection using the configured actor_from function.
Reads the application config for :ectomancer, :actor_from. If set, calls the
function with the conn and returns its result. If unset, returns nil.
The actor_from function can return:
- Any value — the actor (e.g., a
%User{}struct, a string, an atom) {:error, reason}— the request will be rejected with HTTP 401
Configuration
config :ectomancer,
actor_from: fn conn ->
case Plug.Conn.get_req_header(conn, "authorization") do
["Bearer " <> token] -> MyApp.Auth.verify_token(token)
_ -> {:error, :unauthorized}
end
endExamples
# With JWT token verification
config :ectomancer,
actor_from: fn conn ->
with ["Bearer " <> token] <- Plug.Conn.get_req_header(conn, "authorization"),
{:ok, claims} <- MyApp.JWT.verify(token) do
MyApp.Accounts.get_user!(claims["sub"])
else
_ -> {:error, :unauthorized}
end
end
# With session cookie (read from conn before Plug session)
config :ectomancer,
actor_from: fn conn ->
case Plug.Conn.get_req_header(conn, "cookie") do
[cookie] -> MyApp.Auth.verify_session(cookie)
_ -> {:error, :unauthorized}
end
end
# Public API (no auth required)
# Just omit actor_from — returns nil, tools without authorization pass throughThe extracted actor is stored in conn.assigns[:ectomancer_actor] and
propagated to tool handlers via frame.assigns[:ectomancer_actor].
@spec extract_api_key(Plug.Conn.t(), String.t()) :: String.t() | nil
Helper function to extract API key from a custom header.
Examples
api_key = Ectomancer.Plug.extract_api_key(conn, "x-api-key")
@spec extract_bearer_token(Plug.Conn.t()) :: String.t() | nil
Helper function to extract a Bearer token from the Authorization header.
Examples
token = Ectomancer.Plug.extract_bearer_token(conn)
# Returns: "abc123" or nil
@spec get_actor(Plug.Conn.t()) :: any()
Gets the current actor from the connection assigns.
Examples
actor = Ectomancer.Plug.get_actor(conn)