ExMCP

Hex.pmDocumentationCICoverageLicense

A complete Elixir implementation of the Model Context Protocol (MCP) and Agent Client Protocol (ACP)

Getting Started | User Guide | API Docs | Examples | Changelog


Overview

ExMCP is a comprehensive Elixir implementation of the Model Context Protocol and the Agent Client Protocol, enabling AI models to securely interact with local and remote resources through standardized protocols. It provides both client and server implementations with multiple transport options, including native Phoenix integration via Plug compatibility, plus the ability to control coding agents like Gemini CLI, Claude Code, and Codex via ACP.

Key Features

Installation

Add ex_mcp to your dependencies in mix.exs:

def deps do
[
{:ex_mcp, "~> 1.0.0-rc.1"}
]
end

Quick Start

Phoenix Integration

Add MCP server capabilities to your Phoenix app:

# In your Phoenix router
defmodule MyAppWeb.Router do
use MyAppWeb, :router
scope "/api/mcp" do
forward "/", ExMCP.HttpPlug,
handler: MyApp.MCPHandler,
server_info: %{name: "my-phoenix-app", version: "1.0.0"},
sse_enabled: true,
cors_enabled: true
end
end
# Create your MCP handler
defmodule MyApp.MCPHandler do
use ExMCP.Server.Handler
@impl true
def init(_args), do: {:ok, %{}}
@impl true
def handle_initialize(_params, state) do
{:ok, %{
protocolVersion: ExMCP.protocol_version(),
serverInfo: %{name: "my-phoenix-app", version: "1.0.0"},
capabilities: %{tools: %{}, resources: %{}}
}, state}
end
@impl true
def handle_list_tools(_cursor, state) do
tools = [
%{
name: "get_user_count",
description: "Get total number of users",
inputSchema: %{type: "object", properties: %{}}
}
]
{:ok, tools, nil, state}
end
@impl true
def handle_call_tool("get_user_count", _args, state) do
count = MyApp.Accounts.count_users()
{:ok, %{content: [%{type: "text", text: "Total users: #{count}"}]}, state}
end
end

Note: The example above uses raw ExMCP.Server.Handler callbacks (useful for dynamic capabilities). Most Phoenix apps will be simpler with the DSL — see the "DSL Server" section below and the Phoenix Guide.

DSL Server

Define tools, resources, and prompts next to their handlers:

defmodule MyServer do
use ExMCP.Server.Handler
use ExMCP.Server.DSL
tool "greet", "Greets a person by name" do
title "Greeting"
param :name, :string, required: true, description: "Person to greet"
run fn %{name: name}, state ->
{:ok, %{text: "Hello, #{name}!"}, state}
end
end
resource "info://about", "Server information" do
title "About"
mime_type "text/plain"
read fn %{uri: uri}, state ->
{:ok, %{uri: uri, text: "MyServer v1.0", mimeType: "text/plain"}, state}
end
end
prompt "motivate", "Create a short motivational message" do
arg :topic, required: true, description: "Topic to encourage"
render fn %{topic: topic}, state ->
{:ok,
%{
messages: [
%{role: "user", content: %{type: "text", text: "Encourage me about #{topic}"}}
]
}, state}
end
end
end

See the DSL Guide and examples for more patterns.

Standalone Client

# Connect to a stdio-based server
{:ok, client} = ExMCP.Client.start_link(
transport: :stdio,
command: ["node", "my-mcp-server.js"]
)
# List available tools
{:ok, tools} = ExMCP.Client.list_tools(client)
# Call a tool
{:ok, result} = ExMCP.Client.call_tool(client, "search", %{
query: "Elixir programming",
limit: 10
})

BEAM-Local MCP

For trusted Elixir processes in the same VM, use the BEAM-local transport. It carries MCP-shaped messages as Elixir terms, so local calls avoid JSON encode/decode while still using the normal MCP client/server lifecycle.

defmodule MyToolService do
use ExMCP.Server.Handler
use ExMCP.Server.DSL
tool "ping", "Test tool" do
run fn _args, state ->
{:ok, %{content: [%{type: "text", text: "Pong!"}]}, state}
end
end
end
{:ok, server} = MyToolService.start_link(transport: :beam)
{:ok, client} =
ExMCP.Client.start_link(
transport: :beam,
server: server
)
{:ok, tools} = ExMCP.Client.list_tools(client)
{:ok, result} = ExMCP.Client.call_tool(client, "ping", %{})

Fast verification: From the repo root (after mix compile), run mix examples.getting_started for a quick in-process demo of these patterns.

ACP: Control and Build Coding Agents

Use the Agent Client Protocol to control coding agents programmatically or expose an Elixir process as an ACP agent:

# Native ACP agents over stdio (Gemini CLI, Hermes, OpenCode, Qwen Code, etc.)
{:ok, client} = ExMCP.ACP.start_client(command: ["gemini", "--acp"])
# Create a session and send a prompt
{:ok, %{"sessionId" => sid}} = ExMCP.ACP.Client.new_session(client, "/my/project")
{:ok, %{"stopReason" => _}} = ExMCP.ACP.Client.prompt(client, sid, "Fix the failing tests")
# Claude Code via the SDK-compatible adapter
{:ok, client} = ExMCP.ACP.start_client(
command: ["claude"],
adapter: ExMCP.ACP.Adapters.ClaudeSDK,
adapter_opts: [model: "sonnet", cwd: "/my/project"]
)
# Pi coding agent through the ACP-native adapter
{:ok, client} = ExMCP.ACP.start_client(
command: ["pi"],
adapter: ExMCP.ACP.Adapters.Pi,
adapter_opts: [model: "anthropic/claude-sonnet-4", thinking_level: "medium"]
)
# Native Elixir ACP agent over stdio
{:ok, agent} = ExMCP.ACP.start_agent(
handler: MyApp.AgentHandler,
agent_info: %{"name" => "my-agent", "version" => "1.0.0"}
)

See the ACP Guide for full details.

Transport Performance

TransportLatencyBest For
BEAM-local~15usLocal Elixir processes in one VM
stdio~1-5msSubprocess communication
HTTP/SSE~5-20msWeb applications, remote APIs

Documentation

Getting Started

Guides

Development & API

Contributing

Contributions welcome! See the Development Guide for setup and testing instructions.

  1. Fork the repository
  2. Create a feature branch
  3. Run make quality to ensure code quality
  4. Submit a pull request

License

MIT -- see LICENSE.

Acknowledgments