build_agent_card
Python · package toolnexus · SPEC §7B · python/src/toolnexus/serve.py
def build_agent_card( cfg: dict[str, Any], skills: list[SkillInfo], url: str,) -> dict[str, Any]Builds the JSON body served at GET /.well-known/agent-card.json — a peer’s first
call to any A2A agent. Skills come from the toolkit’s SkillSource (SKILL.md name +
description), never raw tools, filtered to cfg["skills"] when given.
When to use it
Section titled “When to use it”- You want to see or test exactly what card a given
a2aconfig + skill set would publish, without spinning up a server. - You are composing your own HTTP layer instead of
Toolkit.serveand need the card JSON to answer the well-known path yourself. - You are debugging why a peer’s
agent_toolscall sees the wrong skills, name, orurl— this is the one function that decides all three.
Why this and not the alternative
Section titled “Why this and not the alternative”The function is pure: no I/O, no server, no store. skills must already be resolved
(a toolkit’s SkillSource.skills.values(), or hand-built SkillInfos for a test) —
build_agent_card never discovers skills itself.
Examples
Section titled “Examples”1. The smallest useful call — defaults with no skills
Section titled “1. The smallest useful call — defaults with no skills”from toolnexus.serve import build_agent_card
card = build_agent_card({}, [], "http://127.0.0.1:8080/")
assert card["name"] == "toolnexus-agent"assert card["description"] == ""assert card["version"] == "0.1.0"assert card["protocolVersion"] == "0.3.0"assert card["capabilities"] == {"streaming": False, "pushNotifications": False}assert card["defaultInputModes"] == ["text"]assert card["defaultOutputModes"] == ["text"]assert card["skills"] == []assert card["url"] == "http://127.0.0.1:8080/"assert "provider" not in card # omitted entirely when not configured
print("ok:", card["name"], card["protocolVersion"])2. The realistic case — skills advertised, never raw tools
Section titled “2. The realistic case — skills advertised, never raw tools”skills mirrors a SkillSource’s inventory — id and name both come from the
skill’s own name.
from toolnexus.skill import SkillInfofrom toolnexus.serve import build_agent_card
skills = [ SkillInfo(name="hello-world", location="/skills/hello-world/SKILL.md", content="...", description="Greets the caller by name."), SkillInfo(name="transcribe", location="/skills/transcribe/SKILL.md", content="...", description="Transcribes an audio file."),]
card = build_agent_card( {"name": "video-desk", "description": "Handles video intake."}, skills, "http://127.0.0.1:8080/",)
assert card["name"] == "video-desk"assert card["description"] == "Handles video intake."ids = [s["id"] for s in card["skills"]]assert ids == ["hello-world", "transcribe"]assert card["skills"][0] == { "id": "hello-world", "name": "hello-world", "description": "Greets the caller by name."}
print("ok:", card["name"], "->", ids)3. The full surface — filtered skills, a provider block, custom version
Section titled “3. The full surface — filtered skills, a provider block, custom version”cfg["skills"] restricts the advertised set (a toolkit may host more skills than it
wants a given peer surface to see). provider appears only when configured.
from toolnexus.skill import SkillInfofrom toolnexus.serve import build_agent_card
skills = [ SkillInfo(name="hello-world", location="/skills/hello-world/SKILL.md", content="..."), SkillInfo(name="internal-only", location="/skills/internal-only/SKILL.md", content="..."),]
card = build_agent_card( { "name": "video-desk", "version": "2.3.1", "skills": ["hello-world"], # "internal-only" is excluded from the card "provider": {"organization": "deemwar", "url": "https://deemwar.com"}, }, skills, "http://127.0.0.1:8080/",)
assert [s["id"] for s in card["skills"]] == ["hello-world"]assert card["version"] == "2.3.1"assert card["provider"] == {"organization": "deemwar", "url": "https://deemwar.com"}# A skill with no description still gets a card entry — description defaults to "".assert card["skills"][0]["description"] == ""
print("ok:", card["version"], card["provider"]["organization"])Options
Section titled “Options”| Parameter | Type | What it does |
|---|---|---|
cfg |
dict |
The A2AConfig — name?, description?, version?, provider?, skills?: list[str], store? (unused here). |
skills |
list[SkillInfo] |
The toolkit’s resolved skill inventory. Filtered to cfg["skills"] when given; None/absent ⇒ all. |
url |
str |
The JSON-RPC POST endpoint peers should call — the served base URL plus /. |
Returns
Section titled “Returns”| Field | Default when omitted |
|---|---|
name |
"toolnexus-agent" |
description |
"" |
version |
"0.1.0" |
protocolVersion |
"0.3.0" (fixed — not configurable) |
capabilities.streaming |
False (fixed — no streaming in core) |
provider |
omitted entirely unless cfg["provider"] is set |
See also
Section titled “See also”Toolkit.serve— 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.build_mcp_server— The inbound MCP profile: any MCP client can call your tools.