Skip to content
Blog

A2A Agent Cards: Publishing and Resolving Agent Capabilities

Author, publish, sign, and consume A2A Agent Cards: the JSON manifest that declares an agent identity, skills, interfaces, and security requirements at /.well-known/agent-card.json, with a worked publishing and resolution example.

Published on • October 11, 2026

AI Assistant

A logistics company built three specialized agents — customs clearance, freight quoting, and route planning — each on a different framework, each behind its own vendor. The integration project stalled on a deceptively small question: when one agent needs another, where does it find out what the other can do, which URL to call, and what credentials to present? Hardcoding a service registry answered none of those questions at runtime, and a wiki page went stale the day after it was written.

The Agent2Agent (A2A) protocol answers this with a single artifact: the Agent Card, a self-describing JSON manifest that every A2A server MUST publish. Clients fetch it, read the skills, pick a supported interface, and authenticate using the declared security scheme — no out-of-band coordination. A2A 1.0.0 lives under the Linux Foundation’s Agentic AI Foundation, and the card is the first thing any interoperable deployment gets right. This post is about the card itself: how to author it, publish it at the well-known URL, sign it, version it, and resolve it from a client.

In this tutorial, you will learn how to:

  • Read every field of the Agent Card data model: identity, interfaces, capabilities, skills, and modes
  • Publish a card at https://{domain}/.well-known/agent-card.json
  • Declare security schemes and security requirements that clients can act on
  • Sign a card with JWS over a JCS-canonicalized payload and verify signatures client-side
  • Cache cards correctly with Cache-Control, ETag, and conditional requests
  • Version a card without breaking existing consumers
  • Resolve a card from a client and select the preferred protocol binding
  • Understand how Agent Cards differ from MCP tool listings

Key technologies: A2A protocol v1.0.0, JSON-RPC 2.0 over HTTP(S), Server-Sent Events, JWS (RFC 7515), JCS (RFC 8785), RFC 9111 HTTP caching, Python 3.12, FastAPI, httpx.

Prerequisites

  • Python 3.12+ with FastAPI and httpx installed
  • Basic familiarity with JSON-RPC 2.0 and OAuth 2.0 security scheme definitions
  • An A2A server implementation (the Python SDK is a2a-sdk) or a plain HTTPS service you control
  • Understanding of the A2A task lifecycle (tasks, messages, and parts) at a conceptual level

What an Agent Card Is (and How It Differs from MCP Tool Listings)

The specification defines the Agent Card as a self-describing manifest providing essential metadata: identity, capabilities, skills, supported communication methods, and security requirements. A2A servers MUST make one available; clients use it to discover suitable agents and configure interactions.

The comparison teams reach for first is MCP’s tools/list, but the two artifacts solve different problems:

DimensionA2A Agent CardMCP tool listing
Unit of advertisementWhole agent: skills, interfaces, authIndividual tools with inputSchema
LifetimeStatic-ish manifest, cached with ETagDynamic per connection / authorization
ConsumerAnother agent or orchestratorA model inside an MCP client
TransportStandalone HTTPS GETJSON-RPC tools/list on the MCP endpoint
NegotiationMedia types, protocol bindings (JSON-RPC, gRPC, HTTP+JSON)JSON Schema for arguments

An MCP server exposes tools to a model; an A2A server advertises itself to peers. Use the card when the question is “which agent should handle this task?” and MCP when the question is “which function should the model call?”

Anatomy of the Card: Field by Field

The core AgentCard object in v1.0.0 has a small required surface:

FieldTypeRequiredNotes
namestringYesHuman-readable, e.g. “GeoSpatial Route Planner Agent”
descriptionstringYesHelps users and agents understand purpose
supportedInterfacesarray of AgentInterfaceYesOrdered by preference; first entry is preferred
versionstringYese.g. “1.2.0” — also feeds cache ETags
capabilitiesAgentCapabilitiesYesstreaming, pushNotifications, extendedAgentCard, extensions
defaultInputModesarray of media typesYese.g. ["application/json", "text/plain"]
defaultOutputModesarray of media typesYese.g. ["application/json", "image/png"]
skillsarray of AgentSkillYesThe agent’s advertised abilities
providerAgentProviderNoorganization + url
securitySchemesmap of SecuritySchemeNoOpenID Connect, OAuth2, HTTP bearer, API key, mutual TLS
securityRequirementsarrayNoWhich schemes (and scopes) are required
documentationUrl, iconUrlstringNoDisplay and docs pointers
signaturesarray of AgentCardSignatureNoJWS signatures over the canonical card

Each AgentSkill requires id, name, description, and tags; it may add examples, per-skill inputModes/outputModes overrides, and per-skill securityRequirements. Skills are descriptive — they tell a client what the agent is likely to succeed at, not a callable RPC signature.

Each AgentInterface requires a url, a protocolBinding (JSONRPC, GRPC, or HTTP+JSON), and a protocolVersion. The URL MUST be a valid absolute HTTPS URL in production. Clients follow a strict selection rule: parse supportedInterfaces, pick the first entry they support, and use that entry’s URL for every subsequent call.

Publishing: The Well-Known URL

Discovery mechanisms in the spec are: the well-known URI, registries or catalogs, and direct configuration. The well-known path is normative and IANA-registered:

https://{server_domain}/.well-known/agent-card.json

Serving it is one route:

from fastapi import FastAPI
from fastapi.responses import JSONResponse

app = FastAPI()

AGENT_CARD = {
    "name": "Invoice Reconciliation Agent",
    "description": (
        "Reconciles invoices against purchase orders and flags mismatches "
        "for human review. Specializes in ERP-exported CSV batches."
    ),
    "supportedInterfaces": [
        {
            "url": "https://agents.example.com/a2a/v1",
            "protocolBinding": "JSONRPC",
            "protocolVersion": "1.0",
        },
        {
            "url": "https://agents.example.com/a2a/json",
            "protocolBinding": "HTTP+JSON",
            "protocolVersion": "1.0",
        },
    ],
    "provider": {
        "organization": "Example Financial Ops",
        "url": "https://www.example.com",
    },
    "version": "1.4.0",
    "capabilities": {
        "streaming": True,
        "pushNotifications": True,
        "extendedAgentCard": True,
    },
    "defaultInputModes": ["application/json", "text/plain"],
    "defaultOutputModes": ["application/json"],
    "skills": [
        {
            "id": "po-invoice-match",
            "name": "PO–Invoice Matcher",
            "description": "Matches invoice line items to purchase orders and reports discrepancies.",
            "tags": ["finance", "invoices", "reconciliation"],
            "examples": [
                "Reconcile this week's invoice batch against open POs.",
                '{"invoice_ids": ["INV-1041", "INV-1042"]}',
            ],
        }
    ],
    "securitySchemes": {
        "corp_oidc": {
            "openIdConnectSecurityScheme": {
                "openIdConnectUrl": "https://auth.example.com/.well-known/openid-configuration"
            }
        }
    },
    "securityRequirements": [
        {"schemes": {"corp_oidc": {"list": ["openid", "profile"]}}}
    ],
}

@app.get("/.well-known/agent-card.json")
def agent_card():
    return JSONResponse(
        AGENT_CARD,
        headers={
            "Cache-Control": "public, max-age=300",
            "ETag": '"inv-recon-1.4.0"',
        },
    )

The caching headers are not decoration. Servers SHOULD include Cache-Control with a max-age appropriate to their update frequency and an ETag derived from the version field or a content hash; Last-Modified is optional. Clients, in turn, SHOULD honor RFC 9111 and use conditional requests (If-None-Match) rather than re-downloading unchanged cards.

For cards too sensitive to publish publicly, set capabilities.extendedAgentCard: true and expose a richer card via the Get Extended Agent Card operation, which MUST require authentication. The public well-known card stays minimal — identity, headline skills, and enough security metadata for a client to obtain credentials.

Security Schemes and Extended Cards

securitySchemes reuses the familiar OpenAPI-style scheme types: API key, HTTP bearer/basic, OAuth 2.0 (authorization code, client credentials, device code flows), OpenID Connect, and mutual TLS. securityRequirements then binds schemes to required scopes, exactly as the sample card in the specification does with {"schemes": {"google": {"list": ["openid", "profile", "email"]}}}.

Two practical notes:

  • Keep the public card free of secrets. Schemes describe how to authenticate, never with what.
  • Extended cards are for post-authentication detail: extra skills, internal endpoints, stricter rate limits. Section 13.3 of the specification requires authentication for the extended-card operation and warns about access control on its contents.

Signing and Verifying Cards

Cards MAY be digitally signed so clients can verify authenticity and integrity. The mechanics are precise, and getting them wrong is the fastest way to break interop:

  1. Canonicalize with JCS (RFC 8785). Before signing, drop optional fields that were never explicitly set, keep explicitly-set defaults, omit empty repeated fields, exclude the signatures field itself, then apply RFC 8785 (lexicographic key order, no insignificant whitespace).
  2. Sign with JWS (RFC 7515). The AgentCardSignature carries protected (base64url JWS header), signature (base64url), and an optional unprotected header. The protected header MUST include alg (e.g. ES256), SHOULD include typ: "JOSE", and MUST include kid; it MAY include jku pointing at a JWKS.
  3. Verify by re-canonicalizing. Clients extract a signature, fetch the key by kid/jku (or from a trusted store), strip defaults and signatures, re-canonicalize, and verify. Multiple signatures support key rotation; expired or revoked keys MUST NOT verify.

Clients SHOULD verify at least one signature before trusting a card from an unknown provider. Domain-allowlisting the jku host prevents a card from pointing verification at an attacker-controlled key set.

Versioning Without Breaking Consumers

The card’s version field is yours to manage; the protocol’s own change-control rules constrain the schema. Practical guidance:

  • Additive changes are safe. New optional fields, new skills, and new interfaces do not break conforming clients — they ignore unknown fields and select the first interface they support.
  • Reorder supportedInterfaces deliberately. The first entry is the preferred binding; moving a legacy binding to the front changes client behavior overnight.
  • Never remove a skill id inside a major version. Clients cache skill IDs when routing tasks. Retire skills by deprecating them in the description first.
  • Bump version on every published change so ETags invalidate and conditional requests pull the new card.
  • Reserve breaking renames for a new endpoint path (e.g. /a2a/v2) advertised in a new interface entry — keep the old URL serving during a migration window.

Resolving Cards Client-Side: A Worked Example

A client does three things: fetch the card, choose an interface, then speak JSON-RPC to that URL.

import httpx

CARD_URL = "https://agents.example.com/.well-known/agent-card.json"

def resolve_agent(client: httpx.Client, card_url: str = CARD_URL) -> tuple[dict, dict]:
    resp = client.get(
        card_url,
        headers={"If-None-Match": client.cookies.get("card_etag", "")},
    )
    if resp.status_code == 304:
        return client.storage["card"], client.storage["iface"]
    resp.raise_for_status()
    card = resp.json()

    iface = next(
        (i for i in card["supportedInterfaces"]
         if i["protocolBinding"] == "JSONRPC"),
        None,
    )
    if iface is None:
        raise RuntimeError("agent offers no JSON-RPC interface")

    client.storage = {"card": card, "iface": iface}
    client.cookies.set("card_etag", resp.headers.get("ETag", ""))
    return card, iface

def send_task(client: httpx.Client, iface: dict, message: str, auth: str) -> dict:
    payload = {
        "jsonrpc": "2.0",
        "id": 1,
        "method": "SendMessage",
        "params": {
            "message": {
                "role": "user",
                "parts": [{"type": "text", "text": message}],
            }
        },
    }
    r = client.post(
        iface["url"],
        json=payload,
        headers={
            "Content-Type": "application/json",
            "Authorization": f"Bearer {auth}",
        },
    )
    r.raise_for_status()
    return r.json()

Before the first SendMessage, the client should complete whatever flow the declared security scheme requires — for the card above, an OpenID Connect authorization against auth.example.com — and attach the resulting bearer token. If the agent supports streaming (capabilities.streaming: true), the client can use the SendStreamingMessage method and consume Server-Sent Events instead of waiting for a single response.

Common Pitfalls

  • Serving the card at the wrong path. The normative path is /.well-known/agent-card.json. Earlier drafts used different filenames; do not copy them.
  • Omitting supportedInterfaces ordering. The order is a preference signal, not a set. Sorting alphabetically changes which transport clients pick.
  • Signing without canonicalizing. A JWS over pretty-printed JSON will not verify anywhere else. JCS is mandatory for interop.
  • Publishing skills without tags. tags is required on every skill and is how orchestrators filter candidates.
  • Treating the card as an API contract for arguments. Skills are descriptive. If clients need typed inputs, expose them in examples or an extended card — not by inventing non-spec fields.
  • Infinite cache lifetimes. Without max-age and an ETag, clients either hammer the endpoint or cache stale capabilities forever.

Publishing Checklist

  • Card served at https://{domain}/.well-known/agent-card.json over HTTPS
  • All required fields present: name, description, supportedInterfaces, version, capabilities, defaultInputModes, defaultOutputModes, skills
  • Every skill has id, name, description, and tags
  • supportedInterfaces ordered by real preference; URLs HTTPS in production
  • securitySchemes describe auth without embedding credentials
  • Cache-Control and ETag headers set; conditional requests honored
  • Card signed with JCS + JWS if consumers require provenance
  • Extended card gated behind authentication when it exposes extra capability
  • Version bumped and ETag rotated on every change

Further reading