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 (removed in 1.1.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: draft MCP features, experimental content transformers, and anything marked deprecated. Default negotiated protocol version is the latest registered in the version registry (currently 2025-11-25).
Quick Start
Start a Client
# Connect to stdio server
{:ok, client} = ExMCP.start_client(
transport: :stdio,
command: ["python", "mcp-server.py"]
)
# Connect with HTTP
{:ok, client} = ExMCP.start_client(
transport: :http,
url: "https://api.example.com"
)Start a Server
{:ok, server} = ExMCP.start_server(
handler: MyApp.MCPHandler,
transport: :stdio
)BEAM-Local Communication
{:ok, server} = MyServer.start_link(transport: :beam)
{:ok, client} = ExMCP.start_client(
transport: :beam,
server: server
)
{:ok, tools} = ExMCP.Client.list_tools(client)Protocol Versions
ExMCP supports multiple MCP protocol versions:
- 2024-11-05 - Base MCP features
- 2025-03-26 - Previous stable (subscriptions, roots, logging, batch support)
- 2025-06-18 - Structured output, OAuth 2.1, elicitation, no batch
- 2025-11-25 - Current latest supported version (tasks, icons, URL elicitation)
See the README.md for a complete feature comparison chart.
Features
- Tools - Register and execute functions with parameters
- Resources - List and read data from various sources
- Prompts - Manage reusable prompt templates
- Sampling - Direct LLM integration for response generation
- Roots - URI-based resource boundaries
- 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)
- HTTP/SSE - 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"])
# 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)
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)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)
{:ok, client} = ExMCP.start_client(transport: :beam, server: server)
{: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 version of the MCP protocol this library implements by default.
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 information about supported protocol versions.
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 version of the MCP protocol this library implements by default.
ExMCP supports multiple protocol versions. This returns the default/latest stable version.
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 information about supported protocol versions.
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.