A2A Agent Cards: Dynamic Discovery and Capability Passports
Source: mortalapps.com- A2A Agent Cards are JSON-based capability passports that allow AI agents to programmatically advertise their functions, identity, and security requirements.
- Solves the problem of hardcoded agent dependencies by enabling runtime discovery of specialist agents in a multi-agent ecosystem.
- Essential for production systems where agent capabilities evolve independently of the supervisor or orchestrator logic.
- Enables supervisor agents to resolve the best specialist for a task based on semantic matching of advertised capabilities and cost profiles.
Why This Matters
In static multi-agent systems, developers manually define which agent handles which task. This approach fails at scale because it creates a brittle, tightly coupled architecture where adding a new capability requires updating the central orchestrator. A2A agent cards provide a standardized metadata layer that transforms agents from black boxes into discoverable services. This JSON capability passport allows an agent to declare its purpose, the schemas it accepts, and the authentication it requires without human intervention. Without a standardized discovery mechanism, enterprise AI ecosystems suffer from 'capability sprawl,' where redundant agents are built because existing ones cannot be found or understood by other systems. From a production engineering perspective, ignoring dynamic discovery leads to massive maintenance overhead and prevents the implementation of 'specialist-first' architectures. By using A2A agent cards, architects can build decentralized systems where a supervisor agent queries a registry at runtime to find the most cost-effective or accurate specialist for a specific sub-task. This mirrors the evolution of microservices from hardcoded IP addresses to service meshes like Istio or Consul, but adapted for the non-deterministic nature of LLM-driven interactions. It ensures that as you deploy new versions of a specialist agent, the rest of the system can adapt to its new capabilities or updated API contracts automatically.
Core Concepts
The A2A Discovery ecosystem relies on several standardized metadata components that define how an agent presents itself to the world.
- Agent Card: A JSON-LD compliant document that serves as the 'passport' for an agent. It contains the agent's identity, version, capabilities, and entry points.
- name: A reverse-DNS string (e.g.,
com.mortalapps.finance.tax-calculator) that provides a unique, stable identifier for the agent across different environments. - Capability Declaration: A structured list of tasks the agent can perform, often mapped to specific JSON schemas or semantic descriptions.
- AuthSchemes: A definition of the security protocols required to communicate with the agent, such as OAuth 2.1, API Keys, or mTLS.
- Discovery Registry: A centralized or federated service where agents register their cards and supervisors query for specialists.
- Resolution Strategy: The logic used by a supervisor to select an agent, ranging from exact ID matching to semantic embedding similarity searches.
How It Works
The lifecycle of dynamic discovery follows a structured sequence from deployment to task execution.
1. Registration and Advertisement
When a specialist agent service starts, it generates or retrieves its A2A Agent Card. This card is pushed to a Discovery Registry or exposed via a well-known /.well-known/agent.json endpoint. The card includes the name, the current semantic version, and a list of supported capabilities.
2. Supervisor Query
When a supervisor agent receives a complex user request, it decomposes the request into sub-tasks. For each sub-task, the supervisor queries the Discovery Registry. The query can be specific (e.g., 'Find an agent with capability tax_calculation version ^2.0.0') or semantic (e.g., 'Find an agent that can process European VAT returns').
3. Resolution and Selection
The registry returns a list of candidate Agent Cards. The supervisor (or a dedicated router) evaluates these candidates based on metadata such as latency, cost per token (advertised in the card), and trust scores. Once a candidate is selected, the supervisor extracts the access_point URL and the required authScheme.
4. Negotiation and Handshake
The supervisor initiates a connection to the specialist. If the specialist requires OAuth 2.1, the supervisor performs the necessary token exchange. The specialist validates the supervisor's identity against its own internal allow-list or RBAC policy before accepting the task delegation.
5. Execution and Feedback
The task is executed via the A2A protocol. Upon completion, the specialist returns the result. If the specialist fails or provides a low-confidence response, the supervisor may return to the registry to resolve an alternative specialist, providing a layer of runtime fault tolerance.
Architecture
The architecture consists of three primary nodes: the Specialist Agent, the Supervisor Agent, and the Discovery Registry. The Specialist Agent acts as the provider, hosting its Agent Card at a reachable URI. The Discovery Registry acts as the broker, maintaining an indexed database of Agent Cards and their associated metadata. The Supervisor Agent acts as the consumer. Data flows start with the Specialist pushing its JSON card to the Registry via a POST request. When a task arrives, the Supervisor sends a GET or SEARCH request to the Registry. The Registry responds with a collection of JSON cards. The Supervisor then establishes a direct peer-to-peer connection with the Specialist using the endpoint defined in the card. Execution occurs over this direct link, while the Registry remains out-of-band for the actual task processing to minimize latency and avoid becoming a bottleneck.
The Agent Card Schema
The Agent Card is the core primitive of the discovery system. It must follow a strict schema to ensure interoperability across different frameworks (e.g., LangGraph, CrewAI, or custom Python agents). A standard card includes a metadata block for identity, a capabilities block for functional descriptions, and a security block for access control.
{
"a2a_version": "1.0.0",
"id": "com.mortalapps.specialist.legal-analyzer",
"version": "2.4.1",
"description": "Analyzes commercial contracts for compliance with GDPR.",
"capabilities": [
{
"name": "contract_review",
"description": "Extracts liability clauses from PDF documents.",
"input_schema": { "type": "object", "properties": { "file_url": { "type": "string" } } }
}
],
"endpoints": [
{ "type": "mcp-http", "uri": "https://legal-agent.internal/api/v1" }
],
"auth": { "type": "oauth2", "issuer": "https://auth.mortalapps.com" }
}
Semantic vs. Functional Discovery
Discovery can be implemented using two distinct strategies: functional and semantic. Functional discovery relies on exact matches of capability names or schema IDs. This is highly reliable and preferred for internal enterprise workflows where contracts are well-defined. Semantic discovery uses LLM embeddings to match a natural language task description against the description fields in the Agent Cards. While more flexible, semantic discovery introduces non-determinism into the routing layer, requiring the supervisor to validate the specialist's output more rigorously.
The name Convention
To prevent naming collisions in a global or cross-departmental ecosystem, A2A cards use a reverse-DNS naming convention. This ensures that a tax-calculator built by the Finance team doesn't conflict with one built by the HR team. The ID also serves as the root for versioning; registries should support querying by ID with semver ranges (e.g., com.mortalapps.finance.* or com.mortalapps.finance.tax-calculator@^2.0.0).
Runtime Resolution Logic
In production, resolution logic must account for agent health. A sophisticated registry doesn't just return the card; it integrates with health check probes. If a specialist agent's container is down, the registry marks the card as inactive and excludes it from discovery results. This allows for seamless blue-green deployments of agents: the new version registers its card, and once it passes health checks, the registry begins pointing supervisors to the new name version.
Security and AuthSchemes
Agent cards must explicitly define how they want to be called. The authSchemes block prevents supervisors from attempting to call agents they are not authorized to use. In an enterprise setting, this is often tied to Entra ID or AWS IAM. The card provides the resource_id or scope required for the supervisor to request a scoped token from the Identity Provider (IdP). This 'security-first' discovery ensures that agents don't even receive a request unless the caller has already satisfied the advertised security requirements.
Code Example
import os
import json
from pydantic import BaseModel, Field
from typing import List, Dict, Any
class Capability(BaseModel):
name: str
description: str
input_schema: Dict[str, Any]
class AgentCard(BaseModel):
a2a_version: str = "1.0.0"
agent_id: str = Field(..., alias="id")
version: str
capabilities: List[Capability]
endpoint: str
def generate_my_card():
# Fetch configuration from environment variables for production readiness
agent_uri = os.environ.get("AGENT_PUBLIC_URI", "https://localhost:8000")
card = AgentCard(
id="com.mortalapps.example.summarizer",
version="1.2.0",
capabilities=[
Capability(
name="summarize_text",
description="Summarizes long documents into 3 bullet points",
input_schema={"type": "object", "properties": {"text": {"type": "string"}}}
)
],
endpoint=f"{agent_uri}/a2a/v1"
)
return card.model_dump_json(by_alias=True)
if __name__ == "__main__":
print(generate_my_card())
{"a2a_version": "1.0.0", "id": "com.mortalapps.example.summarizer", "version": "1.2.0", "capabilities": [{"name": "summarize_text", "description": "Summarizes long documents into 3 bullet points", "input_schema": {"type": "object", "properties": {"text": {"type": "string"}}}}], "endpoint": "https://localhost:8000/a2a/v1"}
import os
import requests
import logging
# Configure logging for production observability
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("supervisor")
def resolve_specialist(capability_name: str):
registry_url = os.environ.get("AGENT_REGISTRY_URL")
if not registry_url:
raise EnvironmentError("AGENT_REGISTRY_URL not set")
try:
# Query the registry for agents supporting the specific capability
response = requests.get(
f"{registry_url}/discover",
params={"capability": capability_name},
timeout=5.0
)
response.raise_for_status()
candidates = response.json().get("agents", [])
if not candidates:
logger.warning(f"No agents found for capability: {capability_name}")
return None
# Simple selection logic: pick the highest version
best_match = sorted(candidates, key=lambda x: x['version'], reverse=True)[0]
logger.info(f"Resolved {best_match['id']} v{best_match['version']}")
return best_match
except requests.exceptions.RequestException as e:
logger.error(f"Discovery failed: {e}")
return None
# Example usage
# specialist = resolve_specialist("contract_review")
Logic returns a dictionary representing the resolved Agent Card or None if no match is found.