Behaviour for widgets that expose MCP tools.
Each interactive widget can implement this behaviour to declare what
semantic actions (tools) are available given its current state. The
TreeWalker traverses the view tree, calls mcp_tools/1 on each
widget that implements this behaviour, and registers the resulting
tools with the MCP Registry.
Tool Specs
mcp_tools/1 returns a list of tool spec maps:
%{
name: "click",
description: "Click the button",
inputSchema: %{type: "object", properties: %{}},
annotations: %{sensitive: true}
}The name is the action name (e.g., "click", "typeinto"). The
TreeWalker namespaces it with the session and widget IDs:
"demo.submit_btn.click". A name that is not a lowercase letter followed
by up to 31 of `[a-z0-9]` derives no tool, and TreeWalker logs a warning
naming it.
The description is static per (provider, action): it never carries app
text (labels, titles, tab names) or the widget ID. Descriptions reach the
client's tool list, which a model reads as instructions, so app text belongs
in tool results only. TreeWalker also strips control, format, separator,
private-use and invisible filler characters from the description and from
every string and key inside inputSchema.
annotations is optional. Of it the derived tool keeps only
sensitive: true and destructiveHint: true; any other key or value
(title, readOnlyHint, sensitive: false) is dropped. Give an action
that changes app state sensitive: true, so Raxol.MCP.Server refuses it
when no authorizer is configured.
Handle Tool Call
handle_tool_call/3 executes the action. It receives the action name,
arguments map, and a context map with :widget_id, :widget_state,
and :dispatcher_pid.
Return values:
{:ok, result}-- pure read, no state change{:ok, result, [message]}-- result + TEA messages to dispatch{:error, reason}-- failure
Messages reach update/2
TreeWalker hands each message, as returned, to the app's update/2
through the session's Dispatcher, and the tool answers only once
update/2 has folded it. Messages are app terms, not %Event{}s: an app
whose view renders a provider's node adds update/2 clauses for the
messages it acts on. id is the node's binary id.
An update/2 that raises or returns an invalid result makes the tool an
error result (:update_failed), and the model is unchanged. The
Dispatcher call uses the default 5 s timeout: a timeout is an error
result, but it does not mean the message was not applied, since
update/2 may still fold it afterwards. Check the app's state before
retrying a write that timed out.
| Provider | Action: message |
|---|---|
Table | select_row: {:select_row, id, index}; sort: {:sort, id, column, direction}, the column string as sent; direction "asc" or "desc" (anything else is an error result) |
SelectList | select: {:select_option, id, index} |
Tabs | select_tab: {:tab_change, id, index}, index range-checked against the node's :tabs |
Modal | dismiss: {:modal_dismiss, id}; confirm: {:modal_confirm, id} |
TextInput | type_into: {:text_input_change, id, text}; clear: {:text_input_change, id, ""} |
TextArea | type_into: {:text_area_change, id, text}; clear: {:text_area_change, id, ""} |
PasswordField | type_into: {:password_field_change, id, text}; clear: {:password_field_change, id, ""} |
Button | click: the node's :on_click message (top level on View DSL nodes, under :attrs on Component trees); an error result when it is nil or a function |
Checkbox | toggle: {:checkbox_toggle, id, new_checked} |
Menu | select: {:menu_select, id, item_id} |
Scrubber | seek: {:scrubber_seek, id, position}; play: {:scrubber_play, id}; pause: {:scrubber_pause, id} |
Tree | expand: {:tree_expand, id, node_id}; collapse: {:tree_collapse, id, node_id}; select_node: {:tree_select, id, node_id} |
Viewport | scroll_to: {:scroll_to, id, row} |
BarChart, LineChart, ScatterChart | read-only |
Every other action is a read ({:ok, result}) and sends nothing.
Example
defmodule MyApp.Button do
@behaviour Raxol.MCP.ToolProvider
@impl Raxol.MCP.ToolProvider
def mcp_tools(%{attrs: %{disabled: true}}), do: []
def mcp_tools(state) do
[%{
name: "click",
description: "Click the button",
inputSchema: %{type: "object", properties: %{}},
annotations: %{sensitive: true}
}]
end
@impl Raxol.MCP.ToolProvider
def handle_tool_call("click", _args, context) do
{:ok, "Clicked", [context.widget_state[:attrs][:on_click]]}
end
end
Summary
Callbacks
Handles an MCP tool call for this widget.
Returns the list of MCP tool specs for the widget's current state.
Functions
Returns true if the given module implements the ToolProvider behaviour.
Types
Callbacks
Handles an MCP tool call for this widget.
name is the action name (e.g., "click", "type_into") without the
widget ID namespace prefix.
args is the arguments map from the MCP client.
context contains :widget_id, :widget_state, and :dispatcher_pid.
Returns the list of MCP tool specs for the widget's current state.
Called by TreeWalker during view tree traversal. The widget state
is the raw map from the view tree (the output of view/1), not the
internal component struct.