Skip to main content

src/barrel_mcp_http.erl

%%%-------------------------------------------------------------------
%%% @author Benoit Chesneau
%%% @copyright 2024-2026 Benoit Chesneau
%%% @doc Simple HTTP transport for MCP (POST/OPTIONS, no sessions/SSE).
%%%
%%% A minimal JSON-RPC-over-HTTP transport on the built-in h1/h2
%%% server ({@link barrel_mcp_http_listener}). For the full
%%% Streamable HTTP transport (sessions, SSE, async tools) use
%%% {@link barrel_mcp_http_stream}.
%%%
%%% == Authentication Options ==
%%%
%%% The `auth' option is a map with `provider', `provider_opts' and
%%% `required_scopes'. See {@link barrel_mcp_auth}.
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_mcp_http).

-export([start/1, stop/0]).

-define(HTTP_LISTENER, barrel_mcp_http_simple_listener).

%%====================================================================
%% API
%%====================================================================

%% @doc Start the simple HTTP server for MCP.
%%
%% Same security defaults as {@link barrel_mcp_http_stream}: binds to
%% `127.0.0.1' by default and requires explicit `allowed_origins' for
%% non-loopback binds.
-spec start(map()) -> {ok, pid()} | {error, term()}.
start(Opts) ->
    Port = maps:get(port, Opts, 9090),
    Ip = maps:get(ip, Opts, {127, 0, 0, 1}),
    Loopback = barrel_mcp_http_engine:is_loopback(Ip),
    case
        barrel_mcp_http_engine:resolve_allowed_origins(
            Loopback, maps:get(allowed_origins, Opts, undefined)
        )
    of
        {error, _} = Err ->
            Err;
        {ok, AllowedOrigins} ->
            AllowMissing = maps:get(allow_missing_origin, Opts, Loopback),
            ResourceMetadata = barrel_mcp_http_engine:normalize_resource_metadata(
                maps:get(resource_metadata, Opts, undefined)
            ),
            AuthConfig0 = barrel_mcp_http_engine:init_auth(
                maps:get(auth, Opts, #{})
            ),
            AuthConfig = barrel_mcp_http_engine:inject_resource_metadata_url(
                AuthConfig0, ResourceMetadata
            ),
            EngineConfig = #{
                mode => simple,
                auth_config => AuthConfig,
                allowed_origins => AllowedOrigins,
                allow_missing_origin => AllowMissing,
                resource_metadata => ResourceMetadata
            },
            ListenOpts = maps:merge(
                #{port => Port, ip => Ip, ssl => normalize_ssl(Opts)},
                maps:with([max_connections, acceptors], Opts)
            ),
            barrel_mcp_http_listener:start(?HTTP_LISTENER, ListenOpts, EngineConfig)
    end.

%% @doc Stop the simple HTTP server.
-spec stop() -> ok | {error, not_found}.
stop() ->
    barrel_mcp_http_listener:stop(?HTTP_LISTENER).

normalize_ssl(Opts) ->
    case maps:get(ssl, Opts, undefined) of
        #{certfile := _, keyfile := _} = Ssl -> Ssl;
        _ -> undefined
    end.