Ectomancer.Tool (Ectomancer v1.2.1)

Copy Markdown View Source

Custom tool DSL for defining MCP tools.

Tools are defined with:

  • description: What the tool does
  • params: Input parameters (with types and requirements)
  • handle: The function that executes the tool logic

The DSL generates a tool module that integrates with Anubis MCP. Validation is handled internally rather than by Anubis's Peri validator to avoid JSON encoding issues with Peri's tuple-based schema format.

Summary

Functions

Defines authorization for a tool.

Flattens mapped changeset errors into a single map with concatenated messages.

Formats error reasons into proper MCP error format with descriptive messages. This function is called from generated tool modules.

Formats a field name for display in error messages. Converts snake_case to Title Case.

Infers the validation type from changeset errors.

Maps Ecto changeset errors to a structured format suitable for MCP responses.

Defines a new tool within an Ectomancer module.

Functions

authorize(handler)

(macro)

Defines authorization for a tool.

Examples

# Inline function
authorize fn actor, action ->
  actor.role == :admin or action in [:list, :get]
end

# Policy module
authorize with: MyApp.Policies.UserPolicy

# No authorization (public)
authorize :none

flatten_errors(errors)

@spec flatten_errors(%{required(atom()) => [String.t()]}) :: %{
  required(atom()) => String.t()
}

Flattens mapped changeset errors into a single map with concatenated messages.

Examples

errors = %{email: ["can't be blank"], name: ["is invalid"]}
Ectomancer.Tool.flatten_errors(errors)
# %{email: "can't be blank", name: "is invalid"}

format_error(changeset)

@spec format_error(any()) :: {integer(), String.t(), map()}

Formats error reasons into proper MCP error format with descriptive messages. This function is called from generated tool modules.

format_field_name(field)

@spec format_field_name(atom() | String.t()) :: String.t()

Formats a field name for display in error messages. Converts snake_case to Title Case.

infer_validation_type(errors)

@spec infer_validation_type(
  %{required(atom()) => String.t()}
  | %{required(atom()) => [String.t()]}
) ::
  atom()

Infers the validation type from changeset errors.

Accepts either a map with list values (from traverse_errors) or a map with string values.

map_changeset_errors(changeset)

@spec map_changeset_errors(Ecto.Changeset.t()) :: %{required(atom()) => [String.t()]}

Maps Ecto changeset errors to a structured format suitable for MCP responses.

Returns a map where keys are field names (atoms) and values are lists of error strings.

Examples

changeset = %Ecto.Changeset{
  errors: [email: {"can't be blank", []}],
  ...
}

Ectomancer.Tool.map_changeset_errors(changeset)
# %{email: ["can't be blank"]}

tool(name, list)

(macro)

Defines a new tool within an Ectomancer module.

Example

tool :greet do
  description("Greet someone by name")
  param(:name, :string, required: true)

  handle(fn params, _actor ->
    name = params["name"]
    {:ok, "Hello, #{name}!"}
  end)
end

Authorization

Tools can have authorization to control access:

# Inline function
tool :admin_only do
  description("Admin only action")
  authorize(fn actor, _action -> actor.role == :admin end)

  handle(fn _params, _actor ->
    {:ok, "Secret data"}
  end)
end

# Policy module
tool :with_policy do
  description("Uses policy module")
  authorize(with: MyApp.Policies.MyPolicy)

  handle(fn _params, _actor ->
    {:ok, "Protected data"}
  end)
end

# No authorization (public)
tool :public do
  description("Public endpoint")
  authorize(:none)

  handle(fn _params, _actor ->
    {:ok, "Public data"}
  end)
end