Publishing an Agent Card from ADK, LangGraph, CrewAI or Semantic Kernel
Every agent framework has its own idea of what an agent is. ADK has agents with tools; LangGraph has a graph with nodes; CrewAI has crews running tasks; Semantic Kernel has plugins and functions. A2A discovery does not care about any of it. Publishing an Agent Card is a modelling exercise — deciding what your agent looks like from outside — and it takes about an hour.
Why this feels harder than it is
The instinct is to look for an adapter: something that reads your framework's objects and emits an Agent Card. Some frameworks now ship exactly that, and you should use it if yours does. But reaching for an adapter first hides the actual problem, which is that your framework's model of your agent is richer and more internal than the one A2A publishes.
The A2A specification defines six fields
that matter for this exercise — name, description,
version, supportedInterfaces, capabilities and skills — and only the
last one requires a judgement call. Everything else you already know from your
deployment configuration.
Skills are not tools
This is the mistake that makes framework integration feel hard, so it is worth settling before touching any code. An agent with twenty tools does not have twenty skills.
A tool is something your agent calls. A skill is something someone can ask your agent to do. A caller cannot invoke your tools — they are behind your endpoint, your model and your prompt. What a caller does is hand over a goal, so skills should be written at the granularity of goals.
| Your framework has | The card should say | Why |
|---|---|---|
search_orders, get_customer, issue_refund tools | One skill: order-investigation | Callers delegate the investigation, not the individual lookups |
| A graph with 14 nodes | One skill per entrypoint the graph exposes | Internal routing is not a public contract |
| A crew of six specialised agents | One skill per outcome the crew produces | The crew is addressed as a unit |
| Three plugins with nine functions | Two or three skills, grouped by job | Function granularity is an implementation detail |
Google ADK
ADK agents already carry most of the card.
An LlmAgent has a name, a
description written specifically so other agents can decide whether to
delegate to it, and a list of tools. The description field in particular was
designed for the same job the card's description does, so it usually transfers
verbatim.
| ADK | Agent Card |
|---|---|
agent.name | name |
agent.description | description |
| Sub-agents and the jobs they cover | skills[] |
tools=[…] | Nothing — internal |
| The host you serve the app from | supportedInterfaces[0].url |
ADK apps are commonly served through a FastAPI application, which gives you a natural place to publish the card. If you are on a version of ADK with built-in A2A support, prefer its helper — the mapping below is what that helper is doing on your behalf.
AGENT_CARD = {
"name": root_agent.name,
"description": root_agent.description,
"version": os.environ.get("BUILD_VERSION", "0.1.0"),
"provider": {
"organization": "Northwind Finance",
"url": "https://northwind.example.com",
},
"supportedInterfaces": [
{
"url": f"{PUBLIC_BASE_URL}/a2a/v1",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0",
}
],
"capabilities": {
"streaming": True,
"pushNotifications": False,
"extendedAgentCard": False,
},
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["application/json"],
"skills": [
{
"id": "order-investigation",
"name": "Order investigation",
"description": (
"Given a customer complaint, locate the order, determine what "
"went wrong, and propose or execute a remedy under policy."
),
"tags": ["orders", "support", "refunds"],
"examples": [
"Order 8823 never arrived - what happened?",
"Refund the duplicate charge on invoice INV-1043.",
],
}
],
}
LangGraph
A LangGraph graph is an execution structure, not a public interface, so resist the urge to describe nodes. What a caller can address is the compiled graph's entrypoint, and that is your skill list — usually one skill, occasionally one per distinct entrypoint if you expose several.
The field LangGraph does answer unambiguously is capabilities.streaming. If
you serve the graph through .stream() and pass tokens or state updates to the
caller, set it to true; if your endpoint blocks until .invoke() returns, set
it to false and let callers plan for a wait.
| LangGraph | Agent Card |
|---|---|
| Graph or assistant name | name |
| What the graph accomplishes end to end | description |
| Each addressable entrypoint | One entry in skills[] |
| Nodes, edges, conditional routing | Nothing — internal |
Serving with .stream() | capabilities.streaming: true |
| Interrupts / human-in-the-loop | Mention in the skill description; callers must expect pauses |
CrewAI
CrewAI is the framework where the tempting-but-wrong
mapping is most tempting:
agents have roles, goals and backstories, so it looks like each agent should
become a skill. It should not. Callers address the crew — one endpoint, one
kickoff, one result — so the crew is the agent, and its skills are the
outcomes it produces.
| CrewAI | Agent Card |
|---|---|
| The crew's purpose | name and description |
Each Task outcome a caller can request | One entry in skills[] |
Agent(role=…, goal=…) | Nothing — internal |
Task.expected_output | Informs defaultOutputModes and the skill description |
Synchronous kickoff() | capabilities.streaming: false |
kickoff(inputs={…}) keys | Describe them in the skill's examples |
The expected_output you wrote on each task is the best raw material you have
for a skill description — it is already a statement of what the caller receives,
which is exactly what a skill description should promise.
Semantic Kernel
Semantic Kernel gives you the most descriptive metadata of the four, because plugin functions carry descriptions written for a model to read. Those descriptions are function-level, though, so they map to skills only after grouping.
| Semantic Kernel | Agent Card |
|---|---|
Agent Name | name |
Agent Instructions, condensed | description |
| A plugin, or a coherent group of functions | One entry in skills[] |
Individual [KernelFunction] members | Nothing — internal |
| Streaming chat completion | capabilities.streaming: true |
| Assembly / package version | version |
app.MapGet("/.well-known/agent-card.json", () =>
Results.Json(AgentCard.Current, contentType: "application/a2a+json"));
// Pre-1.0 clients look here first.
app.MapGet("/.well-known/agent.json", () =>
Results.Redirect("/.well-known/agent-card.json", permanent: true));
Serving the card next to the agent
Whatever the framework, the card has to appear at
/.well-known/agent-card.json on the origin the agent answers on, with content
type application/a2a+json and a permissive CORS header so browser-based
clients can read it.
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
CARD_HEADERS = {
"Cache-Control": "public, max-age=3600",
"Access-Control-Allow-Origin": "*",
}
@app.get("/.well-known/agent-card.json")
def agent_card():
return JSONResponse(
AGENT_CARD,
media_type="application/a2a+json",
headers=CARD_HEADERS,
)
# Alias for pre-1.0 clients.
@app.get("/.well-known/agent.json")
def legacy_agent_card():
return agent_card()
Build the card from the same configuration object that configures the server. Hard-coding the endpoint URL is how staging ends up advertising a production address, and it is the single most common way a card becomes wrong without anyone noticing.
Keeping it honest across deploys
A card is a promise, and promises rot. Three habits keep it aligned with what you actually run:
- Derive, do not duplicate.
versionfrom your build stamp,supportedInterfaces[].urlfrom the same base URL the server binds,nameanddescriptionfrom the agent definition. A card assembled from constants drifts; a card assembled from configuration cannot. - Validate in CI. The card is JSON your tests can load. Assert the eight
required fields exist and that every skill has non-empty
tags— that one check catches the most common cause of a card being rejected downstream. - Check the live URL after deploy. Local validity says nothing about routing, headers or reverse proxies. The well-known URL checker fetches both discovery paths from the real domain and reports what actually came back.
For the field-level detail behind any of the mappings above, the Agent Card schema reference documents every v1.0 field, and the examples library has complete cards for several agent shapes to model yours on.
Frequently asked questions
Does my framework need built-in A2A support to publish an Agent Card?
No. The card is a static JSON document served over HTTP — any web server in front of your agent can return it, regardless of what built the agent. Several frameworks now ship A2A helpers that generate and serve the card for you, which is worth using when available, but nothing about publishing a card depends on them.
Should each tool in my agent become a skill?
Usually not. Tools are internal implementation, and a caller cannot use them individually anyway — they send a task to your agent, not to one of its tools. Skills should describe the jobs an outside agent can delegate, which is normally a much shorter list than your tool registry.
How do multi-agent systems map onto Agent Cards?
By addressable boundary, not by internal structure. A CrewAI crew of six agents behind one endpoint is one Agent Card, because callers address the crew. Six independently deployed agents at six URLs are six cards. If a caller cannot address it directly, it does not get its own card.
What version should I put in the version field?
Your agent build, not the protocol and not your framework. Wire it to the same value your deployment pipeline already stamps — a card claiming 1.0.0 six months after launch tells callers nothing about whether behaviour changed under them.
Can one deployment serve several agents with several cards?
Only if each agent has its own origin, because the discovery path is fixed at /.well-known/agent-card.json per host. Multiple agents on one process usually means either subdomains per agent, or one card whose skills[] covers the whole surface.
References
Related tool: Agent Card Generator
Next step
Your framework already knows the answers. Fill in the form.
Endpoint, binding, provider, capabilities, one skill — the generator turns them into a v1.0 card you can serve from the same process that runs the agent.