Agent Cards & Discovery
An Agent Card is the machine-readable discovery document that describes an A2A agent. Clients fetch the card to learn what the agent can do, how to connect, and what security is required.
The Agent Card
Agent cards are served at /.well-known/agent-card.json and contain:
{
"name": "Calculator Agent",
"description": "Evaluates arithmetic expressions",
"version": "1.0.0",
"supportedInterfaces": [
{
"url": "https://agent.example.com/rpc",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0.0"
},
{
"url": "https://agent.example.com/api",
"protocolBinding": "HTTP+JSON",
"protocolVersion": "1.0.0"
}
],
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["text/plain"],
"skills": [
{
"id": "calc",
"name": "Calculator",
"description": "Evaluates expressions like '3 + 5'",
"tags": ["math", "calculator"],
"examples": ["3 + 5", "10 * 2"]
}
],
"capabilities": {
"streaming": true,
"pushNotifications": false
}
}
Naming a protocol binding
protocolBinding names the transport a client should dial. The spec's
canonical values are "JSONRPC", "GRPC" and "HTTP+JSON" (the AgentInterface.protocolBinding field, §4.4.6; see the §8.5 sample card), and any
binding outside those three is a custom binding (§12).
Custom bindings should be identified by a URI rather than a bare word
(§5.8). A bare name is unique only by convention: two projects can define
incompatible bindings called "WEBSOCKET" and nothing in the card tells a
client which one it is looking at. A URI under a domain the binding's author
controls can only mean one thing. It is an identifier, not a document — it
does not have to resolve.
The two custom bindings in this project both follow that rule:
| Binding | protocolBinding | Constant |
|---|---|---|
| WebSocket | https://a2a-rust.com/bindings/websocket/v1 | a2a_protocol_types::WEBSOCKET_BINDING_URI |
| SLIMRPC | https://a2a-protocol.org/bindings/experimental-slimrpc/v1 | a2a_protocol_slimrpc::SLIMRPC_PROTOCOL_BINDING |
The trailing /v1 carries the binding's version, because §5.8 asks for a
new URI on a breaking change rather than a redefinition of the old one.
Before 0.11.0 this project advertised the bare "WEBSOCKET". A client reading
cards in the wild should accept both spellings for as long as the old one is
still out there.
Required Fields
| Field | Description |
|---|---|
name | Human-readable agent name |
description | What the agent does |
version | Semantic version of this agent |
supportedInterfaces | At least one interface with URL and protocol binding |
defaultInputModes | MIME types accepted (e.g., ["text/plain"]) |
defaultOutputModes | MIME types produced |
skills | At least one skill describing a capability |
capabilities | Capability flags (streaming, push, etc.) |
Skills
Skills describe discrete capabilities:
#![allow(unused)] fn main() { use a2a_protocol_sdk::types::agent_card::AgentSkill; let skill = AgentSkill { id: "summarize".into(), name: "Summarizer".into(), description: "Summarizes long documents".into(), tags: vec!["nlp".into(), "summarization".into()], examples: Some(vec![ "Summarize this research paper".into(), "Give me a 3-sentence summary".into(), ]), input_modes: Some(vec!["text/plain".into(), "application/pdf".into()]), output_modes: Some(vec!["text/plain".into()]), security_requirements: None, }; }
Skills can override the agent's default input/output modes and declare their own security requirements.
Capabilities
The AgentCapabilities struct advertises what the agent supports:
#![allow(unused)] fn main() { use a2a_protocol_sdk::prelude::AgentCapabilities; let caps = AgentCapabilities::none() .with_streaming(true) // Supports SendStreamingMessage .with_push_notifications(true) // Supports push notification configs .with_extended_agent_card(true); // Has an authenticated extended card }
Note:
AgentCapabilitiesis#[non_exhaustive]— always construct it viaAgentCapabilities::none()and the builder methods, never with a struct literal.
The server enforces these flags (spec §3.3.4). When you configure an agent card on the handler (
RequestHandlerBuilder::with_agent_card), the declared capabilities become a contract the server honors:
- If
streamingis nottrue,SendStreamingMessageandSubscribeToTaskreturnUnsupportedOperationError.- If
pushNotificationsis nottrue, the push-config operations (Create/Get/List/Delete) returnPushNotificationNotSupportedError.- If
extendedAgentCardis nottrue,GetExtendedAgentCardreturnsUnsupportedOperationError.So if your agent serves streaming or push, set the matching flag to
true— otherwise clients are told the operation is unsupported. A handler with no agent card configured publishes no contract and is not gated.
Interfaces
Each interface describes a transport endpoint:
#![allow(unused)] fn main() { use a2a_protocol_sdk::types::agent_card::AgentInterface; let interface = AgentInterface { url: "https://agent.example.com/rpc".into(), protocol_binding: "JSONRPC".into(), // or "HTTP+JSON", "GRPC" protocol_version: "1.0.0".into(), tenant: None, // Optional: fixed tenant for this interface }; }
For HTTP-based bindings url is an absolute URL (HTTPS in production). For
the gRPC binding it is a gRPC target, "grpc.example.com:443" — no scheme,
because gRPC names have none; whether the channel uses TLS is the client's
decision (see gRPC).
An agent must have at least one interface. Having multiple interfaces (e.g., JSON-RPC and REST) lets clients choose their preferred transport.
The choice is the client's, not the card's. ClientBuilder::from_card() walks ClientConfig::preferred_bindings in order and takes the first binding the card offers, falling back to the card's first interface only when it offers none of them. Pass your own order with ClientBuilder::from_card_preferring(&card, &["GRPC".into()]).
Because a card gives each binding its own URL, the endpoint follows the binding: selecting GRPC selects that interface's url and tenant as well. The tenant field from the selected interface is preserved in ClientConfig::tenant and sent on every request — all eleven methods, GetExtendedAgentCard included — as spec §8.3.2 rule 4 requires. Methods whose params carry a tenant (send, get, list, push-config) let a per-request value override it.
Extended Agent Card
An agent can expose a richer card via GetExtendedAgentCard for authenticated
clients. This requires setting capabilities.extended_agent_card = true:
#![allow(unused)] fn main() { use a2a_protocol_sdk::prelude::*; let capabilities = AgentCapabilities::none() .with_extended_agent_card(true); }
If the capability is false or absent, the server returns UnsupportedOperationError.
ExtendedAgentCardNotConfiguredError is returned when the handler has no agent
card at all.
The operation must be authenticated (§13.3): without an authenticating
interceptor (e.g. BearerTokenAuthInterceptor, JwtAuthInterceptor) the server
refuses it with InvalidRequest, unless you opt in with
RequestHandlerBuilder::allow_unauthenticated_extended_card().
Serving Agent Cards
Static Handler
For agent cards that don't change at runtime:
use a2a_protocol_sdk::prelude::*; struct MyAgent; agent_executor!(MyAgent, |_ctx, _queue| async { Ok(()) }); fn make_agent_card() -> AgentCard { AgentCard::new("my-agent", "1.0.0", AgentInterface::jsonrpc("http://localhost:3000")) } fn main() { let my_executor = MyAgent; use a2a_protocol_sdk::server::RequestHandlerBuilder; let handler = RequestHandlerBuilder::new(my_executor) .with_agent_card(make_agent_card()) .build() .unwrap(); }
The static handler automatically provides:
- ETag headers for cache validation
- Last-Modified timestamps
- Cache-Control directives
- 304 Not Modified responses for conditional requests
Dynamic Handler
For agent cards that change (e.g., based on feature flags, load, or authentication):
#![allow(unused)] fn main() { use std::future::Future; use std::pin::Pin; use a2a_protocol_sdk::prelude::{A2aResult, AgentInterface}; fn make_agent_card() -> AgentCard { AgentCard::new("my-agent", "1.0.0", AgentInterface::jsonrpc("http://localhost:3000")) } use a2a_protocol_sdk::server::{AgentCardProducer, DynamicAgentCardHandler}; use a2a_protocol_sdk::types::agent_card::AgentCard; struct MyCardProducer; impl AgentCardProducer for MyCardProducer { fn produce<'a>(&'a self) -> Pin<Box<dyn Future<Output = A2aResult<AgentCard>> + Send + 'a>> { Box::pin(async move { // Generate card dynamically Ok(make_agent_card()) }) } } }
The dynamic handler calls the producer on every request, computes a fresh ETag, and handles conditional caching.
Hot-Reload Handler
For agent cards loaded from a JSON file that may change at runtime:
use a2a_protocol_sdk::prelude::*; fn make_agent_card() -> AgentCard { AgentCard::new("my-agent", "1.0.0", AgentInterface::jsonrpc("http://localhost:3000")) } #[tokio::main] async fn main() { let initial_card = make_agent_card(); use a2a_protocol_sdk::server::HotReloadAgentCardHandler; use std::path::Path; use std::time::Duration; let handler = HotReloadAgentCardHandler::new(initial_card); // Cross-platform: poll the file every 30 seconds let watcher = handler.spawn_poll_watcher( Path::new("/etc/a2a/agent.json"), Duration::from_secs(30), ); // Unix only: reload on SIGHUP #[cfg(unix)] let signal_watcher = handler.spawn_signal_watcher( Path::new("/etc/a2a/agent.json"), ); }
HotReloadAgentCardHandler implements AgentCardProducer, so it plugs directly into DynamicAgentCardHandler for full HTTP caching support. The internal Arc<RwLock<AgentCard>> ensures updates are atomic with low contention for concurrent readers.
HTTP Caching
Agent card responses include standard HTTP caching headers (RFC 7232):
| Header | Purpose |
|---|---|
ETag | Content hash for cache validation |
Last-Modified | Timestamp of last change |
Cache-Control | public, max-age=3600 (configurable) |
Clients should send If-None-Match or If-Modified-Since headers. If the card hasn't changed, the server returns 304 Not Modified with no body.
Security
Agent cards declare security schemes and the requirements that reference them. This is the v1.0 wire form, which is what this SDK emits:
{
"securitySchemes": {
"bearer": { "httpAuthSecurityScheme": { "scheme": "bearer" } },
"oauth": { "oauth2SecurityScheme": { "flows": { "clientCredentials": {
"tokenUrl": "https://auth.example.com/token", "scopes": { "read": "Read" } } } } }
},
"securityRequirements": [
{ "schemes": { "bearer": { "list": [] } } },
{ "schemes": { "oauth": { "list": ["read"] } } }
]
}
Each scope list is a StringList object ({"list": [...]}), because the spec
defines the JSON as the ProtoJSON of map<string, StringList>. When reading a
card, a bare array ("oauth": ["read"], as a2a-go v2.5.0 writes it) and null
are accepted too, so a Go agent's card parses; only the {"list": [...]} form
is ever written.
Individual skills can also declare their own security requirements, overriding the global ones.
Extended Agent Cards
If the agent supports extendedAgentCard capability, clients can fetch an authenticated version with additional details via the GetExtendedAgentCard method.
Next Steps
- Tasks & Messages — The data model for agent communication
- Streaming with SSE — Real-time event delivery