# `Cairnloop.Web.MCP.ToolProjector`
[🔗](https://github.com/szTheory/cairnloop/blob/main/lib/cairnloop/web/mcp/tool_projector.ex#L1)

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 from `validate_required/2`
  - `cs.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.

# `spec_to_mcp`

```elixir
@spec spec_to_mcp({module(), Cairnloop.Tool.Spec.t()}) :: map()
```

Projects a `{tool_module, %Cairnloop.Tool.Spec{}}` tuple to an MCP tool definition map.

Returns a plain map with string keys. All values are JSON-safe.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
