%%%------------------------------------------------------------------- %%% @doc Multi-server tool aggregator for agent hosts. %%% %%% Sits on top of {@link barrel_mcp_clients} and turns the set of %%% connected MCP clients into a single namespaced tool catalog the %%% host can hand to an LLM, plus a router that dispatches a model's %%% tool call back to the right MCP server. %%% %%% Tool names are namespaced as `<<"ServerIdToolName">>'. The %%% default separator is `<<":">>'; override with the `separator' %%% option on every call. Hosts that allow `:' in tool names should %%% pick a separator that does not appear in any registered tool. %%% %%% Typical agent loop: %%% ``` %%% Tools = barrel_mcp_agent:to_anthropic(), %%% %% ... ask the model with Tools, receive a tool_use block ... %%% {Name, Args} = barrel_mcp_tool_format:from_anthropic_call(Block), %%% {ok, Result} = barrel_mcp_agent:call_tool(Name, Args). %%% ''' %%% @end %%%------------------------------------------------------------------- -module(barrel_mcp_agent). -export([ list_tools/0, list_tools/1, to_anthropic/0, to_anthropic/1, to_openai/0, to_openai/1, call_tool/2, call_tool/3 ]). -type opts() :: #{separator => binary(), timeout => timeout()}. -export_type([opts/0]). -define(DEFAULT_SEP, <<":">>). %%==================================================================== %% Listing %%==================================================================== %% @doc Return every tool from every registered client, with each %% tool's `<<"name">>' rewritten to `<<"ServerIdToolName">>'. %% Servers that error out are skipped (with a `logger:warning'). -spec list_tools() -> [map()]. list_tools() -> list_tools(#{}). -spec list_tools(opts()) -> [map()]. list_tools(Opts) -> Sep = sep(Opts), Servers = barrel_mcp_clients:list_clients(), lists:flatmap( fun({ServerId, Pid}) -> case fetch_tools(Pid) of {ok, Tools} -> [namespace_tool(ServerId, Sep, T) || T <- Tools]; {error, Reason} -> logger:warning( "barrel_mcp_agent: list_tools ~p failed: ~p", [ServerId, Reason] ), [] end end, Servers ). fetch_tools(Pid) -> barrel_mcp_client:list_tools_all(Pid). namespace_tool(ServerId, Sep, Tool) -> Original = maps:get(<<"name">>, Tool), Tool#{<<"name">> => <<(to_bin(ServerId))/binary, Sep/binary, Original/binary>>}. %%==================================================================== %% Provider formats %%==================================================================== %% @doc Aggregated tools in Anthropic Messages API format. -spec to_anthropic() -> [map()]. to_anthropic() -> to_anthropic(#{}). -spec to_anthropic(opts()) -> [map()]. to_anthropic(Opts) -> barrel_mcp_tool_format:to_anthropic(list_tools(Opts)). %% @doc Aggregated tools in OpenAI Chat Completions tool format. -spec to_openai() -> [map()]. to_openai() -> to_openai(#{}). -spec to_openai(opts()) -> [map()]. to_openai(Opts) -> barrel_mcp_tool_format:to_openai(list_tools(Opts)). %%==================================================================== %% Routing %%==================================================================== %% @doc Route a model's tool call back to the right MCP server. %% `NamespacedName' is `<<"ServerIdToolName">>'; the `ServerId' %% prefix selects the client and the `ToolName' suffix is forwarded. %% Errors: %%
    %%
  • `{error, no_separator}' — the name does not contain the %% configured separator.
  • %%
  • `{error, unknown_server}' — no client is registered under %% the parsed `ServerId'.
  • %%
  • Any error returned by %% {@link barrel_mcp_client:call_tool/4}.
  • %%
-spec call_tool(binary(), map()) -> {ok, map()} | {error, term()}. call_tool(NsName, Args) -> call_tool(NsName, Args, #{}). -spec call_tool(binary(), map(), opts()) -> {ok, map()} | {error, term()}. call_tool(NsName, Args, Opts) -> Sep = sep(Opts), case split_ns(NsName, Sep) of {error, _} = E -> E; {ServerId, ToolName} -> case barrel_mcp_clients:whereis_client(ServerId) of undefined -> {error, unknown_server}; Pid -> Timeout = maps:get(timeout, Opts, 30000), barrel_mcp_client:call_tool( Pid, ToolName, Args, #{timeout => Timeout} ) end end. %%==================================================================== %% Helpers %%==================================================================== sep(Opts) -> maps:get(separator, Opts, ?DEFAULT_SEP). to_bin(B) when is_binary(B) -> B; to_bin(A) when is_atom(A) -> atom_to_binary(A, utf8); to_bin(L) when is_list(L) -> list_to_binary(L); to_bin(I) when is_integer(I) -> integer_to_binary(I). split_ns(Bin, Sep) when is_binary(Bin), is_binary(Sep) -> case binary:split(Bin, Sep) of [_] -> {error, no_separator}; [Server, Tool] -> {Server, Tool} end.