Unified MCP client combining the best features of all implementations.
This module provides a clean, consistent API for interacting with MCP servers while maintaining backward compatibility with existing code.
Features
- Simple connection with URL strings or transport specs
- Automatic transport fallback via TransportManager
- Automatic reconnection with exponential backoff after unexpected
transport closure (see
start_link/1) - Consistent return values with optional normalization
- Convenience methods for common operations
- Clean separation of concerns
Examples
# Connect with URL
{:ok, client} = ExMCP.Client.connect("http://localhost:8080/mcp")
# Connect with transport spec
{:ok, client} = ExMCP.Client.start_link(
transport: :stdio,
command: "mcp-server"
)
# List and call tools
{:ok, %{"tools" => tools}} = ExMCP.Client.list_tools(client)
{:ok, result} = ExMCP.Client.call_tool(client, "weather", %{location: "NYC"})
Summary
Functions
Sends a batch of requests to the server.
Convenience alias for call_tool/4.
Calls a tool with the given arguments.
Requests cooperative cancellation of a task.
Returns a specification to start this module under a supervisor.
Clears all remembered protocol-era observations.
Requests completion suggestions from the server.
Connects to an MCP server using a URL or connection spec.
Disconnects the client gracefully, cleaning up all resources.
Discovers a modern MCP server's versions, capabilities, and identity.
Finds a matching tool from a list of tools.
Finds a tool by name or pattern.
Gets the list of pending request IDs.
Gets a prompt with the given arguments.
Gets the client status.
Reads the current full state of a task.
Lists available prompts.
Lists available resource templates.
Lists available resources.
Lists available roots.
Lists available tools from the server.
Opens a modern immutable notification subscription and waits for the server's acknowledgment.
Sends a log message to the server as a notification.
Sends a log message with additional data to the server as a notification.
Gets the negotiated protocol version with the server.
Sends a notification to the server.
Pings the server.
Reads a resource by URI.
Convenience alias for batch_request/3.
Sends a cancellation notification for a pending request.
Gets server capabilities.
Gets server information.
Sets the log level for the server.
Starts a client process with the given options.
Stops the client.
Subscribes to notifications for a resource.
Convenience alias for list_tools/2.
Unsubscribes from notifications for a resource.
Submits responses to a modern task's outstanding input requests.
Types
Functions
Sends a batch of requests to the server.
This function allows sending multiple requests in a single batch, which can be more efficient than sending them individually. The server processes the requests and returns a batch of responses.
Parameters
client- Client process referencerequests- A list of{method, params}tuples for each request.timeout- Timeout for the entire batch operation (default: 30_000).
Returns
{:ok, results}- On success, whereresultsis a list of{:ok, result}or{:error, error}tuples, in the same order as the original requests.{:error, reason}- If the batch request fails (e.g., timeout).
Example
requests = [
{"tools/list", %{}},
{"prompts/list", %{}}
]
{:ok, [tools_result, prompts_result]} = ExMCP.Client.batch_request(client, requests)
Convenience alias for call_tool/4.
Calls a tool with the given arguments.
Options
:timeout- Request timeout (default: 30000):format- Return format (:map or :struct, default: :struct)
Requests cooperative cancellation of a task.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec clear_era_observations() :: :ok
Clears all remembered protocol-era observations.
This is an operator action intended for configuration changes or recovery
from a previously pinned modern endpoint. To clear only the identity used by
one new connection, pass reset_era_cache: true to start_link/1.
Requests completion suggestions from the server.
Sends a completion/complete request to get completion suggestions based on
a reference (prompt or resource) and partial input.
Parameters
client- Client process referenceref- Reference map describing what to complete:- For prompts:
%{"type" => "ref/prompt", "name" => "prompt_name"} - For resources:
%{"type" => "ref/resource", "uri" => "resource_uri"}
- For prompts:
argument- Argument map with completion context:%{"name" => "argument_name", "value" => "partial_value"}
Options
:timeout- Request timeout (default: 5000):format- Return format (:map or :struct, default: :struct)
Returns
{:ok, result}- Success with completion suggestions:%{ completion: %{ values: ["suggestion1", "suggestion2", ...], total: 10, hasMore: false } }{:error, error}- Request failed with error details
Examples
# Complete prompt argument
{:ok, result} = ExMCP.Client.complete(
client,
%{"type" => "ref/prompt", "name" => "code_generator"},
%{"name" => "language", "value" => "java"}
)
# Complete resource URI
{:ok, result} = ExMCP.Client.complete(
client,
%{"type" => "ref/resource", "uri" => "file:///"},
%{"name" => "path", "value" => "/src"}
)
@spec connect( connection_spec(), keyword() ) :: {:ok, t()} | {:error, any()}
Connects to an MCP server using a URL or connection spec.
Examples
# URL string
{:ok, client} = ExMCP.Client.connect("http://localhost:8080/mcp")
# Transport spec
{:ok, client} = ExMCP.Client.connect({:stdio, command: "mcp-server"})
# Multiple transports with fallback
{:ok, client} = ExMCP.Client.connect([
"http://localhost:8080/mcp",
"stdio://mcp-server"
])Returns {:error, {:invalid_transport_config, reason}} when the connection
spec cannot be normalized into a valid transport configuration.
@spec disconnect(t()) :: :ok
Disconnects the client gracefully, cleaning up all resources.
This function performs a clean shutdown by:
- Closing the transport connection
- Cancelling health checks
- Stopping the receiver task
- Replying to any pending requests with an error
Examples
{:ok, client} = ExMCP.Client.connect("http://localhost:8080/mcp")
:ok = ExMCP.Client.disconnect(client)
Discovers a modern MCP server's versions, capabilities, and identity.
A successful discovery also updates the client's protocol version,
server_info, and server_capabilities state. Automatic discovery during
connection establishment is handled separately by the era probe.
@spec find_matching_tool([map()], String.t() | nil, keyword()) :: {:ok, map()} | {:error, :not_found}
Finds a matching tool from a list of tools.
Parameters
tools- List of tool mapsname- Tool name to find (exact match) or pattern (fuzzy match)opts- Options including :fuzzy for fuzzy matching
Examples
tools = [%{"name" => "calculator"}, %{"name" => "weather"}]
{:ok, tool} = ExMCP.Client.find_matching_tool(tools, "calculator", [])
{:ok, tool} = ExMCP.Client.find_matching_tool(tools, "calc", fuzzy: true)
@spec find_tool(t(), String.t() | nil, keyword()) :: {:ok, map()} | {:error, :not_found} | {:error, any()}
Finds a tool by name or pattern.
Options
:fuzzy- Enable fuzzy matching (default: false):timeout- Request timeout (default: 5000)
@spec get_pending_requests(t()) :: [ExMCP.Types.request_id()]
Gets the list of pending request IDs.
Returns a list of request IDs for requests that are currently in progress.
This can be used with send_cancelled/3 to cancel specific requests.
Examples
{:ok, client} = ExMCP.Client.connect("http://localhost:8080/mcp")
# Start a long-running request
task = Task.async(fn ->
ExMCP.Client.call_tool(client, "slow_tool", %{})
end)
# Get pending requests
pending = ExMCP.Client.get_pending_requests(client)
# => ["req_123", "req_456"]
# Cancel a specific request
ExMCP.Client.send_cancelled(client, "req_123", "User cancelled")
Gets a prompt with the given arguments.
Gets the client status.
Reads the current full state of a task.
@spec list_prompts(t(), keyword() | timeout()) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}
Lists available prompts.
@spec list_resource_templates(t(), keyword() | timeout()) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}
Lists available resource templates.
Sends a resources/templates/list request to the server to retrieve the list of
available resource templates.
@spec list_resources(t(), keyword() | timeout()) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}
Lists available resources.
@spec list_roots(t(), keyword() | timeout()) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}
Lists available roots.
Sends a roots/list request to the server to retrieve the list of
available root URIs.
MCP Roots is deprecated as of 2026-07-28 and retained throughout ExMCP 1.x. New implementations should pass directories or files via tool parameters, resource URIs, or server configuration.
@spec list_tools(t(), keyword() | timeout()) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}
Lists available tools from the server.
Options
:timeout- Request timeout (default: 5000):format- Return format (:map or :struct, default: :struct)
@spec listen(t(), map(), keyword()) :: {:ok, ExMCP.Client.Subscription.Ref.t()} | {:error, term()}
Opens a modern immutable notification subscription and waits for the server's acknowledgment.
The Tasks extension adds a "taskIds" filter whose values receive full
notifications/tasks states. The client must declare
io.modelcontextprotocol/tasks in its configured capabilities.
Sends a log message to the server as a notification.
This function sends log messages from the client to the server for centralized logging and monitoring. The message is sent as a notification (fire-and-forget) following the MCP specification.
MCP protocol Logging is deprecated as of 2026-07-28 and retained throughout ExMCP 1.x. Prefer stderr for stdio or OpenTelemetry for new observability integrations.
Parameters
client- Client process referencelevel- Log level string (e.g., "debug", "info", "warning", "error")message- Log message text
Returns
:ok- Message sent successfully{:error, reason}- Failed to send message
Example
{:ok, client} = ExMCP.Client.start_link(transport: :http, url: "...")
:ok = ExMCP.Client.log_message(client, "info", "Operation completed")
Sends a log message with additional data to the server as a notification.
This function sends detailed log messages from the client to the server for centralized logging and monitoring. The message is sent as a notification (fire-and-forget) following the MCP specification.
MCP protocol Logging is deprecated as of 2026-07-28 and retained throughout ExMCP 1.x. Prefer stderr for stdio or OpenTelemetry for new observability integrations.
Parameters
client- Client process referencelevel- Log level string (e.g., "debug", "info", "warning", "error")message- Log message textdata- Optional additional data (map or any JSON-serializable value)
Supported Log Levels
Standard RFC 5424 levels: "debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"
Returns
:ok- Message sent successfully{:error, reason}- Failed to send message
Examples
{:ok, client} = ExMCP.Client.start_link(transport: :http, url: "...")
# Simple log message
:ok = ExMCP.Client.log_message(client, "info", "User logged in")
# Log message with additional context
:ok = ExMCP.Client.log_message(client, "error", "Database connection failed", %{
host: "db.example.com",
port: 5432,
error_code: "CONNECTION_TIMEOUT"
})
Gets the negotiated protocol version with the server.
Sends a notification to the server.
Notifications are fire-and-forget messages that don't expect a response.
Parameters
client- Client process referencemethod- The method name to notifyparams- Parameters for the notification (map)
Returns
:ok- Notification sent
Examples
:ok = ExMCP.Client.notify(client, "resource_updated", %{"uri" => "file://test.txt"})
Pings the server.
Reads a resource by URI.
Convenience alias for batch_request/3.
Sends a batch of JSON-RPC requests. Available in protocol version 2025-03-26 only.
@spec send_cancelled(t(), ExMCP.Types.request_id(), String.t() | nil) :: :ok | {:error, :cannot_cancel_initialize}
Sends a cancellation notification for a pending request.
For modern streamable HTTP, this closes only the pending request's POST
response stream, which is the protocol-defined cancellation signal. Other
transports send notifications/cancelled. The server MAY stop processing
the request if it hasn't completed yet.
Parameters
client- Client process referencerequest_id- The ID of the request to cancelreason- Optional human-readable reason for cancellation
Returns
:ok- Cancellation notification sent{:error, :cannot_cancel_initialize}- Cannot cancel initialize request
Examples
:ok = ExMCP.Client.send_cancelled(client, "req_123", "User cancelled")
:ok = ExMCP.Client.send_cancelled(client, 12345, nil)
Gets server capabilities.
Gets server information.
@spec set_log_level(GenServer.server(), String.t()) :: {:ok, map()} | {:error, any()}
Sets the log level for the server.
Sends a logging/setLevel request to configure the server's log verbosity.
This is part of the MCP specification for controlling server logging behavior.
MCP protocol Logging is deprecated as of 2026-07-28 and retained throughout ExMCP 1.x. This legacy RPC remains available for compatible peers. Prefer stderr for stdio or OpenTelemetry for new observability integrations.
Parameters
client- Client process referencelevel- Log level string: "debug", "info", "warning", or "error"
Returns
{:ok, result}- Success with any server response data{:error, error}- Request failed with error details
Example
{:ok, client} = ExMCP.Client.start_link(transport: :http, url: "...")
{:ok, _} = ExMCP.Client.set_log_level(client, "debug")
@spec start_link(keyword()) :: GenServer.on_start()
Starts a client process with the given options.
Options
:transport- Transport type (:stdio, :http, :beam, etc.):transports- List of transports for fallback:name- Optional GenServer name:handshake_timeout- Maximum time in milliseconds to wait for the server'sinitializeresponse during connection (default: 10_000). On expirystart_link/1fails with{:error, :handshake_timeout}.:protocol_mode- Era policy::modern_only,:legacy_only,:prefer_modern, or:prefer_legacy.:era_probe_timeout- Dedicated timeout for the side-effect-free modern discovery probe (default: 2_000 milliseconds).:era_cache_legacy_ttl- How long a successful legacy observation is reused before probing for an upgrade again (default: 300_000 milliseconds).:reset_era_cache- Clear the observation for this exact transport, endpoint, and auth configuration before connecting (default:false).:trace_context- Optional W3Ctraceparent,tracestate, and allowlistedbaggagevalues to attach to modern requests.:health_check_interval- Interval in milliseconds between idle health check pings (default: 30_000). Set tonilor0to disable.:reliability- Reliability features configuration (optional):retry_policy- Default retry policy for all client operations (optional):reconnect- Automatically reconnect when the transport closes unexpectedly (default:true):max_reconnect_attempts- Consecutive failed reconnection attempts before giving up (default: 10):reconnect_backoff- Reconnection backoff policy (keyword list)::initial- Delay before the first attempt in ms (default: 1000):max- Maximum delay between attempts in ms (default: 60_000):multiplier- Exponential backoff multiplier (default: 2)
Automatic Reconnection
When the transport closes unexpectedly while the client is connected, all
pending requests fail with a connection error and the client transitions to
:reconnecting. It then re-establishes the connection (including the MCP
handshake) with exponential backoff and jitter. After
:max_reconnect_attempts consecutive failures the client gives up and
settles in :disconnected. Requests made while reconnecting return
{:error, :not_connected}.
Explicit disconnect/1 or stop/2 calls never trigger reconnection.
Passing reconnect: false disables the behavior entirely.
Health Checks
While connected and idle, the client sends a protocol ping every
:health_check_interval milliseconds. If a ping is still unanswered one
full interval later, the transport is treated as closed: pending requests
fail and the reconnection path takes over. Health checks are skipped while
requests are in flight, since those already prove the connection is alive.
The reconnection lifecycle emits telemetry:
[:ex_mcp, :client, :reconnect, :attempt]- measurements%{attempt: n, delay_ms: ms}, emitted when an attempt is scheduled[:ex_mcp, :client, :reconnect, :success]- reconnected and re-initialized[:ex_mcp, :client, :reconnect, :error]- a single attempt failed[:ex_mcp, :client, :reconnect, :timeout]- gave up after the final attempt
Reliability Options
The :reliability option accepts a keyword list with the following options:
:circuit_breaker- Circuit breaker configuration orfalseto disable:failure_threshold- Number of failures before opening (default: 5):success_threshold- Number of successes to close half-open circuit (default: 3):reset_timeout- Time before transitioning from open to half-open (default: 30_000):timeout- Operation timeout in milliseconds (default: 5_000)
:health_check- Health check configuration orfalseto disable:check_interval- Interval between health checks (default: 60_000):failure_threshold- Health check failures before marking unhealthy (default: 3):recovery_threshold- Health check successes before marking healthy (default: 2)
Reliability Examples
# Client with circuit breaker protection
{:ok, client} = ExMCP.Client.start_link(
transport: :stdio,
command: "my-server",
reliability: [
circuit_breaker: [
failure_threshold: 3,
reset_timeout: 10_000
]
]
)
# Client with both circuit breaker and health monitoring
{:ok, client} = ExMCP.Client.start_link(
transport: :http,
url: "http://localhost:8080/mcp",
reliability: [
circuit_breaker: [failure_threshold: 5],
health_check: [check_interval: 30_000]
]
)Retry Policy Options
The :retry_policy option accepts a keyword list with the following options:
:max_attempts- Maximum number of retry attempts (default: 3):initial_delay- Initial delay between retries in milliseconds (default: 200):max_delay- Maximum delay between retries in milliseconds (default: 5000):backoff_factor- Exponential backoff multiplier (default: 2):jitter- Add random jitter to prevent thundering herd (default: true)
Modern HTTP response-stream recovery is deliberately separate from this generic policy because a broken response has ambiguous delivery semantics. Operations accept these options:
:http_stream_retry-:at_least_once(default) reissues one broken request stream with a new JSON-RPC id.:safe_onlyreissues only built-in read operations or operations explicitly markedretry_safe: true, and otherwise returns a transport error whose reason is:outcome_unknown.:http_stream_retry_delay- Delay before the one reissue (default: 200ms), bounded by the operation's original deadline.:retry_safe- Caller-owned safety attestation. Tool annotations such asreadOnlyHintare advisory and are never used as the security decision.
:safe_only is intentionally non-conforming and is rejected when the client
is started with conformance_mode: true. JSON-RPC ids do not deduplicate
application side effects; tool authors should use an application
idempotency key and server-side deduplication where reissue is possible.
Retry Policy Examples
# Client with default retry policy for all operations
{:ok, client} = ExMCP.Client.start_link(
transport: :stdio,
command: "my-server",
retry_policy: [
max_attempts: 5,
initial_delay: 200
]
)
# Individual operation with custom retry policy
{:ok, tools} = ExMCP.Client.list_tools(client,
retry_policy: [max_attempts: 2, backoff_factor: 1.5])
# Operation with no retries (override client default)
{:ok, result} = ExMCP.Client.call_tool(client, "tool", %{},
retry_policy: false)
Stops the client.
@spec subscribe_resource(t(), String.t(), keyword()) :: {:ok, map() | ExMCP.Client.Subscription.Ref.t()} | {:error, any()}
Subscribes to notifications for a resource.
Sends a resources/subscribe request to receive notifications when the
specified resource changes. The server will send notifications/resources/updated
messages when the subscribed resource is modified.
Parameters
client- Client process referenceuri- Resource URI to subscribe to (e.g., "file:///path/to/file")
Options
:timeout- Request timeout (default: 5000):format- Return format (:map or :struct, default: :struct)
Returns
{:ok, result}- Subscription successful{:error, error}- Subscription failed with error details
Examples
{:ok, _result} = ExMCP.Client.subscribe_resource(client, "file:///config.json")
Convenience alias for list_tools/2.
Unsubscribes from notifications for a resource.
Sends a resources/unsubscribe request to stop receiving notifications
for the specified resource.
Parameters
client- Client process referenceuri- Resource URI to unsubscribe from
Options
:timeout- Request timeout (default: 5000):format- Return format (:map or :struct, default: :struct)
Returns
{:ok, result}- Unsubscription successful{:error, error}- Unsubscription failed with error details
Examples
{:ok, _result} = ExMCP.Client.unsubscribe_resource(client, "file:///config.json")
Submits responses to a modern task's outstanding input requests.