Ectomancer (Ectomancer v1.2.1)

Copy Markdown View Source

Ectomancer - Add an AI brain to your Phoenix app.

Automatically exposes your Ecto schemas as MCP (Model Context Protocol) tools, making your Phoenix app conversationally operable by Claude and other LLMs.

Quick Start

  1. Create your MCP module:

    defmodule MyApp.MCP do use Ectomancer,

     name: "my-app-mcp",
     version: "1.0.0"

    # Expose Ecto schemas as MCP tools expose MyApp.Accounts.User,

     actions: [:list, :get, :create, :update]

    # Custom tools with authorization tool :admin_stats do

     description "Get admin statistics"
    
     authorize fn actor, _action ->
       actor != nil && actor.role == :admin
     end
    
     handle fn _params, _actor ->
       {:ok, %{users: 100, revenue: 5000}}
     end

    end

    # Expose Phoenix routes as MCP tools expose_routes MyAppWeb.Router # Generates: get_users, post_users, get_user, put_user, delete_user, etc. end

  2. Add to your router:

    forward "/mcp", Ectomancer.Plug, server: MyApp.MCP

  3. Configure actor extraction:

    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

Available Macros

  • tool/2 - Define custom MCP tools with params and authorization
  • expose/2 - Auto-generate CRUD tools from Ecto schemas
  • expose_routes/1 - Auto-generate tools from Phoenix router routes
  • expose_oban_jobs/0 - Auto-generate Oban job management tools (requires Oban)
  • authorize/1 - Add authorization to tools (use inside tool block)

Authorization

Ectomancer provides flexible authorization:

Inline Function

authorize fn actor, action ->
  actor != nil && actor.role == :admin
end

Policy Module

authorize with: MyApp.Policies.UserPolicy

Public Access

authorize :none

Actor Access in Tools

Tools receive the actor via frame.assigns:

defmodule MyApp.MCP.MyTool do
  def execute(params, frame) do
    actor = frame.assigns[:ectomancer_actor]
    # Use actor for authorization...
  end
end

Actor Extraction

Ectomancer extracts the actor (the user/entity making the request) from the Plug connection and threads it through the system automatically. Here's how it flows:

1. Configuration

Define an actor_from function in your config. This function receives the Plug.Conn struct and returns the actor (or {:error, reason} to reject):

config :ectomancer,
  actor_from: fn conn ->
    case Plug.Conn.get_req_header(conn, "authorization") do
      ["Bearer " <> token] ->
        case MyApp.Auth.verify_token(token) do
          {:ok, user} -> user
          {:error, _} -> {:error, :unauthorized}
        end
      _ ->
        {:error, :unauthorized}
    end
  end

If no actor_from is configured, the actor defaults to nil (unauthenticated).

2. Extraction (Plug layer)

Ectomancer.Plug.extract_actor/1 calls your actor_from function on every incoming request. If the function returns {:error, reason}, the request is rejected with HTTP 401 before reaching any MCP tools.

3. Threading (MCP frame)

The extracted actor is placed into conn.assigns[:ectomancer_actor]. Anubis MCP propagates this into frame.assigns[:ectomancer_actor] for every tool call within that session — you never need to re-extract it.

4. Authorization

Tool handlers and expose-generated CRUD tools receive the actor automatically:

# Custom tool
tool :my_tool do
  authorize fn actor, action -> actor != nil end

  handle fn params, actor, scope ->
    # actor is the user/entity from actor_from
    {:ok, %{message: "Hello, #{actor.name}"}}
  end
end

# Exposed schema (authorization configured separately)
expose MyApp.User, authorize: fn actor, action ->
  actor.role == :admin
end

Extraction Flow Summary

LayerLocationWhat happens
Configconfig :ectomancer, actor_from: ...User provides extraction function
PlugEctomancer.Plug.call/2Calls extract_actor(conn)
Assignsconn.assigns[:ectomancer_actor]Actor stored in connection
Frameframe.assigns[:ectomancer_actor]Anubis propagates to tool context
Toolexecute(params, frame)Actor accessible via frame.assigns
Handlerhandle(params, actor, scope)Actor passed as 2nd argument
AuthorizationAuthorization.check/3Actor + action checked against policy

Summary

Functions

Returns the version of Ectomancer.

Functions

version()

Returns the version of Ectomancer.