Pure total function transform: %Cairnloop.Tool.Spec{} + tool module → MCP tool definition map.
No DB, no side effects, no supervision. Host opts in by mounting
Cairnloop.Web.MCP.Router (D17-08) — this module is called by the Router's
tools/list handler.
MCP tool definition shape
Produces a plain map with string keys matching the MCP 2025-03-26 Tool object:
%{
"name" => "Elixir.Cairnloop.Tools.InternalNote",
"title" => "Add internal note",
"description" => "Appends an operator-only note to the conversation thread.",
"inputSchema" => %{
"type" => "object",
"properties" => %{
"conversation_id" => %{"type" => "string"},
"content" => %{"type" => "string"}
},
"required" => ["conversation_id", "content"]
},
"x-cairnloop-risk-tier" => "low_write",
"x-cairnloop-approval-mode" => "requires_approval"
}inputSchema derivation
inputSchema is derived by calling tool_module.changeset(struct(tool_module), %{})
with empty attrs. This yields:
cs.required— list of required field atoms fromvalidate_required/2cs.types— map of field atom → Ecto type atom
The :id auto-generated field is excluded from properties (Pitfall 2 from RESEARCH.md).
Ecto types are mapped to JSON Schema type strings via a minimal safe mapping table.
Summary
Functions
Projects a {tool_module, %Cairnloop.Tool.Spec{}} tuple to an MCP tool definition map.