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.
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.
Prerequisites
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.
Basic Examples
1. Know the Three Roles Before You Code
- A2A client - the agent or app that starts a request
- A2A server (remote agent) - the peer that exposes an A2A endpoint
- Agent Card - JSON metadata advertising identity, skills, URL, and auth
Client agent
→ GET Agent Card (discovery)
→ JSON-RPC over HTTP (tasks / messages)
Remote agent
→ tools / models / private MCP (opaque)- Clients do not need the remote agent's prompts or tool list
- Skills on the card are the public capability surface
- Tasks may outlive a single HTTP request
Related: What A2A Solves That MCP Doesn't
2. Sketch a Minimal Agent Card
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?"]
}
]
}- Keep skills outcome-oriented, not "list of internal tools"
- Advertise only modalities you actually accept
- Version the card when skills change
Related: Agent Cards: How Agents Advertise Their Capabilities
3. Serve the Card From a Tiny HTTP App
# 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 CARD- Separate discovery URL from RPC endpoint if your layout needs it
- Cards are often public metadata; still avoid secrets in the JSON
- Auth requirements belong on the card when the endpoint is protected
4. Fetch and Validate a Card as a Client
import 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 card- Fail closed on missing endpoint URL or empty skills
- Cache cards with a TTL; re-fetch on version bumps
- Prefer HTTPS and certificate validation in real environments
5. Send a First JSON-RPC Style Message
A2A 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)- Always correlate with JSON-RPC
id - Parts carry text, files, or structured data depending on negotiation
- Result may be a Task (async) or a direct Message (simple turns)
Intermediate Examples
6. Pick a Skill Before You Call
Do 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}")- Match user intent to skill tags/examples, then call
- Log which skill you believed you invoked
- Prefer specialized peers over one mega-agent card
7. Handle Task-Shaped Results
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]- Terminal states need explicit handling (completed, failed, canceled, rejected)
- Store task ids if you will poll, stream, or cancel later
- Truncate before stuffing results into the next model turn
8. Minimal Client Loop: Discover → Select → Send
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?"))- Discovery and invocation are separate steps
- Policy (which bases are trusted) stays in your host
- Soft-fail unknown skills instead of inventing behavior
9. Prefer Official SDKs When Available
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)- SDKs handle streaming, auth headers, and schema drift better than copy-paste RPC
- Still pin versions and regression-test against staging cards
- Framework adapters (when present) should not hide allowlists
10. Keep MCP Tools on the Remote Side
The remote invoice agent can use MCP or ordinary APIs privately.
Your client --A2A--> Invoice agent
└─ MCP/tools → billing DB (credentials stay remote)- Do not re-export remote DB tools to the client "for convenience"
- Artifacts and messages are the integration surface
- Your host still applies budgets, timeouts, and human approval gates
Related: Model Context Protocol Basics
FAQs
Do I need A2A for two agents in one process?
Usually no. Use framework handoffs first. A2A shines when agents are separate services or owners.
Is the Agent Card the same as an OpenAPI document?
No. OpenAPI describes HTTP APIs. Agent Cards describe agent identity, skills, modalities, and how to speak A2A.
What if message/send is named differently in my SDK?
Trust the SDK and current spec. The pattern is "send a user message, get a task or message back."
Can the remote agent call me back?
Some deployments use push notifications / webhooks for long-running tasks. Advertise and implement only if you support them.
Where do secrets go?
In server env and secret stores. Never put API keys in Agent Cards or model-visible parts.
Related
- What A2A Solves That MCP Doesn't
- Agent Cards: How Agents Advertise Their Capabilities
- A2A's Transport Layer: HTTP, SSE, and JSON-RPC 2.0
- When to Adopt A2A vs Build a Custom Handoff Protocol
- Agent-to-Agent Protocols Best Practices
- What MCP Standardizes and Why It Won the Tool-Connection Wars
- Context Handoff: Passing State Between Agents Cleanly
- Multi-Agent Architecture Basics
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.