# `Snodo.Tool`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.2.0/lib/snodo/tool.ex#L1)

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.

# `annotations`

```elixir
@callback annotations() :: map()
```

# `call`

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

# `description`

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

# `input_schema`

```elixir
@callback input_schema() :: map()
```

# `name`

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

# `output_schema`

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

# `annotations`
*macro* 

Sets the tool annotations map that `c: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`

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

Returns the protocol-neutral definition for a tool module.

# `description`
*macro* 

Sets the description that `c: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`
*macro* 

Sets the input JSON Schema that `c: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`
*macro* 

Sets the output JSON Schema that `c: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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
