%%%-------------------------------------------------------------------
%%% @doc Translators between MCP tool shapes and LLM provider tool
%%% shapes (Anthropic Messages API, OpenAI Chat Completions / Responses).
%%%
%%% Use this module to bridge an MCP server's `tools/list' response
%%% into the tool definitions a provider expects, and to translate a
%%% provider's tool-call back into the `(Name, Arguments)' pair you
%%% feed to {@link barrel_mcp_client:call_tool/4}.
%%%
%%% The MCP shape (one entry from `tools/list') is:
%%% ```
%%% #{
%%% <<"name">> := binary(),
%%% <<"description">> => binary(),
%%% <<"inputSchema">> => map(),
%%% <<"title">> => binary(), %% optional
%%% <<"annotations">> => map(), %% optional
%%% ... %% other MCP keys ignored
%%% }
%%% '''
%%%
%%% The Anthropic shape (Messages API `tools' array entry) is:
%%% ```
%%% #{
%%% <<"name">> := binary(),
%%% <<"description">> := binary(),
%%% <<"input_schema">> := map()
%%% }
%%% '''
%%%
%%% The OpenAI shape (Chat Completions `tools' array entry) is:
%%% ```
%%% #{
%%% <<"type">> := <<"function">>,
%%% <<"function">> := #{
%%% <<"name">> := binary(),
%%% <<"description">> := binary(),
%%% <<"parameters">> := map()
%%% }
%%% }
%%% '''
%%% @end
%%%-------------------------------------------------------------------
-module(barrel_mcp_tool_format).
-export([
to_anthropic/1,
to_openai/1,
from_anthropic_call/1,
from_openai_call/1
]).
-type mcp_tool() :: map().
-type provider_tool() :: map().
-type tool_call() :: {Name :: binary(), Args :: map()}.
-export_type([mcp_tool/0, provider_tool/0, tool_call/0]).
%%====================================================================
%% MCP -> provider
%%====================================================================
%% @doc Translate one MCP tool, or a list of MCP tools, into the
%% Anthropic Messages API tool format. Unknown MCP keys are ignored;
%% missing `description' becomes an empty binary.
-spec to_anthropic(mcp_tool() | [mcp_tool()]) ->
provider_tool() | [provider_tool()].
to_anthropic(Tools) when is_list(Tools) ->
[to_anthropic(T) || T <- Tools];
to_anthropic(Tool) when is_map(Tool) ->
Schema = maps:get(
<<"inputSchema">>,
Tool,
#{<<"type">> => <<"object">>}
),
#{
<<"name">> => maps:get(<<"name">>, Tool),
<<"description">> => maps:get(<<"description">>, Tool, <<>>),
<<"input_schema">> => Schema
}.
%% @doc Translate one MCP tool, or a list of MCP tools, into the OpenAI
%% Chat Completions tool format. Wraps each tool in the
%% `{type: "function", function: {...}}' envelope OpenAI requires.
-spec to_openai(mcp_tool() | [mcp_tool()]) ->
provider_tool() | [provider_tool()].
to_openai(Tools) when is_list(Tools) ->
[to_openai(T) || T <- Tools];
to_openai(Tool) when is_map(Tool) ->
Schema = maps:get(
<<"inputSchema">>,
Tool,
#{<<"type">> => <<"object">>}
),
#{
<<"type">> => <<"function">>,
<<"function">> => #{
<<"name">> => maps:get(<<"name">>, Tool),
<<"description">> => maps:get(<<"description">>, Tool, <<>>),
<<"parameters">> => Schema
}
}.
%%====================================================================
%% Provider -> MCP
%%====================================================================
%% @doc Translate a single Anthropic `tool_use' content block into the
%% `(Name, Arguments)' pair you can feed to
%% {@link barrel_mcp_client:call_tool/4}. Accepts both the canonical
%% binary-keyed map and the camelCase variant some clients emit.
-spec from_anthropic_call(map()) -> tool_call().
from_anthropic_call(#{<<"name">> := Name, <<"input">> := Input}) ->
{Name, Input};
from_anthropic_call(#{<<"toolName">> := Name, <<"input">> := Input}) ->
{Name, Input}.
%% @doc Translate a single OpenAI `tool_call' object into the
%% `(Name, Arguments)' pair. Accepts both the parsed-arguments shape
%% (`arguments' is already a map) and the wire shape (`arguments' is a
%% JSON string that needs decoding).
-spec from_openai_call(map()) -> tool_call().
from_openai_call(#{<<"function">> := Fn}) ->
Name = maps:get(<<"name">>, Fn),
Args = decode_args(maps:get(<<"arguments">>, Fn, <<"{}">>)),
{Name, Args}.
decode_args(Args) when is_map(Args) -> Args;
decode_args(Args) when is_binary(Args) ->
case json:decode(Args) of
Map when is_map(Map) -> Map;
_ -> #{}
end.