Skip to content

azmaveth/ex_mcp

Repository files navigation

ExMCP

Hex.pm Documentation CI Coverage License

A complete Elixir implementation of the Model Context Protocol (MCP)

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


Overview

ExMCP is a comprehensive Elixir implementation of the Model Context Protocol, enabling 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, including native Phoenix integration via Plug compatibility.

Key Features

  • Full MCP compliance -- protocol versions 2024-11-05, 2025-03-26, 2025-06-18, and 2025-11-25
  • Multiple transports -- HTTP/SSE, stdio, and native BEAM (~15us local calls)
  • Phoenix Plug -- native Phoenix integration with ExMCP.HttpPlug
  • DSL and Handler APIs -- declarative tool/resource/prompt definitions or callback-based handlers
  • OAuth 2.1 -- Resource Server, JWT client auth (private_key_jwt), enterprise SSO (ID-JAG)
  • OTP-native -- supervision trees, auto-reconnection with exponential backoff, telemetry
  • 2600+ tests -- comprehensive suite including TypeScript SDK interop

Installation

Add ex_mcp to your dependencies in mix.exs:

def deps do
  [
    {:ex_mcp, "~> 0.7.4"}
  ]
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", %{})

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

  • The Model Context Protocol specification creators
  • The Elixir community for excellent tooling and libraries
  • Contributors and early adopters providing feedback

About

Model Context Protocol client/server library for Elixir

Resources

License

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages