# `PhoenixKit.API.Tool`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/tool.ex#L1)

One operation a module offers through the API (REST today, MCP later).

A module returns a list of tools from the optional `c:PhoenixKit.Module.api_tools/0`
callback. Core collects them (`PhoenixKit.API.Registry`), checks the caller's rights,
validates the input against `input_schema`, calls the handler and shapes the answer.

## Fields

  * `:name` - `"<namespace>.<verb>_<noun>"`, lowercase, stable (`"catalogue.set_prices"`),
    at most 100 characters.
  * `:audience` - `:admin` (a key with scopes) or `:public` (no key).
  * `:scope` - the scope a key must carry, `"<module>.<resource>:<action>"`. `nil` for
    public tools and for core's own `system.*` tools, which every valid key may call. A
    public tool must have **no** scope: it is called without a key, so one could never be
    met, and `validate/1` refuses the tool.
    The `system.` namespace belongs to core: a tool a module returns from `api_tools/0`
    under that name is ignored, so a module cannot declare a tool that skips the scope and
    permission checks.
  * `:permission` - the role permission the key's owner must hold right now
    (`"catalogue"`). `nil` under the same rule as `:scope`; a public tool must
    have no permission either.
  * `:title`, `:description` - written for a person and for an agent. The title defaults
    to the name, the description to `""`.
  * `:input_schema` - JSON Schema (string keys) of the input, limited to the keywords
    `PhoenixKit.API.Validator` enforces (`Validator.check_schema/2` refuses the rest).
    Defaults to an object with no properties and `additionalProperties: false`.
    Keep it **static**: build it from literals, not from data that changes at runtime (an
    enum of database values, say). The result of the schema check is remembered per
    distinct schema for the life of the node and never erased, so a schema that keeps
    changing adds an entry each time.
  * `:output_schema` - JSON Schema of a success answer, or `nil`.
  * `:handler` - `{module, function}`, called as `apply(module, function, [input, ctx])`.
  * `:module` - the module that declared the tool. Filled in by the registry.
  * `:annotations` - hints for clients: `:read_only`, `:destructive`, `:idempotent`,
    `:open_world` (all `false` unless given).
  * `:danger` - when a call needs a person's confirmation: `nil` (never, except the
    generic batch-size rule for admin writes), `:always`, or `{module, function}`, a
    check `function(input, ctx) :: boolean()` called with a dry-run context. Any other
    value is refused by `validate/1`. A key marked "requires approval" holds every
    write for a person regardless of `:danger`.
  * `:replay` - whether a retry of a write call with the same `idempotency_key` gets the
    first answer back. `true` by default. Set `false` on a write tool whose answer carries a
    credential (a session token, a claim URL): such an answer is never stored
    (`PhoenixKit.API.Idempotency.credential_bearing?/2`), so the key could not be honoured.
    The tool then takes no `idempotency_key`: the schemas, MCP and OpenAPI do not offer it, a
    body that sends it is a `422`, and the `Idempotency-Key` header is ignored, as it is on a
    read tool. Say in the description that a repeated call is not the same call.
    The field is newer than core 2.63.0, where `new/1` refuses it. A module that must also
    run there passes it only when `Code.ensure_loaded?(PhoenixKit.API.ControlFields) and
    function_exported?(PhoenixKit.API.ControlFields, :replay_tool?, 1)`.
  * `:client_token` - for a `:public` tool, the name of the input field that carries the
    anonymous client's token (`"session_token"`), or `nil` (default). It must be a string
    property of `input_schema`. Core hashes the value (sha256, url-safe base64, never kept
    in clear) into `PhoenixKit.API.Actor.client_token_hash` and uses the hash as the
    caller's identity where there is no key: the per-token rate-limit bucket, the scope of
    the `idempotency_key` and the binding of a confirmation code. A public tool with a
    `danger:` must declare it and list it in `required`; a call without the token is a
    `422` naming the field. An `:admin` tool may not declare one. The field is a credential
    like a key and is redacted from stored answers. It is refused in a URL, and filtered
    from the request log, only when core's credential rule knows its name (the listed names
    and `*_token`; see `PhoenixKit.API.Auth.credential_name?/1`), so name it `*_token`.
    The field is newer than core 2.63.x, where `new/1` refuses it. A module that must also
    run there sets it only when `Map.has_key?(PhoenixKit.API.Tool.__struct__(), :client_token)`
    (a struct key is not a function, so `function_exported?/3` cannot tell; and
    `%PhoenixKit.API.Tool{}` does not compile, it has required keys).
  * `:credentials` - names of **output** fields in which the tool hands out a secret that is
    not named `*_token` (a link with a claim code in it, say). An answer carrying one of
    them is never stored for replay and is redacted from an approval's result. The tool's
    `:client_token` counts as one without being listed. Same rule as `:client_token` for
    older cores: pass it only when the struct has the key.
  * `:max_items` - the largest batch the tool accepts, or `nil`.
  * `:cost` - the rate-limit units one call costs. `nil` (default) means the executor's
    rule: 1, or for a non-read-only admin tool `max(1, ceil(longest list / 50))`. A
    positive integer up to 1000 is a fixed price. `{Module, :function}` is called as
    `Module.function(input)` with the raw, unvalidated input and returns a positive
    integer (a failure falls back to the default rule). Use it for a batch read:
    `cost: {MyModule.API, :batch_cost}`.

## Building a tool

Always build it with `new!/1` (or `new/1` when the caller wants the error list) so the
defaults above are filled in and the shape is checked:

    PhoenixKit.API.Tool.new!(
      name: "catalogue.list_products",
      audience: :admin,
      scope: "catalogue.products:read",
      permission: "catalogue",
      title: "List products",
      handler: {MyModule.API, :list_products},
      annotations: %{read_only: true}
    )

A tool returned from `api_tools/0` that fails `validate/1` is ignored with one warning
per module; the module's other tools still work.

# `audience`

```elixir
@type audience() :: :admin | :public
```

Who may call the tool: `:admin` needs a key, `:public` needs nothing.

# `handler`

```elixir
@type handler() :: {module(), atom()}
```

`{module, function}`, called as `apply(module, function, [input, ctx])`.

# `t`

```elixir
@type t() :: %PhoenixKit.API.Tool{
  annotations: term(),
  audience: term(),
  client_token: term(),
  cost: term(),
  credentials: term(),
  danger: term(),
  description: term(),
  handler: term(),
  input_schema: term(),
  max_items: term(),
  module: term(),
  name: term(),
  output_schema: term(),
  permission: term(),
  replay: term(),
  scope: term(),
  title: term()
}
```

# `credential_keys`

```elixir
@spec credential_keys(t()) :: [String.t()]
```

The output field names whose values must never be stored or shown in clear for `tool`:
its `:credentials` and its `:client_token`. `PhoenixKit.API.Idempotency` adds them to the
names core already treats as credentials.

# `new`

```elixir
@spec new(map() | keyword()) :: {:ok, t()} | {:error, [String.t()]}
```

Builds a tool from a map or keyword list, fills the defaults and validates it.

Returns `{:ok, tool}` or `{:error, messages}` (every problem found, not only the first).

# `new!`

```elixir
@spec new!(map() | keyword()) :: t()
```

Like `new/1`, but raises `ArgumentError` listing every problem.

# `system?`

```elixir
@spec system?(t()) :: boolean()
```

True for core's own `system.*` tools.

# `validate`

```elixir
@spec validate(t()) :: {:ok, t()} | {:error, [String.t()]}
```

Checks the shape of a tool struct (the registry runs this on everything a module returns).

An `:admin` tool needs a `:scope` and a `:permission`, except a core `system.*` tool,
which every valid key may call. This check does not know who built the tool: that the
`system.` namespace is core's alone is enforced where module tools are collected
(`PhoenixKit.ModuleRegistry.all_api_tools/0`).

---

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