Toolnexus.A2a.agent_tools
Elixir · package toolnexus · SPEC §7A · elixir/lib/toolnexus/a2a.ex
@spec agent_tools(map()) :: [Toolnexus.Tool.t()]def agent_tools(ag)
# ag = an Agent descriptor from Toolnexus.A2a.agent/1
# Tool.name = sanitize(card.name) <> "_" <> sanitize(skill.id || skill.name)# Tool.source = "a2a"# Tool.input_schema = %{"type"=>"object", "properties"=>%{"task"=>%{"type"=>"string", ...}},# "required"=>["task"], "additionalProperties"=>false}Fetches the descriptor’s Agent Card (GET <card>) and turns every entry in skills[] into one
uniform Toolnexus.Tool. Each tool’s execute performs the whole §7A wire dance — one
SendMessage then polling GetTask until a terminal state — and maps the finished Task to a
ToolResult. A card fetch or parse failure raises; per-peer isolation is the toolkit’s job
(the toolkit catches it and logs, contributing zero tools for that peer, never failing the whole
build).
When to use it
Section titled “When to use it”- You have an
Agentdescriptor and want callable tools — this is the only function that actually performs I/O to resolve one;agent/1itself never touches the network. - Wiring it into a toolkit by hand, without
mcp_config’sagentsblock — callagent_tools/1and pass the result toToolnexus.Toolkit.register/2orextra_tools:. - Inspecting what a peer advertises before deciding whether to add it — the returned tools’
name/descriptioncome straight from the card’sskills[].
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — resolve a card, no execution yet
Section titled “1. The smallest useful call — resolve a card, no execution yet”alias Toolnexus.A2a
Application.ensure_all_started(:req)
defmodule DocsCardOnlyStub do @behaviour Plug import Plug.Conn
def call(conn, _opts) do card = %{ "name" => "Weather Agent", "skills" => [ %{"id" => "forecast", "description" => "Gives a forecast"}, %{"name" => "alerts!"} ] }
conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(card)) end
def init(opts), do: optsend
{:ok, sock} = :gen_tcp.listen(0, []){:ok, port} = :inet.port(sock):gen_tcp.close(sock)url = "http://127.0.0.1:#{port}"
{:ok, _pid} = Bandit.start_link(plug: DocsCardOnlyStub, scheme: :http, ip: {127, 0, 0, 1}, port: port)
tools = A2a.agent_tools(A2a.agent(card: url <> "/.well-known/agent-card.json"))
true = Enum.map(tools, & &1.name) == ["Weather_Agent_forecast", "Weather_Agent_alerts_"][forecast, alerts] = toolstrue = forecast.source == "a2a"true = forecast.description == "Gives a forecast"# a skill with no id/description falls back to its nametrue = alerts.description == "alerts!"
IO.puts("ok: #{Enum.join(Enum.map(tools, & &1.name), ", ")}")2. The full round trip — execute a resolved tool over real loopback HTTP
Section titled “2. The full round trip — execute a resolved tool over real loopback HTTP”alias Toolnexus.{A2a, Context}
Application.ensure_all_started(:req)
defmodule DocsCalcAgentStub do @behaviour Plug import Plug.Conn
def init(agent), do: agent
def call(conn, agent) do {:ok, body, conn} = read_body(conn) st = Agent.get(agent, & &1)
case {conn.method, conn.request_path} do {"GET", "/.well-known/agent-card.json"} -> card = %{ "name" => "Calc Agent", "url" => st.url, "skills" => [%{"id" => "add", "description" => "Adds two numbers"}] }
conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(card))
{"POST", _} -> rpc = Jason.decode!(body)
result = case rpc["method"] do "SendMessage" -> %{"id" => "t1", "status" => %{"state" => "submitted"}}
"GetTask" -> %{ "id" => "t1", "status" => %{"state" => "completed"}, "artifacts" => [%{"parts" => [%{"kind" => "text", "text" => "5"}]}] } end
payload = %{"jsonrpc" => "2.0", "id" => rpc["id"], "result" => result} conn |> put_resp_content_type("application/json") |> send_resp(200, Jason.encode!(payload)) end endend
{:ok, sock} = :gen_tcp.listen(0, []){:ok, port} = :inet.port(sock):gen_tcp.close(sock)url = "http://127.0.0.1:#{port}"
{:ok, agent_state} = Agent.start_link(fn -> %{url: url <> "/"} end){:ok, _pid} = Bandit.start_link(plug: {DocsCalcAgentStub, agent_state}, scheme: :http, ip: {127, 0, 0, 1}, port: port)
[add] = A2a.agent_tools(A2a.agent(card: url <> "/.well-known/agent-card.json", poll_every: 20))
result = add.execute.(%{"task" => "add 2 and 3"}, %Context{})
false = result.is_errortrue = result.output == "5"true = result.metadata.agent == "Calc Agent"true = result.metadata.task_id == "t1"true = result.metadata.state == "completed"
IO.puts("ok: #{add.name} -> #{result.output}")3. The full surface — a failing peer raises; the toolkit isolates it
Section titled “3. The full surface — a failing peer raises; the toolkit isolates it”alias Toolnexus.{A2a, Toolkit}
Application.ensure_all_started(:req)
defmodule DocsDeadCardStub do @behaviour Plug import Plug.Conn def init(opts), do: opts def call(conn, _opts), do: send_resp(conn, 500, "card lookup failed")end
{:ok, sock} = :gen_tcp.listen(0, []){:ok, port} = :inet.port(sock):gen_tcp.close(sock)url = "http://127.0.0.1:#{port}"
{:ok, _pid} = Bandit.start_link(plug: DocsDeadCardStub, scheme: :http, ip: {127, 0, 0, 1}, port: port)
card_url = url <> "/.well-known/agent-card.json"
# Calling agent_tools/1 directly raises — no isolation at this layer.raised? = try do A2a.agent_tools(A2a.agent(card: card_url)) false rescue RuntimeError -> true end
true = raised?
# create_toolkit!/1 (agents:) DOES isolate it — the peer contributes zero tools.tk = Toolnexus.create_toolkit!(agents: [A2a.agent(card: card_url)], builtins: false)true = Toolkit.tools(tk) == []
IO.puts("ok: agent_tools/1 raises directly, the toolkit isolates it")What you get back
Section titled “What you get back”| Field | Type | What it is |
|---|---|---|
name |
String.t() |
sanitize(card.name) <> "_" <> sanitize(skill.id || skill.name). |
description |
String.t() |
The skill’s description, falling back to its name/id. |
input_schema |
map() |
Always the fixed {task: string} shape (§7A). |
source |
"a2a" |
|
execute |
(args, ctx) -> ToolResult.t() |
SendMessage → poll GetTask → map the terminal Task. |
See also
Section titled “See also”Toolnexus.A2a.agent— point at a remote agent’s card and use it exactly like a local tool.Toolnexus.A2a.parse_agents_config— declare remote peers in config the way MCP servers are declared, with precedence rules.