Snodo.Tool behaviour (snodo v0.2.0)

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.

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

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

input_schema()

@callback input_schema() :: map()

name()

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

output_schema()

@callback output_schema() :: map() | 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.

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"}.

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.