Your team ships a .NET agent that handles invoices. Another team — different language, different framework, maybe a different company — ships an agent that handles procurement. Now someone asks the obvious question: can they talk to each other? Until recently the honest answer was “only if both teams agree on a custom API.” The A2A (Agent2Agent) protocol exists to make the answer “yes, out of the box.”
A2A has moved fast. Google announced it in April 2025, donated it to the Linux Foundation in June 2025, and the v1.0 specification landed in 2026 with backing from over 150 organizations — Microsoft, Google, AWS, Salesforce, SAP, ServiceNow, and IBM among them. For .NET developers the story is unusually good: there is an official C#/.NET SDK (a2aproject/a2a-dotnet), and the Microsoft Agent Framework ships first-class hosting support so an agent you already built can speak A2A with a couple of lines. This article covers what the protocol actually is, how it relates to MCP, and how to use it from C#.
What is the A2A protocol?
A2A is an open standard for agent-to-agent interoperability: a wire protocol that lets one AI agent discover another, send it work, and receive results — regardless of which framework, language, or vendor either agent was built with. Where a REST API exposes endpoints you must read docs to understand, an A2A agent describes itself in a machine-readable way and accepts work through a small, uniform set of operations.
The protocol is deliberately unglamorous plumbing: JSON payloads over HTTPS. The v1.0 spec defines three transport bindings — JSON-RPC 2.0 (the primary one), gRPC, and plain HTTP+JSON/REST — with Server-Sent Events for streaming. If you can host an ASP.NET Core endpoint, you can host an A2A agent.
Three concepts do almost all the work:
Agent cards
An agent card is a JSON document an agent publishes at a well-known discovery URL. It declares the agent’s name, description, version, endpoint URL, supported transports, capabilities (such as streaming), and skills — human- and machine-readable descriptions of what the agent can do. A client fetches the card first, decides whether this agent fits the job, and then knows exactly how to call it. Think of it as OpenAPI’s swagger.json, but for an agent rather than a REST surface.
Tasks
A task is the unit of work. Long-running, multi-turn work is the norm for agents — “reconcile these invoices” is not a 200-OK-in-50ms request — so A2A models a task as a stateful object with an explicit lifecycle. The v1.0 spec defines seven states: submitted, working, input-required (the remote agent needs more information from you), completed, failed, canceled, and rejected. Completed tasks carry typed artifacts — the actual outputs. Clients can poll a task, subscribe to streamed updates, or register for push notifications.
Messages and parts
Within a task (or standalone, for quick exchanges), agents trade messages. Each message has a role (user or agent), an ID, an optional contextId that groups messages into a conversation, and a list of parts — text, files, or structured data. If you have used chat-completion APIs, this will feel familiar; the difference is that both sides of the conversation can be autonomous agents.
A2A vs MCP: complementary, not competing
The most common point of confusion is how A2A relates to the Model Context Protocol. The short version: MCP connects an agent to its tools; A2A connects an agent to other agents. They solve different problems and a serious system will often use both.
MCP standardizes how a single agent reaches capabilities — databases, filesystems, APIs — through servers that expose tools, resources, and prompts. The agent is in charge; the tool executes a well-defined function and returns. (If you’re new to MCP on .NET, start with the MCP guide for the Agent Framework or build an MCP server in C#.)
A2A standardizes how peers collaborate. The remote party is not a deterministic function — it is another agent with its own model, its own tools, and its own opinion about how to do the job. You hand it a goal, not a function signature, and it may come back mid-task saying input-required.
| MCP | A2A | |
|---|---|---|
| Connects | Agent ↔ tools & data | Agent ↔ agent |
| Remote party is | A deterministic capability (tool, resource) | An autonomous peer with its own reasoning |
| You send | A tool call with typed arguments | A goal as messages; work tracked as a task |
| Interaction shape | Request → result | Multi-turn, long-running, may pause for input |
| Discovery | Tool listings from a server | Agent cards at a well-known URL |
| Typical boundary | Your agent and a capability it uses | Two teams, departments, or companies |
| Transport | JSON-RPC over stdio or HTTP | JSON-RPC / gRPC / REST over HTTPS, SSE streaming |
A useful mental model: MCP is vertical integration (an agent reaching down into capabilities), A2A is horizontal integration (agents reaching across to each other). The same instinct from function calling vs MCP applies one level up: use the simplest mechanism that crosses the boundary you actually have. Tools in your own process → C# methods. Shared capabilities → MCP. Autonomous systems owned by someone else → A2A.
The A2A .NET SDK
The official C# implementation lives at a2aproject/a2a-dotnet and ships as two NuGet packages: A2A (core protocol, client and server abstractions) and A2A.AspNetCore (hosting extensions). It targets .NET 8+, implements the v1.0 spec, and supports the JSON-RPC and HTTP+JSON bindings including SSE streaming. It is still marked preview, so expect the API surface to move.
Calling a remote agent
The client flow is two steps: resolve the card, then talk. From the SDK samples:
using A2A;
// 1. Discover the remote agent via its card
A2ACardResolver cardResolver = new(new Uri("http://localhost:5000/"));
AgentCard agentCard = await cardResolver.GetAgentCardAsync();
Console.WriteLine($"Found: {agentCard.Name} — {agentCard.Description}");
// 2. Create a client and send a message
A2AClient client = new(new Uri(agentCard.Url));
Message userMessage = new()
{
Role = MessageRole.User,
MessageId = Guid.NewGuid().ToString(),
Parts = [new TextPart { Text = "Reconcile invoice INV-1042 against PO-77." }]
};
Message reply = (Message)await client.SendMessageAsync(
new MessageSendParams { Message = userMessage });
Console.WriteLine(reply.Parts.OfType<TextPart>().First().Text);
Streaming uses the same shape with SendMessageStreamAsync, which yields Server-Sent Events as the remote agent works — exactly what you want for a long-running task feeding a UI.
Hosting an agent
On the server side, you attach your agent logic to a TaskManager — handlers for incoming messages and for agent-card queries — and map it into ASP.NET Core:
using A2A;
using A2A.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var agent = new InvoiceAgent(); // your logic
var taskManager = new TaskManager();
agent.Attach(taskManager); // wires OnMessageReceived, OnAgentCardQuery
app.MapA2A(taskManager, "/agent"); // JSON-RPC endpoint + protocol compliance
await app.RunAsync();
MapA2A handles discovery, message routing, and task lifecycle; the SDK also provides MapWellKnownAgentCard for the standard card endpoint and an InMemoryTaskStore for development. Your OnMessageReceived handler receives MessageSendParams, does the work (usually by invoking an LLM-backed agent), and returns a Message — or creates a task for anything long-running.
A2A in the Microsoft Agent Framework
If you build on the Microsoft Agent Framework, you don’t wire TaskManager handlers yourself. The Microsoft.Agents.AI.Hosting.A2A.AspNetCore package exposes any AIAgent over A2A directly — the agent you already wrote becomes a protocol-compliant peer:
using Microsoft.Agents.AI.Hosting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton(chatClient); // your IChatClient
// The agent you'd have built anyway
var invoiceAgent = builder.AddAIAgent(
"invoice",
instructions: "You reconcile invoices against purchase orders and flag mismatches.");
var app = builder.Build();
// One call: agent card + A2A endpoints
app.MapA2A(invoiceAgent, path: "/a2a/invoice", agentCard: new()
{
Name = "Invoice Agent",
Description = "Reconciles invoices against purchase orders.",
Version = "1.0"
});
app.Run();
That gives you GET /a2a/invoice/v1/card for discovery and POST /a2a/invoice/v1/message:stream for conversation, with contextId maintaining multi-turn history per conversation. You can map multiple agents in one app (/a2a/invoice, /a2a/audit, …) as long as paths don’t collide. The framework’s Python and Go flavors have the mirror-image consumption side too — wrap a remote A2A endpoint as a local agent, or even hand remote A2A agents to a host agent as tools, which is a very natural pattern for orchestrators.
A practical scenario: two teams, one workflow
Concretely, imagine a finance department at a company like the ones I’ve spent my career in:
- Team A (your team) owns the Invoice Agent — .NET 8, Agent Framework, tools that hit your ERP via MCP. It is exposed over A2A as above.
- Team B owns a Procurement Agent — Python, LangChain, its own data. You never see their code. What you see is their agent card.
Your invoice workflow needs procurement context (“was this PO amended?”). Instead of asking Team B for a bespoke REST API, your agent consumes theirs over A2A. Conceptually (exact helper names vary by SDK version, so treat this as a map, not a contract):
// Conceptual: consuming a peer agent over A2A from your .NET agent
var resolver = new A2ACardResolver(new Uri("https://procurement.internal.example.com/"));
var card = await resolver.GetAgentCardAsync();
var procurement = new A2AClient(new Uri(card.Url));
var question = new Message
{
Role = MessageRole.User,
MessageId = Guid.NewGuid().ToString(),
ContextId = conversationId, // ties follow-ups together
Parts = [new TextPart { Text = "Was PO-77 amended after 1 June? Summarize changes." }]
};
var answer = (Message)await procurement.SendMessageAsync(
new MessageSendParams { Message = question });
// Feed the answer into your invoice agent's reasoning as context or a tool result
Notice what didn’t happen: no shared library, no agreed DTOs, no coordination meeting about API versioning. Team B can rewrite their agent in Go next quarter; as long as the card and the protocol hold, your side doesn’t change. If their agent needs clarification mid-task, the input-required state surfaces that cleanly instead of failing. And inside your own service, this remote agent slots into a multi-agent workflow like any other node — local agents and remote A2A peers composing in one graph.
Note: The A2A .NET SDK and the Agent Framework hosting packages are preview software atop a young spec. The snippets above follow the official samples as of July 2026, but verify against the exact package versions you install. The concepts — cards, tasks, messages, parts — are the stable part.
What to watch before production
A2A widens your attack surface in a specific way: you are now accepting goals from autonomous software you don’t control. Three things deserve early attention:
- Authentication and authorization. Agent cards declare auth requirements, and the spec supports standard HTTP schemes — but you must decide which peers may invoke which skills, and treat inbound messages as untrusted input (prompt injection travels over A2A just fine). The hardening checklist in securing AI agents in .NET applies double at an agent-to-agent boundary.
- Observability. A task that hops across two organizations’ agents is miserable to debug without correlation. Propagate context and trace both sides — the OpenTelemetry setup for .NET agents extends naturally to A2A endpoints since they are ordinary ASP.NET Core routes.
- Task persistence.
InMemoryTaskStoreis for demos. Real long-running tasks need durable storage so a pod restart doesn’t orphan work.
Takeaway
A2A fills the gap MCP deliberately left: MCP standardizes an agent’s reach down into tools and data, A2A standardizes its reach across to other agents — discovery via agent cards, work via tasks with a real lifecycle, conversation via messages and parts, all over boring HTTPS. On .NET the on-ramp is short: the A2A and A2A.AspNetCore packages give you protocol-level clients and servers, and the Microsoft Agent Framework’s MapA2A turns an agent you already have into an interoperable peer in one call. If your agent will only ever talk to its own tools, you don’t need A2A. The moment a second team’s agent enters the picture, you do — and it’s worth learning the three core concepts now, while the ecosystem is settling around v1.0.
Sources and further reading: the A2A protocol specification, the A2A .NET SDK on GitHub, the Agent Framework A2A integration docs, and the A2A .NET SDK announcement.
Have a correction or a topic you want covered? Email mani.bc72@gmail.com.