Ectomancer.Plug (Ectomancer v1.2.1)

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

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

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)