Skip to content

Repository files navigation

ExMCP

Hex.pm Documentation CI Coverage License

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

  • Full MCP support -- 2026-07-28 (latest stable) plus the legacy 2024-11-05, 2025-03-26, 2025-06-18, and 2025-11-25 revisions
  • Modern MCP conformance -- 377/377 client checks and 112/112 server checks (@modelcontextprotocol/conformance@0.2.0-alpha.10, complete 2026-07-28 suites, 2026-08-05)
  • Legacy MCP conformance -- 226/226 client checks and 39/39 server checks (@modelcontextprotocol/conformance@0.1.16, core suite)
  • ACP v1 -- Agent Client Protocol major version 1 (protocolVersion: 1)
  • Multiple transports -- Streamable HTTP, stdio, and BEAM-local MCP (~15μs local calls)
  • Phoenix Plug -- native Phoenix integration with ExMCP.HttpPlug
  • DSL and Handler APIs -- declarative tool/resource/prompt definitions via ExMCP.Server.DSL, or raw callback-based handlers (ExMCP.Server.Tools is deprecated; retained throughout 1.x and planned for removal in 2.0)
  • OAuth 2.1 -- automatic 401→discover→PKCE→token flow, scope step-up, CIMD, JWT client auth (private_key_jwt), enterprise SSO (ID-JAG), token revocation (RFC 7009), pluggable auth providers
  • OTP-native -- supervision trees, auto-reconnection with exponential backoff, 88 telemetry events
  • Agent Client Protocol (ACP) -- control coding agents and build native Elixir ACP agents
  • 3500+ tests -- comprehensive suite including official MCP conformance, integration, and performance

MCP 2026-07-28 is wire-incompatible with earlier revisions. The current source tree supports it through :prefer_modern and :modern_only, while preserving every legacy revision through the 1.x line. Starting in 1.0.0-rc.6, the application default is :prefer_modern: clients try modern discovery first and fall back only when the peer positively identifies itself as legacy. Set protocol_mode: :legacy_only for the exact rc.5 wire path. ExMCP.protocol_version/0 intentionally returns the newest legacy revision, 2025-11-25, for initialize-based compatibility—it is not the latest upstream MCP revision. See Configuration and the 1.0 migration guide.

Release-state note: 1.0.0-rc.6 is the modern-preferred soak release. The previous 1.0.0-rc.5 package remains the legacy-only characterization baseline. Stable 1.0 will preserve rc.6 behavior after the release gates and minimum seven-day soak complete.

Installation

For the dual-era release candidate:

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

To retain the legacy-only rc.5 connection policy during rollout, configure the mode explicitly:

config :ex_mcp, protocol_mode: :legacy_only

API stability (1.0)

Stable Experimental / limited Deprecated (retained through 1.x)
ExMCP.Client, Server.Handler, Server.DSL Content sanitize/transform helpers ExMCP.Server.Tools (+ helpers)
Transports (:stdio, :http, :beam, :test) Some draft MCP handler features Image compress/resize/thumbnail stubs
ExMCP.HttpPlug, Authorization, ACP adapters ACP session/fork (unstable upstream) MCP HTTP+SSE, Roots, Sampling, and protocol Logging
ExMCP.Content builders (text/image/audio)

Runnable examples live in the GitHub repo under examples/ (not shipped in the Hex package).

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,
      protocol_mode: :prefer_modern,
      server_info: %{name: "my-phoenix-app", version: "1.0.0"},
      handler_call_timeout: 10_000,
      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

handler_call_timeout is the server-side Handler deadline in milliseconds; client request and SSE timeouts are configured separately on ExMCP.Client.

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"],
  protocol_mode: :prefer_modern
)

# 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,
    protocol_mode: :prefer_modern
  )

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

{: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

Transport Latency Best For
BEAM-local ~15us Local Elixir processes in one VM
stdio ~1-5ms Subprocess communication
Streamable HTTP ~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

About

Model Context Protocol (MCP) and Agent Client Protocol (ACP) client/server library for Elixir

Resources

Security policy

Stars

18 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages