%%%-------------------------------------------------------------------
%%% @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.