%%%------------------------------------------------------------------- %%% @author Benoit Chesneau %%% @copyright 2024-2026 Benoit Chesneau %%% @doc MCP Streamable HTTP Transport (Protocol Version 2025-03-26). %%% %%% Implements the MCP Streamable HTTP transport for Claude Code %%% integration on the built-in h1/h2 server %%% ({@link barrel_mcp_http_listener}). This transport uses: %%% %%% %%% The protocol logic lives in {@link barrel_mcp_http_engine}; this %%% module only wires the user options to the engine and starts the %%% listener. %%% %%% @reference MCP Transport Specification %%% @end %%%------------------------------------------------------------------- -module(barrel_mcp_http_stream). -export([start/1, stop/0]). -define(STREAM_LISTENER, barrel_mcp_http_stream_listener). %%==================================================================== %% API %%==================================================================== %% @doc Start the Streamable HTTP server. %% %% == Security defaults == %% %% The server binds to `127.0.0.1' by default. Public binds (any %% non-loopback IP) require an explicit `allowed_origins' to prevent %% DNS-rebinding and CORS-style attacks. The `Origin' header is %% validated on every request; mismatches get HTTP 403. %% %% == Options == %% %% -spec start(Opts) -> {ok, pid()} | {error, term()} when Opts :: #{ port => pos_integer(), ip => inet:ip_address(), auth => map(), session_enabled => boolean(), ssl => map(), allowed_origins => [binary()] | any, allow_missing_origin => boolean(), max_connections => pos_integer() }. start(Opts) -> Port = maps:get(port, Opts, 9090), Ip = maps:get(ip, Opts, {127, 0, 0, 1}), SessionEnabled = maps:get(session_enabled, Opts, true), 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), _ = case SessionEnabled of true -> barrel_mcp_http_engine:ensure_session_manager(); false -> ok end, 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 => stream, auth_config => AuthConfig, session_enabled => SessionEnabled, allowed_origins => AllowedOrigins, allow_missing_origin => AllowMissing, sse_buffer_size => maps:get(sse_buffer_size, Opts, 256), 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(?STREAM_LISTENER, ListenOpts, EngineConfig) end. %% @doc Stop the Streamable HTTP server. -spec stop() -> ok | {error, not_found}. stop() -> barrel_mcp_http_listener:stop(?STREAM_LISTENER). normalize_ssl(Opts) -> case maps:get(ssl, Opts, undefined) of #{certfile := _, keyfile := _} = Ssl -> Ssl; _ -> undefined end.