MCP Servers

A collection of Model Context Protocol servers, templates, tools and more.

MCP (Model Context Protocol) server library for Erlang

Created 3/24/2026
Updated about 19 hours ago
Repository documentation and setup instructions

barrel_mcp

MCP (Model Context Protocol) library for Erlang. Implements the MCP specification for both server and client modes, including the Streamable HTTP transport (protocol 2025-03-26) for Claude Code integration.

Features

  • Full MCP Protocol Support: Tools, Resources, Prompts, and Sampling
  • Multiple Transports: Streamable HTTP (Claude Code), HTTP (Cowboy), and stdio (Claude Desktop)
  • Pluggable Authentication: Bearer JWT, API keys, Basic auth, or custom providers
  • Supervised Registry: gen_statem-based registry with atomic operations
  • Fast Reads: ETS + persistent_term for O(1) handler lookups (no process call)
  • Ready/Not-Ready States: Flexible initialization pattern
  • Client Library: Connect to external MCP servers
  • Zero-dependency JSON: Uses OTP 27+ built-in json module

Installation

Add to your rebar.config:

{deps, [
    {barrel_mcp, {git, "https://github.com/your-org/barrel_mcp.git", {branch, "main"}}}
]}.

Architecture

barrel_mcp uses a supervised gen_statem process to manage the handler registry:

  • Writes (reg/unreg) go through the gen_statem for atomic operations
  • Reads (find/all/run) use persistent_term directly for O(1) lookups
  • States: not_readyready for flexible initialization
  • Postpone pattern: Calls in not_ready state are postponed until ready
┌─────────────────────────────────────────────────────────────────┐
│                       barrel_mcp_sup                             │
│                      (supervisor)                                │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    barrel_mcp_registry                           │
│                      (gen_statem)                                │
│                                                                  │
│  States: not_ready ──────────────────► ready                    │
│              │         (self ! ready)                            │
│              │              or                                   │
│              └──── wait for external process ────►               │
│                                                                  │
│  ┌─────────────┐        ┌─────────────────────────────────────┐ │
│  │  ETS Table  │───────►│     persistent_term (read-only)     │ │
│  │ (authority) │  sync  │         O(1) lookups                │ │
│  └─────────────┘        └─────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
         ▲                              │
         │ reg/unreg                    │ find/all/run
         │ (atomic, postponed           │ (lock-free)
         │  if not ready)               │

Configuration

To make the registry wait for an external process before becoming ready:

%% In sys.config or application env
{barrel_mcp, [
    {wait_for_proc, my_init_process}  % Wait for this process to be registered
]}.

If wait_for_proc is not set, the registry becomes ready immediately after init.

Quick Start

Starting the Application

%% Start barrel_mcp application
application:ensure_all_started(barrel_mcp).

%% Wait for registry to be ready (optional, for custom initialization)
ok = barrel_mcp_registry:wait_for_ready().

Registering Tools

%% Register a tool
barrel_mcp:reg_tool(<<"search">>, my_module, search, #{
    description => <<"Search for items">>,
    input_schema => #{
        type => <<"object">>,
        properties => #{
            query => #{type => <<"string">>, description => <<"Search query">>},
            limit => #{type => <<"integer">>, default => 10}
        },
        required => [<<"query">>]
    }
}).

%% Your handler function (must accept a map and be exported with arity 1)
-module(my_module).
-export([search/1]).

search(#{<<"query">> := Query} = Args) ->
    Limit = maps:get(<<"limit">>, Args, 10),
    %% Return binary, map, or list of content blocks
    <<"Found results for: ", Query/binary>>.

Registering Resources

barrel_mcp:reg_resource(<<"config">>, my_module, get_config, #{
    name => <<"Application Config">>,
    uri => <<"config://app/settings">>,
    description => <<"Application configuration">>,
    mime_type => <<"application/json">>
}).

Registering Prompts

barrel_mcp:reg_prompt(<<"summarize">>, my_module, summarize_prompt, #{
    description => <<"Summarize content">>,
    arguments => [
        #{name => <<"content">>, description => <<"Content to summarize">>, required => true},
        #{name => <<"style">>, description => <<"Summary style">>, required => false}
    ]
}).

%% Handler returns prompt messages
summarize_prompt(Args) ->
    Content = maps:get(<<"content">>, Args),
    #{
        description => <<"Summarize the following content">>,
        messages => [
            #{role => <<"user">>, content => #{type => <<"text">>, text => Content}}
        ]
    }.

Starting Streamable HTTP Server (Claude Code)

For Claude Code integration, use the Streamable HTTP transport:

%% Start Streamable HTTP server on port 9090
{ok, _} = barrel_mcp:start_http_stream(#{port => 9090}).

%% With API key authentication
{ok, _} = barrel_mcp:start_http_stream(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_apikey,
        provider_opts => #{
            keys => #{<<"my-key">> => #{subject => <<"user">>}}
        }
    }
}).

Then add to Claude Code:

claude mcp add my-server --transport http http://localhost:9090/mcp \
  --header "X-API-Key: my-key"

See guides/http-stream.md for full documentation.

Starting HTTP Server (Legacy)

%% Start HTTP server on port 9090
{ok, _} = barrel_mcp:start_http(#{port => 9090}).

%% Or with custom IP binding
{ok, _} = barrel_mcp:start_http(#{port => 9090, ip => {127, 0, 0, 1}}).

Authentication

barrel_mcp provides pluggable authentication following OAuth 2.1 patterns as recommended by the MCP specification. Authentication is optional and configurable per HTTP server.

Built-in Providers

| Provider | Description | |----------|-------------| | barrel_mcp_auth_none | No authentication (default) | | barrel_mcp_auth_bearer | Bearer token (JWT or opaque) | | barrel_mcp_auth_apikey | API key authentication | | barrel_mcp_auth_basic | HTTP Basic authentication | | barrel_mcp_auth_custom | Custom auth module (simple interface) |

Bearer Token (JWT) Authentication

%% Start HTTP server with JWT authentication
{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_bearer,
        provider_opts => #{
            secret => <<"your-jwt-secret-key">>,
            issuer => <<"https://auth.example.com">>,
            audience => <<"https://mcp.example.com">>,
            clock_skew => 60  % seconds
        },
        required_scopes => [<<"mcp:read">>, <<"mcp:write">>]
    }
}).

For RS256/ES256 or opaque tokens, use a custom verifier:

%% Custom token verifier (e.g., for token introspection)
Verifier = fun(Token) ->
    case my_auth_service:validate(Token) of
        {ok, Claims} -> {ok, Claims};
        error -> {error, invalid_token}
    end
end,

{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_bearer,
        provider_opts => #{verifier => Verifier}
    }
}).

API Key Authentication

%% Simple API key list
{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_apikey,
        provider_opts => #{
            keys => #{
                <<"key-abc123">> => #{subject => <<"user1">>, scopes => [<<"read">>]},
                <<"key-xyz789">> => #{subject => <<"user2">>, scopes => [<<"read">>, <<"write">>]}
            }
        }
    }
}).

%% With hashed keys for security (recommended for production)
HashedKey = barrel_mcp_auth_apikey:hash_key(<<"my-secret-key">>),
{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_apikey,
        provider_opts => #{
            keys => #{HashedKey => #{subject => <<"user1">>}},
            hash_keys => true
        }
    }
}).

Basic Authentication

%% Simple username/password
{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_basic,
        provider_opts => #{
            credentials => #{
                <<"admin">> => <<"password123">>,
                <<"user">> => <<"secret">>
            },
            realm => <<"MCP Server">>
        }
    }
}).

%% With hashed passwords (recommended)
HashedPwd = barrel_mcp_auth_basic:hash_password(<<"my-password">>),
{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_basic,
        provider_opts => #{
            credentials => #{<<"admin">> => HashedPwd},
            hash_passwords => true
        }
    }
}).

Custom Authentication (Simple Interface)

For integrating with existing auth systems, use barrel_mcp_auth_custom with a simple two-function module:

-module(my_auth).
-export([init/1, authenticate/2]).

init(_Opts) ->
    {ok, #{}}.

authenticate(Token, State) ->
    case my_key_store:validate(Token) of
        {ok, Info} ->
            {ok, #{subject => Info}, State};
        error ->
            {error, invalid_token, State}
    end.

Configure it:

{ok, _} = barrel_mcp:start_http(#{
    port => 9090,
    auth => #{
        provider => barrel_mcp_auth_custom,
        provider_opts => #{
            module => my_auth
        }
    }
}).

See guides/custom-authentication.md for full documentation.

Custom Authentication Provider (Full Behaviour)

For more control, implement the full barrel_mcp_auth behaviour:

-module(my_auth_provider).
-behaviour(barrel_mcp_auth).

-export([init/1, authenticate/2, challenge/2]).

init(Opts) ->
    {ok, Opts}.

authenticate(Request, State) ->
    Headers = maps:get(headers, Request, #{}),
    case barrel_mcp_auth:extract_bearer_token(Headers) of
        {ok, Token} ->
            %% Your validation logic
            case validate_with_my_service(Token) of
                {ok, User} ->
                    {ok, #{
                        subject => User,
                        scopes => [<<"read">>],
                        claims => #{}
                    }};
                error ->
                    {error, invalid_token}
            end;
        {error, no_token} ->
            {error, unauthorized}
    end.

challenge(Reason, _State) ->
    {401, #{<<"www-authenticate">> => <<"Bearer realm=\"mcp\"">>}, <<>>}.

Accessing Auth Info in Handlers

Authentication info is available in the request context:

my_tool_handler(Args) ->
    %% Auth info is passed in the _auth key
    case maps:get(<<"_auth">>, Args, undefined) of
        undefined ->
            <<"No auth info">>;
        AuthInfo ->
            Subject = maps:get(subject, AuthInfo),
            <<"Hello ", Subject/binary>>
    end.

Starting stdio Server (for Claude Desktop)

%% This blocks and handles MCP over stdin/stdout
barrel_mcp:start_stdio().

Using as Client

%% Connect to an MCP server
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"http://localhost:9090/mcp">>}
}).

%% Initialize connection
{ok, ServerInfo, Client1} = barrel_mcp_client:initialize(Client).

%% List available tools
{ok, Tools, Client2} = barrel_mcp_client:list_tools(Client1).

%% Call a tool
{ok, Result, Client3} = barrel_mcp_client:call_tool(Client2, <<"search">>, #{
    <<"query">> => <<"hello world">>
}).

%% Close connection
ok = barrel_mcp_client:close(Client3).

Claude Desktop Configuration

When using barrel_mcp with stdio transport for Claude Desktop:

{
  "mcpServers": {
    "my-server": {
      "command": "/path/to/my_app/bin/my_app",
      "args": ["mcp"]
    }
  }
}

Your application's entry point should call barrel_mcp:start_stdio().

API Reference

Tools

| Function | Description | |----------|-------------| | barrel_mcp:reg_tool(Name, Module, Function, Opts) | Register a tool | | barrel_mcp:unreg_tool(Name) | Unregister a tool | | barrel_mcp:call_tool(Name, Args) | Call a tool locally | | barrel_mcp:list_tools() | List all registered tools |

Resources

| Function | Description | |----------|-------------| | barrel_mcp:reg_resource(Name, Module, Function, Opts) | Register a resource | | barrel_mcp:unreg_resource(Name) | Unregister a resource | | barrel_mcp:read_resource(Name) | Read a resource locally | | barrel_mcp:list_resources() | List all registered resources |

Prompts

| Function | Description | |----------|-------------| | barrel_mcp:reg_prompt(Name, Module, Function, Opts) | Register a prompt | | barrel_mcp:unreg_prompt(Name) | Unregister a prompt | | barrel_mcp:get_prompt(Name, Args) | Get a prompt locally | | barrel_mcp:list_prompts() | List all registered prompts |

Registry

| Function | Description | |----------|-------------| | barrel_mcp_registry:start_link() | Start the registry (called by supervisor) | | barrel_mcp_registry:wait_for_ready() | Wait for registry to be ready | | barrel_mcp_registry:wait_for_ready(Timeout) | Wait with custom timeout |

Server

| Function | Description | |----------|-------------| | barrel_mcp:start_http_stream(Opts) | Start Streamable HTTP server (Claude Code) | | barrel_mcp:stop_http_stream() | Stop Streamable HTTP server | | barrel_mcp:start_http(Opts) | Start HTTP server (legacy) | | barrel_mcp:stop_http() | Stop HTTP server | | barrel_mcp:start_stdio() | Start stdio server (blocking) |

Client

| Function | Description | |----------|-------------| | barrel_mcp_client:connect(Opts) | Connect to MCP server | | barrel_mcp_client:initialize(Client) | Initialize connection | | barrel_mcp_client:list_tools(Client) | List available tools | | barrel_mcp_client:call_tool(Client, Name, Args) | Call a tool | | barrel_mcp_client:list_resources(Client) | List available resources | | barrel_mcp_client:read_resource(Client, Uri) | Read a resource | | barrel_mcp_client:list_prompts(Client) | List available prompts | | barrel_mcp_client:get_prompt(Client, Name, Args) | Get a prompt | | barrel_mcp_client:close(Client) | Close connection |

Authentication

| Function | Description | |----------|-------------| | barrel_mcp_auth:extract_bearer_token(Headers) | Extract Bearer token from headers | | barrel_mcp_auth:extract_api_key(Headers, Opts) | Extract API key from headers | | barrel_mcp_auth:extract_basic_auth(Headers) | Extract Basic auth credentials | | barrel_mcp_auth_apikey:hash_key(Key) | Hash an API key (SHA256) | | barrel_mcp_auth_basic:hash_password(Password) | Hash a password (SHA256) |

MCP Protocol Support

Supported Methods

Lifecycle:

  • initialize / initialized
  • ping

Tools:

  • tools/list
  • tools/call

Resources:

  • resources/list
  • resources/read
  • resources/templates/list
  • resources/subscribe / resources/unsubscribe

Prompts:

  • prompts/list
  • prompts/get

Sampling:

  • sampling/createMessage

Logging:

  • logging/setLevel

Development

# Compile
rebar3 compile

# Run tests
rebar3 eunit

# Dialyzer
rebar3 dialyzer

# Shell
rebar3 shell

License

Apache-2.0

Quick Setup
Installation guide for this server

Installation Command (package not published)

git clone https://github.com/barrel-platform/barrel_mcp
Manual Installation: Please check the README for detailed setup instructions and any additional dependencies required.

Cursor configuration (mcp.json)

{ "mcpServers": { "barrel-platform-barrel-mcp": { "command": "git", "args": [ "clone", "https://github.com/barrel-platform/barrel_mcp" ] } } }