# `ConduitMcp.Client.Naming`
[🔗](https://github.com/nyo16/conduit_mcp/blob/v0.11.0/lib/conduit_mcp/client/naming.ex#L1)

Builds the exposed (model-facing) tool names for one or more servers.

`build/2` turns `[{server_id, [%Tool{}]}]` into a `ConduitMcp.Client.ToolSet`
plus a report of every tool it left out, in four steps:

  1. **Spec charset.** A tool whose name is not 1-128 characters of
     `[A-Za-z0-9_.-]` is rejected with reason `:invalid_name`. This catches
     homoglyphs, zero-width and bidi characters, spaces and empty names
     before any mangling can hide them.
  2. **Mangle.** The exposed name is `prefix <> server_id <> "__" <> sanitize(tool)`,
     where `sanitize/1` maps every character outside `[A-Za-z0-9_-]` to `_`.
     With `namespace: false` (a single server) it is `prefix <> sanitize(tool)`.
  3. **Length.** A name over 64 bytes becomes `prefix <> "trunc_" <> hex8`,
     where `hex8` is the first 8 lower-case hex digits of
     `sha256(server_id <> <<0>> <> tool)`.
  4. **Collisions.** When two or more tools mangle to the same exposed name,
     **all** of them are rejected with reason `:collision`. Picking a
     winner would let one server shadow another's tool.

Every exposed name therefore matches `^[A-Za-z0-9_-]{1,64}$`, and the
mapping from accepted tools to exposed names is injective by construction.

## Server ids

A `server_id` is operator configuration, never the self-reported
`serverInfo.name`. It must match `^[a-z][a-z0-9_]{0,31}$`, must not contain
`__` and must not end in `_`. The last two rules make the `__` separator
unambiguous: without them `"a_" <> "__" <> "x"` and `"a" <> "__" <> "_x"`
would both be `"a___x"`. `validate_server_ids!/1` also rejects duplicates.

## Report

`build/2` returns `{toolset, report}` where `report` is

    %{
      rejected: [{server_id, tool_name, :invalid_name | :collision}],
      collisions: [{exposed_name, [{server_id, tool_name}, ...]}]
    }

in input order, so the runtime can emit telemetry and log a warning for
each entry. This module emits nothing itself.

# `reason`

```elixir
@type reason() :: :invalid_name | :collision
```

# `report`

```elixir
@type report() :: %{
  rejected: [{server_id(), String.t(), reason()}],
  collisions: [
    {ConduitMcp.Client.ToolSet.exposed_name(), [{server_id(), String.t()}, ...]}
  ]
}
```

# `server_id`

```elixir
@type server_id() :: String.t()
```

# `build`

```elixir
@spec build([{server_id(), [ConduitMcp.Client.Tool.t()]}], keyword()) ::
  {ConduitMcp.Client.ToolSet.t(), report()}
```

Builds the toolset and its report.

`servers` is `[{server_id, tools}]` in configuration order. Each tool's
`server_id` must equal the server id it is listed under.

## Options

  * `:prefix` - a string of `[A-Za-z0-9_-]` prepended to every exposed
    name, at most 50 bytes. It counts toward the 64-byte
    limit and is kept on truncated names. Default `""`.
  * `:namespace` - `true` (default) puts `server_id <> "__"` in front of
    each tool name. `false` exposes bare (sanitized) tool names and accepts
    exactly one server; its id is then not checked against the server id
    rules.

Raises `ArgumentError` on an invalid or duplicate server id, a bad
`:prefix`, `namespace: false` with more than one server, or a tool whose
`server_id` differs from the one it is listed under.

## Example

    iex> {:ok, a} = ConduitMcp.Client.Tool.from_wire("files", %{"name" => "fs.read", "inputSchema" => %{}})
    iex> {:ok, b} = ConduitMcp.Client.Tool.from_wire("files", %{"name" => "fs_read", "inputSchema" => %{}})
    iex> {:ok, c} = ConduitMcp.Client.Tool.from_wire("files", %{"name" => "list", "inputSchema" => %{}})
    iex> {toolset, report} = ConduitMcp.Client.Naming.build([{"files", [a, b, c]}])
    iex> ConduitMcp.Client.ToolSet.names(toolset)
    ["files__list"]
    iex> report.collisions
    [{"files__fs_read", [{"files", "fs.read"}, {"files", "fs_read"}]}]

# `exposed_name`

```elixir
@spec exposed_name(server_id(), String.t(), keyword()) ::
  ConduitMcp.Client.ToolSet.exposed_name()
```

The exposed name for `server_id`'s tool `tool_name`, before collision
handling. Takes the same `:prefix` and `:namespace` options as `build/2`
but does not validate them.

# `sanitize`

```elixir
@spec sanitize(String.t()) :: String.t()
```

Maps every character outside `[A-Za-z0-9_-]` to `_` (one `_` per
character, not per byte; an invalid UTF-8 sequence also becomes `_`).

    iex> ConduitMcp.Client.Naming.sanitize("fs.read-all")
    "fs_read-all"

# `valid_tool_name?`

```elixir
@spec valid_tool_name?(term()) :: boolean()
```

Whether `name` satisfies the spec tool-name charset: 1-128 characters of
`[A-Za-z0-9_.-]`.

# `validate_server_id`

```elixir
@spec validate_server_id(term()) ::
  :ok
  | {:error,
     :not_a_string | :invalid_format | :double_underscore | :trailing_underscore}
```

Checks one server id against the naming rules.

Returns `:ok` or `{:error, reason}` with `reason` one of `:not_a_string`,
`:invalid_format`, `:double_underscore`, `:trailing_underscore`.

    iex> ConduitMcp.Client.Naming.validate_server_id("github")
    :ok
    iex> ConduitMcp.Client.Naming.validate_server_id("git__hub")
    {:error, :double_underscore}

# `validate_server_ids!`

```elixir
@spec validate_server_ids!([term()]) :: :ok
```

Validates a list of server ids: each must pass `validate_server_id/1` and
none may repeat. Raises `ArgumentError` naming the offending id.

---

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