Raxol.MCP.ToolSynchronizer (Raxol MCP v2.8.0)

Copy Markdown View Source

Per-session GenServer that bridges the render pipeline to the MCP Registry.

When an app module is provided, pulls the current model and derives its initial tools synchronously before startup returns. Subsequent [:raxol, :runtime, :view_tree_updated] telemetry events refresh tools from the rendered view tree. Rapid renders are debounced (:debounce_ms).

Also manages model-projected resources when the app implements Raxol.MCP.ResourceProvider, and registers context tree + widget tree resources for the session.

Started by Raxol.Headless when creating a session, which passes itself as :owner: when the owner exits, the synchronizer stops and its terminate/2 unregisters the session's tools and resources. It also monitors the concrete Registry process it registered with; if that process exits, the synchronizer stops rather than silently following its name to an empty replacement.

Every tool it registers is named under the session id: derived tools are "session_id.widget_id.action" and the meta-tool is "session_id.discover_tools", so sessions rendering the same widget never share a tool. A tool Raxol.MCP.Registry refuses (its client-side name is already taken), the meta-tool included, is left out of the session's set and offered again on every sync; the refusals are logged when they change, not on every sync, and name_collisions/1 answers them.

A session whose id is not 1-32 characters of [A-Za-z0-9_-] (Raxol.MCP.TreeWalker.id?/1) registers no tools, its meta-tool included, and no resources (the id would be in their URIs and descriptions), and is warned about once. A derived tool whose name OMP would show hashed (longer than 64 characters with the mcp__raxol_raxol_ prefix of the recommended raxol:raxol plugin key) is registered and warned about when a render first derives it. That warning and each widget refusal Raxol.MCP.TreeWalker reports are given once per session, also when the widget leaves the view and comes back; what was warned about is remembered for the 256 most recently rendered, so a view with per-item ids keeps memory bounded and is warned about again only for an item that comes back after 256 newer ones.

Sticky tool sets

A session's tool set is every tool its views have derived so far, keyed by name, provider module and a hash of the listed definition (description, input schema, annotations). A widget that leaves the view keeps its tools, so a modal opened, dismissed and opened again changes the set once; a tool whose provider or definition changes is registered again. Each change sends one notifications/tools/list_changed to the :server. The whole set is unregistered when the session stops, and the Registry removes it if this process dies without stopping (it registers as the tools' :owner).

With an app module, a derived tool resolves its widget at call time from a fresh view/1 of the dispatcher's current model, so it always acts on the widget as it is now (see Raxol.MCP.TreeWalker), and a call that dispatched messages syncs this session's tool set with their effect before it answers.

Options

  • :registry, :dispatcher_pid, :session_id - required
  • :app_module - the session's TEA module
  • :owner - a pid to monitor independently of the starter; the synchronizer stops and runs terminate/2 when it exits
  • :server - the Raxol.MCP.Server that list_changed notifications go to (default Raxol.MCP.Server)
  • :debounce_ms - how long a render waits for the next before it is synced; 0 syncs each render as it arrives, with no timer. Default: config :raxol_mcp, :tool_synchronizer, debounce_ms: ..., else 50.

Usage

{:ok, pid} = ToolSynchronizer.start_link(
  registry: Raxol.MCP.Registry,
  dispatcher_pid: dispatcher_pid,
  session_id: "demo",
  app_module: MyApp
)

Summary

Functions

Returns a specification to start this module under a supervisor.

Applies a view-tree update still waiting out the debounce now, and returns once the registry reflects it.

The session's tools the registry refused at the last sync because another tool, holder, has the same client-side name (Raxol.MCP.Registry.omp_name/1), as {name, holder}. Only name collisions: a widget id or action name Raxol.MCP.TreeWalker refuses, or a session id that names no tools, is not here.

Starts a ToolSynchronizer under the caller's lifecycle.

Force an immediate tool sync from the current view tree.

Record the focused widget ID.

Record the hovered widget ID.

Functions

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

flush(synchronizer, sender \\ nil)

@spec flush(GenServer.server(), pid() | nil) :: :ok

Applies a view-tree update still waiting out the debounce now, and returns once the registry reflects it.

A barrier: once flush/2 returns, the session's tool set includes every tool the last view tree that reached this process derives. Messages from one process to another arrive in the order sent, but nothing orders the caller's flush after a view tree another process sent. sender names that process (the session's rendering engine, whose render casts the view tree): the synchronizer takes a reply from sender first, which arrives after every view tree sender sent before it, so a render that answered before flush/2 was called is always applied. sender must not be the caller, which cannot answer while it waits. Without sender only view trees already received are applied.

name_collisions(synchronizer)

@spec name_collisions(GenServer.server()) :: [{String.t(), String.t()}]

The session's tools the registry refused at the last sync because another tool, holder, has the same client-side name (Raxol.MCP.Registry.omp_name/1), as {name, holder}. Only name collisions: a widget id or action name Raxol.MCP.TreeWalker refuses, or a session id that names no tools, is not here.

start_link(init_opts \\ [])

@spec start_link(keyword()) :: GenServer.on_start()

Starts a ToolSynchronizer under the caller's lifecycle.

The optional :owner is monitored independently; its exit also stops the synchronizer and runs terminate/2.

sync(synchronizer, view_tree)

@spec sync(GenServer.server(), map()) :: :ok

Force an immediate tool sync from the current view tree.

update_focus(synchronizer, widget_id)

@spec update_focus(GenServer.server(), String.t() | nil) :: :ok

Record the focused widget ID.

When a widget gains focus (keyboard or mouse click), call this. It does not change which tools are registered: every derived tool stays registered whatever has focus.

Emits :pane_focus behavior data with the focused ID in :pane_id, plus [:raxol, :mcp, :focus_changed] telemetry.

update_hover(synchronizer, widget_id)

@spec update_hover(GenServer.server(), String.t() | nil) :: :ok

Record the hovered widget ID.

Like update_focus/2, it does not change which tools are registered: the ID is only stored. Hover emits a separate :widget_hover behavior event and [:raxol, :mcp, :hover_changed] telemetry; it does not change adaptive keyboard focus.