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
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
@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)