Packages

MCP server exposing live Elixir runtime introspection tools to AI agents.

Current section

Files

Jump to
extools lib extools tools ets_inspect.ex
Raw

lib/extools/tools/ets_inspect.ex

defmodule Extools.Tools.EtsInspect do
@moduledoc """
List all ETS tables in the runtime, optionally sampling the first N
entries of a given table. Useful for inspecting caches and state.
"""
use Hermes.Server.Component, type: :tool
alias Hermes.Server.Response
schema do
field(:table, :string,
description: "Optional table name to sample. Default: list all tables only."
)
field(:sample, :integer,
description: "Number of entries to sample from the given table. Default: 10."
)
end
@impl true
def execute(params, frame) do
params = Extools.Tools.Params.normalize(params)
tables = list_all_tables()
payload =
case params["table"] do
nil ->
%{tables: tables, count: length(tables)}
table_str ->
table = String.to_atom(table_str)
sample = params["sample"] || 10
case sample_table(table, sample) do
{:ok, entries} ->
Map.merge(%{tables: tables, count: length(tables)}, %{
sampled: %{table: table_str, entries: entries}
})
{:error, reason} ->
Map.merge(%{tables: tables, count: length(tables)}, %{
sample_error: reason
})
end
end
{:reply, Response.tool() |> Response.json(payload), frame}
end
defp list_all_tables do
:ets.all()
|> Enum.map(fn table ->
info = :ets.info(table)
%{
# Named tables are atoms; unnamed tables are tid references —
# inspect/1 both so the payload is always JSON-encodable.
name: if(is_atom(table), do: to_string(table), else: inspect(table)),
type: Keyword.get(info, :type),
size: Keyword.get(info, :size),
memory: Keyword.get(info, :memory),
owner: inspect(Keyword.get(info, :owner)),
protection: Keyword.get(info, :protection)
}
end)
|> Enum.sort_by(& &1.memory, :desc)
end
defp sample_table(table, sample_size) do
if :ets.whereis(table) == :undefined do
{:error, "Table not found: #{table}"}
else
entries =
try do
:ets.tab2list(table)
|> Enum.take(sample_size)
|> Enum.map(&format_entry/1)
rescue
e ->
{:error, "Could not sample table: #{Exception.format(:error, e, __STACKTRACE__)}"}
catch
_, _ -> {:error, "Could not sample table (protected or crashed)"}
end
case entries do
{:error, _} = err -> err
list -> {:ok, list}
end
end
end
defp format_entry({key, value}) when is_tuple(value) and tuple_size(value) > 0 do
%{key: inspect(key), value: inspect(value)}
end
defp format_entry({key, value}) do
%{key: inspect(key), value: inspect(value)}
end
defp format_entry(other), do: %{value: inspect(other)}
end