Converts between a ConduitMcp.Client.ToolSet and Gemini API
(generateContent) function calling. Pure functions over string-keyed
maps; no HTTP.
Experimental
This adapter is experimental: its shapes come from the Gemini API reference, not from end-to-end runs against the API, and may change in a minor release.
{toolset, _} = ConduitMcp.Client.Naming.build([{"files", tools}])
request = %{"contents" => contents,
"tools" => [%{"functionDeclarations" => Gemini.function_declarations(toolset)}]}
{calls, rejected} = Gemini.function_calls(response, toolset)
# run each %ToolCall{} through the client pipeline (or let
# ConduitMcp.Client.Adapters.execute_tool_calls/4 do all of this), then:
parts = Enum.map(outcomes, fn {call, outcome} ->
{:ok, name} = ConduitMcp.Client.ToolSet.exposed_name(toolset, call.server_id, call.name)
Gemini.function_response(name, outcome, id: call.call_id)
end)
# next turn: %{"role" => "user", "parts" => rejected ++ parts}Verified rules (2026-10-17)
From the FunctionDeclaration, FunctionCall and FunctionResponse
reference at https://ai.google.dev/api/generate-content and the guide at
https://ai.google.dev/gemini-api/docs/function-calling:
FunctionDeclaration.namemay containa-z A-Z 0-9 _ : . -, at most 128 characters. The toolset's exposed names (^[A-Za-z0-9_-]{1,64}$) are a subset.parametersJsonSchematakes the parameters as JSON Schema (an object schema);parameterstakes the OpenAPI 3.0Schemasubset instead. The two are mutually exclusive. This adapter sends the MCPinputSchemaunchanged asparametersJsonSchema.descriptionis required, so a tool without one gets"".FunctionCallis{id?, name, args?};FunctionResponseis{id?, name, response}whereresponsemust be a JSON object. By the reference's convention the output goes under"output"and a failure under"error".
Results
function_response/3 puts the result under response.output: the
structuredContent object when present (default structured: :prefer),
otherwise the content as text, with media as placeholders such as
"[image: image/png, 1024 bytes]". A result with is_error: true, or a
ConduitMcp.Client.Error, goes under response.error as text.
Descriptions have invisible characters removed and are capped at 4 KiB
(PI-1); only tools in the toolset are emitted (PO-7).
Summary
Functions
Extracts the function calls from a response.
The functionDeclarations for toolset, in toolset order. Wrap them as
%{"functionDeclarations" => declarations} in the request's tools.
Builds the functionResponse part answering a call to name (the
exposed name the model used).
Functions
@spec function_calls(map() | [map()], ConduitMcp.Client.ToolSet.t()) :: {[ConduitMcp.Client.ToolCall.t()], [map()]}
Extracts the function calls from a response.
Accepts a generateContent response (the first candidate's content is
used), a Content ("parts"), or a list of parts. Each functionCall
part whose name resolves in toolset becomes a
ConduitMcp.Client.ToolCall with call_id set to the call's id (which
Gemini may omit, giving nil).
A call naming a tool not in the toolset, or with args that are not an
object, produces no call (HB-2). Instead it yields a functionResponse
part with an "error", returned in the second list, to send back to the
model. Other parts are ignored.
@spec function_declarations(ConduitMcp.Client.ToolSet.t()) :: [map()]
The functionDeclarations for toolset, in toolset order. Wrap them as
%{"functionDeclarations" => declarations} in the request's tools.
@spec function_response( String.t(), ConduitMcp.Client.ToolResult.t() | ConduitMcp.Client.Error.t(), keyword() ) :: map()
Builds the functionResponse part answering a call to name (the
exposed name the model used).
Options
:id- thefunctionCall.idbeing answered, when Gemini sent one.:structured-:prefer(default) sendsstructuredContentitself asoutputwhen present;:auto,:appendand:ignorerender the content as text, handlingstructuredContentas described inConduitMcp.Client.Adapters.OpenAI.tool_output/3.