Noizu.MCP.Server.Toolkit (Noizu MCP v0.1.6)

Copy Markdown View Source

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"] || ""}
end

Register 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 (see Noizu.MCP.Description)
  • :descriptions / :verbosity_map / :runners โ€” named description variants and their verbosity/runner selection rules (spec ยง3); see Noizu.MCP.Description
  • :category โ€” grouping label; defaults to the toolkit-level category: use option. 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 โ€” true omits the tool from tools/list (still callable)
  • :visible โ€” visible: false is an alias for hidden: true (an explicit :hidden key 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.