arizona_dev_mcp (arizona v0.4.0)
View SourceA dev-time MCP server for an Arizona app: it exposes the running node to a coding agent.
Mount it in your dev config and point a coding agent (Claude Code, Cursor, the MCP Inspector, ...) at it; the agent can then introspect and drive the running app:
%% in your dev server's routes:
arizona_dev_mcp:route(~"/mcp")Tools:
list_routes-- the app's routes (from thearizonaserverapp env).describe_component-- a component's kind (stateful/stateless), its exports, and moduledoc.get_docs-- a module's or function's documentation (EEP-48).get_source_location-- where a module (or function) is defined.reloader_status-- the dev reloader's current compile error, if any, plus loaded-vs-disk drift: modules whose loaded code differs from their beam on disk (a node serving stale code otherwise reads "ok").get_logs-- recent log output from the running node (tail,level,grep). The one diagnostic an agent cannot otherwise reach: it has no terminal, so a crash report -- the artifact naming the module and line -- is invisible without it. Seearizona_dev_log.view_state-- what a live view currently believes: its bindings, by view id (omit the id to list the live views and their child views). The counterpart toget_logs: that answers what happened, this answers what the view thinks now, which is how a slot rendering a stale value gets separated from a slot with no dependency.app_info-- Arizona version, OTP release, node, process count.render_component-- render a component to HTML with given bindings.reload-- force a compile+reload sync now (recompile viarebar_agentunderrebar3 shell, then reload any module whose beam on disk differs from the loaded code) -- the action to take whenreloader_statusreports drift.eval-- run Erlang in the live node, with bindings that persist across calls. Always available (see the safety note below).
eval is arbitrary remote code execution, so the dev route is localhost-only
by default: route/1,2 set allow_remote_access => false, and the MCP handler
refuses any request whose peer is not a loopback address -- no matter which
interface the server bound. That peer check is the primary guard -- the route
protects itself rather than being gated per tool -- together with the Origin
check and keeping this a dev-only dependency. Only relax it on a network
you trust, ideally behind an auth hook:
%% localhost-only (default) -- safe for eval
arizona_dev_mcp:route(~"/mcp")
%% opt in to remote clients (trusted network only)
arizona_dev_mcp:route(~"/mcp", #{allow_remote_access => true, auth => Hook})Limitation -- the peer check is void behind a proxy or tunnel. It trusts the
immediate TCP peer, so a same-host reverse proxy or a dev tunnel
(ngrok/cloudflared/port-forward) makes every remote client look like loopback,
and binding the listener to loopback does not help (the tunnel connects to
loopback anyway). Whenever the port is reachable through anything but a direct
local socket, treat the peer check as void and gate eval with an auth hook (a
shared secret). Binding loopback (proto_opts => #{ip => {127,0,0,1}}) only
hardens against a direct remote connection, and proxy_protocol => true lets a
client spoof the peer outright. Never expose the dev MCP to an untrusted network.
Session mode (sessions => true, the default) makes eval's bindings persist
across calls (a REPL the agent drives) and runs a slow eval/render_component
in the session's worker rather than blocking the session; the default route also
caps concurrent sessions (max_sessions => 32) so a client cannot pin unbounded
memory by looping initialize.
Summary
Functions
A ready-to-mount dev route. Add arizona_dev_mcp:route(~"/mcp") to your dev
server's routes. It defaults to sessions => true (so a session's eval
bindings persist) and max_sessions => 32 (so a client cannot pin unbounded
memory). A CLI agent (Claude Code, the Inspector's proxy) connects with no extra
config; origins stays opt-in (omitted blocks all browser origins, the safe
DNS-rebinding posture). eval is always available, and the route is
localhost-only (allow_remote_access => false) so it stays off the network
-- see the module doc.
Like route/1, with extra MCP route opts merged over the defaults:
#{allow_remote_access => true} lets non-loopback clients reach the route
(localhost-only by default; trusted networks only -- see the module doc),
#{origins => [~"http://localhost:5173"]} also allows a browser client,
#{auth => Hook} gates the route, #{sessions => false} opts out of session
mode.
Functions
-spec route(Path) -> Route when Path :: binary(), Route :: {mcp, binary(), module(), arizona_mcp_handler:route_opts()}.
A ready-to-mount dev route. Add arizona_dev_mcp:route(~"/mcp") to your dev
server's routes. It defaults to sessions => true (so a session's eval
bindings persist) and max_sessions => 32 (so a client cannot pin unbounded
memory). A CLI agent (Claude Code, the Inspector's proxy) connects with no extra
config; origins stays opt-in (omitted blocks all browser origins, the safe
DNS-rebinding posture). eval is always available, and the route is
localhost-only (allow_remote_access => false) so it stays off the network
-- see the module doc.
-spec route(Path, Opts) -> Route when Path :: binary(), Opts :: arizona_mcp_handler:route_opts(), Route :: {mcp, binary(), module(), arizona_mcp_handler:route_opts()}.
Like route/1, with extra MCP route opts merged over the defaults:
#{allow_remote_access => true} lets non-loopback clients reach the route
(localhost-only by default; trusted networks only -- see the module doc),
#{origins => [~"http://localhost:5173"]} also allows a browser client,
#{auth => Hook} gates the route, #{sessions => false} opts out of session
mode.