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:
- 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. - Mangle. The exposed name is
prefix <> server_id <> "__" <> sanitize(tool), wheresanitize/1maps every character outside[A-Za-z0-9_-]to_. Withnamespace: false(a single server) it isprefix <> sanitize(tool). - Length. A name over 64 bytes becomes
prefix <> "trunc_" <> hex8, wherehex8is the first 8 lower-case hex digits ofsha256(server_id <> <<0>> <> tool). - 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.
Summary
Functions
Builds the toolset and its report.
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.
Maps every character outside [A-Za-z0-9_-] to _ (one _ per
character, not per byte; an invalid UTF-8 sequence also becomes _).
Whether name satisfies the spec tool-name charset: 1-128 characters of
[A-Za-z0-9_.-].
Checks one server id against the naming rules.
Validates a list of server ids: each must pass validate_server_id/1 and
none may repeat. Raises ArgumentError naming the offending id.
Types
Functions
@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) putsserver_id <> "__"in front of each tool name.falseexposes 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"}]}]
@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.
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"
Whether name satisfies the spec tool-name charset: 1-128 characters of
[A-Za-z0-9_.-].
@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}
@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.