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 runsterminate/2when it exits:server- theRaxol.MCP.Serverthatlist_changednotifications go to (defaultRaxol.MCP.Server):debounce_ms- how long a render waits for the next before it is synced;0syncs 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
Returns a specification to start this module under a supervisor.
See Supervisor.
@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.
@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.
@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.
@spec sync(GenServer.server(), map()) :: :ok
Force an immediate tool sync from the current view tree.
@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.
@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.