Extools
MCP server that lives inside your Elixir app's BEAM and exposes live runtime introspection tools to AI agents (Hermes, Claude Code, Cursor, any MCP client). Framework-agnostic: works in Phoenix apps and plain Elixir apps alike.
The 12 tools
| Tool | Description |
|---|---|
project_eval | Evaluate Elixir code in the live runtime |
execute_sql_query | Read-only SQL via the auto-detected Ecto Repo |
get_logs | Live logs from an in-memory ETS buffer (level/grep/tail filters) |
get_docs | Module/function docs via Code.fetch_docs |
get_source_location | file:line for a module or function |
get_ecto_schemas | Ecto schemas with fields and types (Ecto 3.14 compatible) |
get_process_info | Top BEAM processes by memory |
get_app_state | :sys.get_state of a named process |
ets_inspect | List ETS tables and sample entries |
supervision_tree | Full supervision tree, rendered + structured |
trace_function | Trace function calls with :dbg (safety-capped) |
config_dump | Application environment dump with key filter |
Installation
Phoenix app
# mix.exs
defp deps do
[
{:extools, "~> 0.1", only: :dev}
]
end
# lib/my_app_web/endpoint.ex
if Mix.env() == :dev do
plug Extools
end
MCP endpoint: http://localhost:4000/extools/mcp
Non-Phoenix app
# mix.exs
defp deps do
[
{:extools, "~> 0.1", only: :dev},
{:bandit, "~> 1.0", only: :dev}
]
end
# config/dev.exs
config :extools, port: 4040
MCP endpoint: http://localhost:4040/extools/mcp (standalone Bandit,
started automatically by the :extools application).
Connecting an MCP client
The endpoint speaks MCP Streamable HTTP (protocol 2025-03-26). Clients
must Accept: application/json, text/event-stream and complete the
initialize handshake; the session id is returned in the
mcp-session-id response header.
Example handshake with curl:
# 1. initialize (capture the mcp-session-id response header)
curl -i -X POST http://localhost:4040/extools/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"me","version":"0.1"}}}'
# 2. notifications/initialized (with the session header), then:
curl -X POST http://localhost:4040/extools/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'mcp-session-id: <session-id>' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"project_eval","arguments":{"code":"1+1"}}}'
Configuration
| Key | Default | Description |
|---|---|---|
config :extools, :port | nil | Standalone Bandit port (non-Phoenix). Unset = no standalone server. |
config :extools, :repo | auto-detect | Explicit Ecto Repo module for execute_sql_query. |
config :extools, :otp_app | auto-detect | OTP app to scan for the Ecto Repo. |
Security
Dev-only tool. The standalone server binds to 127.0.0.1 only, and
the package is meant to be included with only: :dev. project_eval
executes arbitrary code in your runtime — that is the point, and exactly
why you must never ship it to production. execute_sql_query only
accepts read-only statements (select/with/explain).
License
MIT — 2026 Alvaro Lizama