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

The agent-loop side of the LLM adapters: list a hub's (or one client's)
tools in a provider's format, then run the tool calls a model made and
answer them in the same format.

    {:ok, listing} = ConduitMcp.Client.Adapters.tools(MyApp.Hub, :anthropic)
    request = %{"model" => model, "messages" => messages, "tools" => listing.tools}

    # ... the response stops with "tool_use" ...
    case ConduitMcp.Client.Adapters.execute_tool_calls(MyApp.Hub, :anthropic, response,
           toolset: listing.toolset, principal: user_id) do
      {:ok, results} -> continue_with(%{"role" => "user", "content" => results})
      {:halt, results} -> stop_the_loop(results)
    end

The provider modules (`ConduitMcp.Client.Adapters.Anthropic`, `OpenAI`,
`Gemini`) stay pure; this module adds what needs processes: listing,
running calls concurrently through `ConduitMcp.Client.Pipeline`, and
telemetry.

## Hub or client

`hub_or_client` is a `ConduitMcp.Client.Hub` name (tools are named
`server__tool`, see `ConduitMcp.Client.Hub`) or a `ConduitMcp.Client`
name (tools keep their own, sanitized names: a `namespace: false`
toolset). A name registered as both is treated as the hub.

Both are governed: a hub always has a policy (R6). A client without
`:policy`, `:hooks`, `:tools` or `:validate` has no policy, so model
calls through it are sent as asked; configure a policy on clients you
hand to a model.

## Results

`execute_tool_calls/4` answers every call in the model's order, each in
the provider's result shape keyed by the call's id (`call.call_id`; for
Gemini also the exposed name, via `ConduitMcp.Client.ToolSet.exposed_name/3`):

  * a call to a name the toolset does not hold, or with non-object
    arguments, gets the provider adapter's error answer and sends
    nothing (HB-2);
  * a call that completes gets its `ConduitMcp.Client.ToolResult`
    (`is_error` results included), shaped for a model (`shape: :llm`,
    PI-2) unless `shape: nil` is given;
  * a call that fails (denied, timed out, server unavailable, ...) gets
    an error answer with the `ConduitMcp.Client.Error` message.

When an approver answered `{:deny, reason, :halt}` for any call, the
return is `{:halt, results}`: answer the model if you like, but stop the
agent loop. Otherwise it is `{:ok, results}`.

## Taint (HB-3)

The calls of one turn run concurrently and see the same `:taint`. Once
their results are in the model's context, add their servers
(`call.server_id`) to the next turn's `taint:` so
`ConduitMcp.Client.Policy.tainted/1` rules can react.

# `listing`

```elixir
@type listing() :: %{
  tools: [map()],
  toolset: ConduitMcp.Client.ToolSet.t(),
  null_paths: %{
    optional(String.t()) =&gt; [ConduitMcp.Client.Adapters.OpenAI.null_path()]
  }
}
```

What `tools/3` returns: the provider's tool definitions, the toolset they
were built from (pass it to `execute_tool_calls/4` as `:toolset`) and,
for OpenAI strict mode, the `null_paths` to pass as `:null_paths`.

# `provider`

```elixir
@type provider() :: :anthropic | :openai | :gemini
```

# `execute_tool_calls`

```elixir
@spec execute_tool_calls(term(), provider(), term(), keyword()) ::
  {:ok, [map()]} | {:halt, [map()]} | {:error, ConduitMcp.Client.Error.t()}
```

Runs the tool calls in a model response through the governed pipeline and
returns the provider-shaped answers in the model's order (see
"Results").

`calls_or_response` is what the provider adapter parses (a response,
message, content/part list or output item list), or a list of
`ConduitMcp.Client.ToolCall`s for tools in the toolset.

## Options

  * `:toolset` - the toolset the model was offered (from `tools/3`);
    built with `toolset/2` when absent. Calls resolve against it only.
  * `:api` - `:chat | :responses`, required for `:openai`.
  * `:null_paths` - OpenAI strict-mode null paths (from `tools/3`).
  * `:max_concurrency` - calls run at once (default
    `System.schedulers_online/0`).
  * `:call_timeout` - milliseconds one call may take in all, approval and
    hooks included (default 300 000); a call over it is killed and
    answered with a `:timeout` error.
  * `:structured`, `:error_prefix` - passed to the provider's result
    builder.
  * `:shape` - `:llm` (default) or `nil`.
  * every other option goes to `ConduitMcp.Client.Pipeline.run/3`
    (`:principal`, `:assigns`, `:taint`, `:timeout`, `:meta`, progress
    options). `:task` is not supported.

# `tools`

```elixir
@spec tools(term(), provider(), keyword()) ::
  {:ok, listing()} | {:error, ConduitMcp.Client.Error.t()}
```

Lists `hub_or_client`'s tools (`toolset/2`) in `provider`'s format.

Returns `{:ok, %{tools: definitions, toolset: toolset, null_paths: map}}`:

  * `:anthropic` - `ConduitMcp.Client.Adapters.Anthropic.tools/2`
    (`:cache_control`);
  * `:openai` - `ConduitMcp.Client.Adapters.OpenAI.tools/2` (`:api`
    required, `:strict`); each tool sent non-strict under `strict: :auto`
    emits `[:conduit_mcp, :client, :adapter, :strict_fallback]`;
  * `:gemini` - `ConduitMcp.Client.Adapters.Gemini.function_declarations/1`
    (wrap them in `%{"functionDeclarations" => tools}`).

Only tools in the toolset are emitted, so filtered, colliding and down
servers' tools never reach the model (PO-7). Takes the `toolset/2`
options too.

# `toolset`

```elixir
@spec toolset(term(), keyword()) ::
  {:ok, ConduitMcp.Client.ToolSet.t()} | {:error, ConduitMcp.Client.Error.t()}
```

The toolset of `hub_or_client` for a model: `ConduitMcp.Client.Hub.list_tools/2`
for a hub; for a client, its presented cached tools
(`ConduitMcp.Client.cached_tools/2`, listing them first when nothing is
cached) under their own names (`namespace: false`).

## Options

`:assigns`, `:principal`, `:timeout`, `:refresh` (hub only) and `:prefix`
(client only; a hub uses its own `:prefix`).

---

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