Ectomancer

CIHex.pmHex DocsDownloadsLicense

Auto-generate MCP tools from your Ecto schemas — your Phoenix app, now conversationally operable by Claude and any LLM.

Ectomancer sits on top of anubis_mcp and turns your database schemas into live MCP tools. Add it to your router, start the server, and your AI assistant can query, create, and update records through natural language — no hand-written tool definitions, no boilerplate.

Features

Demo

Watch Ectomancer in action — a full Phoenix app with User, Post, and Comment schemas exposed as MCP tools, running on the Streamable HTTP transport.

Ectomancer Demo

3 Ecto schemas → 15 MCP tools with zero boilerplate. Replay locally with asciinema play priv/demo.cast.

The demo shows:

Try it yourself:

git clone https://github.com/GustavoZiaugra/ectomancer_demo
cd ectomancer_demo
mix setup
mix phx.server
# Connect your MCP client to http://localhost:4000/mcp

Installation

def deps do
[
{:ectomancer, "~> 1.2"}
]
end

Quick Start

1. Create your MCP module

defmodule MyApp.MCP do
use Ectomancer,
name: "myapp-mcp",
version: "0.1.0"
expose MyApp.Accounts.User,
actions: [:list, :get, :create, :update]
expose MyApp.Blog.Post,
actions: [:list, :get]
tool :search_users do
description "Search users by email"
param :query, :string, required: true
param :limit, :integer
handle fn %{"query" => q, "limit" => l}, _actor ->
{:ok, MyApp.Accounts.search_users(q, limit: l || 10)}
end
end
resource :system_status do
description "Current system health metrics"
uri "metrics://status"
mime_type "application/json"
read fn _params, _actor ->
{:ok, Jason.encode!(%{status: "healthy", uptime: System.uptime()})}
end
end
end

2. Start the MCP server

Add to your Application supervisor:

children = [
# ... other children ...
{Anubis.Server.Supervisor, {MyApp.MCP, transport: {:streamable_http, start: true}}},
MyAppWeb.Endpoint
]

3. Mount in your router

scope "/mcp" do
pipe_through :api
forward "/", Ectomancer.Plug, server: MyApp.MCP
end

4. Configure actor extraction (optional)

config :ectomancer,
repo: MyApp.Repo,
actor_from: fn conn ->
conn.assigns.current_user
end

Done. Claude can now query your database through natural language at /mcp.

Authorization

Three strategies, choose what fits:

StyleExampleUse case
Inlineauthorize fn actor, _ -> actor.role == :admin endQuick rules
Policy moduleauthorize with: MyApp.Policies.UserPolicyComplex logic, reusable
Noneauthorize :nonePublic endpoints

Schema-level and action-specific rules work too:

expose MyApp.Accounts.User,
actions: [:list, :get, :create, :update],
authorize: [
list: :none,
get: fn actor, _ -> actor != nil end,
create: :admin_only,
update: with: MyApp.Policies.UserPolicy
]

Configuration

Sources

config :ectomancer, repo: MyApp.Repo

Actor extraction

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

Rate limiting

config :ectomancer, :rate_limits,
enabled: true,
global: [max_requests: 100, time_window_ms: 60_000],
per_tool: [search_users: [max_requests: 10, time_window_ms: 60_000]]

Telemetry

Ectomancer emits :telemetry events for monitoring, observability, and debugging. Events are enabled by default. Disable by setting telemetry: false:

config :ectomancer, telemetry: false

Events emitted:

EventWhenMeasurementsMetadata
[:ectomancer, :tool, :start]Tool execution beginssystem_time:tool
[:ectomancer, :tool, :stop]Tool execution endsduration:tool
[:ectomancer, :tool, :exception]Tool handler raisesduration:tool
[:ectomancer, :repo, :start]CRUD operation beginssystem_time:action, :schema
[:ectomancer, :repo, :stop]CRUD operation endsduration:action, :schema
[:ectomancer, :repo, :exception]Repo operation raisesduration:action, :schema
[:ectomancer, :authorization, :denied]Auth check fails(none):actor, :action, :handler
[:ectomancer, :rate_limit, :exceeded]Rate limit exceeded(none):key, :window_ms

Example: Attaching a handler

:telemetry.attach("ectomancer-logger", [:ectomancer, :tool, :stop], fn _name, measurements, metadata, _config ->
IO.puts("Tool #{metadata.tool} completed in #{System.convert_time_unit(measurements.duration, :native, :millisecond)}ms")
end, nil)

Multi-repo

expose MyApp.OtherSchema, repo: MyApp.ReplicaRepo

Pages

PathDescription
/mcpMCP endpoint (SSE + JSON-RPC)

Open priv/ectomancer.html in a browser for a visual playground — browse tools, fill params, call them, and inspect results. No build step, no npm install, no dependencies.

Documentation

Testing

mix test

Zero compiler warnings, full Credo and Dialyzer compliance.

Current version: 1.4.0

License

MIT