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

ETS-backed registry for MCP tools and resources.

Any module can register tools and resources. The registry stores definitions
alongside callback functions that are invoked when tools are called or
resources are read.

Reads (`list_tools`, `call_tool`, `list_resources`, `read_resource`) go
directly to ETS with `read_concurrency: true` -- no GenServer bottleneck.
Writes (`register_*`, `unregister_*`) serialize through the GenServer.

## Tool Registration

    tools = [
      %{
        name: "raxol_screenshot",
        description: "Capture a screenshot",
        inputSchema: %{type: "object", properties: %{id: %{type: "string"}}},
        callback: fn args -> {:ok, [%{type: "text", text: "screenshot data"}]} end
      }
    ]
    Registry.register_tools(registry, tools)

A tool registered with `register_tools(registry, tools, derived: true)` is a
derived tool: `static_tools/1` lists only the others, and that is what
`Raxol.MCP.Server`'s boot check scans. Only `Raxol.MCP.ToolSynchronizer`
passes the option, for a session's tools (those `Raxol.MCP.TreeWalker`
derives from its view, and its `discover_tools` meta-tool); a `:derived`
key in a tool map is ignored.

## Resource Registration

    resources = [
      %{
        uri: "raxol://session/demo/model",
        name: "Session Model",
        description: "Current TEA model state",
        callback: fn -> {:ok, %{counter: 5}} end
      }
    ]
    Registry.register_resources(registry, resources)

## Which errors open a circuit

Every callback runs behind a `Raxol.MCP.CircuitBreaker` keyed on the tool,
resource or prompt. A raise or an exit always counts as a failure; a
returned `{:error, reason}` counts unless the registration declares a
`:fault?` classifier:

    %{
      name: "web3_get_transaction",
      description: "...",
      inputSchema: %{type: "object"},
      callback: &Tools.get_transaction/1,
      fault?: &Tools.fault?/1
    }

`:fault?` is a 1-arity function called with the `reason` the callback
returned. `false` means the error is an answer about the question -- a
not-found, a malformed cursor -- and records nothing. Anything else is an
availability fault and records a failure. The registry stays generic: it
knows no taxonomy, only who to ask.

A callback that raises or exits answers `{:error, {:callback_raised,
module}}` or `{:error, {:callback_exited, atom}}`. The exception message is
logged, never returned, because the return value reaches a model.

## Owners

`register_tools/3`, `register_resources/3` and `register_prompts/3` take an
`owner: pid`. The registry monitors each owner, and when one exits it
removes everything the owner still holds and, if that included tools,
sends one `notifications/tools/list_changed` to its `:server` (start
option, default `Raxol.MCP.Server`). Registering a name again moves it to
the new registration's owner, or to none. A registration without an owner
is never removed automatically, so a short-lived caller (a test, a task)
does not take its tools with it when it exits.

Unregistering, by name or by an owner's exit, also forgets the entry's
circuit-breaker state. `await_owner_cleanup/2` is a barrier for callers
that must not answer until a dead owner's entries are gone.

# `prompt_def`

```elixir
@type prompt_def() :: %{
  :name =&gt; String.t(),
  :description =&gt; String.t(),
  :arguments =&gt; [map()],
  :callback =&gt; (map() -&gt; {:ok, [map()]} | {:error, term()}),
  optional(:fault?) =&gt; (term() -&gt; boolean())
}
```

# `resource_def`

```elixir
@type resource_def() :: %{
  :uri =&gt; String.t(),
  :name =&gt; String.t(),
  :description =&gt; String.t(),
  :callback =&gt; (-&gt; {:ok, term()} | {:error, term()}),
  optional(:fault?) =&gt; (term() -&gt; boolean())
}
```

# `tool_def`

```elixir
@type tool_def() :: %{
  :name =&gt; String.t(),
  :description =&gt; String.t(),
  :inputSchema =&gt; map(),
  :callback =&gt; (map() -&gt; {:ok, term()} | {:error, term()}),
  optional(:fault?) =&gt; (term() -&gt; boolean())
}
```

# `await_owner_cleanup`

```elixir
@spec await_owner_cleanup(GenServer.server(), pid()) :: :ok
```

Wait until the registry holds no tools, resources or prompts for `owner`.

# `call_tool`

```elixir
@spec call_tool(GenServer.server(), String.t(), map()) ::
  {:ok, term()} | {:error, term()}
```

Call a registered tool by name with arguments.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `circuit_status`

```elixir
@spec circuit_status(GenServer.server(), Raxol.MCP.CircuitBreaker.key()) :: map()
```

Get circuit breaker status for a tool, resource, or prompt key.

# `get_prompt`

```elixir
@spec get_prompt(GenServer.server(), String.t(), map()) ::
  {:ok, [map()]} | {:error, term()}
```

Get a prompt by name, rendering it with the given arguments.

# `handle_manager_cast`

# `list_prompts`

```elixir
@spec list_prompts(GenServer.server()) :: [map()]
```

List all registered prompts (definitions without callbacks).

# `list_resources`

```elixir
@spec list_resources(GenServer.server()) :: [map()]
```

List all registered resources (definitions without callbacks).

# `list_tools`

```elixir
@spec list_tools(GenServer.server()) :: [map()]
```

List all registered tools (definitions without callbacks).

# `omp_name`

```elixir
@spec omp_name(String.t()) :: String.t()
```

The name OMP (`mcp/tool-bridge.ts`, at the version ADR-0042 pins) makes of
a tool name before it adds its `mcp__<server>_` prefix: lowercased, each run
outside `[a-z0-9_]` turned into `_`, runs of `_` collapsed, `_` trimmed from
both ends, and `"tool"` if nothing is left.

OMP also strips an exact `<server>_` prefix from the result once, which
this does not model. Raxol's server key is `raxol`, so `raxol_start`
refuses the session id `raxol`: that session's `raxol.nav_menu.click`
would reach OMP as session `nav`'s `nav.menu.click`. OMP's 64-character
limit (55 characters, `_` and an 8-character hash) applies to the prefixed
name and, barring a hash collision, keeps distinct names distinct, so it
is not modeled either: past it OMP shows a hashed name, about which
`Raxol.MCP.ToolSynchronizer` warns.

# `read_resource`

```elixir
@spec read_resource(GenServer.server(), String.t()) ::
  {:ok, term()} | {:error, term()}
```

Read a registered resource by URI.

# `register_all`

```elixir
@spec register_all(GenServer.server(), keyword()) ::
  :ok
  | {:error, {:invalid_tool, non_neg_integer(), [atom()]}}
  | {:error, {:name_collision, [{String.t(), String.t()}]}}
```

Registers tools and/or resources in one call after validating each tool
shape via `Raxol.MCP.ToolDef.validate/1`.

Reduces the common consumer boilerplate:

    :ok = Registry.register_tools(registry, tools)
    :ok = Registry.register_resources(registry, resources)

to a single call:

    :ok = Registry.register_all(registry, tools: tools, resources: resources)

Returns `{:error, {:invalid_tool, index, reasons}}` when validation fails
and `{:error, {:name_collision, [{refused_name, holder_name}]}}` when a
tool's `omp_name/1` is taken, by a registered tool or by another tool in
the batch. Either way nothing is registered: no tools, resources or
prompts.

Note: this helper requires the `Raxol.MCP.Registry` module to be loaded
at runtime. Consumers with `raxol_mcp` as an optional dep should still
gate the call with `Code.ensure_loaded?(Raxol.MCP.Registry)`. Optional
deps are an Elixir-level concern; no helper can paper over a missing
module.

# `register_prompts`

```elixir
@spec register_prompts(GenServer.server(), [prompt_def()]) :: :ok
```

Register one or more prompts.

# `register_prompts`

```elixir
@spec register_prompts(GenServer.server(), [prompt_def()], keyword()) :: :ok
```

`register_prompts/2` with `owner: pid` (see "Owners").

# `register_resources`

```elixir
@spec register_resources(GenServer.server(), [resource_def()]) :: :ok
```

Register one or more resources.

# `register_resources`

```elixir
@spec register_resources(GenServer.server(), [resource_def()], keyword()) :: :ok
```

`register_resources/2` with `owner: pid` (see "Owners").

# `register_tools`

```elixir
@spec register_tools(GenServer.server(), [tool_def()], keyword()) ::
  :ok
  | {:error, {:name_collision, [{String.t(), String.t()}]}}
  | {:error, {:invalid_tool_name, term()}}
```

Register one or more tools.

A client may rename tools: OMP (at the version
`docs/adr/0042-lean-on-omp.md` pins) lowercases a tool name, turns each run
of characters outside `[a-z0-9_]` into `_`, collapses repeated `_` and trims
them from both ends (`omp_name/1`). Two distinct names that come out the
same there, such as `"s.x_y.click"` and `"s.x.y.click"`, would reach the
client as one tool. So a tool whose `omp_name/1` is already held by a
differently named tool is refused, and the rest are registered:
`{:error, {:name_collision, [{refused_name, holder_name}]}}`. Registering
a name again replaces its tool, as before. `register_all/2` refuses the
whole batch instead.

A tool whose `:name` is not a binary (or that has none) refuses the whole
batch, `{:error, {:invalid_tool_name, name}}`, and the registry keeps
serving the tools it holds.

`derived: true` registers them as derived, which leaves them out of
`static_tools/1` and so out of `Raxol.MCP.Server`'s boot check (the
`tools/call` refusal still holds them). It is for
`Raxol.MCP.ToolSynchronizer` alone: a tool registered with it skips the
check that refuses to boot an unguarded server on a sensitive tool.

`owner: pid` makes the registration owned (see "Owners").

# `reset_circuit`

```elixir
@spec reset_circuit(GenServer.server(), Raxol.MCP.CircuitBreaker.key()) :: :ok
```

Manually reset a circuit breaker.

# `start_link`

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

Start the registry, linked to the calling process.

`:server` names the `Raxol.MCP.Server` told when an owner's exit removes
tools: an atom, a pid, `{:global, name}` or `{:via, module, name}`; any
other value is `{:error, {:invalid_server, server}}` and starts nothing.
The default is the application's `Raxol.MCP.Server`, so a registry started
beside a private server passes that server here, or its owners' exits
notify the application's clients instead.

# `static_tools`

```elixir
@spec static_tools(GenServer.server()) :: [map()]
```

`list_tools/1` without the tools registered with `derived: true`: the
tools an embedder registered itself, rather than a session's view.

# `unregister_prompts`

```elixir
@spec unregister_prompts(GenServer.server(), [String.t()]) :: :ok
```

Unregister prompts by name.

# `unregister_resources`

```elixir
@spec unregister_resources(GenServer.server(), [String.t()], keyword()) :: :ok
```

Unregister resources by URI. With `owner: pid`, only those `pid` still
owns (see `unregister_tools/3`).

# `unregister_tools`

```elixir
@spec unregister_tools(GenServer.server(), [String.t()], keyword()) :: :ok
```

Unregister tools by name.

With `owner: pid`, only the named tools `pid` still owns go (see
"Owners"): a name another owner has registered since stays.

---

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