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
ExMCP- This module (convenience functions and metadata)ExMCP.Client- MCP client implementationExMCP.Server- MCP server helper functionsExMCP.Server.Handler- Callback behaviour for MCP serversExMCP.Server.DSL- Declarative tool/resource/prompt definitionsExMCP.Transport- Transport behaviour definition
Optional Features
ExMCP.Authorization- OAuth 2.1 authorization flows (MCP optional feature)ExMCP.ACP- Agent Client Protocol client and native agent helpers
Supporting Modules
ExMCP.Content- Content type helpers (builders; advanced transform/sanitize is experimental)ExMCP.Types- Type definitions (stable across versions)ExMCP.HttpPlug- Phoenix/Plug MCP endpointExMCP.Error/ExMCP.Response- Error and response helpers
Deprecated (retained through 1.x; planned for removal in 2.0.0)
ExMCP.Server.Toolsand related modules — useExMCP.Server.DSL
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
Functions
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)
@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)
@spec disconnect(client()) :: :ok
Disconnects from an MCP server.
@spec info() :: map()
Gets library configuration and capabilities.
@spec ping( connection_spec(), keyword() ) :: :ok | {:error, any()}
Tests connectivity to an MCP server without establishing a persistent connection.
@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.
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)
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.
Starts an ACP client connected to an agent subprocess.
See ExMCP.ACP.start_client/1 for details.
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"
)
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
)
Gets connection status and server information.
Returns {:ok, status} on success, or {:error, reason} if the client
is dead/unresponsive.
@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.
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.
@spec version() :: String.t()
Returns the version of the ExMCP library.