# `Raxol.MCP.TreeWalker`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.8.0/lib/raxol/mcp/tree_walker.ex#L1)

Traverses a view element tree and derives namespaced MCP tools from widgets.

The view tree is the output of a TEA app's `view/1` function -- nested maps
with `:type`, `:id`, `:attrs`, `:children` keys. TreeWalker walks this tree,
finds widgets that implement `Raxol.MCP.ToolProvider`, calls `mcp_tools/1`
to get tool specs, namespaces them by session and widget ID, and wraps
callbacks that dispatch TEA messages through the session's Dispatcher.

## Usage

    context = %{dispatcher_pid: dispatcher_pid, session_id: "demo"}
    tools = TreeWalker.derive_tools(view_tree, context)
    Raxol.MCP.Registry.register_tools(tools)

Tool names are namespaced as `"session_id.widget_id.action"`, e.g.,
`"demo.search_input.type_into"`, so two sessions rendering the same widget
each get their own tool. Nodes without an `:id` key are skipped (layout
containers like `:column`, `:row`). The session id and the widget id become
part of a tool name the client lists, so a session whose id is not 1-32
characters of `[A-Za-z0-9_-]` derives no tools at all, and a provider whose
widget id is not derives none of its own (`id?/1`). Neither may hold a `.`,
so the first segment of a derived name is always its whole session id. The
action name is the last segment: one that is not a lowercase letter
followed by up to 31 of `[a-z0-9_]` derives no tool.

Everything else a provider writes reaches the client's tool list as well,
which a model reads as instructions. Control, format (bidi, zero-width,
BOM, tag), line/paragraph separator, private-use and invisible filler
characters (variation selectors, combining grapheme joiner, Hangul
fillers, braille blank) are stripped from a tool's `description` and from
every string and every key inside its `inputSchema` (titles, enums,
defaults, examples, property names). Of a spec's `annotations` only
`sensitive: true` and `destructiveHint: true` are kept: they can only add
the authorizer requirement, while any other key (`title`, `readOnlyHint`,
...) is a provider's self-report a client would act on.

An atom is encoded as its name, so it is cleaned as that name: a
`description` atom becomes its cleaned name, and a schema key or leaf
atom stays an atom when its name is clean and becomes the cleaned string
otherwise. A spec derives no tool, and is refused like a misnamed action,
when its `description` is neither a string nor an atom (`nil` means none),
when its `inputSchema` holds anything JSON cannot encode (a tuple, a
struct, a pid, a function, an improper list, a key that is neither a
string nor an atom), when two keys of one map are equal once cleaned
and stringified (`:amount` and `"amount"`, or `"amount"` and
`"amount\u200B"`), or when its `inputSchema` nests maps and lists deeper
than 32 levels or holds more than 10,000 values (each map, list and leaf
counts one). One such spec would otherwise make every client's
`tools/list` unencodable, let one property definition silently replace
another, or hold up every render while it is cleaned.

A derived tool is registered as one by `Raxol.MCP.ToolSynchronizer`
(`Raxol.MCP.Registry.register_tools/3` with `derived: true`), which is
what `Raxol.MCP.Server`'s boot check reads to tell it from a static tool.

## Excluding Widgets

Set `mcp_exclude: true` in a widget's attrs to suppress tool derivation:

    %{type: :text_input, id: "internal", attrs: %{mcp_exclude: true}}

The widget and its children still render normally -- only MCP tool
exposure is suppressed. Useful for decorative or internal widgets.

## Explicit provider marker (`attrs.component_module`)

Components whose root emits a plain layout type (`:column`, `:row`) can
declare their providing module explicitly instead of growing this
package's type map (the harness block Components do this):

    %{type: :column, id: "blk", attrs: %{component_module: MyBlock}}

An explicit marker wins over the built-in type map; the same
`ToolProvider` guard applies, so a module that does not implement the
behaviour still derives nothing, and a non-atom marker is ignored.

# `context`

```elixir
@type context() :: %{
  :session_id =&gt; String.t(),
  optional(:dispatcher_pid) =&gt; pid() | nil,
  optional(:type_map) =&gt; %{required(atom()) =&gt; module()},
  optional(:view) =&gt; (-&gt; {:ok, map() | [map()]} | {:error, term()}),
  optional(:synced) =&gt; (-&gt; term())
}
```

# `refusal`

```elixir
@type refusal() ::
  {:widget_id, String.t()}
  | {:action_name, String.t(), term()}
  | {:action_spec, String.t(), String.t(), spec_fault()}
```

Why a provider derived less than it declared: `{:widget_id, id}` for a
widget whose id derives no tools, `{:action_name, widget_id, name}` for
one spec whose action name derives no tool, `{:action_spec, widget_id,
name, fault}` for one whose description or input schema does (see the
moduledoc).

# `spec_fault`

```elixir
@type spec_fault() :: :not_json | {:duplicate_key, String.t()} | :too_large
```

`:not_json` for a term JSON cannot encode, `{:duplicate_key, key}` for two
keys that are both `key` once cleaned, `:too_large` for an input schema
deeper than 32 levels or larger than 10,000 values.

# `action_name?`

```elixir
@spec action_name?(term()) :: boolean()
```

Whether `name` may name an action: a lowercase letter followed by up to 31
of `[a-z0-9_]`. It becomes part of a listed tool name.

# `clean_spec`

```elixir
@spec clean_spec(term(), term()) ::
  {:ok, String.t() | nil, term()} | {:error, spec_fault()}
```

An action spec's `description` and `inputSchema` as the client's tool list
gets them: cleaned as the moduledoc describes, or the `t:spec_fault/0`
that refuses the spec. Public so that a tool derived from another source
goes through the same cleaning.

# `default_type_map`

```elixir
@spec default_type_map() :: %{required(atom()) =&gt; module()}
```

The built-in declaration-type -> Component-module map used when `context`
carries no `:type_map`.

Exposed so the main app can assert it against `Raxol.UI.Registry`; this
package cannot derive the map from that registry (main raxol is not a
dependency), so conformance is a test, not a compile-time guarantee.

# `derive_tools`

```elixir
@spec derive_tools(map() | [map()], context()) :: [Raxol.MCP.Registry.tool_def()]
```

Derives MCP tool definitions from a view element tree, and logs every
refusal (a widget id, action name or action spec that derives nothing) as
a warning.

Returns a list of `Registry.tool_def()` maps with namespaced names and
wrapped callbacks that dispatch TEA messages. Also logs a session id
refused (`id?/1`). A caller that derives on every render and wants each
refusal logged once uses `derive_with_refusals/2` instead.

## Options in context

  * `:session_id` - the session's id, the first segment of every tool name
    (required)
  * `:dispatcher_pid` - pid of the session's Dispatcher (for message dispatch)
  * `:type_map` - optional override for widget type -> module mapping
  * `:view` - a function answering the session's current view tree,
    `{:ok, tree}` or `{:error, reason}`. With it, every call resolves its
    widget by id in that tree, so it acts on the widget as it is now, not
    as it was when the tool was derived. Without it (a static tree), a call
    acts on the node it was derived from.
  * `:synced` - a function called after a call's messages have been
    dispatched and before it answers, to bring the session's tool set up
    to date with their effect

## Live resolution

With `:view` in the context, a call whose widget is no longer in the view,
no longer offers the action (a disabled Button offers none) or is now
another kind of widget answers `{:error, {:widget_unavailable, widget_id,
:absent | :disabled | :type_changed}}` (`widget_unavailable?/1`). Those
answers are about the question, not the tool's health, so they never count
toward its circuit breaker (`:fault?`). When several widgets in the view
share the id, the call acts on the first, in view order, that is of the
tool's kind and offers the action. A call into a session whose dispatcher
has exited answers `{:error, {:session_not_running, session_id}}`.

Every tool also carries `:provider`, the widget's module: with its name and
listed definition it is what `Raxol.MCP.ToolSynchronizer` keys a session's
tool set on.

# `derive_with_refusals`

```elixir
@spec derive_with_refusals(map() | [map()], context()) ::
  {[Raxol.MCP.Registry.tool_def()], MapSet.t(refusal())}
```

`derive_tools/2` without logging: returns the tools and the set of
refusals (see `t:refusal/0`) instead. A session id that is not (`id?/1`)
derives no tools and refuses no widget. A caller that derives on every
render (`Raxol.MCP.ToolSynchronizer`) logs each new refusal once with
`warn_refusal/1`, and a refused session id with
`warn_refused_session_id/1`.

# `id?`

```elixir
@spec id?(term()) :: boolean()
```

Whether `id` may be a segment of a derived tool name: a binary of 1-32
characters of `[A-Za-z0-9_-]`. The session id and every widget id must be.
A segment is the text the client lists, so it holds no `.` (which would
read as a segment boundary) and no control or format characters.

# `warn_refusal`

```elixir
@spec warn_refusal(refusal()) :: :ok
```

Logs why `refusal` derived nothing.

# `warn_refused_session_id`

```elixir
@spec warn_refused_session_id(term()) :: :ok
```

Logs that the session `id` derives no tools because of its id.

# `widget_unavailable?`

```elixir
@spec widget_unavailable?(term()) :: boolean()
```

Whether `reason` is the `{:widget_unavailable, widget_id, why}` a derived
tool answers (see "Live resolution" in `derive_tools/2`): `widget_id`
passes `id?/1` and `why` is `:absent`, `:disabled` or `:type_changed`. Any
tool can return a term of that shape, so this is what makes it safe to name
the widget to a client.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
