ExMCP

[![Hex.pm](https://img.shields.io/hexpm/v/ex_mcp.svg)](https://hex.pm/packages/ex_mcp) [![Documentation](https://img.shields.io/badge/docs-hexdocs-blue.svg)](https://hexdocs.pm/ex_mcp) [![CI](https://github.com/azmaveth/ex_mcp/workflows/CI/badge.svg)](https://github.com/azmaveth/ex_mcp/actions) [![Coverage](https://img.shields.io/codecov/c/github/azmaveth/ex_mcp.svg)](https://codecov.io/gh/azmaveth/ex_mcp) [![License](https://img.shields.io/hexpm/l/ex_mcp.svg)](https://github.com/azmaveth/ex_mcp/blob/master/LICENSE) **A complete Elixir implementation of the Model Context Protocol (MCP) and Agent Client Protocol (ACP)** [Getting Started](https://github.com/azmaveth/ex_mcp/tree/master/docs/getting-started) | [User Guide](docs/guides/USER_GUIDE.md) | [API Docs](https://hexdocs.pm/ex_mcp) | [Examples](https://github.com/azmaveth/ex_mcp/tree/master/examples) | [Changelog](CHANGELOG.md)

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, "~> 0.9.0"}
  ]
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, %{
      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, [%{type: "text", text: "Total users: #{count}"}], state}
  end
end

DSL Server

Define tools, resources, and prompts declaratively:

defmodule MyServer do
  use ExMCP.Server

  deftool "greet" do
    meta do
      name "Greet"
      description "Greets a person by name"
    end

    input_schema %{
      type: "object",
      properties: %{name: %{type: "string", description: "Person to greet"}},
      required: ["name"]
    }
  end

  defresource "info://about" do
    meta do
      name "About"
      description "Server information"
    end

    mime_type "text/plain"
  end

  @impl true
  def handle_tool_call("greet", %{"name" => name}, state) do
    {:ok, %{content: [text("Hello, #{name}!")]}, state}
  end

  @impl true
  def handle_resource_read("info://about", _uri, state) do
    {:ok, [text("MyServer v1.0")], state}
  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
})

Native BEAM Transport

For trusted Elixir clusters, use the native BEAM transport:

defmodule MyToolService do
  use ExMCP.Service, name: :my_tools

  @impl true
  def handle_mcp_request("list_tools", _params, state) do
    tools = [
      %{
        "name" => "ping",
        "description" => "Test tool",
        "inputSchema" => %{"type" => "object", "properties" => %{}}
      }
    ]
    {:ok, %{"tools" => tools}, state}
  end

  @impl true
  def handle_mcp_request("tools/call", %{"name" => "ping"}, state) do
    {:ok, %{"content" => [%{"type" => "text", "text" => "Pong!"}]}, state}
  end
end

# Start your service (automatically registers with ExMCP.Native)
{:ok, _} = MyToolService.start_link()

# Direct service calls (~15us latency)
{:ok, tools} = ExMCP.Native.call(:my_tools, "list_tools", %{})

ACP: Control Coding Agents

Use the Agent Client Protocol to control coding agents programmatically:

# Native ACP agents (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")

# Adapters for non-native agents (Claude Code, Codex, Pi)
{:ok, client} = ExMCP.ACP.start_client(
  command: ["claude"],
  adapter: ExMCP.ACP.Adapters.Claude
)

# Pi coding agent with full RPC support
{:ok, client} = ExMCP.ACP.start_client(
  command: ["pi"],
  adapter: ExMCP.ACP.Adapters.Pi,
  adapter_opts: [model: "anthropic/claude-sonnet-4"]
)

See the ACP Guide for full details.

Transport Performance

Transport Latency Best For
Native BEAM ~15us Elixir cluster communication
stdio ~1-5ms Subprocess communication
HTTP/SSE ~5-20ms Web 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