Elixir client and server library for the Agent2Agent (A2A) protocol. Provides typed structs, REST and JSON-RPC transports, SSE streaming utilities, Plug integration, and a task store abstraction with an ETS adapter.
- Agent card discovery, validation, and optional signature verification.
- Client APIs for message, task, streaming, and push notification flows.
- Server plugs for REST and JSON-RPC transports with SSE streaming support.
- Task store behavior with ETS adapter.
- Extension negotiation helpers and header utilities.
- gRPC transport placeholder returning unsupported operation errors.
- v0.3.0 and latest Release Candidate v1.0-compatible mode.
- REST + SSE and JSON-RPC + SSE.
By default, REST uses spec-compliant JSON wire format (wire_format: :spec_json), e.g.
role: "user" | "agent" and parts.
For interoperability with SDKs that use protobuf-style REST JSON (ROLE_USER / content),
you can opt in to compatibility mode with wire_format: :proto_json.
Client example:
config =
A2A.Client.Config.new("https://example.com",
transport: A2A.Transport.REST,
version: :v0_3,
wire_format: :proto_json
)
{:ok, result} =
A2A.Client.send_message(config,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Hello"}]
}
)Server example:
plug A2A.Server.REST.Plug,
executor: MyApp.A2AExecutor,
version: :v0_3,
wire_format: :proto_jsonUse :proto_json only as an interoperability workaround; keep :spec_json for standards-compliant v0.3 REST deployments.
Add a2a_ex to your dependencies:
def deps do
[
{:a2a_ex, "~> 0.1.0"}
]
end{:ok, card} = A2A.Client.discover("https://example.com")
{:ok, task_or_message} =
A2A.Client.send_message(card,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Draft an outline"}]
}
){:ok, stream} =
A2A.Client.stream_message(card,
message: %A2A.Types.Message{
role: :user,
parts: [%A2A.Types.TextPart{text: "Stream updates"}]
}
)
for event <- stream do
case event do
%A2A.Types.StreamResponse{} ->
IO.inspect(event)
%A2A.Types.StreamError{error: error} ->
IO.warn("Stream error: #{error.type} #{error.message}")
end
endStream cancellation: halting enumeration cancels the underlying Req stream.
You can also call A2A.Client.Stream.cancel(stream) to cancel explicitly.
Streaming order: when the handler returns a Task, the stream begins with a Task event
followed by update events. To emit the initial Task immediately, call emit.(task) early
in your executor before streaming status or artifact updates.
plug A2A.Server.REST.Plug,
executor: MyApp.A2AExecutor,
task_store: {A2A.TaskStore.ETS, name: MyApp.A2ATaskStore}
plug A2A.Server.AgentCardPlug,
card: MyApp.AgentCard.build(),
legacy_path: "/.well-known/agent.json"defmodule MyApp.Router do
use Plug.Router
import A2A.Server.Router
rest "/v1", executor: MyApp.A2AExecutor
jsonrpc "/rpc", executor: MyApp.A2AExecutor
endA2A.Server.Push.deliver(config, task,
security: [
strict: true,
allowed_hosts: ["example.com"],
replay_protection: true,
signing_key: System.fetch_env!("PUSH_SIGNING_KEY")
]
)examples/helloworld_server.exsexamples/helloworld_client.exsexamples/rest_server.exsexamples/rest_client.exsexamples/jsonrpc_server.exsexamples/jsonrpc_client.exsexamples/e2e_protocol_suite.exs(real transport E2E protocol smoke suite)examples/e2e_elixir_client_js_server.exs(Elixir client -> JS SDK server)examples/e2e_js_client_elixir_server.mjs(JS SDK client -> Elixir server)
Run:
elixir examples/e2e_protocol_suite.exsThis script starts real local servers and validates, end-to-end:
- REST + JSON-RPC discovery and core task/message flows.
- Streaming (
message:stream) event delivery on both transports. - Task lifecycle operations (
get,list,cancel). - Push notification lifecycle (
set/get/list/delete) with real webhook delivery and token checks. - JSON-RPC required extension enforcement and positive/negative extension-header paths.
These scripts use a pinned JS harness at examples/js_sdk_e2e/package.json with
@a2a-js/sdk version 0.3.10.
Run Elixir client against JS SDK server:
elixir examples/e2e_elixir_client_js_server.exsCovers:
- discovery and interface advertisement checks
- auth challenge/retry (401 +
WWW-Authenticate) - REST + JSON-RPC
send_message - JSON-RPC streaming assertion
- transport path checks against official JS SDK server mounts
Run JS SDK client against Elixir server:
node examples/e2e_js_client_elixir_server.mjsCovers:
- discovery and transport negotiation via
ClientFactory - auth challenge/retry using
createAuthenticatingFetchWithRetry - JSON-RPC
sendMessage+sendMessageStream+ push config lifecycle via JS SDK - REST
sendMessagevia JS SDK against Elixirwire_format: :proto_jsoncompatibility mode - REST
message:stream+ push config lifecycle using authenticated raw transport calls
Note: some JS SDK REST stream/push paths still have shape/decoding inconsistencies in this interop setup,
so the suite validates those REST endpoints with raw authenticated HTTP calls while keeping SDK coverage
for JSON-RPC and REST sendMessage.
- Push notifications should use HTTPS webhooks and SSRF protections such as allowlists or URL validation before delivery.
- Reject redirects to non-HTTPS URLs and enforce request timeouts/size limits.
- Validate resolved IPs (block localhost, private ranges, and metadata endpoints).
- Resolve DNS per request to reduce DNS rebinding risk.
- Use replay protection for webhook payloads (timestamps, nonces/jti, exp windows).
- Verify webhook auth headers or
X-A2A-Notification-Tokenwhen configured. - If agent cards include signatures, enable verification with
verify_signatures: trueand provide asignature_verifiercallback. - Signature verification is application-provided; A2A_EX does not implement JWS/JCS itself.
HexDocs: https://hexdocs.pm/a2a_ex/0.1.0
Generate docs locally:
mix docsmix format --check-formatted
mix test
mix dialyzerApache 2.0. See LICENSE.