Raxol.Headless.McpTools (Raxol v2.7.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. The other three are reads and 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.

Summary

Functions

Injects Raxol tools into Tidewave's ETS-based tool registry.

Register all headless tools with the given MCP Registry.

Returns the list of Raxol MCP tool definitions.

Functions

inject_into_tidewave(opts \\ [])

@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 to Raxol.MCP.Server.

register(registry \\ Raxol.MCP.Registry)

@spec register(GenServer.server()) :: :ok

Register all headless tools with the given MCP Registry.

Called from Raxol.Application after the MCP supervisor and Headless are both running.

tools()

@spec tools() :: [Raxol.MCP.Registry.tool_def()]

Returns the list of Raxol MCP tool definitions.