ExMCP (ex_mcp v1.0.0-rc.8)

Copy Markdown View Source

ExMCP - Complete Elixir implementation of the Model Context Protocol.

ExMCP enables AI models to securely interact with local and remote resources through a standardized protocol. It provides both client and server implementations with multiple transport options.

Public API

ExMCP provides a clean, focused public API. Only use these modules in your applications:

Core Modules

Optional Features

Supporting Modules

Deprecated (retained through 1.x; planned for removal in 2.0.0)

Internal Modules

All other modules under the ExMCP namespace are internal implementation details and may change without notice. Do not depend on them directly in your applications.

Stability

Stable for 1.0: Client, Server Handler/DSL, documented transports, HttpPlug, Types, Content builders, Authorization entry points, ACP client/agent/adapters.

May change in minors: experimental content transformers and anything marked deprecated. MCP 2026-07-28 is the latest stable revision and is available through :prefer_modern and :modern_only. Starting in rc.6, new connections default to :prefer_modern; :legacy_only preserves the legacy protocol era, not an exact rc.5 package rollback. The zero-arity compatibility helpers continue to report the newest initialize-compatible legacy revision, 2025-11-25.

Quick Start

Start a Client

# Connect to stdio server
{:ok, client} = ExMCP.start_client(
  transport: :stdio,
  command: ["python", "mcp-server.py"],
  protocol_mode: :prefer_modern
)

# Connect with HTTP
{:ok, client} = ExMCP.start_client(
  transport: :http,
  url: "https://api.example.com",
  protocol_mode: :prefer_modern
)

Start a Server

{:ok, server} = ExMCP.start_server(
  handler: MyApp.MCPHandler,
  transport: :stdio,
  protocol_mode: :prefer_modern
)

BEAM-Local Communication

{:ok, server} = MyServer.start_link(transport: :beam)

{:ok, client} = ExMCP.start_client(
  transport: :beam,
  server: server,
  protocol_mode: :prefer_modern
)

{:ok, tools} = ExMCP.Client.list_tools(client)

Protocol Versions

ExMCP supports two wire-incompatible MCP eras:

  • 2026-07-28 - Latest stable revision; stateless discovery, per-request context, result envelopes, MRTR, and subscriptions/listen
  • 2025-11-25 - Newest legacy revision; tasks, icons, and URL elicitation
  • 2025-06-18 - Structured output, OAuth 2.1, elicitation, no batch
  • 2025-03-26 - Subscriptions, roots, logging, and batch support
  • 2024-11-05 - Initial stable MCP revision

rc.7 defaults to protocol_mode: :prefer_modern, which tries the modern revision first and retains evidence-based legacy fallback. Use protocol_mode: :modern_only for a closed modern ecosystem or protocol_mode: :legacy_only to preserve the legacy protocol era. Exact rc.5 wire and session behavior still requires package rollback to 1.0.0-rc.5.

See the Configuration and Migration guides for the era comparison and rollout policy.

Features

  • Tools - Register and execute functions with parameters
  • Resources - List and read data from various sources
  • Prompts - Manage reusable prompt templates
  • Sampling - Protocol-deprecated in MCP 2026-07-28; retained throughout ExMCP 1.x for compatibility. Prefer direct LLM provider APIs for new code
  • Roots - Protocol-deprecated in MCP 2026-07-28; retained throughout ExMCP 1.x. Prefer tool parameters, resource URIs, or server configuration
  • Subscriptions - Monitor resources for changes
  • Progress - Track long-running operations
  • Notifications - Real-time updates for changes
  • BEAM-local MCP - High-performance Elixir-to-Elixir communication

Transport Options

  • stdio - Process communication (standard MCP)
  • Streamable HTTP - Web-friendly transport (standard MCP)
  • BEAM-local MCP - Direct Erlang process communication (ExMCP extension)

Examples

Basic Client Usage

{:ok, client} =
  ExMCP.start_client(
    transport: :stdio,
    command: ["mcp-server"],
    protocol_mode: :prefer_modern
  )

# List and call tools
{:ok, %{tools: tools}} = ExMCP.Client.list_tools(client)
{:ok, result} = ExMCP.Client.call_tool(client, "search", %{query: "elixir"})

# Read resources
{:ok, content} = ExMCP.Client.read_resource(client, "file:///data.json")

Basic Server Usage

Tip

Most servers are easier to write with the DSL:

defmodule MyServer do
  use ExMCP.Server.Handler
  use ExMCP.Server.DSL, name: "my-server", version: "1.0.0"

  tool "echo", "Echo the message" do
    param :message, :string, required: true
    run fn %{message: msg}, state ->
      {:ok, %{content: [%{type: "text", text: msg}]}, state}
    end
  end
end

{:ok, server} =
  MyServer.start_link(transport: :stdio, protocol_mode: :prefer_modern)
defmodule MyHandler do
  use ExMCP.Server.Handler

  @impl true
  def handle_initialize(_params, state) do
    {:ok, %{
      protocolVersion: ExMCP.protocol_version(),
      serverInfo: %{name: "my-handler", version: "1.0.0"},
      capabilities: %{tools: %{}}
    }, state}
  end

  @impl true
  def handle_list_tools(_cursor, state) do
    tools = [%{name: "echo", description: "Echo input", inputSchema: %{type: "object"}}]
    {:ok, tools, nil, state}
  end

  @impl true
  def handle_call_tool("echo", params, state) do
    {:ok, %{content: [%{type: "text", text: params["message"]}]}, state}
  end
end

{:ok, server} =
  ExMCP.start_server(
    handler: MyHandler,
    transport: :stdio,
    protocol_mode: :prefer_modern
  )

BEAM-Local Service

defmodule MyService do
  use ExMCP.Server.Handler
  use ExMCP.Server.DSL

  tool "ping", "Health check" do
    run fn _args, state ->
      {:ok, %{content: [%{type: "text", text: "pong"}]}, state}
    end
  end
end

{:ok, server} =
  MyService.start_link(transport: :beam, protocol_mode: :prefer_modern)

{:ok, client} =
  ExMCP.start_client(
    transport: :beam,
    server: server,
    protocol_mode: :prefer_modern
  )
{:ok, result} = ExMCP.Client.call_tool(client, "ping", %{})

Summary

Functions

Calls a tool on the connected server.

Connects to an MCP server using the unified client implementation.

Disconnects from an MCP server.

Gets library configuration and capabilities.

Tests connectivity to an MCP server without establishing a persistent connection.

Returns the legacy protocol revision used by zero-arity compatibility paths.

Reads a resource from the connected server.

Lists available resources from the connected server.

Starts an ACP client connected to an agent subprocess.

Convenience function to start an MCP client.

Convenience function to start an MCP server.

Gets connection status and server information.

Returns the initialize-compatible legacy protocol revisions.

Lists available tools from the connected server.

Returns the version of the ExMCP library.

Types

client()

@type client() :: pid()

connection_spec()

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

Functions

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

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

Calls a tool on the connected server.

Returns {:ok, result} on success or {:error, reason} if the request fails or the client is dead/unresponsive. With normalize: true (the default) result is the extracted text content; with normalize: false it is the raw response.

Options

  • :timeout - Request timeout in milliseconds (default: 30_000)
  • :normalize - Whether to normalize the response (default: true)

Examples

# Simple call
{:ok, result} = ExMCP.call(client, "calculator", %{op: "add", a: 1, b: 2})

# With options
{:ok, result} = ExMCP.call(client, "slow_tool", %{data: "..."}, timeout: 60_000)

connect(connection_spec, opts \\ [])

@spec connect(
  connection_spec(),
  keyword()
) :: {:ok, client()} | {:error, any()}

Connects to an MCP server using the unified client implementation.

This function provides a simplified interface to the MCP client with automatic connection configuration and transport selection.

Options

  • :timeout - Connection timeout in milliseconds (default: 10_000)
  • :retry_attempts - Number of retry attempts (default: 3)
  • Transport-specific options (see ExMCP.Client docs)

Examples

# HTTP connection
{:ok, client} = ExMCP.connect("http://localhost:8080")

# Stdio connection
{:ok, client} = ExMCP.connect({:stdio, command: "my-server"})

# Multiple transports with fallback (uses first available)
{:ok, client} = ExMCP.connect([
  "http://primary:8080",
  "http://backup:8080"
])

# Using ClientConfig for advanced configuration
config = ExMCP.ClientConfig.new(:production)
|> ExMCP.ClientConfig.put_transport(:http, url: "https://api.example.com")
|> ExMCP.ClientConfig.put_auth(:bearer, token: "secret")
|> ExMCP.ClientConfig.put_retry_policy(max_attempts: 5)
{:ok, client} = ExMCP.connect(config)

disconnect(client)

@spec disconnect(client()) :: :ok

Disconnects from an MCP server.

info()

@spec info() :: map()

Gets library configuration and capabilities.

ping(connection_spec, opts \\ [])

@spec ping(
  connection_spec(),
  keyword()
) :: :ok | {:error, any()}

Tests connectivity to an MCP server without establishing a persistent connection.

protocol_version()

@spec protocol_version() :: String.t()

Returns the legacy protocol revision used by zero-arity compatibility paths.

This returns "2025-11-25", the newest initialize-based legacy revision. MCP 2026-07-28 is the latest stable revision but is selected through :protocol_mode, not this scalar helper.

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

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

Reads a resource from the connected server.

Returns {:ok, content} on success, or {:error, reason} if the request fails or the client is dead/unresponsive.

Options

  • :timeout - Request timeout in milliseconds (default: 10_000)
  • :parse_json - Automatically parse JSON content (default: false)

Examples

# Read text content
{:ok, content} = ExMCP.read(client, "file://data.txt")

# Read and parse JSON
{:ok, data} = ExMCP.read(client, "file://config.json", parse_json: true)

resources(client, opts \\ [])

@spec resources(
  client(),
  keyword()
) :: {:ok, [map()]} | {:error, any()}

Lists available resources from the connected server.

Returns {:ok, resources} on success, or {:error, reason} if the request fails or the client is dead/unresponsive.

start_acp_client(opts)

Starts an ACP client connected to an agent subprocess.

See ExMCP.ACP.start_client/1 for details.

start_client(opts)

@spec start_client(keyword()) :: {:ok, pid()} | {:error, term()}

Convenience function to start an MCP client.

This is equivalent to ExMCP.Client.start_link/1 but provides a simpler entry point for common use cases.

Examples

# stdio transport
{:ok, client} = ExMCP.start_client(
  transport: :stdio,
  command: ["python", "mcp-server.py"]
)

# HTTP transport
{:ok, client} = ExMCP.start_client(
  transport: :http,
  url: "https://api.example.com"
)

start_server(opts)

@spec start_server(keyword()) :: {:ok, pid()} | {:error, term()}

Convenience function to start an MCP server.

This is equivalent to ExMCP.Server.HandlerServer.start_link/1 but provides a simpler entry point for common use cases.

Examples

{:ok, server} = ExMCP.start_server(
  handler: MyApp.Handler,
  transport: :stdio
)

status(client)

@spec status(client()) :: {:ok, map()} | {:error, any()}

Gets connection status and server information.

Returns {:ok, status} on success, or {:error, reason} if the client is dead/unresponsive.

supported_versions()

@spec supported_versions() :: [String.t()]

Returns the initialize-compatible legacy protocol revisions.

Modern 2026-07-28 support is enabled through :prefer_modern or :modern_only and is intentionally not added to this legacy compatibility list during the RC soak.

tools(client, opts \\ [])

@spec tools(
  client(),
  keyword()
) :: {:ok, [map()]} | {:error, any()}

Lists available tools from the connected server.

Returns {:ok, tools} where tools is a list of tool definitions with their schemas and descriptions, or {:error, reason} if the request fails or the client is dead/unresponsive.

version()

@spec version() :: String.t()

Returns the version of the ExMCP library.