Raxol. Headless. McpTools
(Raxol v2.8.0)
View Source
MCP tool definitions for Raxol headless sessions.
Registers raxol_start, raxol_screenshot, raxol_send_key,
raxol_get_model, raxol_stop, and raxol_list as MCP tools
via Raxol.MCP.Registry.
Authorization
The three tools that change something -- raxol_start, raxol_send_key,
raxol_stop -- are annotated sensitive, so Raxol.MCP.Server refuses to run
them when no authorizer is configured. So is raxol_get_model, a read that
returns the whole model: an app keeps its secrets there, a masked input's
plaintext included. raxol_screenshot (which draws a masked input as its
mask) and raxol_list are not gated.
The annotation is only half of it: it does nothing until an authorizer exists
at the call site, and denies permanently if one never does. Raxol.Application
supplies one (config :raxol, :mcp_authorizer, defaulting to allow_all/0
outside production and to a deny-by-default allowlist in production), which is
what makes these annotations meaningful rather than merely restrictive.
Both entry points land on that same gate. register/1 is the ordinary one;
inject_into_tidewave/1 routes through Raxol.MCP.Server as well rather than
handing Tidewave the raw callbacks, so neither surface can drift from the
policy the other enforces.
Startable modules
A "module" argument starts only a module on startable/0. That is
config :raxol, :headless_startable when it is set, and otherwise the
playground demos except Raxol.Playground.Demos.ReplDemo (it evaluates
Elixir) and Raxol.Playground.Demos.BeamDashboardDemo (it sets the
VM-wide :scheduler_wall_time flag and shows VM internals). A set list
REPLACES the demos, so a deployment names exactly what clients may start
by module:
config :raxol, :headless_startable, [MyApp]To keep the demos as well, list them too (default_startable/0 returns
them). Every entry must be a loadable module (Code.ensure_loaded?/1):
register/1 raises on a value that is not, so a bad config stops the boot,
and a value changed at runtime makes raxol_start answer with the same
config error rather than start anything.
The list does not bind the "path" argument. When a deployment sets
RAXOL_HEADLESS_PATH_ROOT (or config :raxol, :headless_path_root),
raxol_start compiles and starts any module defined in a file under that
root, listed or not, because the module is only known once the file is
compiled. Unset, the argument is refused.
The list binds the MCP tool, which starts a module for an untrusted
caller. Elixir callers of Raxol.Headless.start/2 (Raxol.MCP.Test,
Raxol.Recording.Video) are trusted and start any Raxol application.
Summary
Functions
Every playground demo except Raxol.Playground.Demos.ReplDemo, which
evaluates Elixir, and Raxol.Playground.Demos.BeamDashboardDemo, which
sets the VM-wide :scheduler_wall_time flag and shows VM internals: what
raxol_start may start when config :raxol, :headless_startable is not
set.
Injects Raxol tools into Tidewave's ETS-based tool registry.
Register all headless tools with the given MCP Registry.
The modules raxol_start may start: config :raxol, :headless_startable
when set (it replaces the default), else default_startable/0.
Returns the list of Raxol MCP tool definitions.
The Raxol.MCP.Server :unknown_tool_reply for Headless sessions
(Raxol.Application passes it).
Functions
@spec default_startable() :: [module()]
Every playground demo except Raxol.Playground.Demos.ReplDemo, which
evaluates Elixir, and Raxol.Playground.Demos.BeamDashboardDemo, which
sets the VM-wide :scheduler_wall_time flag and shows VM internals: what
raxol_start may start when config :raxol, :headless_startable is not
set.
@spec inject_into_tidewave(keyword()) :: :ok | {:error, :inject_timeout | :tidewave_not_started | :tidewave_owner_not_alive | {:sys_replace_failed, term()}}
Injects Raxol tools into Tidewave's ETS-based tool registry.
Call after Tidewave.MCP has initialized (typically from Application.start). Safe to call multiple times -- tools are merged, not duplicated.
What is injected is NOT the raw tool callback. Tidewave dispatches straight out
of its own map, so a callback written there would never reach
Raxol.MCP.Server and no authorizer, annotation, or elicitation would apply to
it -- the one route that bypasses every gate the rest of this module relies on.
Each entry is instead a closure that re-enters through tools/call on the
server named by :server, so this surface inherits exactly the policy the
supervisor was started with. If that server is not running, calls are refused
rather than executed.
This gates Raxol's tools, not Tidewave's
Tidewave's own tools are untouched and ungated by anything here; project_eval
in particular evaluates Elixir in the running node. Whether that endpoint may
be reached at all is an endpoint-level decision (authentication, bind address),
not one this function can make.
The Tidewave ETS table is :protected, so the insert must run in the
owning process. We spawn a task linked to that process to do the write.
Options
:server-- the MCP server calls are routed through. Defaults toRaxol.MCP.Server.
@spec register(GenServer.server(), keyword()) :: :ok | {:error, {:name_collision, [{String.t(), String.t()}]}}
Register all headless tools with the given MCP Registry.
Called from Raxol.Application after the MCP supervisor and Headless
are both running. opts go to Raxol.MCP.Registry.register_tools/3
(owner: pid). Raises ArgumentError when
config :raxol, :headless_startable is invalid (see startable/0), so a
bad config stops the boot instead of surfacing at the first raxol_start.
Answers {:error, {:name_collision, [{refused, holder}]}} when the
registry refused some of the tools because another tool, holder, already
has the same client-side name (Raxol.MCP.Registry.register_tools/2); the
rest are registered.
The modules raxol_start may start: config :raxol, :headless_startable
when set (it replaces the default), else default_startable/0.
Answers {:error, sentence} when the configured value is not a list of
loadable modules (Code.ensure_loaded?/1, which loads one that is on the
code path but not loaded yet).
@spec tools(GenServer.server()) :: [Raxol.MCP.Registry.tool_def()]
Returns the list of Raxol MCP tool definitions.
raxol_start starts sessions whose derived tools are registered in
registry (default Raxol.MCP.Registry), the registry register/1
puts these tools in.
The Raxol.MCP.Server :unknown_tool_reply for Headless sessions
(Raxol.Application passes it).
A client keeps the tool names it listed last (OMP caches them across runs),
so a call to <session>.<widget>.<action> or <session>.discover_tools
whose session is not in Raxol.Headless.list/0 answers "session
<session> is not running in this server; call raxol_list", which the
caller can act on. A name of that shape whose session is running, whose
session is one of the ids raxol_start reserves (raxol, agent), or
whose segments no session could derive (Raxol.MCP.TreeWalker.id?/1), and
any other name answer nil: the server's -32601. The server also hosts
any dotted tools an embedder registers, so an unknown one of that shape is
answered as a stopped session too.