a2acli
Standalone A2A CLI client built on top of a2a-client.
This crate is published as a2a-cli and installs the a2acli binary.
Install
From the workspace checkout:
From crates.io after release:
Verifying a download
The prebuilt archives attached to an a2a-cli-v* GitHub Release are signed
with Sigstore and carry SLSA build provenance.
Both are keyless: there is no long-lived key, and the certificate binds the
artifact to the workflow that produced it.
Check the signature (<archive>.sigstore.json, a Sigstore bundle carrying
both the signature and the signing certificate):
Check the provenance — what built the archive, from which commit:
The <archive>.sha256 file alongside them is a plain integrity checksum. It
tells you the download is intact, not who produced it — only the signature and
the provenance do that.
What It Provides
- Fetch and print the public agent card for an A2A deployment, named by
-a/--agent-cardas a host, a full card URL, or a local file — or skipped entirely with-e/--endpoint - Send one-shot or streaming messages, with multi-part content
(
--text-part/--file-part/--data-part/--media-type) - Blocks by default until a task settles (terminal or interrupted state);
--asyncreturns immediately instead - Inspect (optionally waiting on it with
--wait), list, cancel, and subscribe to tasks - Create, fetch, list, and delete task push notification configs
- Request an extended agent card when the server exposes one
- Bearer token, API key, and general service-parameter authentication on
every request, with an ordered
--transportpreference and an--insecureescape hatch (development only, always warns) for self-signed endpoints - Human-readable
textoutput by default, or the protocol's own JSON (-o/--output json); every failure is a machine-readable error object with a stable code and exit status - Full configuration precedence (flag > environment variable > local
.envglobal
.env> built-in default) with a read-onlyconfig showto inspect it - Shell completion scripts for bash, zsh, fish, PowerShell, and elvish via
completion <shell> card get --validateagainst the A2A JSON schema, reporting every violation with its JSON pointer path
Run
Commands are namespaced by the resource they act on (card get, task get,
task push-config create, …), matching the command taxonomy in the
a2a-cli specification:
By default the CLI targets http://localhost:3000. Use --transport jsonrpc
or --transport rest (repeatable and ordered, highest preference first) to pin
the transport when the agent card exposes more than one compatible interface.
The global --tenant, --bearer, --api-key, and repeated
--svc-param Name:Value options also apply to task push-config commands.
Capability pre-flight
The Agent Card is the contract, so a capability-gated operation is checked
against it before the call goes out (§13.3): streaming for send --stream
and task subscribe, extendedAgentCard for card get --extended, and
pushNotifications for every task push-config subcommand. An absent field
counts as undeclared, the same way card get prints it.
send --stream against a card that does not declare streaming still returns
a result — it sends without streaming and polls, with the reason on stderr —
because the request was for a result, not specifically for a stream. The
others have no equivalent path, so they fail with the reason instead, using
the same protocol error code the agent itself would have returned.
--endpoint resolves no card at all, so there is nothing to verify and the
pre-flight stands down: the agent answers for itself, as before.
Naming the agent
-a/--agent-card <ref> names the agent as an Agent Card reference, in any
of three forms:
A bare host or origin gets the well-known path /.well-known/agent-card.json
appended, and takes http:// when it is loopback (localhost, 127.0.0.1,
[::1]) or https:// otherwise. A reference that already carries a path is a
full card URL and is used as-is. A file:// URL or a plain filesystem path is
read from disk, so the CLI can be driven with no agent running at all — a
missing file reports A2ACLI_ERR_CARD_NOT_FOUND and a file that is not a card
reports A2ACLI_ERR_CARD_INVALID, matching how the HTTP path classifies an
unreachable card and an unparseable body.
-e/--endpoint <url> skips card resolution entirely and connects straight to
an agent interface. Since no card is fetched, there is nothing to declare the
protocol binding, so it requires exactly one --transport and cannot be
combined with --agent-card:
--base-url is a deprecated alias for the bare-origin form. It still works so
existing invocations keep running, but it warns and is hidden from --help;
prefer --agent-card, which accepts all three forms.
Authentication and transport
--a2a-version pins the protocol version signaled on every request. Absent
it, the version is negotiated down to the highest one both a2acli and the
agent's selected card interface declare, bounded to 1.x and never below 1.0 —
A2A reads an empty or pre-1.0 value as 0.3, so a version outside 1.x is
refused as a usage error rather than downgraded, and any effective version
other than the tool's own is named on stderr rather than applied silently.
--bearer/--api-key (env A2ACLI_BEARER/A2ACLI_API_KEY) supply credentials;
--svc-param is a separate, general-purpose transport-level key-value pair,
never itself a credential flag. --insecure disables TLS certificate
verification and always prints a warning naming the risk when a credential is
also configured — it never disables verification silently.
--debug writes developer diagnostics to stderr, including the raw protocol
messages exchanged on the wire: the request and response body of every call
and each streamed event. Credential values never appear there, regardless of
verbosity — a credential header is shown as authorization: (redacted), so
the log confirms it was attached without disclosing it. Redaction is applied
where the log line is built rather than filtered afterwards, so no flag or
environment variable can turn it off. Protocol metadata such as
a2a-version stays visible, since version negotiation is one of the things
--debug exists to explain. --tenant supplies a routing tenant when the
selected Agent Card interface declares none; when the interface does declare
one, A2A §8.3.2 requires that declared value to be sent exactly, so it is
used and --tenant has no effect.
Blocking and polling
send blocks by default until the resulting task reaches a terminal
(COMPLETED/FAILED/CANCELED/REJECTED) or interrupted
(INPUT_REQUIRED/AUTH_REQUIRED) state, polling task get under the hood;
pass --async to get the task identifiers back immediately instead. task get is one-shot by default — add --wait to poll it the same way. Tune the
loop with --poll-interval (default 2s) and --timeout (default 30s,
after which the command exits with a timeout error).
Stream resumption
task subscribe reconnects when its connection is cut before the task
settles — an SSE stream held open for a long-running task will eventually be
dropped by a proxy, a load balancer, or a laptop going to sleep, and a dropped
connection looks identical to a finished task unless something distinguishes
them. --poll-interval and --timeout apply here too: --poll-interval is
the delay between reconnect attempts, and --timeout bounds the reconnection
budget cumulatively across all of them — not any single attempt, and not a
healthy stream that never disconnects at all. Each attempt is reported on
stderr; stdout carries only the protocol events.
Reconnecting never issues a task get: the server's first event after
resuming is the full current Task, which is enough to reconcile state
through the protocol itself. If that reconciling event reports the same
state the connection had already reached before the cut, it is suppressed
rather than printed a second time — under -o json --stream the output is
JSONL a consumer reads incrementally, and a duplicate Task line there would
look like a second, spurious transition.
Message parts
A message can carry more than one part, built from repeatable, order-preserving flags:
--text-part adds a text part, --file-part <path|url> a file part (a local
path is inlined as base64 bytes, a URL is carried by reference and never
fetched by the CLI), and --data-part <path|-> a structured JSON part read
from a file, or from stdin when the value is -; anything else is parsed as
an inline JSON string. --media-type sets the media type of the part flag
immediately preceding it. The plain positional form (send "hello") is
shorthand for a single --text-part and cannot be combined with the part
flags above.
Output and errors
text (labeled Label: value fields, with a copy-pasteable resume command
whenever a task pauses at INPUT_REQUIRED/AUTH_REQUIRED) is the default,
human-readable format. Pass -o json (or --output json) for the protocol's
own JSON types instead — --stream then switches its cardinality from one
document to JSON Lines (one object per event); --compact only affects the
single-document form.
A failure — from the agent or from the tool itself, including a malformed flag or a missing subcommand — always prints one compact JSON error object to stderr, in every output mode:
A task that the CLI conducted and reported exits 0 even when the agent did
not succeed, so the exit status alone cannot say that. A FAILED or
REJECTED outcome, and a task paused at INPUT_REQUIRED/AUTH_REQUIRED,
is therefore also named in a one-line warning on stderr — in every output
mode, leaving stdout exactly the payload. CANCELED is not warned about:
after task cancel it is the outcome you asked for.
code is the A2A protocol's own error name for a protocol failure (with the
numeric a2aCode alongside it), or an A2ACLI_ERR_* symbol for a failure
the protocol never saw (a bad flag, an unreachable agent, a --timeout
expiry). The exit status reports only whether the CLI did its job — 0 even
when a task ends FAILED/REJECTED or pauses at
INPUT_REQUIRED/AUTH_REQUIRED — while 1/2/3/4/5 distinguish a
generic failure, a usage error, an unreachable agent, a rejected credential,
and a timeout respectively.
Configuration
Every global option listed above can also be set from the environment or a
.env file, so repeated invocations stay short. Precedence, highest wins:
- an explicit flag,
- a real environment variable,
- a local
.env(found by walking up from the working directory, or the file named by--config <path>), - the global
.envat~/.config/a2a-cli/.env($XDG_CONFIG_HOMEhonored), - the built-in default.
The environment variable name is A2ACLI_ followed by the long flag name,
upper-snake-cased — --bearer → A2ACLI_BEARER, --transport →
A2ACLI_TRANSPORT (comma-separated: A2ACLI_TRANSPORT=jsonrpc,rest),
--context-id → A2ACLI_CONTEXT_ID. The same names work in a .env file,
one KEY=value per line; blank lines and # comments are ignored, a leading
export is tolerated, and one layer of surrounding quotes is stripped:
A2ACLI_AGENT_CARD=https://agent.example.com
A2ACLI_TRANSPORT=rest,jsonrpc
A2ACLI_TIMEOUT=60s
A2ACLI_A2A_VERSION=1.0
# credentials
A2ACLI_BEARER="Bearer <token>"
--stream, -h/--help, and -v/--version are never read from the
environment or a file — they must always be passed explicitly.
config show is read-only: it prints each effective setting and the source
it resolved from (redacting credential values), so you can confirm
precedence without guessing. Change settings by exporting the variable or
editing a .env file directly — the command never mutates anything. a2acli
never writes a .env file itself, but warns if one it reads is readable by
users other than its owner (mode should be 0600).
Schema validation
card get --validate checks the fetched card against the A2A JSON schema
(§10.1) — a stricter, complementary check to the type-checked deserialization
card get always does. The two catch different things: the type check
rejects a card that does not parse as an AgentCard at all, while the schema
additionally rejects a card that parses fine but violates the schema in a way
the Rust type is too permissive to notice — an unrecognised property, most
commonly, since AgentCard's fields accept any JSON object as long as the
ones the type cares about are present and well-typed. --validate therefore
needs the bytes the agent actually served, not a round trip through the typed
value, which would have already dropped whatever the schema is being asked to
catch.
A card that passes is printed exactly as card get prints it. A card that
fails exits with A2ACLI_ERR_CARD_INVALID and every violation under the error
envelope's details, each with a JSON pointer path into the card and a
message — never only the first one found. The schema is vendored at a
pinned A2A version rather than fetched, and that version is reported in the
envelope's hint alongside A2ACLI_ERR_CARD_INVALID, since "invalid" means
nothing without saying invalid against what.
--validate combined with --extended validates the extended card the same
way, with one gap worth stating: that card arrives through the A2A client's
typed protocol pipeline rather than as bytes this CLI reads itself, so it
cannot see an unrecognised property the same pipeline already discarded. A
wrong type or an unrecognised enum value are still caught on that path.
Shell completion
completion <shell> writes a completion script to stdout and nothing else, so
it can be redirected to a file or eval'd directly. It needs no agent and
makes no network call, so it works before anything is configured.
# bash — user-level; use /etc/bash_completion.d/a2acli to install system-wide
# zsh — any directory on $fpath
# fish
# elvish
# PowerShell — add to $PROFILE to persist
a2acli completion powershell | Out-String | Invoke-Expression
Naming an unsupported shell is a usage error (exit 2) listing the accepted values.
Conformance
This CLI is tracked against the
a2a-cli specification
Tier 1 ("Core") requirements; see
a2aproject/a2a-rs#164 for the
current gap list and in-progress work (blocking-by-default send/polling,
human-readable text output, message parts, and more). Tier 2 ("Extended")
requirements are tracked in
a2aproject/a2a-rs#181;
completion <shell> above is the first of them.