Agent-to-Agent Protocols Basics
10 examples to get you started with Agent Cards, discovering a remote agent, and sending a first task-style message over A2A-shaped HTTP + JSON-RPC.
Search across all documentation pages
10 examples to get you started with Agent Cards, discovering a remote agent, and sending a first task-style message over A2A-shaped HTTP + JSON-RPC.
Read What A2A Solves That MCP Doesn't for the protocol story. These snippets focus on client-side discovery and a minimal remote agent surface, not a full production mesh.
You need Python 3.10+, httpx (or similar), and comfort with JSON and async HTTP.
python -m venv .venv
source .venv/bin/activate
pip install httpxConcept focus: Field names and method strings evolve with the A2A spec and SDKs. Treat examples as patterns and verify against the current A2A specification and official SDKs at build.
Client agent
→ GET Agent Card (discovery)
→ JSON-RPC over HTTP (tasks / messages)
Remote agent
→ tools / models / private MCP (opaque)Related: What A2A Solves That MCP Doesn't
Publish a machine-readable ad at a well-known URL (commonly under /.well-known/; exact path may be agent-card.json - verify at build).
{
"name": "Invoice Specialist",
"description": "Answers questions about invoice status and payment windows.",
"url": "https://agents.example.com/invoice/a2a",
"version": "1.0.0",
"protocolVersion": "0.3",
"defaultInputModes": ["text/plain", "application/json"],
"defaultOutputModes": ["text/plain", "application/json"],
"capabilities": {
"streaming": true,
"pushNotifications": false
},
"skills": [
{
"id": "invoice_status",
"name": "Invoice status",
"description": "Look up invoice state by id. Read-only.",
"tags": ["billing", "read"],
"examples": ["What is the status of invoice INV-1001?"]
}
]
}Related: Agent Cards: How Agents Advertise Their Capabilities
# card_server.py - demo only
from fastapi import FastAPI
CARD = {
"name": "Invoice Specialist",
"description": "Invoice status helper",
"url": "http://127.0.0.1:8000/a2a",
"version": "1.0.0",
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"capabilities": {"streaming": False, "pushNotifications": False},
"skills": [
{
"id": "invoice_status",
"name": "Invoice status",
"description": "Read-only invoice lookup by id.",
"tags": ["billing"],
}
],
}
app = FastAPI()
@app.get("/.well-known/agent-card.json")
def agent_card() -> dict:
return CARDimport httpx
async def fetch_card(base: str) -> dict:
url = base.rstrip("/") + "/.well-known/agent-card.json"
async with httpx.AsyncClient(timeout=10.0) as client:
r = await client.get(url)
r.raise_for_status()
card = r.json()
for key in ("name", "url", "skills"):
if key not in card:
raise ValueError(f"Agent Card missing {key}")
return cardA2A commonly binds operations as JSON-RPC 2.0 over HTTP POST. Exact method names vary by spec version; the shape below is the pattern.
import uuid
import httpx
async def send_text_message(rpc_url: str, text: str) -> dict:
payload = {
"jsonrpc": "2.0",
"id": str(uuid.uuid4()),
"method": "message/send", # verify method name at build
"params": {
"message": {
"role": "user",
"messageId": str(uuid.uuid4()),
"parts": [{"kind": "text", "text": text}],
}
},
}
async with httpx.AsyncClient(timeout=30.0) as client:
r = await client.post(rpc_url, json=payload)
r.raise_for_status()
body = r.json()
if "error" in body:
raise RuntimeError(body["error"])
return body.get("result", body)idDo not send arbitrary work to a peer that never advertised it.
def skill_ids(card: dict) -> set[str]:
return {s["id"] for s in card.get("skills", []) if "id" in s}
def assert_can_handle(card: dict, skill_id: str) -> None:
if skill_id not in skill_ids(card):
raise PermissionError(f"Remote agent lacks skill {skill_id}")def summarize_result(result: dict) -> str:
# Shapes differ by binding/version - normalize carefully
if not isinstance(result, dict):
return str(result)
if "status" in result and "id" in result:
state = result.get("status", {})
state_name = state.get("state", state) if isinstance(state, dict) else state
return f"task={result['id']} state={state_name}"
if "parts" in result:
texts = [
p.get("text", "")
for p in result["parts"]
if isinstance(p, dict) and p.get("text")
]
return " ".join(texts)[:500]
return str(result)[:500]import asyncio
async def ask_invoice_agent(base: str, question: str) -> str:
card = await fetch_card(base)
assert_can_handle(card, "invoice_status")
rpc = card["url"]
result = await send_text_message(rpc, question)
return summarize_result(result)
# asyncio.run(ask_invoice_agent("http://127.0.0.1:8000", "Status of INV-1001?"))Raw httpx teaches the wire.
Production clients should use maintained A2A SDKs when your language has one.
# Pseudocode - replace with the official Python A2A client when you pin a version
# client = A2AClient.from_agent_card_url("https://agents.example.com")
# task = await client.send_message("What is the status of INV-1001?")
# print(task)The remote invoice agent can use MCP or ordinary APIs privately.
Your client --A2A--> Invoice agent
└─ MCP/tools → billing DB (credentials stay remote)Related: Model Context Protocol Basics
Usually no. Use framework handoffs first. A2A shines when agents are separate services or owners.
No. OpenAPI describes HTTP APIs. Agent Cards describe agent identity, skills, modalities, and how to speak A2A.
Trust the SDK and current spec. The pattern is "send a user message, get a task or message back."
Some deployments use push notifications / webhooks for long-running tasks. Advertise and implement only if you support them.
In server env and secret stores. Never put API keys in Agent Cards or model-visible parts.
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.
Reviewed by Chris St. John·Last updated Jul 16, 2026