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

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]]
  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"] || ""}
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
  • :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
  • :hiddentrue omits the tool from tools/list (still callable)
  • :visiblevisible: 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).

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.