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.
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 input JSON Schema that input_schema/0 returns.
Sets the output JSON Schema that output_schema/0 returns.
Callbacks
@callback annotations() :: map()
@callback call(map(), Snodo.Context.t()) :: {:ok, Snodo.Result.t() | term()} | {:error, Snodo.Error.t() | term()}
@callback description() :: String.t() | nil
@callback input_schema() :: map()
@callback name() :: String.t()
@callback output_schema() :: map() | nil
Functions
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 %{}.
@spec definition(module()) :: Snodo.Tool.Definition.t()
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.
The value must be a string or nil; the router checks it on registration.
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"}.
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.