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

ToolDescription
project_evalEvaluate Elixir code in the live runtime
execute_sql_queryRead-only SQL via the auto-detected Ecto Repo
get_logsLive logs from an in-memory ETS buffer (level/grep/tail filters)
get_docsModule/function docs via Code.fetch_docs
get_source_locationfile:line for a module or function
get_ecto_schemasEcto schemas with fields and types (Ecto 3.14 compatible)
get_process_infoTop BEAM processes by memory
get_app_state:sys.get_state of a named process
ets_inspectList ETS tables and sample entries
supervision_treeFull supervision tree, rendered + structured
trace_functionTrace function calls with :dbg (safety-capped)
config_dumpApplication 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

KeyDefaultDescription
config :extools, :portnilStandalone Bandit port (non-Phoenix). Unset = no standalone server.
config :extools, :repoauto-detectExplicit Ecto Repo module for execute_sql_query.
config :extools, :otp_appauto-detectOTP 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