Skip to content

Erlang SDK

This document demonstrates how to use the MCP over MQTT Erlang SDK to create a simple MCP over MQTT server and client.

Example

Create a Simple MCP Client

erlang
-module(mcp_mqtt_erl_client_demo).

-behaviour(mcp_mqtt_erl_client_session).

-export([
    client_name/0,
    client_version/0,
    client_capabilities/0,
    received_non_mcp_message/3
]).
-export([start_link/0]).

%% The client name, version, and capabilities. These details will be sent to the server during MCP initialization.
client_name() ->
    <<"emqx_tools/cli_demo">>.

client_version() ->
    <<"1.0">>.

client_capabilities() -> #{}.

%% Callback for non-MCP messages
received_non_mcp_message(MqttClient, Msg, State) ->
    io:format("~p Received non-MCP message: ~p~n", [MqttClient, Msg]),
    State.

%% Start the MCP over MQTT client
start_link() ->
    mcp_mqtt_erl_client:start_link(
        #{
            server_name_filter => <<"#">>,
            callback_mod => ?MODULE,
            broker_address => {"127.0.0.1", 1883},
            mqtt_options => #{
                clientid => <<"emqx_tools_cli_demo">>,
                username => <<"emqx">>,
                password => <<"public">>
            }
        }).

Here, server_name_filter is used to subscribe to the MQTT topic filter for MCP servers, and mqtt_options are options passed to the underlying MQTT client.

Create a Simple MCP Server

Below is a simple MCP server implementation that supports two tools: tool1 and tool2.

erlang
-module(mcp_mqtt_erl_server_demo).

-behaviour(mcp_mqtt_erl_server_session).

-include_lib("mcp_mqtt_erl/include/mcp_mqtt_erl_types.hrl").
-include_lib("mcp_mqtt_erl/include/emqx_mcp_tools.hrl").

-export([
    start_link/2
]).

-export([
    server_name/0,
    server_id/2,
    server_version/0,
    server_capabilities/0,
    server_instructions/0,
    server_meta/0
]).

-export([
    initialize/2,
    list_resources/1,
    read_resource/2,
    call_tool/3,
    list_tools/1
]).

-type loop_data() :: #{
    server_id => binary(),
    client_info => map(),
    client_capabilities => map(),
    mcp_client_id => binary(),
    _ => any()
}.

-spec start_link(integer(), mcp_mqtt_erl_server:config()) -> gen_statem:start_ret().
start_link(Idx, Conf) ->
    mcp_mqtt_erl_server:start_link(Idx, Conf).

server_version() ->
    <<?PLUGIN_VSN>>.

server_name() ->
    <<"emqx_tools/info_apis">>.

server_id(ClientIdPrefix, Idx) ->
    Idx1 = integer_to_binary(Idx),
    Node = atom_to_binary(node()),
    <<ClientIdPrefix/binary, ":", Node/binary, ":", Idx1/binary>>.

server_capabilities() ->
    #{
        resources => #{
            subscribe => true,
            listChanged => true
        },
        tools => #{
            listChanged => true
        }
    }.

server_instructions() ->
    <<"">>.

server_meta() ->
    #{
        authorization => #{
            roles => [<<"admin">>, <<"user">>]
        }
    }.

-spec initialize(binary(), client_params()) -> {ok, loop_data()}.
initialize(ServerId, #{client_info := ClientInfo, client_capabilities := Capabilities, mcp_client_id := McpClientId}) ->
    io:format("initialize --- server_id: ~p, client_info: ~p, client_capabilities: ~p, mcp_client_id: ~p~n", [ServerId, ClientInfo, Capabilities, McpClientId]),
    {ok, #{
        server_id => ServerId,
        client_info => ClientInfo,
        client_capabilities => Capabilities,
        mcp_client_id => McpClientId
    }}.

-spec call_tool(binary(), map(), loop_data()) -> {ok, call_tool_result() | [call_tool_result()], loop_data()}.
call_tool(ToolName, Args, LoopData) ->
    io:format("call_tool --- tool_name: ~p, args: ~p~n", [ToolName, Args]),
    Result = #{
        type => text,
        text => <<"This is the result of the tool call">>
    },
    {ok, Result, LoopData}.

-spec list_tools(loop_data()) -> {ok, [tool_def()], loop_data()}.
list_tools(LoopData) ->
    io:format("list_tools --- ~n", []),
    Tools = [
        #{
            name => <<"tool1">>,
            description => <<"This is tool 1">>,
            inputSchema => #{
                type => <<"object">>,
                properties => #{
                    arg1 => #{
                        type => <<"string">>,
                        description => <<"Argument 1">>
                    },
                    arg2 => #{
                        type => <<"integer">>,
                        description => <<"Argument 2">>
                    }
                }
            }
        },
        #{
            name => <<"tool2">>,
            description => <<"This is tool 2">>,
            inputSchema => #{
                type => <<"object">>,
                properties => #{
                    arg1 => #{
                        type => <<"string">>,
                        description => <<"Argument 1">>
                    },
                    arg2 => #{
                        type => <<"boolean">>,
                        description => <<"Argument 2">>
                    }
                }
            }
        }
    ],
    {ok, Tools, LoopData}.