agentTools
JavaScript · package toolnexus · SPEC §7A · js/src/a2a.ts
function agentTools(ag: Agent): Promise<Tool[]>Resolve an Agent descriptor to its tools: GET the Agent Card, read its skills[], and build
one Tool per skill — name = sanitize(card.name) + "_" + sanitize(skill.id ?? skill.name),
source: "a2a". This is the exact primitive createToolkit({ agents }) calls internally, once per
configured Agent.
When to use it
Section titled “When to use it”Reach for agentTools when you want a peer’s tools without going through a Toolkit — you are
wiring your own tool list for a hand-rolled loop, or you want to inspect/filter what a peer exposes
before deciding whether to register any of it.
Why this and not the alternative
Section titled “Why this and not the alternative”A failing agent card fetch throws from agentTools itself — it is the toolkit layer
(createToolkit) that catches and isolates that failure per agent. Calling agentTools directly
means you own that try/catch.
Examples
Section titled “Examples”1. The smallest useful call — one skill, one tool
Section titled “1. The smallest useful call — one skill, one tool”import assert from "node:assert"import http from "node:http"import { agent, agentTools } from "toolnexus"
const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/.well-known/agent-card.json") { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ name: "greeter", url: `http://127.0.0.1:${(server.address() as any).port}/`, skills: [{ id: "hello", name: "Hello", description: "Say hello" }], })) return } res.writeHead(404) res.end()})await new Promise<void>((r) => server.listen(0, "127.0.0.1", r))const port = (server.address() as any).port
const tools = await agentTools(agent({ card: `http://127.0.0.1:${port}/.well-known/agent-card.json` }))
assert.equal(tools.length, 1)assert.equal(tools[0].name, "greeter_hello")assert.equal(tools[0].source, "a2a")assert.deepEqual(tools[0].inputSchema.required, ["task"])
server.close()console.log("ok:", tools[0].name)2. A realistic case — several skills, then actually calling one
Section titled “2. A realistic case — several skills, then actually calling one”Every card skill becomes a tool with the same one-field { task: string } schema; execute runs
the full SendMessage → poll GetTask cycle and returns the completed artifact text.
import assert from "node:assert"import http from "node:http"import { randomUUID } from "node:crypto"import { agent, agentTools } from "toolnexus"
const tasks = new Map<string, { polls: number }>()const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/.well-known/agent-card.json") { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ name: "research-desk", url: `http://127.0.0.1:${(server.address() as any).port}/`, skills: [ { id: "search", name: "Search", description: "Search the web" }, { id: "summarize", name: "Summarize", description: "Summarize a document" }, ], })) return } let body = "" req.on("data", (c) => (body += c)) req.on("end", () => { const rpc = JSON.parse(body) const send = (result: unknown) => { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ jsonrpc: "2.0", id: rpc.id, result })) } if (rpc.method === "SendMessage") { const id = randomUUID() tasks.set(id, { polls: 0 }) send({ id, status: { state: "submitted" } }) } else if (rpc.method === "GetTask") { const t = tasks.get(rpc.params.id)! t.polls++ if (t.polls < 2) send({ id: rpc.params.id, status: { state: "working" } }) else send({ id: rpc.params.id, status: { state: "completed" }, artifacts: [{ parts: [{ kind: "text", text: "3 results found" }] }] }) } })})await new Promise<void>((r) => server.listen(0, "127.0.0.1", r))const port = (server.address() as any).port
const tools = await agentTools(agent({ card: `http://127.0.0.1:${port}/.well-known/agent-card.json`, pollEvery: 10 }))
assert.deepEqual(tools.map((t) => t.name).sort(), ["research-desk_search", "research-desk_summarize"])
const searchTool = tools.find((t) => t.name === "research-desk_search")!const res = await searchTool.execute({ task: "toolnexus release notes" })assert.equal(res.isError, false)assert.equal(res.output, "3 results found")assert.equal((res.metadata as any).agent, "research-desk")
server.close()console.log("ok:", res.output)3. The full surface — a failing task and its uniform error shape
Section titled “3. The full surface — a failing task and its uniform error shape”A failed terminal task maps to isError: true with the status message text appended; the tool
never throws — every outcome, success or failure, is a ToolResult.
import assert from "node:assert"import http from "node:http"import { randomUUID } from "node:crypto"import { agent, agentTools } from "toolnexus"
const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/.well-known/agent-card.json") { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ name: "flaky", url: `http://127.0.0.1:${(server.address() as any).port}/`, skills: [{ id: "risky", description: "Sometimes fails" }], })) return } let body = "" req.on("data", (c) => (body += c)) req.on("end", () => { const rpc = JSON.parse(body) const send = (result: unknown) => { res.writeHead(200, { "content-type": "application/json" }) res.end(JSON.stringify({ jsonrpc: "2.0", id: rpc.id, result })) } if (rpc.method === "SendMessage") send({ id: randomUUID(), status: { state: "submitted" } }) else if (rpc.method === "GetTask") send({ id: rpc.params.id, status: { state: "failed", message: { role: "agent", parts: [{ kind: "text", text: "quota exceeded" }] } } }) })})await new Promise<void>((r) => server.listen(0, "127.0.0.1", r))const port = (server.address() as any).port
const [riskyTool] = await agentTools(agent({ card: `http://127.0.0.1:${port}/.well-known/agent-card.json`, pollEvery: 5 }))
const res = await riskyTool.execute({ task: "do the risky thing" })assert.equal(res.isError, true)assert.match(res.output, /failed/)assert.match(res.output, /quota exceeded/, "the status message rides along in the output")assert.equal((res.metadata as any).state, "failed")
server.close()console.log("ok:", res.output)Options
Section titled “Options”| Field | Type | What it does |
|---|---|---|
ag |
Agent |
The descriptor built by agent(), or parsed by parseAgentsConfig. Required. |
What you get back
Section titled “What you get back”Promise<Tool[]> — one Tool per skill on the resolved Agent Card, each source: "a2a", with
inputSchema = { type: "object", properties: { task: { type: "string" } }, required: ["task"] }.
Calling execute runs the full submit→poll cycle described in
SPEC §7A.
See also
Section titled “See also”agent— Point at a remote agent’s card and use it exactly like a local tool.parseAgentsConfig— Declare remote peers in config the way MCP servers are declared, with precedence rules.