arizona_dev_mcp (arizona v0.4.0)

View Source

A 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 the arizona server app 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. See arizona_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 to get_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 via rebar_agent under rebar3 shell, then reload any module whose beam on disk differs from the loaded code) -- the action to take when reloader_status reports 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

handle_tool(Tool, Args, Ctx, State)

init(InitParams)

route(Path)

-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.

route(Path, Opts)

-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.

tools(State)