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]]
# โฆ๐
๐๐๐ฆโง read_file :: auto-generated pointer for public function read_file
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)"
# โฆ๐พ๐ฟ๐ฎ๐ซโง server_time :: auto-generated pointer for public function server_time
def server_time, do: {:ok, to_string(DateTime.utc_now())}
@mcp visible: false
@mcp input: """
{"type": "object", "properties": {"q": {"type": "string"}}}
"""
# โฆ๐๐ ๐
ง๐โง lookup :: auto-generated pointer for public function lookup
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. Accepts a plain string or a verbosity variant list (seeNoizu.MCP.Description):descriptions/:verbosity_map/:runnersโ named description variants and their verbosity/runner selection rules (spec ยง3); seeNoizu.MCP.Description: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).
Evals (@eval)
Attach description-tuning evals (spec ยง4) to a tool with the @eval module
attribute (accumulate: true). Each @eval drains onto the following
@mcp tool, mirroring how @mcp itself is collected โ declare them together,
immediately before the function:
@eval name: :simple_task,
prompt: [%{role: "user", content: "Read config.exs"}],
rubric: [reads_path: "the call passes the requested path"]
@mcp description: "Read a file", input: [path: [type: :string, required: true]]
def read_file(%{path: path}, _ctx), do: File.read(path)Eval specs are compile-time metadata for the mix noizu.mcp.eval harness and
never appear on the wire. See Noizu.MCP.Eval for the spec shape.
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.