ExMCP.Client (ex_mcp v0.9.0)

View Source

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

Returns a specification to start this module under a supervisor.

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.

Finds a matching tool from a list of tools.

Gets the list of pending request IDs.

Gets the client status.

Lists available resource templates.

Lists available tools from the server.

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.

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.

Subscribes to notifications for a resource.

Convenience alias for list_tools/2.

Unsubscribes from notifications for a resource.

Types

connection_spec()

@type connection_spec() :: String.t() | {atom(), keyword()} | [{atom(), keyword()}]

t()

@type t() :: GenServer.server()

Functions

batch_request(client, requests, timeout \\ 30000)

@spec batch_request(t(), [{String.t(), map()}], timeout()) ::
  {:ok, [any()]} | {:error, any()}

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 reference
  • requests - A list of {method, params} tuples for each request.
  • timeout - Timeout for the entire batch operation (default: 30_000).

Returns

  • {:ok, results} - On success, where results is 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)

call(client, tool_name, args \\ %{}, opts \\ [])

@spec call(t(), String.t(), map(), keyword()) :: {:ok, any()} | {:error, any()}

Convenience alias for call_tool/4.

call_tool(client, tool_name, arguments, timeout_or_opts \\ 30000)

@spec call_tool(t(), String.t(), map(), keyword() | timeout()) ::
  {:ok, any()} | {:error, any()}

Calls a tool with the given arguments.

Options

  • :timeout - Request timeout (default: 30000)
  • :format - Return format (:map or :struct, default: :map)

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

complete(client, ref, argument, opts \\ [])

@spec complete(t(), map(), map(), keyword()) :: {:ok, map()} | {:error, any()}

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 reference
  • ref - Reference map describing what to complete:
    • For prompts: %{"type" => "ref/prompt", "name" => "prompt_name"}
    • For resources: %{"type" => "ref/resource", "uri" => "resource_uri"}
  • 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: :map)

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

connect(connection_spec, opts \\ [])

@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"
])

disconnect(client)

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

find_matching_tool(tools, name, opts \\ [])

@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 maps
  • name - 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)

find_tool(client, name_or_pattern \\ nil, opts \\ [])

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

get_pending_requests(client)

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

get_prompt(client, prompt_name, arguments \\ %{}, timeout_or_opts \\ [])

@spec get_prompt(t(), String.t(), map(), keyword() | timeout()) ::
  {:ok, any()} | {:error, any()}

Gets a prompt with the given arguments.

get_status(client)

@spec get_status(t()) :: {:ok, map()}

Gets the client status.

list_prompts(client, timeout_or_opts \\ [])

@spec list_prompts(t(), keyword() | timeout()) ::
  {:ok, %{required(String.t()) => [map()]}} | {:error, any()}

Lists available prompts.

list_resource_templates(client, timeout_or_opts \\ [])

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

list_resources(client, timeout_or_opts \\ [])

@spec list_resources(t(), keyword() | timeout()) ::
  {:ok, %{required(String.t()) => [map()]}} | {:error, any()}

Lists available resources.

list_roots(client, timeout_or_opts \\ [])

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

list_tools(client, timeout_or_opts \\ [])

@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: :map)

log_message(client, level, message)

@spec log_message(t(), String.t(), String.t()) :: :ok | {:error, any()}

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.

Parameters

  • client - Client process reference
  • level - 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")

log_message(client, level, message, data)

@spec log_message(t(), String.t(), String.t(), any()) :: :ok | {:error, any()}

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.

Parameters

  • client - Client process reference
  • level - Log level string (e.g., "debug", "info", "warning", "error")
  • message - Log message text
  • data - 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"
})

negotiated_version(client)

@spec negotiated_version(t()) :: {:ok, String.t()} | {:error, any()}

Gets the negotiated protocol version with the server.

notify(client, method, params \\ %{})

@spec notify(t(), String.t(), map()) :: :ok

Sends a notification to the server.

Notifications are fire-and-forget messages that don't expect a response.

Parameters

  • client - Client process reference
  • method - The method name to notify
  • params - Parameters for the notification (map)

Returns

  • :ok - Notification sent

Examples

:ok = ExMCP.Client.notify(client, "resource_updated", %{"uri" => "file://test.txt"})

ping(client, opts_or_timeout \\ [])

@spec ping(t(), keyword() | integer()) :: {:ok, map()} | {:error, any()}

Pings the server.

read_resource(client, uri, timeout_or_opts \\ [])

@spec read_resource(t(), String.t(), keyword() | timeout()) ::
  {:ok, any()} | {:error, any()}

Reads a resource by URI.

send_batch(client, requests, timeout \\ 30000)

@spec send_batch(t(), [map()], timeout()) :: {:ok, [any()]} | {:error, any()}

Convenience alias for batch_request/3.

Sends a batch of JSON-RPC requests. Available in protocol version 2025-03-26 only.

send_cancelled(client, request_id, reason \\ nil)

@spec send_cancelled(t(), ExMCP.Types.request_id(), String.t() | nil) ::
  :ok | {:error, :cannot_cancel_initialize}

Sends a cancellation notification for a pending request.

This function sends a notifications/cancelled message to inform the server that a previously-sent request should be cancelled. The server MAY stop processing the request if it hasn't completed yet.

Parameters

  • client - Client process reference
  • request_id - The ID of the request to cancel
  • reason - 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)

server_capabilities(client)

@spec server_capabilities(t()) :: {:ok, map()} | {:error, any()}

Gets server capabilities.

server_info(client)

@spec server_info(t()) :: {:ok, map()} | {:error, any()}

Gets server information.

set_log_level(client, level)

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

Parameters

  • client - Client process reference
  • level - 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")

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts a client process with the given options.

Options

  • :transport - Transport type (:stdio, :http, :sse, etc.)
  • :transports - List of transports for fallback
  • :name - Optional GenServer name
  • :health_check_interval - Interval for health checks (default: 30_000)
  • :reliability - Reliability features configuration (optional)
  • :retry_policy - Default retry policy for all client operations (optional)

Reliability Options

The :reliability option accepts a keyword list with the following options:

  • :circuit_breaker - Circuit breaker configuration or false to 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 or false to 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: 100)
  • :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)

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)

stop(client, reason \\ :normal)

@spec stop(t(), term()) :: :ok

Stops the client.

subscribe_resource(client, uri, opts \\ [])

@spec subscribe_resource(t(), String.t(), keyword()) :: {:ok, map()} | {: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 reference
  • uri - Resource URI to subscribe to (e.g., "file:///path/to/file")

Options

  • :timeout - Request timeout (default: 5000)
  • :format - Return format (:map or :struct, default: :map)

Returns

  • {:ok, result} - Subscription successful
  • {:error, error} - Subscription failed with error details

Examples

{:ok, _result} = ExMCP.Client.subscribe_resource(client, "file:///config.json")

tools(client, opts \\ [])

@spec tools(
  t(),
  keyword()
) :: {:ok, %{required(String.t()) => [map()]}} | {:error, any()}

Convenience alias for list_tools/2.

unsubscribe_resource(client, uri, opts \\ [])

@spec unsubscribe_resource(t(), String.t(), keyword()) ::
  {:ok, map()} | {:error, any()}

Unsubscribes from notifications for a resource.

Sends a resources/unsubscribe request to stop receiving notifications for the specified resource.

Parameters

  • client - Client process reference
  • uri - Resource URI to unsubscribe from

Options

  • :timeout - Request timeout (default: 5000)
  • :format - Return format (:map or :struct, default: :map)

Returns

  • {:ok, result} - Unsubscription successful
  • {:error, error} - Unsubscription failed with error details

Examples

{:ok, _result} = ExMCP.Client.unsubscribe_resource(client, "file:///config.json")