Validated builder for MCP tool definitions.
Raxol.MCP.Registry.register_tools/2 accepts a list of maps with
:name, :description, :inputSchema, and :callback keys. The shape
is documented as a tool_def type but isn't validated until call
time -- typos like :input_schema instead of :inputSchema silently
miss the registration; tools without callbacks blow up only when an
agent tries to invoke them.
This module catches those mistakes at the seam:
tool =
Raxol.MCP.ToolDef.new!("symphony_list_runs",
description: "Returns a snapshot of active Symphony runs.",
input_schema: %{type: "object", properties: %{}},
callback: fn _args -> %{ok: true} end
)
Raxol.MCP.Registry.register_tools(registry, [tool])new/2 returns {:ok, def} or {:error, reasons}. new!/2 raises
on validation failure.
Both spellings of the schema key are accepted (:inputSchema and
:input_schema); the canonical key on the returned map is
:inputSchema to match the MCP spec.
Summary
Functions
Builds a validated tool definition.
Like new/2 but raises ArgumentError on validation failure.
Whether tool declares itself sensitive -- destructive, or otherwise
something that must not run unattended (moving money is the motivating case).
Validates an existing tool map (e.g., one built directly without new/2).
Types
@type t() :: Raxol.MCP.Registry.tool_def()
@type validation_error() ::
:missing_name
| :missing_description
| :missing_callback
| :invalid_callback_arity
| :missing_input_schema
| :invalid_input_schema
Functions
@spec new( String.t(), keyword() ) :: {:ok, t()} | {:error, [validation_error()]}
Builds a validated tool definition.
Required:
name-- string, non-empty.:description-- string, non-empty.:callback-- 1-arity function (receives the arguments map).:input_schema(or:inputSchema) -- map with at minimum:type/"type"set to"object".
Returns {:ok, def} (a map matching Raxol.MCP.Registry.tool_def())
or {:error, [validation_error()]}.
Like new/2 but raises ArgumentError on validation failure.
Whether tool declares itself sensitive -- destructive, or otherwise
something that must not run unattended (moving money is the motivating case).
Recognized, any of which is enough:
annotations.destructiveHint == true-- the MCP-standard hint;annotations.sensitive == true-- for a tool that is not destructive in the MCP sense but still must be gated.
This predicate is deliberately one-directional: an annotation can only ever
ADD an obligation, never remove one. A tool claiming destructiveHint: false or readOnlyHint: true gets no exemption from anything -- it is a
self-report, and the harness already rules elsewhere (U21-R3) that a
self-reported mutating: false cannot remove a call from the mutation set.
Declaring yourself dangerous is credible; declaring yourself safe is not.
@spec validate(map()) :: :ok | {:error, [validation_error()]}
Validates an existing tool map (e.g., one built directly without new/2).
Useful when adopting ToolDef incrementally -- run validation on a list
of hand-built tools at registration time and fail fast.