Define several MCP tools in one module by annotating functions with @mcp.
defmodule MyApp.Toolkit do
use Noizu.MCP.Server.Toolkit, category: "Utility" # optional default category
@mcp name: "files.read", category: "Files", description: "Read a file",
input: [path: [type: :string, required: true]],
output: [data: [type: :string, required: true]]
def read_file(%{path: path}, _ctx) do
case File.read(path) do
{:ok, data} -> {:ok, %{data: data}}
{:error, reason} -> {:error, "read failed: #{reason}"}
end
end
@mcp description: "Server time (name derives from the function)"
def server_time, do: {:ok, to_string(DateTime.utc_now())}
@mcp visible: false
@mcp input: """
{"type": "object", "properties": {"q": {"type": "string"}}}
"""
def lookup(args, _ctx), do: {:ok, args["q"] || ""}
endRegister the whole kit on a server with a single tool declaration —
every annotated function becomes a tool:
defmodule MyApp.MCP do
use Noizu.MCP.Server, name: "myapp", version: "1.0.0"
tool MyApp.Toolkit
# registration opts apply to every tool in the kit:
# tool MyApp.Toolkit, hidden: true
# tool MyApp.Toolkit, category: "Admin"
# (`name:`/`description:` overrides are not supported for toolkits —
# they are ambiguous across multiple tools)
end@mcp options
:name— wire name; defaults to the function name (server_time→"server_time"):title— human-readable display name:description— tells the model when and why to use the tool:category— grouping label; defaults to the toolkit-levelcategory:useoption. Rides on the wire in_meta.category.:input— input schema as a data-form field spec (keyword list, see below), a raw JSON Schema map, or raw JSON text:output— output schema in the same three forms:input_schema/:output_schema— raw schema only (map or JSON text); never interpreted as a field spec:annotations— behavior-hint keyword list (:read_only_hint, ...):icons,:meta— passed through to the wire definition:hidden—trueomits the tool fromtools/list(still callable):visible—visible: falseis an alias forhidden: true(an explicit:hiddenkey wins when both are given)
Multiple @mcp lines before one function merge into a single option set
(later lines win on key conflict).
Input forms
A keyword list is the data-form field spec — the data equivalent of the
classic input do ... end DSL:
input: [
message: [type: :string, required: true, description: "..."],
repeat: [type: :integer, min: 1, max: 10, default: 1],
mode: [type: :enum, values: [:plain, :loud], default: :plain],
address: [type: :object, fields: [street: [type: :string]]],
tags: [type: {:array, :string}],
rows: [type: {:array, :object}, fields: [id: [type: :integer]]],
note: :string # shorthand: bare type
]Arguments are then validated and delivered atom-keyed with defaults applied and enum values cast to atoms, exactly like the classic DSL.
A map is a raw JSON Schema (string keys); a binary is raw JSON text decoded at compile time (malformed JSON is a compile error). With raw schemas arguments are validated but delivered string-keyed, uncast.
Annotated functions
Annotated functions must be public (def) with arity 0, 1 (args), or 2
(args, ctx) — the runtime trims the standard (args, ctx) invocation to
the declared arity. Return values follow the same contract as
Noizu.MCP.Server.Tool.call/2: {:ok, text | map | Content | ToolResult}
or {:error, ...}; structured map results are checked against the output
schema when one is declared.
Tool names must be unique within a toolkit — duplicates are a compile error.