Codex.MCP.Client (Codex SDK v0.7.2)

Copy Markdown View Source

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}

  • [: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()}

  • [: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()}

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

capabilities()

@type capabilities() :: %{optional(String.t()) => term()}

t()

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

transport_ref()

@type transport_ref() :: {module(), term()}

Functions

call_tool(client, tool, args, opts \\ [])

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

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:

  • :ok or any truthy value - Approved
  • :deny or false - 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
)

capabilities(client)

@spec capabilities(t()) :: capabilities()

Returns capabilities advertised by the MCP server.

handshake(transport, opts \\ [])

@spec handshake(
  transport_ref(),
  keyword()
) :: {:ok, t()} | {:error, term()}

Backwards compatible alias for initialize/2.

initialize(transport, opts \\ [])

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

list_prompts(client, opts \\ [])

@spec list_prompts(
  t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Lists available prompts via prompts/list.

Options

  • :cursor - Pagination cursor (default: nil)
  • :timeout_ms - Request timeout (default: 30000)

list_resources(client, opts \\ [])

@spec list_resources(
  t(),
  keyword()
) :: {:ok, map()} | {:error, term()}

Lists available resources via resources/list.

Options

  • :cursor - Pagination cursor (default: nil)
  • :timeout_ms - Request timeout (default: 30000)

list_tools(client, opts \\ [])

@spec list_tools(
  t(),
  keyword()
) :: {:ok, [map()], t()} | {:error, term()}

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

qualify_tool_name(server_name, tool_name)

@spec qualify_tool_name(String.t(), String.t()) :: String.t()

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