beamdev #
A BEAM development tool for coding agents: a Model Context Protocol (MCP) server that connects to Erlang/BEAM nodes over the Erlang distribution protocol and runs code on them.
Introspection, tracing and debugging are not tools here — they are functions the node already
has, and eval reaches all of them.
Development tool. Code runs unsandboxed, with the full authority of the target node. The cookie is the only boundary.
Install #
cargo build --release # binary at target/release/beamdev
nix build # or via the flake
Use #
Start a node, then point an MCP client at the binary:
{
"mcpServers": {
"erlang": {
"command": "/path/to/beamdev",
"args": ["--node", "myapp@localhost", "--mode", "elixir"]
}
}
}
--mode sets how terms are rendered: erlang (default), elixir, gleam, or lfe.
--log-level takes the usual five. Both are fixed at startup.
--node is required and names the one node this server works against, for its whole life.
The cookie comes from --cookie-file (default .erlang.cookie, relative to the working
directory the client launches the server in); an unreadable cookie is fatal. Nothing is
exposed to the caller for connecting or disconnecting, so an agent has no reason to go
looking for a cookie.
The node itself need not be up. The server connects in the background and retries with
backoff, and a tool call that finds the link down retries immediately rather than waiting
out the timer, so restarting the node mid-session is unremarkable. A reconnect is logged on
the node, which puts a marker in get_logs: everything before it — buffered events, loaded
modules, process state — belongs to the previous incarnation.
Working against several nodes means running several servers. Reaching another node in an
existing cluster does not: rpc:call/4 from the connected one already gets there.
Tools #
| Tool | Description |
|---|---|
list_nodes |
Report the node and whether the connection to it is up |
eval |
Evaluate Erlang or Elixir source, bounded by timeout_ms (default 15s) |
load_module |
Compile module source on the node and load it |
get_logs |
Read what the node has logged since we connected, filtered by level and regex |
Nothing needs installing on the target: the tools drive the node's own scanner, parser,
evaluator and compiler. The first eval loads one small runner module, which bounds each
evaluation in time and memory and kills it on overrun.
get_logs needs a second module. A node's log stream does not cross the distribution link,
so connecting installs a logger handler that buffers the last 1000 formatted events on the
node. Only events logged after the connection are captured,
and only those the node's primary logger level already lets through. Reports are rendered by
the node's own inspect where it has Elixir, so redact: true and hand-written Inspect
implementations apply.
eval(code: "erlang:process_info(whereis(user), [memory]).")
load_module(source: "-module(h).\n-export([n/0]).\nn() -> node().")
get_logs(level: "warning", grep: "timeout", tail: 20)
Erlang source passed to eval must end with a period; Elixir must not.
When Elixir code raises, the exception is rendered on the node with
Exception.format/3, so the Inspect protocol applies and a struct's redacted fields
stay redacted. Erlang has no equivalent: an Erlang exception's reason is rendered here,
term for term, including anything it happens to be carrying.
Development #
cargo fmt && cargo clippy --all-targets --all-features -- -D warnings && cargo test
The integration tests start and stop their own BEAM nodes, so they need erl and elixir on
PATH -- nix develop provides both.
License #
This project is licensed under the Apache License, Version 2.0 — see the LICENSE file for details.
Acknowledgements #
- erl_dist - Erlang distribution protocol client
- rmcp - Model Context Protocol server framework
- Model Context Protocol - Protocol specification