By James Aspinwall, co-written by Alfred Pennyworth (my trusted AI) — March 2, 2026, 17:00
Google’s A2A (Agent-to-Agent) protocol is a thin JSON-RPC 2.0 layer that lets AI agents discover each other and exchange tasks over HTTP. It is intentionally minimal — agent card for discovery, tasks for execution, and that is it. No opinions about what your agent does internally, how it stores state, or what LLM it uses. Just a wire format for “here is what I can do” and “please do this thing.”
We implemented both sides in our Elixir MCP server: an A2A server that exposes our 86 MCP tools as A2A skills, and a stateless HTTP client that can talk to any A2A-compliant agent. This article covers what we built, what worked, what did not, and where this protocol is actually useful versus where it is premature engineering.
What We Built
The Server Side
Our MCP server already had a rich tool system — CRM operations, task management, article summaries, WhatsApp messaging, access control, monitoring, and more — all protected by a granular permission system. The A2A server sits in front of this and translates between the A2A protocol and our internal tool execution engine.
Two endpoints:
-
GET /.well-known/agent.json— public discovery, returns an agent card with a safe subset of tools (greeting, time, basic math — nothing that reads or writes user data) -
POST /a2a— authenticated JSON-RPC 2.0, routestasks/send,tasks/get, andtasks/cancelto the server logic
When a remote agent sends a task, the server:
- Parses the message parts — text for human-readable context, data for structured tool invocation
-
Creates a task row in SQLite with state
"working" - Executes the tool through our existing MCP Manager, which enforces permission checks
-
Stores the result as an artifact, sets state to
"completed"or"failed" - Returns the full task object in the JSON-RPC response
Every MCP tool the user has permission to access becomes an A2A skill automatically. No manual mapping. A developer who adds a new MCP tool gets A2A exposure for free.
The Client Side
A2AClient is a stateless Req-based HTTP client with four functions: discover, send_task, get_task, cancel_task. It handles JSON-RPC framing, cookie authentication, and TLS (with verification disabled for self-signed certs in development).
# Discover what an agent can do
{:ok, card} = A2AClient.discover("https://remote-agent.example.com")
# Send a task with a specific tool
{:ok, task} = A2AClient.send_task(
"https://remote-agent.example.com",
"Add these numbers",
cookie: cookie,
tool: "add_numbers",
arguments: %{"a" => 5, "b" => 12}
)
# Check on it later
{:ok, task} = A2AClient.get_task("https://remote-agent.example.com", task["id"], cookie: cookie)
The client does not maintain state. Every call is independent. This makes it easy to use from IEx, from background tasks, from other GenServers — anywhere you can make an HTTP call.
How a Developer Uses This
Exposing Your Tools to Other Agents
If you are building on our MCP server and you add a new protected module — say, an inventory tracker — it automatically becomes available over A2A to any authenticated client whose user has the right permissions. The flow:
-
You write your module with
@permission,@module_name,use AccessControlled - You add MCP tool definitions in the permissions wrapper
-
You register the tools in
MyMCPServer -
Done. A2A discovery now lists those tools as skills, and
tasks/sendcan invoke them
No A2A-specific code required. The bridge is automatic.
Calling Remote Agents
From IEx or any module:
# Step 1: See what the remote agent offers
{:ok, card} = A2AClient.discover("https://partner-agent.example.com")
Enum.each(card["skills"], fn s -> IO.puts(" #{s["id"]}: #{s["description"]}") end)
# Step 2: Call a specific skill
{:ok, result} = A2AClient.send_task(
"https://partner-agent.example.com",
"Summarize this article",
cookie: auth_cookie,
tool: "summary_request",
arguments: %{"url" => "https://example.com/article"}
)
# Step 3: Extract the result
for artifact <- result["artifacts"],
part <- artifact["parts"] do
IO.puts(part["text"])
end
The Demo Module
A2ADemo provides two modes for testing:
-
A2ADemo.run_local()— bypasses HTTP entirely, callsA2AServerfunctions directly. Fast feedback, no network dependencies. Good for verifying tool execution logic. -
A2ADemo.run(cookie)— full HTTP round-trip throughA2AClient→ router →A2AServer. Validates the entire stack: TLS, cookie auth, JSON-RPC serialization, tool dispatch.
Start with run_local, then graduate to run(cookie) once you are confident the logic works.
How Clients Benefit
For AI Agent Developers
An AI agent built on any framework — LangChain, CrewAI, AutoGen, a custom Python script — can discover and use our tools without knowing anything about Elixir, MCP, or our internal architecture. They hit a standard HTTP endpoint, get back JSON, and integrate the results into their own workflow.
A client agent could:
-
Query our CRM — search contacts, check follow-up schedules, log interactions — all through
nis_*tools exposed as A2A skills - Manage tasks — create, query, complete tasks in our task system from their own planning loop
- Trigger monitoring — start health checks, pull anomaly reports, check service status
- Summarize content — submit URLs for summarization using our LLM pipeline with provider switching
The permission system means you can give a partner agent access to exactly the tools they need and nothing more. A reporting agent gets read-only CRM access. An operations agent gets monitoring tools. No one gets access control or WhatsApp without explicit grants.
For Platform Operators
If you run multiple specialized agents — one for CRM, one for DevOps, one for content — A2A gives them a standard way to collaborate. The CRM agent can ask the content agent to summarize a prospect’s blog. The DevOps agent can ask the CRM agent to notify a contact when their service is restored. Each agent exposes its skills, each agent discovers and calls the others.
For End Users
Users do not interact with A2A directly. They benefit indirectly: their AI assistant can now reach capabilities hosted on other servers without custom integrations for each one. The assistant discovers what is available, matches it to the user’s request, and calls the right skill. The user just sees the result.
Advantages
Zero-config tool exposure. Every MCP tool becomes an A2A skill automatically. The permission system filters what each user sees. No parallel registration system to maintain.
Standard protocol. JSON-RPC 2.0 is well-understood, well-tooled, and language-agnostic. Any HTTP client in any language can talk A2A. No Elixir dependency, no custom SDK required.
Discovery built in. The agent card at /.well-known/agent.json means clients can find and enumerate your capabilities without documentation or out-of-band coordination. Public card for strangers, authenticated card for known users.
Permission-gated by default. Every tool call goes through Permissions.* wrappers before execution. A2A does not bypass security — it rides on top of the existing access control system.
Persistence. Tasks are stored in SQLite with full history and artifacts. You can retrieve a task hours later and see exactly what was requested and what was returned. This is audit-ready.
Minimal surface area. Three methods: send, get, cancel. That is the entire task API. Simple to implement, simple to debug, simple to reason about.
Disadvantages
Synchronous execution. Our implementation executes tasks inline — the client blocks until the tool finishes. For fast tools (add numbers, get time) this is fine. For slow tools (LLM summarization, network-dependent operations) this means the HTTP connection sits open for seconds or minutes. No streaming, no webhooks for completion notification.
No rate limiting. Currently zero throttling on the A2A endpoint. An aggressive client could hammer /a2a with task requests and overload the server. Every task executes immediately — no queue, no backpressure.
Cookie-only authentication. The A2A spec supports multiple auth schemes, but we only implemented cookie auth. This works for browser-adjacent flows but is awkward for server-to-server communication where there is no browser session. Bearer tokens would be more natural for agent-to-agent scenarios.
No streaming. The agent card declares streaming: false. For long-running tasks or tasks that produce incremental output, clients must poll tasks/get rather than receiving a stream. This is a protocol-level limitation of our current implementation, not A2A itself.
Task isolation. Each A2A task is independent. There is no conversation context, no session, no way to chain tasks that share state. If a client needs a multi-step workflow (search contacts, then log interaction on one of them), each step is a separate task with no shared context.
No push notifications. The agent card declares pushNotifications: false. Clients must poll for task completion. For asynchronous workflows, this means wasted requests and latency.
Case Studies
Case 1: Cross-Agent CRM Enrichment
A content analysis agent runs on a separate server. It specializes in reading company websites and extracting key information — industry, tech stack, team size, recent news. Our CRM agent needs this enrichment for newly added companies.
With A2A: Our CRM module, after creating a new company, calls the content agent via A2AClient.send_task with the company’s website URL. The content agent returns structured data. Our CRM module writes it back to the company record. No custom integration code beyond the A2A call.
Without A2A: You build a custom HTTP integration, agree on a bespoke API schema, handle authentication differently, and maintain both sides independently. When the content agent adds a new capability, you update your integration manually.
Case 2: Delegated Task Execution
A planning agent (Claude, GPT, or custom) decides the user needs to schedule a follow-up with a contact. The planning agent does not have direct database access. It discovers our agent via A2A, finds nis_log_interaction in the skill list, and sends a task with the contact ID, interaction content, and next action date.
The A2A server checks permissions (the planning agent’s user must have NIS access), executes the tool, and returns confirmation. The planning agent reports success to the user.
Case 3: Multi-Agent Monitoring
A fleet of specialized monitoring agents each watch different systems — one for uptime, one for log analysis, one for performance metrics. A central dashboard agent needs to aggregate their findings.
Each monitoring agent exposes its capabilities via A2A. The dashboard agent discovers all of them, calls monitor_summary on each, collects the results, and presents a unified view. Adding a new monitoring agent requires zero changes to the dashboard — it discovers the new agent automatically.
Pitfalls
Do not expose everything publicly. The public agent card should contain only harmless demonstration tools. We limit it to five. Exposing data-access tools on the unauthenticated endpoint is a security hole. The authenticated card, filtered by permissions, is where real tools live.
Do not assume fast execution. If your tools can take more than a few seconds, clients will time out. Document expected latencies per tool, or better, implement async task execution where tasks/send returns immediately with state "submitted" and the client polls tasks/get.
Do not skip permission checks. It is tempting to create a “service account” with full access for agent-to-agent communication. Do not. Create dedicated users with the minimum permissions needed for each integration. If a partner agent only needs CRM read access, that is all they should get.
Watch for task accumulation. Every tasks/send creates a row in SQLite. Without cleanup, the a2a_tasks table grows indefinitely. Implement a retention policy — tasks older than 30 days, or completed tasks older than 7 days, should be pruned.
Validate incoming tool arguments. A2A clients can send arbitrary JSON in the arguments map. Every tool should validate its inputs. Our MCP tools already do this (they check required fields and types), but if you add new tools, do not assume the A2A client sends well-formed data.
Cookie expiration. If a remote agent caches a cookie and it expires, every subsequent task fails with 401. Agents need retry logic with re-authentication, or the server should support long-lived bearer tokens for service accounts.
Recommended Enhancements
1. Bearer Token Authentication
Add bearer token support alongside cookies. For server-to-server A2A calls, generate a long-lived API token tied to a user and permission set. The client sends Authorization: Bearer <token> instead of a cookie. We already have this for MCP — extending it to A2A is straightforward.
2. Async Task Execution
Decouple task submission from execution:
tasks/send → returns immediately, state: "submitted"
Background worker picks up task → state: "working"
Tool completes → state: "completed"
Client polls tasks/get → gets result
This prevents HTTP timeouts on slow tools and enables queuing with backpressure.
3. Server-Sent Events for Task Status
Add a tasks/subscribe method or an SSE endpoint:
GET /a2a/tasks/:id/stream → SSE stream of state transitions
The client subscribes once and receives events as the task progresses. No polling. We already have SSE infrastructure (SsePush module) — wiring it to A2A task state changes is natural.
4. Rate Limiting
Add per-user rate limits on tasks/send. A simple token bucket — say, 60 tasks per minute per user — prevents abuse without affecting normal usage. Return JSON-RPC error -32000 with a Retry-After hint when throttled.
5. Task Cleanup Job
Schedule an alarm (we have the Alarm module) to delete completed and failed tasks older than a configurable retention period. Keep canceled tasks longer for audit purposes.
6. Batch Operations
Add a tasks/sendBatch method that accepts an array of task requests and returns an array of results. This reduces HTTP round-trips for clients that need to invoke multiple tools in sequence.
7. Task Context / Sessions
Allow tasks to reference a session ID so that multiple tasks share context. The server stores accumulated context in the session, and each subsequent task in that session can access prior results. This enables multi-step workflows without the client managing state.
8. Authenticated Discovery
Add a POST /.well-known/agent.json or a methods/list JSON-RPC method that returns the full skill set for the authenticated user. Currently the public endpoint shows the minimal card, and authenticated users only see their full tools implicitly through tasks/send. Explicit authenticated discovery makes client integration easier.
When A2A Makes Sense
Use A2A when you have multiple independent agents that need to collaborate, each owned by different teams or running on different infrastructure. The protocol gives you a standard contract without tight coupling.
Use A2A when you want external agents to access your tools without embedding your SDK or understanding your internal architecture. Discovery plus JSON-RPC is enough.
Use A2A when you need an audit trail of cross-agent interactions. Every task is persisted with full request and response history.
Do not use A2A when a direct function call would do. If both agents run in the same BEAM node, calling the module directly is faster, simpler, and type-safe. A2A adds HTTP overhead, JSON serialization, and authentication ceremony that is unnecessary within a single system.
Do not use A2A when you need real-time streaming. The protocol supports it in theory, but our implementation does not, and the overhead of SSE subscription management for simple tool calls is not justified.
Do not use A2A when you need complex orchestration. A2A is task-level — one request, one response. For multi-step workflows with branching, retries, and rollbacks, use an orchestration layer that calls A2A tasks as individual steps.
The Bottom Line
A2A is a thin, useful protocol that solves a real problem: how do agents discover and talk to each other without custom integration per pair? Our implementation proves that bolting A2A onto an existing tool system is cheap — the server is 300 lines, the client is 150 lines, the demo is 170 lines. The permission system, tool definitions, and execution engine were already there. A2A just gave them a front door that any HTTP client can walk through.
The protocol is young and our implementation is basic. Synchronous execution, cookie-only auth, no streaming, no rate limiting. These are solvable problems, and the architecture supports adding them incrementally. The foundation — standard discovery, standard task lifecycle, standard wire format — is solid.
Build tools first. Expose them over MCP for AI assistants. Expose them over A2A for other agents. The tools are the value. The protocols are just doors.