An MCP client: one supervised connection to one MCP server, speaking the
modern (2026-07-28) or a legacy (2025-11-25, 2025-06-18) protocol
era, whichever the server supports.
Starting
Add a client to your supervision tree. Starting never blocks on the server: the connection is established in the background.
children = [
{ConduitMcp.Client,
name: MyApp.Docs,
transport: {ConduitMcp.Client.Transport.InProcess, server: MyApp.DocsServer}}
]Options are validated when the child spec is built, so a typo fails at
boot naming the key; see ConduitMcp.Client.Config for all of them.
:name may be any term ({MyApp.Docs, tenant_id} works) and is how
every function below addresses the client. The application starts no
clients itself; it only runs the registry they register in.
Each client is a supervisor (:one_for_all) of a Task.Supervisor, the
owner of its governance table (grants, the approval baseline, built
schemas; see "Governance") and a ConduitMcp.Client.Connection. Use
await_ready/2 when you need the handshake done before going on;
requests made earlier simply wait for it (within their own timeout).
Calling
Every request function runs in the calling process and returns
{:ok, value} or {:error, %ConduitMcp.Client.Error{}}. A tool that ran
and reported failure is not an error: call_tool/4 returns
{:ok, %ConduitMcp.Client.ToolResult{is_error: true}}. Errors are for
protocol, transport and limit failures (match on kind).
Request options, accepted by every request function:
:timeout- milliseconds before the request is cancelled (default the client's:request_timeout, 60 s).:on_progress- a one-arity function, or a pid sent{:mcp_progress, name, params}, fornotifications/progress.:reset_timeout_on_progress- restart:timeouton each progress notification, capped by:max_total_timeout.:max_total_timeout- see above; also caps a deadline paused while the client's handler answers a legacy server request tied to the call.:meta- a map merged into the request's_meta. Keys the protocol reserves (progressToken,io.modelcontextprotocol/*) are dropped.:assigns- a map given toConduitMcp.Client.Handlercallbacks asctx.assignsfor input requests made on behalf of this call (default the client's:assigns).:traceparent- the W3C trace context to send in a modern request's_meta(default: the caller'sLogger.metadata()[:traceparent], as OpenTelemetry-style integrations set it). Dropped unless well formed; atraceparentin:metawins. Onlytraceparentis propagated, nevertracestateorbaggage.
Eras
With the default protocol: :auto the client probes server/discover
and falls back to initialize only on legacy evidence. The era decides
the wire format; the functions here work in both, with these
exceptions: ping/2 sends server/discover to a modern server,
set_log_level/2 is a request in legacy and a per-request _meta value
in modern, and notify_roots_changed/1 is legacy only.
In the modern era call_tool/4, read_resource/3 and get_prompt/4 may
get resultType: "input_required"; the client runs the
multi-round-trip loop (ConduitMcp.Client.Request.MRTR) in the caller,
answering each input request with its ConduitMcp.Client.Handler in the
calling process. An input request the handler does not declare fails the
call with kind: :protocol before any callback runs; a callback that
fails or refuses fails it with kind: :input_handler. Legacy servers ask
with JSON-RPC requests instead, which the connection hands to the handler
in a supervised task (see ConduitMcp.Client.Connection). Both go
through the same safety checks (ConduitMcp.Client.Handler).
Listing and the cache
list_tools/2 and friends fetch one page. list_all_tools/2 and friends
fetch every page (capped: 100 pages, 10 000 items, a repeated or
over-1 KiB cursor fails with kind: :pagination_cycle) and keep the
result in the client's ConduitMcp.Client.Cache, which serves it again
while the server's ttlMs hint says it is fresh. cached_tools/2 and
tool/3 read the cache without asking the server. A result the server
marks private is served only while the client sends the credentials it
was fetched with (set_auth/3 changes them; see "Private results" in
ConduitMcp.Client.Cache).
Every listed tool goes through intake: a tool whose name is outside the
spec charset ([A-Za-z0-9_.-], 1-128 characters), whose definition is
malformed, whose name repeats, or (over HTTP) whose x-mcp-header
annotations are invalid is dropped with a warning and
[:conduit_mcp, :client, :tool, :rejected]; the others carry a
ConduitMcp.Client.Fingerprint.
Tasks
call_tool/4 with task: :return | :await lets a long tool call run as a
server task (the modern tasks extension, or a legacy task-augmented
call); see ConduitMcp.Client.Task, get_task/3, await_task/3 and
cancel_task/3.
Governance
A plain client sends what it is asked to. Configure any of :policy
(ConduitMcp.Client.Policy, with an :on_ask approver and
ConduitMcp.Client.Grants), :hooks (ConduitMcp.Client.Hooks),
:tools (a ConduitMcp.Client.ToolFilter) or :validate
(ConduitMcp.Client.SchemaValidator) and every call_tool/4 runs
through ConduitMcp.Client.Pipeline, in the caller: hidden tools vanish
from listings, pre-hooks and validation run before policy decides on the
final arguments, and post-hooks see the result. Hooks and approvers run
in tasks under the client's Task.Supervisor, never in the connection.
Notifications
subscribe_notifications/2 sends server notifications to a process as
{:mcp_notification, name, method, params}; see
ConduitMcp.Client.Connection for coalescing of list_changed.
subscribe/2 asks for updates of one resource in either era (legacy
resources/subscribe, modern subscriptions/listen); a list_changed
invalidates and refreshes the cached list it names.
Transports
ConduitMcp.Client.Transport.Stdio runs a local server process;
ConduitMcp.Client.Transport.StreamableHTTP (needs :finch) talks to a
remote one; ConduitMcp.Client.Transport.InProcess serves a server module
in this VM for tests. Over HTTP, a modern call_tool/4 sends the tool's
Mcp-Param-* headers computed from the cached tool definition (see
list_all_tools/2); a server rejecting them (-32020) makes the client
refresh the tools and send the call once more.
Summary
Types
The :name a client was started with.
A page of a list request; next_cursor is nil on the last page.
What the server said about itself in the handshake.
Functions
call_tool/4 in a task under the client's Task.Supervisor
(Task.Supervisor.async_nolink/2): await it with Task.await/2 or
Task.yield/2. A crash in the task does not take the caller down.
Exits with :noproc if the client is not running.
Tells a client using OAuth (auth: {:oauth, ...}) that the host stored
new tokens: a connection waiting for interactive authorization checks the
token store now and resumes, instead of on its next request.
ConduitMcp.Client.OAuth.handle_callback/3 calls it when given
client:. Returns :ok without waiting for the check, or
{:error, %Error{kind: :auth, reason: :oauth_not_configured}}.
Waits until the client is ready to send requests. Returns :ok, or
{:error, %Error{kind: :timeout}} after timeout ms, or the :closed
error at once if the connection gave up.
Waits for a server task to finish and returns its tool result, polling at
the task's pollIntervalMs and answering its input requests with the
client's handler (modern) or reading tasks/result (legacy). :timeout
bounds the whole wait (default the client's :request_timeout); when it
passes the task is cancelled and {:error, %Error{kind: :timeout}}
returned. :assigns is passed to the handler.
The tools of the last list_all_tools/2 (or list_changed refresh) as
cached, fresh or not, without asking the server.
{:error, %Error{kind: :not_found, reason: :not_cached}} before the first
listing. Takes :assigns and :principal (see list_tools/2).
Calls tool name with args (string- or atom-keyed; sent as given).
Asks the server to cancel a task (tasks/cancel). Cancellation is
cooperative: the task may still finish.
The child spec for a client. Validates the options (raising
ArgumentError naming a bad key); the id is {ConduitMcp.Client, name},
so one supervisor can hold many clients.
Closes the connection: pending requests fail with kind: :closed, the
transport is closed, and the client stays in :closed (answering every
request at once) until reconnect/1.
Asks for completions of argument of ref. Returns the server's
string-keyed CompleteResult ("completion").
The negotiated protocol era (:modern or :legacy), kept while the
client reconnects. {:error, %Error{kind: :unavailable}} before the first
handshake.
Gets prompt name with args. Prompt arguments are strings on the wire,
so keys and values are converted: atoms, numbers and booleans with
to_string/1, other non-strings as JSON. nil values are dropped.
Returns the server's string-keyed GetPromptResult ("messages").
Reads the current state of a server task (tasks/get).
The server's instructions as they may be put in a model's context
(PI-1): {:ok, nil} unless the client was started with
include_instructions: true (they arrive with the handshake and are
written by the server, so they are kept out of system prompts by default).
When included they are capped at 4 KiB with invisible and
direction-changing characters removed, and, unless the client is
trusted: true, prefixed with a line naming the server they came from.
server_info/1 still returns them raw.
Lists every prompt through the cache, as list_all_tools/2 does for
tools.
Lists every resource template through the cache, as list_all_tools/2
does for tools.
Lists every resource through the cache, as list_all_tools/2 does for
tools.
Lists every tool, following nextCursor, through the client's
ConduitMcp.Client.Cache: a fresh cached list is returned without asking
the server; otherwise the connection refreshes it (one refresh at a time,
shared by concurrent callers) and the new list is returned and cached.
Lists one page of prompts (:cursor as in list_tools/2).
Lists one page of resource templates, as the server's string-keyed
ResourceTemplate maps (:cursor as in list_tools/2).
Lists one page of resources (:cursor as in list_tools/2).
Lists one page of tools. Pass the previous page's next_cursor as
:cursor for the next one. Tools that fail intake are left out (see
"Listing and the cache" above); the page is never cached.
Sends notifications/roots/list_changed (legacy only; the client
declares roots.listChanged when its handler implements list_roots/1).
In the modern era, or without a roots handler, returns
{:error, %Error{kind: :protocol}}.
Checks the server answers: a legacy ping, or server/discover for a
modern server (which has no ping).
Reads the resource at uri; returns the server's string-keyed
ReadResourceResult ("contents").
Drops the current connection (pending requests fail with
kind: :transport) and connects again at once, with the reconnect count
reset. Also revives a :closed client.
What the server reported in the handshake: its ServerInfo, capabilities
and instructions, with the negotiated era and version.
{:error, %Error{kind: :unavailable, reason: :not_ready}} while the
client is not ready.
Replaces the credentials the client sends with auth, any form the
transport's :auth option takes (ConduitMcp.Client.Transport.StreamableHTTP:
nil, {:bearer, token}, a one-arity function, {:oauth, opts},
{:client_credentials, opts}), checked like that option. Raises
ArgumentError when it is invalid, or when the transport takes no
:auth.
Sets the server's log level ("debug" … "emergency", string or atom).
Legacy: sends logging/setLevel. Modern: the level is sent in the
_meta of every later request; a modern server sends no log
notifications for requests without one.
Starts a client (see ConduitMcp.Client.Config for the options). Returns
once the processes exist, without waiting for the server.
The connection's current state, read without messaging it.
Subscribes to updates of the resource at uri, whichever era the server
speaks. Updates arrive as {:mcp_notification, name, "notifications/resources/updated", %{"uri" => uri}} to the processes
subscribed with subscribe_notifications/2.
Sends server notifications to pid (default: the caller) as
{:mcp_notification, name, method, params}. Idempotent; the subscription
ends when pid exits or unsubscribe_notifications/2 is called.
notifications/progress goes to the request's caller instead.
The cached definition of tool name (one cache row; the server is not
asked). {:error, %Error{kind: :not_found}} when the last listing did not
include it, or nothing was listed yet, or (on a governed client) the tool
is hidden. Takes :assigns and :principal (see list_tools/2).
Ends a subscribe/2 subscription to uri (legacy
resources/unsubscribe; modern: a listen without uri).
Stops sending server notifications to pid (default: the caller).
Types
@type client() :: term()
The :name a client was started with.
@type page(key, item) :: %{required(key) => [item], next_cursor: String.t() | nil}
A page of a list request; next_cursor is nil on the last page.
@type server_info() :: %{ era: :modern | :legacy, version: String.t(), server_info: ConduitMcp.Client.ServerInfo.t() | nil, capabilities: map(), instructions: String.t() | nil }
What the server said about itself in the handshake.
@type status() :: %{ state: :connecting | :discovering | :initializing | :ready | :authorizing | :reconnecting | :closed, era: :modern | :legacy | nil, version: String.t() | nil, reason: term() }
The connection's state as status/1 reports it. reason explains a
:closed state and is nil otherwise.
Functions
call_tool/4 in a task under the client's Task.Supervisor
(Task.Supervisor.async_nolink/2): await it with Task.await/2 or
Task.yield/2. A crash in the task does not take the caller down.
Exits with :noproc if the client is not running.
task = ConduitMcp.Client.async_call_tool(:docs, "echo", %{"message" => "hi"})
{:ok, %ConduitMcp.Client.ToolResult{}} = Task.await(task)
@spec authorized(client()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Tells a client using OAuth (auth: {:oauth, ...}) that the host stored
new tokens: a connection waiting for interactive authorization checks the
token store now and resumes, instead of on its next request.
ConduitMcp.Client.OAuth.handle_callback/3 calls it when given
client:. Returns :ok without waiting for the check, or
{:error, %Error{kind: :auth, reason: :oauth_not_configured}}.
:ok = ConduitMcp.Client.authorized(:docs)
@spec await_ready(client(), timeout()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Waits until the client is ready to send requests. Returns :ok, or
{:error, %Error{kind: :timeout}} after timeout ms, or the :closed
error at once if the connection gave up.
:ok = ConduitMcp.Client.await_ready(:docs, 5_000)
@spec await_task(client(), ConduitMcp.Client.Task.t() | String.t(), keyword()) :: {:ok, ConduitMcp.Client.ToolResult.t()} | {:error, ConduitMcp.Client.Error.t()}
Waits for a server task to finish and returns its tool result, polling at
the task's pollIntervalMs and answering its input requests with the
client's handler (modern) or reading tasks/result (legacy). :timeout
bounds the whole wait (default the client's :request_timeout); when it
passes the task is cancelled and {:error, %Error{kind: :timeout}}
returned. :assigns is passed to the handler.
{:ok, %ConduitMcp.Client.ToolResult{}} = ConduitMcp.Client.await_task(:docs, task)
@spec cached_tools(client(), keyword()) :: {:ok, [ConduitMcp.Client.Tool.t()]} | {:error, ConduitMcp.Client.Error.t()}
The tools of the last list_all_tools/2 (or list_changed refresh) as
cached, fresh or not, without asking the server.
{:error, %Error{kind: :not_found, reason: :not_cached}} before the first
listing. Takes :assigns and :principal (see list_tools/2).
{:ok, tools} = ConduitMcp.Client.cached_tools(:docs)
@spec call_tool(client(), String.t(), map(), keyword()) :: {:ok, ConduitMcp.Client.ToolResult.t() | ConduitMcp.Client.Task.t()} | {:error, ConduitMcp.Client.Error.t()}
Calls tool name with args (string- or atom-keyed; sent as given).
A tool that ran and failed is {:ok, %ToolResult{is_error: true}}.
Tasks
task: :return- if the server runs the call as a task, return{:ok, %ConduitMcp.Client.Task{}}at once (poll it withget_task/3,await_task/3).task: :await- if the server runs the call as a task, poll it until it finishes and return itsToolResult.:timeoutbounds the whole wait; when it passes the task is cancelled (tasks/cancel), as it is if the calling process dies.task_ttl: ms- the legacytask.ttlto request.
Modern servers decide per call whether to create a task; the client
declares the tasks extension only when :task is given, so without it the
server must answer directly. Legacy servers get a task-augmented call only
when they declare tasks.requests.tools.call and the tool's
execution.taskSupport allows it: "required" always (awaited when no
:task is given), "optional" only with :task. With :task, a call
made before the first handshake waits for it (within :timeout), since
the era decides how the call becomes a task; without it the call is
queued as usual and sent plain.
{:ok, %ConduitMcp.Client.ToolResult{content: [%{"text" => "hi"}]}} =
ConduitMcp.Client.call_tool(:docs, "echo", %{"message" => "hi"})Governance
When the client is configured with :policy, :hooks, :tools or
:validate, or the call passes :shape, the call runs through
ConduitMcp.Client.Pipeline (selection, pre-hooks, validation, policy
and approval, post-hooks); otherwise it is sent as given. Pipeline
options:
:principal- who the call is made for (inctx, and the key of "always allow" grants). Ignored by a plain client.:shape-:llmto shape the result for a model.
A governed call may also fail with kind: :not_found (hidden or unknown
tool), :denied (policy, approver or hook) or :hook (a pre-hook
failed).
@spec cancel_task(client(), ConduitMcp.Client.Task.t() | String.t(), keyword()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Asks the server to cancel a task (tasks/cancel). Cancellation is
cooperative: the task may still finish.
:ok = ConduitMcp.Client.cancel_task(:docs, task)
@spec child_spec(keyword()) :: Supervisor.child_spec()
The child spec for a client. Validates the options (raising
ArgumentError naming a bad key); the id is {ConduitMcp.Client, name},
so one supervisor can hold many clients.
iex> spec =
...> ConduitMcp.Client.child_spec(
...> name: :docs,
...> transport: {ConduitMcp.Client.Transport.InProcess, server: ConduitMcp.TestServer}
...> )
iex> spec.id
{ConduitMcp.Client, :docs}
@spec close(client()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Closes the connection: pending requests fail with kind: :closed, the
transport is closed, and the client stays in :closed (answering every
request at once) until reconnect/1.
:ok = ConduitMcp.Client.close(:docs)
@spec complete( client(), {:prompt | :resource, String.t()} | map(), {String.t(), String.t()} | map(), keyword() ) :: {:ok, map()} | {:error, ConduitMcp.Client.Error.t()}
Asks for completions of argument of ref. Returns the server's
string-keyed CompleteResult ("completion").
ref is {:prompt, name}, {:resource, uri_template} or a wire
reference map; argument is {name, value} or a wire map.
Options
The request options, plus :context - already-resolved arguments,
%{"arguments" => %{...}}.
{:ok, %{"completion" => %{"values" => _}}} =
ConduitMcp.Client.complete(:docs, {:prompt, "greeting"}, {"name", "A"})
@spec era(client()) :: {:ok, :modern | :legacy} | {:error, ConduitMcp.Client.Error.t()}
The negotiated protocol era (:modern or :legacy), kept while the
client reconnects. {:error, %Error{kind: :unavailable}} before the first
handshake.
{:ok, :modern} = ConduitMcp.Client.era(:docs)
@spec get_prompt(client(), String.t(), map(), keyword()) :: {:ok, map()} | {:error, ConduitMcp.Client.Error.t()}
Gets prompt name with args. Prompt arguments are strings on the wire,
so keys and values are converted: atoms, numbers and booleans with
to_string/1, other non-strings as JSON. nil values are dropped.
Returns the server's string-keyed GetPromptResult ("messages").
On a governed client the :on_prompt_get hooks (which may rewrite
args) and the policy's prompt rules run first, as for
read_resource/3.
{:ok, %{"messages" => [_ | _]}} =
ConduitMcp.Client.get_prompt(:docs, "greeting", %{name: "Ada"})
@spec get_task(client(), ConduitMcp.Client.Task.t() | String.t(), keyword()) :: {:ok, ConduitMcp.Client.Task.t()} | {:error, ConduitMcp.Client.Error.t()}
Reads the current state of a server task (tasks/get).
{:ok, %ConduitMcp.Client.Task{status: :working}} = ConduitMcp.Client.get_task(:docs, task)
@spec instructions_for_llm(client()) :: {:ok, String.t() | nil} | {:error, ConduitMcp.Client.Error.t()}
The server's instructions as they may be put in a model's context
(PI-1): {:ok, nil} unless the client was started with
include_instructions: true (they arrive with the handshake and are
written by the server, so they are kept out of system prompts by default).
When included they are capped at 4 KiB with invisible and
direction-changing characters removed, and, unless the client is
trusted: true, prefixed with a line naming the server they came from.
server_info/1 still returns them raw.
{:ok, nil} = ConduitMcp.Client.instructions_for_llm(:docs)
@spec list_all_prompts(client(), keyword()) :: {:ok, [ConduitMcp.Client.Prompt.t()]} | {:error, ConduitMcp.Client.Error.t()}
Lists every prompt through the cache, as list_all_tools/2 does for
tools.
{:ok, [%ConduitMcp.Client.Prompt{} | _]} = ConduitMcp.Client.list_all_prompts(:docs)
@spec list_all_resource_templates(client(), keyword()) :: {:ok, [map()]} | {:error, ConduitMcp.Client.Error.t()}
Lists every resource template through the cache, as list_all_tools/2
does for tools.
{:ok, [%{"uriTemplate" => _} | _]} = ConduitMcp.Client.list_all_resource_templates(:docs)
@spec list_all_resources(client(), keyword()) :: {:ok, [ConduitMcp.Client.Resource.t()]} | {:error, ConduitMcp.Client.Error.t()}
Lists every resource through the cache, as list_all_tools/2 does for
tools.
{:ok, [%ConduitMcp.Client.Resource{} | _]} = ConduitMcp.Client.list_all_resources(:docs)
@spec list_all_tools(client(), keyword()) :: {:ok, [ConduitMcp.Client.Tool.t()]} | {:error, ConduitMcp.Client.Error.t()}
Lists every tool, following nextCursor, through the client's
ConduitMcp.Client.Cache: a fresh cached list is returned without asking
the server; otherwise the connection refreshes it (one refresh at a time,
shared by concurrent callers) and the new list is returned and cached.
Options
:timeout- per page, and the longest the caller waits (default the client's:request_timeout). A refresh the caller stopped waiting for still completes and fills the cache.:refresh-trueto ignore a fresh cached list (defaultfalse).:assigns,:principal- for a governed client's filter and hooks.
Pagination is capped (100 pages, 10 000 items, a repeated or over-1 KiB
cursor): over a cap the result is {:error, %Error{kind: :pagination_cycle}} and nothing is cached.
{:ok, [%ConduitMcp.Client.Tool{} | _]} = ConduitMcp.Client.list_all_tools(:docs)
@spec list_prompts(client(), keyword()) :: {:ok, page(:prompts, ConduitMcp.Client.Prompt.t())} | {:error, ConduitMcp.Client.Error.t()}
Lists one page of prompts (:cursor as in list_tools/2).
{:ok, %{prompts: [%ConduitMcp.Client.Prompt{name: "greeting"} | _]}} =
ConduitMcp.Client.list_prompts(:docs)
@spec list_resource_templates(client(), keyword()) :: {:ok, page(:resource_templates, map())} | {:error, ConduitMcp.Client.Error.t()}
Lists one page of resource templates, as the server's string-keyed
ResourceTemplate maps (:cursor as in list_tools/2).
{:ok, %{resource_templates: [%{"uriTemplate" => "item://{id}"} | _]}} =
ConduitMcp.Client.list_resource_templates(:docs)
@spec list_resources(client(), keyword()) :: {:ok, page(:resources, ConduitMcp.Client.Resource.t())} | {:error, ConduitMcp.Client.Error.t()}
Lists one page of resources (:cursor as in list_tools/2).
{:ok, %{resources: [%ConduitMcp.Client.Resource{} | _]}} =
ConduitMcp.Client.list_resources(:docs)
@spec list_tools(client(), keyword()) :: {:ok, page(:tools, ConduitMcp.Client.Tool.t())} | {:error, ConduitMcp.Client.Error.t()}
Lists one page of tools. Pass the previous page's next_cursor as
:cursor for the next one. Tools that fail intake are left out (see
"Listing and the cache" above); the page is never cached.
On a governed client (see call_tool/4) the page holds only the tools
the :tools filter shows (function patterns see :assigns and
:principal), with a usable schema when :validate is on, after the
:on_tools_listed hooks (ConduitMcp.Client.Hooks); the same applies to
list_all_tools/2, cached_tools/2 and tool/3.
{:ok, %{tools: [%ConduitMcp.Client.Tool{name: "echo"} | _], next_cursor: nil}} =
ConduitMcp.Client.list_tools(:docs)
@spec notify_roots_changed(client()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Sends notifications/roots/list_changed (legacy only; the client
declares roots.listChanged when its handler implements list_roots/1).
In the modern era, or without a roots handler, returns
{:error, %Error{kind: :protocol}}.
:ok = ConduitMcp.Client.notify_roots_changed(:docs)
@spec ping(client(), keyword()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Checks the server answers: a legacy ping, or server/discover for a
modern server (which has no ping).
:ok = ConduitMcp.Client.ping(:docs, timeout: 1_000)
@spec read_resource(client(), String.t(), keyword()) :: {:ok, map()} | {:error, ConduitMcp.Client.Error.t()}
Reads the resource at uri; returns the server's string-keyed
ReadResourceResult ("contents").
On a governed client the :on_resource_read hooks run first, then the
policy's resource rules when its :scope includes :resources (an ask
rule denies: reads have no approver); :principal is passed to them.
{:ok, %{"contents" => [%{"text" => _} | _]}} =
ConduitMcp.Client.read_resource(:docs, "test://resource1")
@spec reconnect(client()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Drops the current connection (pending requests fail with
kind: :transport) and connects again at once, with the reconnect count
reset. Also revives a :closed client.
:ok = ConduitMcp.Client.reconnect(:docs)
@spec server_info(client()) :: {:ok, server_info()} | {:error, ConduitMcp.Client.Error.t()}
What the server reported in the handshake: its ServerInfo, capabilities
and instructions, with the negotiated era and version.
{:error, %Error{kind: :unavailable, reason: :not_ready}} while the
client is not ready.
{:ok, %{capabilities: %{"tools" => _}}} = ConduitMcp.Client.server_info(:docs)
@spec set_auth(client(), term(), keyword()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Replaces the credentials the client sends with auth, any form the
transport's :auth option takes (ConduitMcp.Client.Transport.StreamableHTTP:
nil, {:bearer, token}, a one-arity function, {:oauth, opts},
{:client_credentials, opts}), checked like that option. Raises
ArgumentError when it is invalid, or when the transport takes no
:auth.
Everything bound to the old credentials ends: the client reconnects as
reconnect/1 does (requests in flight fail with kind: :transport,
listen streams and a legacy session close, an OAuth authorization in
progress is abandoned). Cached results the server marked private are
deleted before this returns when the credentials' identity changes, and
fetched again under the new ones; public results stay (see "Private
results" in ConduitMcp.Client.Cache). A :closed client stays closed.
OAuth options naming no :store keep the client's current token store;
change :principal to act for another user (tokens are stored per
principal). The change lasts as long as the client process: a client
restarted by its supervisor starts with its configured credentials.
:ok = ConduitMcp.Client.set_auth(:docs, {:bearer, new_token})Options:
:credential_id- with an:authfunction, any term naming whose tokens it returns (the transport's:credential_id). Without it, private results fetched with the function are never served from the cache.
@spec set_log_level(client(), String.t() | atom()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Sets the server's log level ("debug" … "emergency", string or atom).
Legacy: sends logging/setLevel. Modern: the level is sent in the
_meta of every later request; a modern server sends no log
notifications for requests without one.
:ok = ConduitMcp.Client.set_log_level(:docs, :warning)
@spec start_link(keyword() | ConduitMcp.Client.Config.t()) :: Supervisor.on_start()
Starts a client (see ConduitMcp.Client.Config for the options). Returns
once the processes exist, without waiting for the server.
{:ok, _pid} =
ConduitMcp.Client.start_link(
name: :docs,
transport: {ConduitMcp.Client.Transport.InProcess, server: MyApp.DocsServer}
)
@spec status(client()) :: {:ok, status()} | {:error, ConduitMcp.Client.Error.t()}
The connection's current state, read without messaging it.
{:ok, %{state: :ready, era: :modern}} = ConduitMcp.Client.status(:docs)
@spec subscribe(client(), String.t(), keyword()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Subscribes to updates of the resource at uri, whichever era the server
speaks. Updates arrive as {:mcp_notification, name, "notifications/resources/updated", %{"uri" => uri}} to the processes
subscribed with subscribe_notifications/2.
- Legacy: sends
resources/subscribe(the server must declareresources.subscribe, elsekind: :protocol, reason:subscriptions_not_supported) and answers with the server's answer. - Modern: adds
urito thesubscriptions/listenfilter (a new listen replaces the old one once the server acknowledges it) and answers:okwhen the acknowledgment honoursuri, elsekind: :protocol, reason:subscription_not_acknowledged.
The subscription is kept across reconnects and session renewals (sent
again each time the connection becomes ready). A call made before the
connection is ready waits for it, up to :timeout (default the client's
:request_timeout).
:ok = ConduitMcp.Client.subscribe(:docs, "file:///notes.md")
@spec subscribe_notifications(client(), pid()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Sends server notifications to pid (default: the caller) as
{:mcp_notification, name, method, params}. Idempotent; the subscription
ends when pid exits or unsubscribe_notifications/2 is called.
notifications/progress goes to the request's caller instead.
:ok = ConduitMcp.Client.subscribe_notifications(:docs)
receive do
{:mcp_notification, :docs, "notifications/tools/list_changed", _params} -> :refresh
end
@spec tool(client(), String.t(), keyword()) :: {:ok, ConduitMcp.Client.Tool.t()} | {:error, ConduitMcp.Client.Error.t()}
The cached definition of tool name (one cache row; the server is not
asked). {:error, %Error{kind: :not_found}} when the last listing did not
include it, or nothing was listed yet, or (on a governed client) the tool
is hidden. Takes :assigns and :principal (see list_tools/2).
{:ok, %ConduitMcp.Client.Tool{name: "echo"}} = ConduitMcp.Client.tool(:docs, "echo")
@spec unsubscribe(client(), String.t(), keyword()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Ends a subscribe/2 subscription to uri (legacy
resources/unsubscribe; modern: a listen without uri).
:ok = ConduitMcp.Client.unsubscribe(:docs, "file:///notes.md")
@spec unsubscribe_notifications(client(), pid()) :: :ok | {:error, ConduitMcp.Client.Error.t()}
Stops sending server notifications to pid (default: the caller).
:ok = ConduitMcp.Client.unsubscribe_notifications(:docs)