ConduitMcp.Client.Adapters (ConduitMCP v0.11.0)

Copy Markdown View Source

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.

Summary

Types

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.

Functions

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").

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

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).

Types

listing()

@type listing() :: %{
  tools: [map()],
  toolset: ConduitMcp.Client.ToolSet.t(),
  null_paths: %{
    optional(String.t()) => [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()

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

Functions

execute_tool_calls(hub_or_client, provider, calls_or_response, opts \\ [])

@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.ToolCalls 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(hub_or_client, provider, opts \\ [])

@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}}:

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(hub_or_client, opts \\ [])

@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).