MCP JSON-RPC client for stdio and streamable HTTP transports.
Tool Name Qualification
When list_tools/2 is called with qualify?: true, tool names are qualified with
the server name prefix in the format mcp__<server>__<tool>. This follows the
OpenAI tool name constraint (^[a-zA-Z0-9_-]+$).
If the qualified name exceeds 64 characters, it is truncated and a SHA1 hash suffix is appended to ensure uniqueness.
Tool Invocation
The call_tool/4 function invokes tools on the MCP server with support for:
- Retry Logic - Configurable retries with exponential backoff
- Approval Integration - Optional approval callbacks before invocation
- Timeout Control - Per-call timeout settings
- Telemetry - Events emitted for observability
Telemetry Events
The following telemetry events are emitted during tool invocation:
[:codex, :mcp, :tool_call, :start]- When a tool call begins- Measurements:
%{system_time: integer()} Metadata:
%{tool: String.t(), arguments: map(), server_name: String.t() | nil}
- Measurements:
[:codex, :mcp, :tool_call, :success]- When a tool call succeeds- Measurements:
%{duration: integer()} Metadata:
%{tool: String.t(), arguments: map(), server_name: String.t() | nil, attempt: integer()}
- Measurements:
[:codex, :mcp, :tool_call, :failure]- When a tool call fails- Measurements:
%{duration: integer()} Metadata:
%{tool: String.t(), arguments: map(), server_name: String.t() | nil, reason: term(), attempt: integer()}
- Measurements:
Summary
Functions
Invokes a tool on the MCP server.
Returns capabilities advertised by the MCP server.
Backwards compatible alias for initialize/2.
Performs MCP initialization against the given transport.
Lists available prompts via prompts/list.
Lists available resources via resources/list.
Lists available tools, applying allow/block filters and caching results unless cache?: false
is supplied.
Qualifies a tool name with the server prefix.
Types
@type t() :: %Codex.MCP.Client{ capabilities: capabilities(), instructions: String.t() | nil, protocol_version: String.t() | nil, server_info: map() | nil, server_name: String.t() | nil, tool_cache: map(), transport: transport_ref() }
Functions
Invokes a tool on the MCP server.
Options
:retries- Number of retry attempts (default:3):backoff- Backoff function(attempt -> :ok)(default: exponential backoff):timeout_ms- Request timeout in milliseconds (default:60000):approval- Approval callback function(tool, args, context) -> :ok | {:deny, reason}:context- Tool context map passed to approval callback (default:%{})
Backoff
The default backoff uses exponential delays: 100ms, 200ms, 400ms, 800ms, ... up to 5000ms max.
Provide a custom function to override: backoff: fn attempt -> Process.sleep(attempt * 100) end
Approval Callbacks
Approval callbacks are invoked before the first attempt. They can be:
- A 3-arity function
(tool, args, context) -> result - A 2-arity function
(tool, args) -> result - A 1-arity function
(tool) -> result
Where result is one of:
:okor any truthy value - Approved:denyorfalse- Denied{:deny, reason}- Denied with reason
Telemetry
Emits the following events:
[:codex, :mcp, :tool_call, :start]- When the call begins[:codex, :mcp, :tool_call, :success]- On successful completion[:codex, :mcp, :tool_call, :failure]- On failure (after all retries exhausted)
Returns
{:ok, result}- Tool execution succeeded{:error, {:approval_denied, reason}}- Approval callback denied the call{:error, reason}- Tool execution failed after all retries
Examples
# Basic invocation with defaults
{:ok, result} = Codex.MCP.Client.call_tool(client, "echo", %{"text" => "hello"})
# With custom retry and backoff
{:ok, result} = Codex.MCP.Client.call_tool(client, "fetch", %{"url" => url},
retries: 5,
backoff: fn attempt -> Process.sleep(attempt * 200) end,
timeout_ms: 30_000
)
# With approval callback
{:ok, result} = Codex.MCP.Client.call_tool(client, "write_file", args,
approval: fn tool, args, _ctx ->
if safe_tool?(tool, args), do: :ok, else: {:deny, "unsafe"}
end
)
@spec capabilities(t()) :: capabilities()
Returns capabilities advertised by the MCP server.
@spec handshake( transport_ref(), keyword() ) :: {:ok, t()} | {:error, term()}
Backwards compatible alias for initialize/2.
@spec initialize( transport_ref(), keyword() ) :: {:ok, t()} | {:error, term()}
Performs MCP initialization against the given transport.
This sends initialize and then emits the notifications/initialized notification.
Options
:client- Client name to send during initialization (default:"codex-elixir"):client_title- Optional client title for UI display:version- Client version (default:"0.0.0"):capabilities- Client capability map (default:%{}):protocol_version- MCP protocol version (default:2025-06-18):server_name- Server name for tool name qualification (e.g.,"shell"):timeout_ms- Timeout for initialization (default:10000)
Lists available prompts via prompts/list.
Options
:cursor- Pagination cursor (default:nil):timeout_ms- Request timeout (default:30000)
Lists available resources via resources/list.
Options
:cursor- Pagination cursor (default:nil):timeout_ms- Request timeout (default:30000)
Lists available tools, applying allow/block filters and caching results unless cache?: false
is supplied.
Options
:cache?- Whether to use cached results (default:true):allow- List of tool names to allow (allowlist filter):deny- List of tool names to deny (blocklist filter):filter- Custom filter function(tool -> boolean):qualify?- Whether to add qualified names with server prefix (default:false):cursor- Pagination cursor (default:nil):timeout_ms- Request timeout (default:30000)
Returns
{:ok, tools, updated_client}on success{:error, reason}on failure
Qualifies a tool name with the server prefix.
Returns the fully qualified name in the format mcp__<server>__<tool>.
If the qualified name exceeds 64 characters, it is truncated and a SHA1
hash suffix is appended to ensure uniqueness.
Examples
iex> Codex.MCP.Client.qualify_tool_name("server1", "tool_a")
"mcp__server1__tool_a"
iex> long_tool = String.duplicate("a", 80)
iex> result = Codex.MCP.Client.qualify_tool_name("srv", long_tool)
iex> String.length(result)
64