Backplane. McpProtocol. Client
(backplane_mcp_protocol v0.5.0)
Copy Markdown
MCP (Model Context Protocol) client for connecting to MCP servers.
This module provides a fully functional MCP client with automatic supervision, transport management, and all standard MCP operations. No macros needed — just add it to your supervision tree with the desired configuration.
Usage
Add the client to your supervision tree:
children = [
{Backplane.McpProtocol.Client,
name: MyApp.MCPClient,
transport: {:stdio, command: "uvx", args: ["mcp-server-anthropic"]},
client_info: %{"name" => "MyApp", "version" => "1.0.0"},
capabilities: %{"roots" => %{}},
protocol_version: :auto}
]Use the client by passing the registered name:
{:ok, tools} = Backplane.McpProtocol.Client.list_tools(MyApp.MCPClient)
{:ok, result} = Backplane.McpProtocol.Client.call_tool(MyApp.MCPClient, "search", %{query: "elixir"})Capabilities
Capabilities are passed as a map with string keys:
%{"roots" => %{}, "sampling" => %{}}For convenience, use parse_capability/2 to build from atoms:
capabilities =
[:roots, {:sampling, list_changed?: true}]
|> Enum.reduce(%{}, &Backplane.McpProtocol.Client.parse_capability/2)Transport Configuration
When starting the client, provide transport configuration:
{:stdio, command: "cmd", args: ["arg1", "arg2"]}{:sse, base_url: "http://localhost:8000"}{:websocket, url: "ws://localhost:8000/ws"}{:streamable_http, url: "http://localhost:8000/mcp"}
Process Naming
The :name option controls process registration. You can use any valid
GenServer.name() — an atom, a PID, or a {:via, module, term} tuple:
# Atom name
{Backplane.McpProtocol.Client, name: MyApp.MCPClient, transport: ...}
# For distributed systems with registries (e.g., Horde)
{Backplane.McpProtocol.Client,
name: {:via, Horde.Registry, {MyCluster, "client_1"}},
transport_name: {:via, Horde.Registry, {MyCluster, "transport_1"}},
transport: ...}When using via tuples or other non-atom names, you must explicitly provide
the :transport_name option. For atom names, the transport is automatically
named as Module.concat(ClientName, "Transport").
Dynamic Client Management
For applications that need to manage multiple client connections dynamically
(e.g., user-configured MCP servers), use a DynamicSupervisor:
DynamicSupervisor.start_child(
MyApp.DynamicSupervisor,
{Backplane.McpProtocol.Client,
name: {:via, Registry, {MyApp.Registry, client_id}},
transport_name: {:via, Registry, {MyApp.Registry, {client_id, :transport}}},
transport: {:streamable_http, base_url: url},
client_info: %{"name" => "MyApp", "version" => "1.0.0"},
capabilities: %{},
protocol_version: :auto}
)
Summary
Types
MCP client capabilities
MCP client metadata info
Elicitation callback function type.
Log callback function type.
MCP client initialization options
Progress callback function type.
Root directory specification.
MCP client transport options
Functions
Adds a root directory to the client's roots list.
Blocks until the client has completed protocol negotiation.
Calls a tool on the server.
Cancels all pending requests.
Cancels an in-progress request.
Returns a child specification for starting the client under a supervisor.
Clears all root directories.
Closes the client connection and terminates the process.
Closes a modern subscription with the cancellation mechanism required by its transport.
Requests autocompletion suggestions for prompt arguments or resource URIs.
Returns the protocol preference used when a client does not explicitly pin a version.
Gets a specific prompt from the server.
Returns the negotiated protocol version, era, peer metadata, and negotiation status.
Gets the server's capabilities as reported during initialization.
Gets the server's information as reported during initialization.
Guard to check if an atom is a valid client capability.
Guard to check if a capability is supported by checking map keys.
Lists available prompts from the server.
Lists available resource templates from the server.
Lists available resources from the server.
Gets a list of all root directories.
Lists available tools from the server.
Opens a modern subscriptions/listen stream and waits for its acknowledgement.
Merges additional capabilities into the client's capabilities.
Converts a capability atom or tuple into a map entry.
Sends a legacy ping request to the server to check connection health.
Reads a specific resource from the server.
Registers a callback function to handle elicitation requests from the server.
Registers a callback function to be called when log messages are received.
Registers a callback function to be called when progress notifications are received for the specified progress token.
Registers a callback function to handle sampling requests from the server.
Removes a root directory from the client's roots list.
Sends a progress notification to the server for a long-running operation.
Sets the minimum log level for a legacy server to send log messages.
Starts the client supervision tree (client + transport).
Subscribes to updates for a specific resource URI.
Unregisters the elicitation callback.
Unregisters a previously registered log callback.
Unregisters a previously registered progress callback for the specified token.
Unregisters the sampling callback.
Unsubscribes from updates for a previously-subscribed resource URI.
Types
@type capabilities() :: %{ optional(:roots | String.t()) => %{ optional(:listChanged | String.t()) => boolean() }, optional(:sampling | String.t()) => %{}, optional(:elicitation | String.t()) => %{} }
MCP client capabilities
:roots- Capabilities related to the roots resource:listChanged- Whether the client can handle listChanged notifications
:sampling- Capabilities related to sampling:elicitation- Capabilities related to elicitation (server-initiated user input requests, 2025-06-18)
MCP describes these client capabilities on its specification
@type capabilities_input() :: [ capability() | {capability(), capability_opts()} | map() ]
@type capability() :: :roots | :sampling | :elicitation
@type capability_opts() :: [{:list_changed?, boolean()}]
@type client_info() :: %{ required(:name | String.t()) => String.t(), optional(:version | String.t()) => String.t() }
MCP client metadata info
:name- The name of the client (required):version- The version of the client
@type elicitation_callback() :: (message :: String.t(), requested_schema :: map() -> {:accept, map()} | :decline | :cancel | {:error, String.t()})
Elicitation callback function type.
Called when the server sends an elicitation/create request. The callback
receives the human-readable message and the requestedSchema (a restricted
JSON Schema subset). It must return one of:
{:accept, content}— user submittedcontent(a flat map matching the schema):decline— user explicitly declined:cancel— user dismissed without an explicit choice{:error, reason}— internal error; sent back as a JSON-RPC error
Log callback function type.
Called when log message notifications are received from the server.
Parameters
level- Log level as a string (e.g., "debug", "info", "warning", "error")data- Log message data, typically a map with message detailslogger- Optional logger name identifying the source
Returns
- The return value is ignored
@type option() :: {:name, GenServer.name()} | {:transport, transport()} | {:client_info, map()} | {:capabilities, map()} | {:protocol_version, String.t() | :auto} | GenServer.option()
MCP client initialization options
:name- Following theGenServerpatterns described on "Name registration".:transport- The MCP transport options:client_info- Information about the client:capabilities- Client capabilities to advertise to the MCP server:protocol_version- Protocol preference (:autoby default, or an explicit version string)
Any other option support by GenServer.
@type progress_callback() :: (progress_token :: String.t() | integer(), progress :: number(), total :: number() | nil -> any())
Progress callback function type.
Called when progress notifications are received for a specific progress token.
Parameters
progress_token- String or integer identifier for the progress operationprogress- Current progress valuetotal- Total expected value (nil if unknown)
Returns
- The return value is ignored
Root directory specification.
Represents a root directory that the client has access to.
Fields
:uri- File URI for the root directory (e.g., "file:///home/user/project"):name- Optional human-readable name for the root
@type t() :: GenServer.server()
@type transport() :: [ layer: Backplane.McpProtocol.Transport.STDIO | Backplane.McpProtocol.Transport.SSE | Backplane.McpProtocol.Transport.WebSocket | Backplane.McpProtocol.Transport.StreamableHTTP, name: GenServer.server() ]
MCP client transport options
:layer- The transport layer to use, eitherBackplane.McpProtocol.Transport.STDIO,Backplane.McpProtocol.Transport.SSE,Backplane.McpProtocol.Transport.WebSocket, orBackplane.McpProtocol.Transport.StreamableHTTP(required):name- The transport optional custom name
Functions
Adds a root directory to the client's roots list.
Parameters
client- The client processuri- The URI of the root directory (must start with "file://")name- Optional human-readable name for the rootopts- Additional options:timeout- Request timeout in milliseconds
@spec await_ready( t(), keyword() ) :: :ok | {:error, Backplane.McpProtocol.MCP.Error.t()}
Blocks until the client has completed protocol negotiation.
Returns :ok once modern discovery or legacy initialization succeeds, and
{:error, error} after a terminal negotiation failure. Otherwise, the caller
is parked until negotiation completes or the GenServer call times out.
Options
:timeout- Maximum time to wait in milliseconds (default: 30s)
Examples
{:ok, _supervisor} = Backplane.McpProtocol.Client.start_link(opts)
:ok = Backplane.McpProtocol.Client.await_ready(MyApp.MCPClient, timeout: 10_000)
{:ok, tools} = Backplane.McpProtocol.Client.list_tools(MyApp.MCPClient)
@spec call_tool(t(), String.t(), map() | nil, keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Calls a tool on the server.
Options
:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec cancel_all_requests(t(), String.t(), opts :: Keyword.t()) :: {:ok, [Backplane.McpProtocol.Client.Request.t()]} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Cancels all pending requests.
Parameters
client- The client processreason- Optional reason for cancellation (defaults to "client_cancelled")
Returns
{:ok, requests}- A list of the Request structs that were cancelled{:error, reason}- If an error occurred
@spec cancel_request(t(), String.t(), String.t(), opts :: Keyword.t()) :: :ok | {:error, Backplane.McpProtocol.MCP.Error.t()}
Cancels an in-progress request.
Parameters
client- The client processrequest_id- The ID of the request to cancelreason- Optional reason for cancellation
Returns
:okif the cancellation was successful{:error, reason}if an error occurred{:not_found, request_id}if the request ID was not found
Returns a child specification for starting the client under a supervisor.
This starts a supervision tree containing both the client GenServer and
the configured transport process, linked with a :one_for_all strategy.
Clears all root directories.
Parameters
client- The client processopts- Additional options:timeout- Request timeout in milliseconds
@spec close(t()) :: :ok
Closes the client connection and terminates the process.
@spec close_subscription( t(), Backplane.McpProtocol.Client.Subscription.t(), keyword() ) :: :ok | {:error, Backplane.McpProtocol.MCP.Error.t()}
Closes a modern subscription with the cancellation mechanism required by its transport.
@spec complete(t(), map(), map(), keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Requests autocompletion suggestions for prompt arguments or resource URIs.
Parameters
client- The client processref- Reference to what is being completed (required)- For prompts:
%{"type" => "ref/prompt", "name" => prompt_name} - For resources:
%{"type" => "ref/resource", "uri" => resource_uri}
- For prompts:
argument- The argument being completed (required)%{"name" => arg_name, "value" => current_value}
opts- Additional options:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
Returns
Returns {:ok, response} with completion suggestions if successful, or {:error, reason} if an error occurs.
The response result contains a "completion" object with:
values- List of completion suggestions (maximum 100)total- Optional total number of matching itemshasMore- Boolean indicating if more results are available
@spec default_protocol_preference() :: :auto
Returns the protocol preference used when a client does not explicitly pin a version.
@spec get_prompt(t(), String.t(), map() | nil, keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Gets a specific prompt from the server.
Options
:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
Returns the negotiated protocol version, era, peer metadata, and negotiation status.
Gets the server's capabilities as reported during initialization.
Returns nil if the client has not been initialized yet.
Gets the server's information as reported during initialization.
Returns nil if the client has not been initialized yet.
Guard to check if an atom is a valid client capability.
Guard to check if a capability is supported by checking map keys.
@spec list_prompts( t(), keyword() ) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Lists available prompts from the server.
Options
:cursor- Pagination cursor for continuing a previous request:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec list_resource_templates( t(), keyword() ) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Lists available resource templates from the server.
Options
:cursor- Pagination cursor for continuing a previous request:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec list_resources( t(), keyword() ) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Lists available resources from the server.
Options
:cursor- Pagination cursor for continuing a previous request:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
Gets a list of all root directories.
Parameters
client- The client processopts- Additional options:timeout- Request timeout in milliseconds
@spec list_tools( t(), keyword() ) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Lists available tools from the server.
Options
:cursor- Pagination cursor for continuing a previous request:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec listen_subscriptions(t(), [String.t()] | map(), keyword()) :: {:ok, Backplane.McpProtocol.Client.Subscription.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Opens a modern subscriptions/listen stream and waits for its acknowledgement.
A list of notification method names is accepted as shorthand for the frozen subscription filter. Pass the wire filter map directly when subscribing to individual resource URIs.
The returned handle receives matched notifications in the subscriber process
as {:mcp_subscription, handle, notification}. If the client connection is
lost, the handle closes; call this function again to open a new subscription
with a new ID after reconnecting.
Merges additional capabilities into the client's capabilities.
@spec parse_capability(capability() | {capability(), capability_opts()}, map()) :: map()
Converts a capability atom or tuple into a map entry.
Useful for building capability maps from ergonomic shorthand:
capabilities =
[:roots, {:sampling, list_changed?: true}]
|> Enum.reduce(%{}, &Backplane.McpProtocol.Client.parse_capability/2)
# => %{"roots" => %{}, "sampling" => %{}}
@spec ping( t(), keyword() ) :: :pong | {:error, Backplane.McpProtocol.MCP.Error.t()}
Sends a legacy ping request to the server to check connection health.
Returns :pong if successful. Modern 2026-07-28 peers return a local
:unsupported_operation error because that revision removed ping.
Options
:timeout- Request timeout in milliseconds (default: 30s):meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec read_resource(t(), String.t(), keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Reads a specific resource from the server.
Options
:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client:progress- Progress tracking options:token- A unique token to track progress (string or integer):callback- A function to call when progress updates are received
@spec register_elicitation_callback(t(), elicitation_callback()) :: :ok
Registers a callback function to handle elicitation requests from the server.
The client must advertise the elicitation capability during initialization
for servers to send elicitation/create requests.
Per the MCP specification, the client SHOULD present the request to the user with clear UI, allow them to review and modify their response, and provide decline/cancel options.
@spec register_log_callback(t(), log_callback(), opts :: Keyword.t()) :: :ok
Registers a callback function to be called when log messages are received.
Parameters
client- The client processcallback- A function that takes three arguments: level, data, and logger name
The callback function will be called whenever a log message notification is received.
@spec register_progress_callback( t(), String.t() | integer(), progress_callback(), opts :: Keyword.t() ) :: :ok
Registers a callback function to be called when progress notifications are received for the specified progress token.
Parameters
client- The client processprogress_token- The progress token to watch for (string or integer)callback- A function that takes three arguments: progress_token, progress, and total
The callback function will be called whenever a progress notification with the matching token is received.
Registers a callback function to handle sampling requests from the server.
The callback function will be called when the server sends a sampling/createMessage request.
The callback should implement user approval and return the LLM response.
Callback Function
The callback receives the sampling parameters and must return:
{:ok, response_map}- Where response_map contains:"role"- Usually "assistant""content"- Message content (text, image, or audio)"model"- The model that was used"stopReason"- Why generation stopped (e.g., "endTurn")
{:error, reason}- If the user rejects or an error occurs
Removes a root directory from the client's roots list.
Parameters
client- The client processuri- The URI of the root directory to removeopts- Additional options:timeout- Request timeout in milliseconds
@spec send_progress( t(), String.t() | integer(), number(), number() | nil, opts :: Keyword.t() ) :: :ok | {:error, term()}
Sends a progress notification to the server for a long-running operation.
Parameters
client- The client processprogress_token- The progress token provided in the original request (string or integer)progress- The current progress value (number)total- The optional total value for the operation (number)
Returns :ok if notification was sent successfully, or {:error, reason} otherwise.
@spec set_log_level(t(), String.t()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Sets the minimum log level for a legacy server to send log messages.
Modern 2026-07-28 peers return a local :unsupported_operation error
because that revision removed logging/setLevel.
Parameters
client- The client processlevel- The minimum log level (debug, info, notice, warning, error, critical, alert, emergency)
Returns {:ok, result} if successful, {:error, reason} otherwise.
@spec start_link(keyword()) :: Supervisor.on_start()
Starts the client supervision tree (client + transport).
This is the primary entry point for starting a client. It creates a supervisor that manages both the client GenServer and the transport process.
@spec subscribe_resource(t(), String.t(), keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Subscribes to updates for a specific resource URI.
After a successful subscribe, the server may send notifications/resources/updated
notifications for this URI. The server must declare the resources.subscribe
capability for this method to succeed.
Options
:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client
@spec unregister_elicitation_callback(t()) :: :ok
Unregisters the elicitation callback.
Unregisters a previously registered log callback.
Parameters
client- The client processcallback- The callback function to unregister
Unregisters a previously registered progress callback for the specified token.
Parameters
client- The client processprogress_token- The progress token to stop watching (string or integer)
@spec unregister_sampling_callback(t()) :: :ok
Unregisters the sampling callback.
@spec unsubscribe_resource(t(), String.t(), keyword()) :: {:ok, Backplane.McpProtocol.MCP.Response.t()} | {:error, Backplane.McpProtocol.MCP.Error.t()}
Unsubscribes from updates for a previously-subscribed resource URI.
Options
:timeout- Request timeout in milliseconds:meta- Additional request metadata; protocol-reserved fields are supplied by the client