Building a Client

The ClientBuilder creates an A2aClient configured for your target agent. It handles transport selection, timeouts, authentication, and interceptors.

Basic Client

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;

let client = ClientBuilder::new("http://agent.example.com")
    .build()
    .expect("build client");
}

Without a binding the builder uses JSON-RPC; it does not look at the URL to choose. To choose another:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
// Force JSON-RPC transport
let client = ClientBuilder::new("http://agent.example.com")
    .with_protocol_binding("JSONRPC")
    .build()
    .unwrap();

// Force REST transport
let client = ClientBuilder::new("http://agent.example.com")
    .with_protocol_binding("REST")
    .build()
    .unwrap();
}

Configuration Options

Timeouts

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
let url = "http://agent.example.com";
use std::time::Duration;

let client = ClientBuilder::new(url)
    .with_timeout(Duration::from_secs(60))              // Per-request timeout (default: 30s)
    .with_connection_timeout(Duration::from_secs(5))     // TCP connect timeout (default: 10s)
    .with_stream_connect_timeout(Duration::from_secs(15)) // stream headers (default: 30s)
    .with_stream_first_event_timeout(Duration::from_secs(60)) // first event after that (default: 5 min)
    .with_stream_idle_timeout(Some(Duration::from_secs(120))) // silence allowed mid-stream (default: 5 min)
    .build()
    .unwrap();
}

Output Modes

Specify which MIME types the client can handle:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
let url = "http://agent.example.com";
let client = ClientBuilder::new(url)
    .with_accepted_output_modes(vec![
        "text/plain".into(),
        "application/json".into(),
        "image/png".into(),
    ])
    .build()
    .unwrap();
}

Default: ["text/plain", "application/json"]

History Length

Control how many historical messages are included in responses:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
let url = "http://agent.example.com";
let client = ClientBuilder::new(url)
    .with_history_length(10)  // Include last 10 messages
    .build()
    .unwrap();
}

Interceptors

Add request/response hooks:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
let url = "http://agent.example.com";
use std::sync::Arc;
use a2a_protocol_client::{
    BearerAuthInterceptor, StaticTokenProvider, TracePropagationInterceptor,
};

let token = Arc::new(StaticTokenProvider::new("my-token"));
let client = ClientBuilder::new(url)
    .with_interceptor(BearerAuthInterceptor::new(token))
    .with_interceptor(TracePropagationInterceptor)
    .build()
    .unwrap();
}

Your own hooks implement CallInterceptor and are added the same way.

Retry Policy

Enable automatic retries on transient failures (connection errors, timeouts, HTTP 429/502/503/504):

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
use std::time::Duration;
let url = "http://agent.example.com";
use a2a_protocol_client::RetryPolicy;

let client = ClientBuilder::new(url)
    .with_retry_policy(RetryPolicy::default())  // 3 retries, 500ms initial backoff
    .build()
    .unwrap();

// Custom retry configuration
let client = ClientBuilder::new(url)
    .with_retry_policy(
        RetryPolicy::default()
            .with_max_retries(5)
            .with_initial_backoff(Duration::from_secs(1))
            .with_max_backoff(Duration::from_secs(60))
            .with_backoff_multiplier(3.0),
    )
    .build()
    .unwrap();
}

Sends (send_message, stream_message, creating a push config) are retried only on 429/503, which prove the server did not process them. After a timeout or a dropped connection they are not re-sent unless they carry an idempotency key the peer honours; see Idempotent Sends.

Return Immediately

For push notification workflows, return the task immediately without waiting for completion:

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::client::ClientBuilder;
let url = "http://agent.example.com";
let client = ClientBuilder::new(url)
    .with_return_immediately(true)
    .build()
    .unwrap();
}

Builder Reference

MethodDefaultDescription
new(url)—Base URL of the agent (required)
from_card(&AgentCard)—Build from an agent card, preferring ClientConfig's default binding order (["JSONRPC"]), falling back to the card's first compatible interface. Only interfaces whose protocolVersion has major 1 count (empty counts; v1.0 counts), so a v0.3 endpoint listed first is skipped; a card with none is refused with what it offers. If the chosen interface cannot be built (gRPC under sync build(), an unknown binding, a bad URL), build() moves to the next one
from_card_preferring(&AgentCard, &[String])—Same, with your own binding order. The first preference the card offers wins; matching is case-insensitive
with_protocol_binding(str)"JSONRPC"Force transport: "JSONRPC", "HTTP+JSON" (or "REST"), or "GRPC", in any case. On a builder made from a card, moves the endpoint and tenant to that binding's interface too, and turns off build()'s fallback to other interfaces
with_custom_transport(impl Transport)NoneUse a custom transport (e.g., GrpcTransport)
with_timeout(Duration)30sPer-request timeout
with_connection_timeout(Duration)10sTCP connection timeout
with_stream_connect_timeout(Duration)30sEstablishing a stream (headers, or an error body)
with_stream_first_event_timeout(Duration)5 minWait for a stream's first data once established
with_max_event_size(usize)16 MiBLargest single stream event accepted; larger ones are refused and skipped
with_stream_idle_timeout(Option<Duration>)5 minSilence allowed between chunks after a stream's first data; SSE keep-alives reset it; None disables
with_retry_policy(RetryPolicy)NoneRetry on transient errors with jittered exponential backoff
with_accepted_output_modes(Vec<String>)["text/plain", "application/json"]MIME types the client handles
with_history_length(u32)NoneMessages to include in responses
with_return_immediately(bool)falseDon't wait for task completion
with_tenant(str)None (auto from AgentCard)Default tenant for multi-tenancy
with_interceptor(impl CallInterceptor)Empty chainAdd request/response hook
with_max_response_size(usize)32 MiBLargest buffered (non-streaming) response body
without_tls()TLS on with the tls-rustls featureDisable TLS (plain HTTP only)
with_peer_honouring_idempotency(bool)false (true when the card advertises the extension)Let a keyed send be retried after an ambiguous failure; see Idempotent Sends
with_grpc_bare_address_scheme(GrpcBareAddressScheme)HttpsExceptLoopbackHow build_grpc dials a bare host:port
with_grpc_tls_config(ClientTlsConfig)None (bundled Mozilla roots)TLS settings for build_grpc (grpc-tls feature)
build()—Construct a JSON-RPC or REST client
build_grpc() (async)—Construct a gRPC client (grpc feature)

Client Reuse (Best Practice)

Create clients once and reuse them across requests. Each A2aClient holds a connection pool internally (via hyper), so reuse avoids repeated DNS resolution, TCP handshakes, and TLS negotiation on every call.

#![allow(unused)]
fn main() {
use a2a_protocol_sdk::prelude::*;
// ✅ Good: build once, reuse across requests
struct MyOrchestrator {
    analyzer: A2aClient,
    builder: A2aClient,
}

impl MyOrchestrator {
    fn new(analyzer_url: &str, builder_url: &str) -> Self {
        Self {
            analyzer: ClientBuilder::new(analyzer_url).build().unwrap(),
            builder: ClientBuilder::new(builder_url)
                .with_protocol_binding("REST")
                .build()
                .unwrap(),
        }
    }

    async fn run(&self, params: MessageSendParams) {
        // Reuse the same client for every request
        let _ = self.analyzer.send_message(params.clone()).await;
        let _ = self.builder.send_message(params).await;
    }
}
}
#![allow(unused)]
fn main() {
use a2a_protocol_sdk::prelude::*;
// ❌ Avoid: rebuilding the client on every call
async fn bad_pattern(url: &str, params: MessageSendParams) {
    // This works but wastes resources — connection pool is discarded each time
    let client = ClientBuilder::new(url).build().unwrap();
    let _ = client.send_message(params).await;
}
}

gRPC Client

Requires the grpc feature: a2a-protocol-client = { version = "0.14", features = ["grpc"] }

ClientBuilder::new(url).build_grpc().await? (or from_card(&card)?.build_grpc()) builds a gRPC client that uses this builder's timeouts and limits; a transport passed to with_custom_transport keeps only its own GrpcTransportConfig.

To construct the transport yourself, use GrpcTransport::connect() with with_custom_transport():

use a2a_protocol_sdk::client::ClientBuilder;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use a2a_protocol_client::GrpcTransport;

let transport = GrpcTransport::connect("http://agent.example.com:50051").await?;
let client = ClientBuilder::new("http://agent.example.com:50051")
    .with_custom_transport(transport)
    .build()?;
Ok(())
}

Configure with GrpcTransportConfig:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
use a2a_protocol_client::transport::grpc::{GrpcTransport, GrpcTransportConfig};
use std::time::Duration;

let config = GrpcTransportConfig::default()
    .with_timeout(Duration::from_secs(60))
    .with_max_message_size(8 * 1024 * 1024);

let transport = GrpcTransport::connect_with_config(
    "http://agent.example.com:50051",
    config,
).await?;
Ok(())
}

Thread Safety

A2aClient is Send + Sync and can be shared across tasks via Arc:

use a2a_protocol_sdk::prelude::*;
#[tokio::main]
async fn main() {
let url = "http://agent.example.com";
let params = MessageSendParams::new(Message::new("m", MessageRole::User, vec![Part::text("hi")]));
let (params1, params2) = (params.clone(), params);
use std::sync::Arc;

let client = Arc::new(
    ClientBuilder::new(url).build().unwrap()
);

// Share across async tasks
let c1 = Arc::clone(&client);
tokio::spawn(async move { c1.send_message(params1).await });

let c2 = Arc::clone(&client);
tokio::spawn(async move { c2.send_message(params2).await });
}

Next Steps