Skip to main content

guides/client.md

# MCP Client (reference)

> **Looking for a walkthrough?** Read
> [Building a client](building-a-client.md) for the task-oriented
> guide; this file is the older API reference and stays for cross
> linking. The new guide covers the gen_statem API, OAuth,
> handlers, federation, and runnable examples.

barrel_mcp includes a client library for connecting to external MCP servers.
This allows your Erlang application to consume tools, resources, and prompts
from other MCP-compatible services.

## Connecting to a Server

### HTTP Transport

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

%% With custom options
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"https://api.example.com/mcp">>},
    timeout => 30000,  %% Request timeout in ms
    headers => #{
        <<"Authorization">> => <<"Bearer your-token">>
    }
}).
```

### stdio Transport

For connecting to local MCP servers via stdin/stdout:

```erlang
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {stdio, "/path/to/mcp-server", ["--arg1", "--arg2"]}
}).
```

## Initializing the Connection

After connecting, initialize to exchange capabilities:

```erlang
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"http://localhost:9090/mcp">>}
}),

{ok, ServerInfo, Client1} = barrel_mcp_client:initialize(Client),

%% ServerInfo contains:
%% #{
%%     <<"protocolVersion">> => <<"2024-11-05">>,
%%     <<"serverInfo">> => #{
%%         <<"name">> => <<"example-server">>,
%%         <<"version">> => <<"1.0.0">>
%%     },
%%     <<"capabilities">> => #{
%%         <<"tools">> => #{},
%%         <<"resources">> => #{},
%%         <<"prompts">> => #{}
%%     }
%% }
```

## Working with Tools

### List Available Tools

```erlang
{ok, Tools, Client2} = barrel_mcp_client:list_tools(Client1),

%% Tools is a list of tool definitions:
%% [
%%     #{
%%         <<"name">> => <<"search">>,
%%         <<"description">> => <<"Search for information">>,
%%         <<"inputSchema">> => #{...}
%%     },
%%     ...
%% ]
```

### Call a Tool

```erlang
{ok, Result, Client3} = barrel_mcp_client:call_tool(Client2, <<"search">>, #{
    <<"query">> => <<"erlang mcp">>
}),

%% Result contains the tool output:
%% #{
%%     <<"content">> => [
%%         #{<<"type">> => <<"text">>, <<"text">> => <<"Results...">>}
%%     ]
%% }
```

### Error Handling

```erlang
case barrel_mcp_client:call_tool(Client, <<"unknown">>, #{}) of
    {ok, Result, NewClient} ->
        process_result(Result);
    {error, {method_not_found, _}, NewClient} ->
        logger:warning("Tool not found"),
        handle_missing_tool();
    {error, Reason, NewClient} ->
        logger:error("Tool call failed: ~p", [Reason]),
        handle_error(Reason)
end.
```

## Working with Resources

### List Resources

```erlang
{ok, Resources, Client2} = barrel_mcp_client:list_resources(Client1),

%% Resources list:
%% [
%%     #{
%%         <<"uri">> => <<"file:///config">>,
%%         <<"name">> => <<"Configuration">>,
%%         <<"mimeType">> => <<"application/json">>
%%     },
%%     ...
%% ]
```

### Read a Resource

```erlang
{ok, Content, Client3} = barrel_mcp_client:read_resource(Client2, <<"file:///config">>),

%% Content structure:
%% #{
%%     <<"contents">> => [
%%         #{
%%             <<"uri">> => <<"file:///config">>,
%%             <<"text">> => <<"{\"key\": \"value\"}">>
%%         }
%%     ]
%% }
```

## Working with Prompts

### List Prompts

```erlang
{ok, Prompts, Client2} = barrel_mcp_client:list_prompts(Client1),

%% Prompts list:
%% [
%%     #{
%%         <<"name">> => <<"summarize">>,
%%         <<"description">> => <<"Summarize content">>,
%%         <<"arguments">> => [
%%             #{<<"name">> => <<"content">>, <<"required">> => true}
%%         ]
%%     },
%%     ...
%% ]
```

### Get a Prompt

```erlang
{ok, PromptResult, Client3} = barrel_mcp_client:get_prompt(Client2, <<"summarize">>, #{
    <<"content">> => <<"Long text to summarize...">>
}),

%% PromptResult contains messages:
%% #{
%%     <<"messages">> => [
%%         #{
%%             <<"role">> => <<"user">>,
%%             <<"content">> => #{
%%                 <<"type">> => <<"text">>,
%%                 <<"text">> => <<"Please summarize...">>
%%             }
%%         }
%%     ]
%% }
```

## Connection Management

### Closing Connections

Always close connections when done:

```erlang
ok = barrel_mcp_client:close(Client).
```

### Connection State

The client maintains state that must be threaded through calls:

```erlang
%% Pattern: Always use the returned client for subsequent calls
{ok, Client1} = barrel_mcp_client:connect(Opts),
{ok, _, Client2} = barrel_mcp_client:initialize(Client1),
{ok, Tools, Client3} = barrel_mcp_client:list_tools(Client2),
{ok, Result, Client4} = barrel_mcp_client:call_tool(Client3, <<"tool">>, #{}),
ok = barrel_mcp_client:close(Client4).
```

### Using with gen_server

```erlang
-module(my_mcp_worker).
-behaviour(gen_server).

-export([start_link/1, call_tool/2]).
-export([init/1, handle_call/3, handle_cast/2, terminate/2]).

start_link(Opts) ->
    gen_server:start_link(?MODULE, Opts, []).

call_tool(Pid, {ToolName, Args}) ->
    gen_server:call(Pid, {call_tool, ToolName, Args}).

init(Opts) ->
    {ok, Client} = barrel_mcp_client:connect(Opts),
    {ok, _, Client1} = barrel_mcp_client:initialize(Client),
    {ok, #{client => Client1}}.

handle_call({call_tool, Name, Args}, _From, #{client := Client} = State) ->
    case barrel_mcp_client:call_tool(Client, Name, Args) of
        {ok, Result, NewClient} ->
            {reply, {ok, Result}, State#{client := NewClient}};
        {error, Reason, NewClient} ->
            {reply, {error, Reason}, State#{client := NewClient}}
    end.

handle_cast(_Msg, State) ->
    {noreply, State}.

terminate(_Reason, #{client := Client}) ->
    barrel_mcp_client:close(Client),
    ok.
```

## Authentication

### Bearer Token

```erlang
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"https://api.example.com/mcp">>},
    headers => #{
        <<"Authorization">> => <<"Bearer eyJhbGciOiJIUzI1NiIs...">>
    }
}).
```

### API Key

```erlang
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"https://api.example.com/mcp">>},
    headers => #{
        <<"X-API-Key">> => <<"your-api-key">>
    }
}).
```

### Basic Auth

```erlang
Credentials = base64:encode(<<"user:password">>),
{ok, Client} = barrel_mcp_client:connect(#{
    transport => {http, <<"https://api.example.com/mcp">>},
    headers => #{
        <<"Authorization">> => <<"Basic ", Credentials/binary>>
    }
}).
```

## Error Handling

Common errors and how to handle them:

```erlang
case barrel_mcp_client:call_tool(Client, Name, Args) of
    {ok, Result, NewClient} ->
        {ok, Result, NewClient};

    {error, {http_error, 401}, NewClient} ->
        %% Authentication failed - refresh token and retry
        NewHeaders = refresh_auth_token(),
        retry_with_new_auth(NewClient, NewHeaders);

    {error, {http_error, 404}, NewClient} ->
        %% Endpoint not found
        {error, server_not_found, NewClient};

    {error, {http_error, 500}, NewClient} ->
        %% Server error - maybe retry
        {error, server_error, NewClient};

    {error, timeout, NewClient} ->
        %% Request timed out
        {error, timeout, NewClient};

    {error, {method_not_found, _}, NewClient} ->
        %% Tool/resource/prompt doesn't exist
        {error, not_found, NewClient};

    {error, Reason, NewClient} ->
        %% Other errors
        logger:error("MCP client error: ~p", [Reason]),
        {error, Reason, NewClient}
end.
```

## Best Practices

1. **Always thread client state** - Each operation returns an updated client
2. **Close connections** - Use `close/1` or handle in `terminate/2`
3. **Handle errors** - MCP servers may be unavailable or return errors
4. **Set appropriate timeouts** - Especially for slow operations
5. **Cache tool/resource lists** - Don't fetch on every call if they don't change