Ectomancer.Plug (Ectomancer v1.4.0)

Copy Markdown View Source

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
end

Actor 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
  end

If 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

call(conn, map)

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.

extract_actor(conn)

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

Examples

# 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 through

The extracted actor is stored in conn.assigns[:ectomancer_actor] and propagated to tool handlers via frame.assigns[:ectomancer_actor].

extract_api_key(conn, header_name \\ "x-api-key")

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

extract_bearer_token(conn)

@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

get_actor(conn)

@spec get_actor(Plug.Conn.t()) :: any()

Gets the current actor from the connection assigns.

Examples

actor = Ectomancer.Plug.get_actor(conn)