JSON Schema for tool input and output schemas.
Provides an Ecto-like DSL for defining input schemas that both generate JSON Schema for clients AND validate/coerce incoming params at dispatch time.
Router usage
When defining tools in a Phantom.Router, use a do block with field declarations:
tool :search, description: "Search for stuff" do
field :query, :string, required: true
field :limit, :integer, default: 10
field :tags, {:array, :string}
endThis generates the equivalent JSON Schema sent to clients and validates incoming arguments at dispatch time, returning structured errors for invalid input.
You can still use the map-based input_schema option for full control:
tool :search,
description: "Search for stuff",
input_schema: %{
required: ~w[query],
properties: %{
query: %{type: "string", description: "Search query"},
limit: %{type: "integer", description: "Max results"}
}
}The map-based form skips server-side validation — it only advertises the schema to clients.
Type system
| DSL type | Elixir guard | JSON Schema type |
|---|---|---|
:string | is_binary | "string" |
:integer | is_integer | "integer" |
:number | is_number | "number" |
:boolean | is_boolean | "boolean" |
{:array, subtype} | is_list + validate each | "array" with items |
:map with do block | is_map + validate nested | "object" with properties |
ModuleName | delegate to module schema | delegate to module schema |
Field options
Every field accepts the following options:
| Option | Type | Description |
|---|---|---|
:required | boolean | Mark the field as required (default: false) |
:default | any | Default value injected when the field is absent |
:description | string | Description sent to clients in the JSON Schema |
:enum or :in | list | Restrict to an allowed set of values |
:exclusion | list | Reject specific values |
:minimum or :greater_than_or_equal_to | number | Minimum value (inclusive) |
:maximum or :less_than_or_equal_to | number | Maximum value (inclusive) |
:greater_than | number | Exclusive minimum |
:less_than | number | Exclusive maximum |
:min_length | integer | Minimum string length or list length |
:max_length | integer | Maximum string length or list length |
:length | integer | Exact string length or list length |
:pattern or :format | string or Regex | Regex pattern the string must match |
:message | string | Custom error message on validation failure |
:validate | function | Custom validator (see below) |
Options like :in, :greater_than_or_equal_to, :less_than_or_equal_to,
and :format are Ecto-style aliases for their JSON Schema equivalents.
Nested objects
Use a do block on a :map field to define nested properties:
tool :search, description: "Search" do
field :query, :string, required: true
field :filters, :map do
field :category, :string, in: ~w[books movies music]
field :min_price, :number, minimum: 0
end
endCustom validators
The :validate option accepts an anonymous function, a local function name,
or an MFA tuple. The function receives the field value and must return
:ok or {:error, reason}:
tool :create_user, description: "Create a user" do
field :email, :string,
required: true,
pattern: ~r/@/,
validate: fn value ->
if String.contains?(value, "@"),
do: :ok,
else: {:error, "must be a valid email"}
end
# Or reference a local function by name:
field :username, :string, required: true, validate: :validate_username
end
def validate_username(value) do
if String.length(value) >= 3, do: :ok, else: {:error, "too short"}
endReusable schemas
Define reusable schemas as modules with use Phantom.Tool.JSONSchema:
defmodule MyApp.MCP.Schemas.Filters do
use Phantom.Tool.JSONSchema
input_schema do
field :category, :string
field :min_price, :number, minimum: 0
end
endThen reference the module as a field type:
tool :search, description: "Search" do
field :query, :string, required: true
field :filters, MyApp.MCP.Schemas.Filters
end
Summary
Functions
Build a field map from name, type, and opts.
Build a %JSONSchema{} from a list of field maps, pre-computing JSON Schema properties.
Define an input schema. Returns a %JSONSchema{} struct with validation
metadata that is consumed by the following tool macro via the
@phantom_input_schema attribute.
Passthrough when no schema is present, or when schema has no DSL fields.
Validate params against the field definitions. Returns {:ok, params_with_defaults}
or {:error, [error_messages]}.
Types
@type field() :: %{ name: atom(), type: atom() | {:array, atom()} | module(), required: boolean(), default: any(), description: String.t() | nil, message: String.t() | nil, validate: (any() -> :ok | {:error, String.t()}) | {module(), atom(), list()} | atom() | nil, enum: list() | nil, minimum: number() | nil, maximum: number() | nil, greater_than: number() | nil, less_than: number() | nil, min_length: non_neg_integer() | nil, max_length: non_neg_integer() | nil, length: non_neg_integer() | nil, pattern: String.t() | nil, exclusion: list() | nil, children: [field()] | nil }
Functions
Build a field map from name, type, and opts.
Build a %JSONSchema{} from a list of field maps, pre-computing JSON Schema properties.
Define an input schema. Returns a %JSONSchema{} struct with validation
metadata that is consumed by the following tool macro via the
@phantom_input_schema attribute.
input_schema do
field :query, :string, required: true
field :limit, :integer, default: 10
end
tool :search, description: "Search"
Passthrough when no schema is present, or when schema has no DSL fields.
Validate params against the field definitions. Returns {:ok, params_with_defaults}
or {:error, [error_messages]}.