Snodo.Tool behaviour (snodo v0.4.1)

Copy Markdown View Source

Behaviour and compile-time convenience DSL for MCP tools.

The DSL emits ordinary module functions, and schemas remain unmodified Elixir maps representing JSON Schema documents.

Failure has two shapes, and they are not interchangeable

call/2 can fail in two ways, and they reach the client differently:

# The call succeeded; the tool is reporting a bad outcome.
# tools/call returns a result with isError: true.
{:ok, Snodo.Result.error("hex.pm returned 503")}

# The request itself was wrong or could not be attempted.
# The JSON-RPC request fails with an error object and no result.
{:error, Snodo.Error.invalid_params("version must be a semantic version")}

The first is for anything the tool understands: an upstream service failing, a lookup finding nothing, a domain precondition being rejected. A client sees a normal response it can show a model, and one failing tool does not look like a broken server.

The second escalates to the protocol. Reserve it for a request that should never have been dispatched.

Arguments that are missing from the schema's required list, or that the installed validator rejects, never reach call/2. The router answers with an isError result naming the problem, as the 2026-07-28 tools specification asks, so a model can correct its call.

For compatibility, {:error, reason} without an Snodo.Error is normalized into a tool error result. Prefer an explicit Snodo.Result.error/2 for domain outcomes and a typed Snodo.Error when the JSON-RPC request must fail.

Schemas are advertised, and required arguments are enforced

input_schema/1 is published verbatim in tools/list. The router enforces its required list before dispatch, so a handler may pattern match on those keys; a call missing one gets an isError result. Every other keyword is advertised but only enforced when the runtime installs a Snodo.Schema.Validator; the default is pass-through. See Snodo.Schema.Validator.Basic for the bundled common subset.

A property may carry "x-mcp-header": "Name" to be mirrored into an Mcp-Param-Name header over Streamable HTTP. The annotation is checked when the tool compiles: see the Components guide for the rules.

A tool may also declare :title, :icons, and :metadata in use Snodo.Tool or with title/1, icons/1, and metadata/1. Icons use the same shape as resource and prompt icons. :metadata becomes _meta in tools/list. Direct implementations may omit these callbacks; the defaults are nil, [], and %{}.

use Snodo.Tool also accepts wrap:, a list of Snodo.Component.Wrap modules or {module, options} tuples. The wrappers run inside call/2 after router checks.

Summary

Functions

Sets the tool annotations map that annotations/0 returns, for example %{"readOnlyHint" => true}.

Returns the protocol-neutral definition for a tool module.

Sets the description that description/0 returns, replacing the :description option given to use Snodo.Tool.

Sets the icon list returned by icons/0. Each icon requires an absolute URI "src".

Sets the input JSON Schema that input_schema/0 returns.

Sets the protocol _meta map returned by metadata/0.

Sets the output JSON Schema that output_schema/0 returns.

Sets the optional human-readable title returned by title/0.

Callbacks

annotations()

@callback annotations() :: map()

call(map, t)

@callback call(map(), Snodo.Context.t()) ::
  {:ok, Snodo.Result.t() | term()} | {:error, Snodo.Error.t() | term()}

description()

@callback description() :: String.t() | nil

icons()

(optional)
@callback icons() :: [map()]

input_schema()

@callback input_schema() :: map()

metadata()

(optional)
@callback metadata() :: map()

name()

@callback name() :: String.t()

output_schema()

@callback output_schema() :: map() | nil

title()

(optional)
@callback title() :: String.t() | nil

Functions

annotations(value)

(macro)

Sets the tool annotations map that annotations/0 returns, for example %{"readOnlyHint" => true}.

The value must be a map of JSON values; otherwise the module does not compile. Defaults to %{}.

definition(tool)

@spec definition(module()) :: Snodo.Tool.Definition.t()

Returns the protocol-neutral definition for a tool module.

description(value)

(macro)

Sets the description that description/0 returns, replacing the :description option given to use Snodo.Tool.

The value must be a string or nil; the router checks it on registration.

icons(value)

(macro)

Sets the icon list returned by icons/0. Each icon requires an absolute URI "src".

input_schema(value)

(macro)

Sets the input JSON Schema that input_schema/0 returns.

The value must be a map of JSON values with "type" => "object" at the root, and any x-mcp-header annotations must be valid; otherwise the module does not compile. Defaults to %{"type" => "object"}.

metadata(value)

(macro)

Sets the protocol _meta map returned by metadata/0.

output_schema(value)

(macro)

Sets the output JSON Schema that output_schema/0 returns.

The value must be nil (the default) or a JSON Schema map; otherwise the module does not compile. When a schema is set, a call must return structured content (Snodo.Result.structured/2, a non-binary value, or a Snodo.Result.raw/1 map with "structuredContent") unless it returns Snodo.Result.error/2 or Snodo.Result.input_required/1. The runtime's schema validator checks that content. Any other result fails the request with a -32603 error.

title(value)

(macro)

Sets the optional human-readable title returned by title/0.