How to Implement Agent-to-Agent (A2A) Protocol in ASP.NET Core 10 Using C# – Complete Guide (2026)

A help-desk agent may need to delegate a request to another agent hosted by a different team. It needs a way to discover the remote agent’s capabilities, choose a compatible binding, and handle work that might finish later.

This tutorial builds an ASP.NET Core 10 Agent2Agent endpoint with the official preview .NET SDK. The example serves a discoverable Agent Card and responds to a readiness message. It also explains how a client should handle asynchronous task responses in a service that supports them.

How is A2A different from MCP?

A caller reads the Agent Card, chooses a supported binding, sends a message, and handles either an immediate message or a task.
A caller reads the Agent Card, chooses a supported binding, sends a message, and handles either an immediate message or a task.
Protocol Typical boundary Example
A2A Agent to independently hosted agent Delegate a status inquiry or longer business task
MCP Host or agent to tools, resources and prompts Invoke a calculator or discover document resources

An A2A Agent Card describes skills, supported interfaces and capabilities. It is discovery metadata. Treat it as untrusted until you verify the source and destination. Do not accept a user-provided URL and immediately fetch its card from a server with privileged network access. The agent interface advertises both protocol version and binding. Match them to your client instead of assuming every endpoint speaks the same JSON-RPC shape. See the A2A specification and ASP.NET PB MCP server guide.

Build a discoverable agent

Install .NET 10 and place these two files in an empty directory. The code pins the A2A .NET preview package and uses the version 1.0 interface with JSON-RPC. Preview libraries may introduce breaking changes, so keep an interoperability test in CI and pin both peers before deployment.

Lab.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup><TargetFramework>net10.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup>
  <ItemGroup><PackageReference Include="A2A.AspNetCore" Version="1.0.0-preview4" /></ItemGroup>
</Project>
Program.cs
using A2A;
using A2A.AspNetCore;

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.ConfigureKestrel(o => o.Limits.MaxRequestBodySize = 16 * 1024);
builder.Services.AddA2AAgent<StatusAgent>(StatusAgent.Card);
var app = builder.Build();
app.Use(async (ctx, next) =>
{
    if (ctx.Request.Host.Host != "127.0.0.1" || ctx.Request.Headers.ContainsKey("Origin"))
    { ctx.Response.StatusCode = 403; return; }
    await next(ctx);
});
app.MapA2A("/status");
app.MapWellKnownAgentCard(app.Services.GetRequiredService<AgentCard>());
await app.RunAsync();

public sealed class StatusAgent : IAgentHandler
{
    public Task ExecuteAsync(RequestContext context, AgentEventQueue queue, CancellationToken ct)
    {
        var reply = new MessageResponder(queue, context.ContextId);
        return reply.ReplyAsync(context.UserText?.Trim() == "status"
            ? "Ready" : "Send status to check readiness.", null, ct).AsTask();
    }

    public static AgentCard Card => new()
    {
        Name = "Status Agent", Description = "Public demonstration status",
        Version = "1.0.0", DefaultInputModes = ["text/plain"],
        DefaultOutputModes = ["text/plain"],
        SupportedInterfaces = [new AgentInterface
        {
            Url = "http://127.0.0.1:5080/status", ProtocolBinding = "JSONRPC",
            ProtocolVersion = "1.0"
        }],
        Capabilities = new AgentCapabilities { Streaming = false },
        Skills = [new AgentSkill { Id = "status", Name = "Status",
            Description = "Returns a fixed readiness message", Tags = ["status"] }]
    };
}

The card declares a fixed local endpoint, one plain-text skill, and no streaming. The handler uses MessageResponder for a simple immediate reply. It does not create a background task. Do not advertise task streaming, push notifications, or a security scheme unless the deployed endpoint actually implements them. The loopback check and lack of a state-changing tool make this a small local demonstration. See the official A2A .NET SDK.

Send an A2A message

Run the agent and inspect the card
dotnet run --no-launch-profile -- --urls http://127.0.0.1:5080
curl -sS http://127.0.0.1:5080/.well-known/agent-card.json

Send a JSON-RPC request to the URL advertised by the card. This example uses a caller-generated message ID and a separate JSON-RPC request ID. The latter correlates the wire response. The former identifies the application message.

Invoke SendMessage
curl -sS http://127.0.0.1:5080/status \
  -H 'Content-Type: application/json' -H 'A2A-Version: 1.0' \
  -d '{"jsonrpc":"2.0","id":1,"method":"SendMessage","params":{"message":{"messageId":"status-1","role":"ROLE_USER","parts":[{"text":"status"}]}}}'

Expected behavior is an immediate agent message whose text is Ready. Verify the card URL, response shape and message role in a real client. Test invalid binding and unknown methods separately from an application-level negative reply.

The C# project compiled and a local HTTP test returned the Agent Card and an immediate ROLE_AGENT message containing Ready. Cross-SDK interoperability and authenticated remote deployment were not tested.

How do task lifecycles change the client?

A production agent can return a task instead of an immediate message when work needs tracking. Persist its task ID, caller and current state. The client should handle working states, input-required states and terminal states explicitly. It can use the binding’s task retrieval operation and use cancellation when the deployed agent supports it. Cancellation is not a transaction rollback. A task may complete before a cancellation arrives.

For restarts and multiple replicas, use a durable task store. Apply authorization to reads, cancel operations, subscriptions and artifact access independently. Never let a caller access a task merely by knowing its ID. Use a stable idempotency policy for a retried request that may create a second task. If a task asks for user input, distinguish the next input message from an unrelated new task.

Secure a remote service

Serve HTTPS, validate access tokens at ingress and in the application as required by your deployment, and scope calls to the intended audience and skill. Publish accurate security requirements in the card. Use per-caller quotas, validated destination allowlists, and timeouts for outbound agent calls. Treat artifacts as untrusted. If a remote agent sends links, check them before fetching and enforce your outbound network restrictions.

Symptom First check
Card loads but request fails Advertised URL and binding
Old client cannot parse the card Protocol version and compatibility adapter
Unknown task Store durability and authorization scope
A late reply overwrites a newer state Task version and terminal-state handling
Browser call receives 403 This lab rejects browser Origin headers

FAQ and references

Is A2A tied to one model provider? No. It defines an interoperability boundary. A server can use any compatible internal implementation or a deterministic handler.

Can the card replace authentication? No. It describes requirements. The server still verifies credentials and access for every operation.

Should every agent return a task? An immediate message is sufficient for quick responses. Use tasks when status, follow-up input, or cancellation is part of the contract.

Start with one deterministic skill and verify discovery plus invocation before adding model output, durable tasks, or federation.

Comments