# `PhoenixKit.API.ToolLint`
[🔗](https://github.com/BeamLabEU/phoenix_kit/blob/v2.64.1/lib/phoenix_kit/api/tool_lint.ex#L1)

Static checks for API tools, for the test suite of every module that
declares `c:PhoenixKit.Module.api_tools/0`.

    test "the tools are well formed" do
      assert :ok = PhoenixKit.API.ToolLint.check_tools(MyModule.api_tools())
    end

`PhoenixKit.API.Validator` enforces a small subset of JSON Schema and
silently ignores every other keyword. A schema that uses an ignored keyword
in a tool's **input** promises a check nobody makes, so this module flags:

  * in an input schema, **every keyword outside the supported subset**:
    anything but `type`, `required`, `properties`,
    `additionalProperties`, `enum`, `minimum`, `maximum`, `minLength`,
    `maxLength`, `minItems`, `maxItems`, `items`, `pattern` and the
    annotations the validator accepts and ignores
    (`PhoenixKit.API.Validator.annotation?/1`).
    That is a whitelist: `oneOf`, `const`, `nullable`, `dependencies`,
    `uniqueItems` and a keyword nobody has heard of are all caught. (In an
    output schema the combinators are fine: core generates `anyOf` itself
    for the MCP `outputSchema`, and outputs are not validated at run time.)
  * `$ref`, `$defs` and `definitions` anywhere: schemas are served as they
    are and nothing resolves a reference.

It also checks the shape of what the validator does read (`type`,
`required`, `properties`, `additionalProperties`, `items`, `enum`, the
numeric and count bounds, `pattern`), that both roots are objects (MCP
needs an object `inputSchema`, and `structuredContent` is an object), that a
`required` name is declared when the object is closed, that a tool has a
description, that a public tool with a `danger:` declares a `client_token:`
(or, until 3.0.0, has the input `PhoenixKit.API.Legacy.legacy_client_token/1`
names, which the executor reads the same way) and lists that field in `required` (a dry run is answered before the client
token is checked, so the schema has to refuse a call without one), that a
`client_token:` is a string property of the input schema and belongs to a
public tool (an admin tool is identified by its key), and that no two tools
of one audience share a name or an MCP name (`a.b_c` and `a_b.c` are both
`a_b_c` on MCP).

## Relation to the validator

`PhoenixKit.API.Tool.new/1` already runs `PhoenixKit.API.Validator.check_schema/2`
on every tool, so a tool built through it is refused for an unsupported
keyword, a bad bound or a bad `pattern`. This module is defence in depth:
it also sees a tool built as a struct literal, reports every problem with its
path instead of the first one, and checks what the validator does not (a
`required` name that is not declared, an object root, an empty description,
the MCP-name collisions). Where both read a keyword they agree: `pattern` is compiled with
the validator's options, the annotations come from the validator, and an atom
`type` or `required` name is read like a string. A test pins the agreement.
The lint is stricter in a few places on purpose: a `type` list with a
repeated name, a `minimum` above a `maximum`, an empty `enum`, an empty
description.

Every function returns `:ok` or `{:error, messages}` with all the problems
found, in English. A schema message starts with the path inside the schema
it is about (`properties.sku: ...`, `(root): ...`); `check_tool/1` adds
`input_schema:` or `output_schema:` in front, and `check_tools/1` the tool
name. A name message (`a.b_c, a_b.c share the MCP name a_b_c`) starts with
the names.

# `check_schema`

```elixir
@spec check_schema(term(), :input | :output) :: :ok | {:error, [String.t()]}
```

Checks one JSON Schema. `kind` is `:input` for a tool's `input_schema` and
`:output` for its `output_schema`.

# `check_tool`

```elixir
@spec check_tool(PhoenixKit.API.Tool.t()) :: :ok | {:error, [String.t()]}
```

Checks the schemas, the roots and the description of one tool.

# `check_tools`

```elixir
@spec check_tools([PhoenixKit.API.Tool.t()]) :: :ok | {:error, [String.t()]}
```

Checks every tool of a set and the names across the set: a name declared
twice in one audience, and two names of one audience that are the same on
MCP.

---

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