Publishing an Agent Card from ADK, LangGraph, CrewAI or Semantic Kernel

10 min read
Google ADKLangGraphCrewAISemantic Kernelagent-card.jsonnamedescriptionversionsupportedInterfaces[]capabilitiesskills[]
Whatever builds the agent, discovery reads the same six fields. The card is a projection of your framework, not a rewrite of it.

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 hasThe card should sayWhy
search_orders, get_customer, issue_refund toolsOne skill: order-investigationCallers delegate the investigation, not the individual lookups
A graph with 14 nodesOne skill per entrypoint the graph exposesInternal routing is not a public contract
A crew of six specialised agentsOne skill per outcome the crew producesThe crew is addressed as a unit
Three plugins with nine functionsTwo or three skills, grouped by jobFunction 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.

ADKAgent Card
agent.namename
agent.descriptiondescription
Sub-agents and the jobs they coverskills[]
tools=[…]Nothing — internal
The host you serve the app fromsupportedInterfaces[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.

Python — building the card from the agent you already defined
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.

LangGraphAgent Card
Graph or assistant namename
What the graph accomplishes end to enddescription
Each addressable entrypointOne entry in skills[]
Nodes, edges, conditional routingNothing — internal
Serving with .stream()capabilities.streaming: true
Interrupts / human-in-the-loopMention 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.

CrewAIAgent Card
The crew's purposename and description
Each Task outcome a caller can requestOne entry in skills[]
Agent(role=…, goal=…)Nothing — internal
Task.expected_outputInforms defaultOutputModes and the skill description
Synchronous kickoff()capabilities.streaming: false
kickoff(inputs={…}) keysDescribe 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 KernelAgent Card
Agent Namename
Agent Instructions, condenseddescription
A plugin, or a coherent group of functionsOne entry in skills[]
Individual [KernelFunction] membersNothing — internal
Streaming chat completioncapabilities.streaming: true
Assembly / package versionversion
C# — publishing the card from the same app that hosts the agent
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.

Python — FastAPI, in front of any of the above
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:

  1. Derive, do not duplicate. version from your build stamp, supportedInterfaces[].url from the same base URL the server binds, name and description from the agent definition. A card assembled from constants drifts; a card assembled from configuration cannot.
  2. 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.
  3. 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

  1. A2A protocol specification (a2aproject/A2A)github.com/a2aproject/A2A
  2. Google Agent Development Kit documentationgoogle.github.io/adk-docs/
  3. LangGraph documentationlangchain-ai.github.io/langgraph/
  4. CrewAI documentationdocs.crewai.com/
  5. Semantic Kernel documentationlearn.microsoft.com/en-us/semantic-kernel/

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.