Skip to content

buildAgentCard

JavaScript · package toolnexus · SPEC §7B · js/src/serve.ts

interface A2AConfig {
name?: string
description?: string
version?: string
provider?: { organization: string; url: string }
skills?: string[]
store?: TaskStore | "memory" | string
}
function buildAgentCard(cfg: A2AConfig, skills: SkillInfo[], url: string): AgentCard

The pure function behind GET /.well-known/agent-card.json: take the a2a profile config, the toolkit’s SkillInfo[], and the base URL, and produce the JSON document a peer’s agent() call fetches. Skills, never raw tools — a served toolkit’s bash/read/MCP tool names never leak into the card, only its SKILL.md catalog.

Call buildAgentCard directly when you want to inspect, test, or hand-roll the card without starting a server — checking what a given a2a config would advertise, or building your own HTTP layer around the same card shape startA2AServer mounts for you.

1. The smallest useful call — defaults, no skills

Section titled “1. The smallest useful call — defaults, no skills”

Every field has a sensible default; an empty skill list produces an empty skills array, not an error.

import assert from "node:assert"
import { buildAgentCard } from "toolnexus"
const card = buildAgentCard({}, [], "http://127.0.0.1:4000/")
assert.equal(card.name, "toolnexus-agent")
assert.equal(card.description, "")
assert.equal(card.version, "0.1.0")
assert.equal(card.protocolVersion, "0.3.0")
assert.deepEqual(card.capabilities, { streaming: false, pushNotifications: false })
assert.deepEqual(card.skills, [])
assert.equal(card.url, "http://127.0.0.1:4000/")
console.log("ok:", card.name)

2. A realistic case — named agent, real skills, a subset filter

Section titled “2. A realistic case — named agent, real skills, a subset filter”

cfg.skills narrows what’s advertised to a named subset — useful when a toolkit has internal skills it shouldn’t hand out to every peer.

import assert from "node:assert"
import { buildAgentCard, type SkillInfo } from "toolnexus"
const skills: SkillInfo[] = [
{ name: "hello-world", description: "Say hello", location: "/skills/hello-world/SKILL.md", content: "" },
{ name: "internal-audit", description: "Not for peers", location: "/skills/internal-audit/SKILL.md", content: "" },
]
const publicCard = buildAgentCard(
{ name: "video-desk", description: "Handles video pipeline requests", skills: ["hello-world"] },
skills,
"http://127.0.0.1:4000/",
)
assert.equal(publicCard.name, "video-desk")
assert.deepEqual(publicCard.skills!.map((s) => s.id), ["hello-world"], "internal-audit filtered out")
assert.equal(publicCard.skills![0].description, "Say hello")
console.log("ok:", publicCard.skills!.map((s) => s.id))

3. The full surface — provider metadata and every advertised field

Section titled “3. The full surface — provider metadata and every advertised field”
import assert from "node:assert"
import { buildAgentCard, type SkillInfo } from "toolnexus"
const skills: SkillInfo[] = [
{ name: "search", description: "Search the catalog", location: "-", content: "" },
]
const card = buildAgentCard(
{
name: "catalog-desk",
description: "Product catalog search agent",
version: "2.3.0",
provider: { organization: "acme-corp", url: "https://acme.example" },
store: "memory", // not reflected on the card — controls Task persistence
},
skills,
"http://catalog.internal:8080/",
)
assert.equal(card.version, "2.3.0")
assert.deepEqual(card.provider, { organization: "acme-corp", url: "https://acme.example" })
assert.deepEqual(card.defaultInputModes, ["text"])
assert.deepEqual(card.defaultOutputModes, ["text"])
assert.equal(card.capabilities!.streaming, false, "core has no streaming/push support")
assert.equal(card.skills!.length, 1)
assert.equal(card.skills![0].name, "search")
console.log("ok:", JSON.stringify(card.provider))
Field Type What it does
cfg.name string Advertised agent name — also the outbound tool-name prefix a caller sees. Default "toolnexus-agent".
cfg.description string Default "".
cfg.version string Default "0.1.0".
cfg.provider { organization, url } Included on the card only when set.
cfg.skills string[] Subset of skill names/ids to advertise. Omit ⇒ all.
cfg.store TaskStore | "memory" | string Not reflected on the card; controls startA2AServer’s Task persistence — see FileTaskStore.
skills SkillInfo[] The toolkit’s SkillSource entries; each becomes { id: name, name, description }.
url string The JSON-RPC POST endpoint — set to card.url.

An AgentCard: { name, description, version, protocolVersion: "0.3.0", capabilities: { streaming: false, pushNotifications: false }, defaultInputModes: ["text"], defaultOutputModes: ["text"], skills, url, provider? } — exactly the JSON GET /.well-known/agent-card.json returns.

  • startA2AServer — Publish an Agent Card and answer JSON-RPC over the client loop — your toolkit becomes someone else’s remote agent.
  • FileTaskStore — Persist inbound A2A tasks so a suspended request survives a restart.
  • buildMcpServer — The inbound MCP profile: any MCP client can call your tools.