PhoenixKit.API.Tool (phoenix_kit v2.63.0)

Copy Markdown View Source

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

A module returns a list of tools from the optional 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 ("ecommerce.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 ("shop.manage_catalog"). 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.
  • :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.

Summary

Types

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

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

t()

Functions

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

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

True for core's own system.* tools.

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

Types

audience()

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

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

handler()

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

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

t()

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

Functions

new(attrs)

@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!(attrs)

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

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

system?(tool)

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

True for core's own system.* tools.

validate(tool)

@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).