Skip to content
Kumar Chandrachooda
.NET

An Agent Card for Your API

FastEndpoints.A2A publishes an agent card at a well-known URL and answers agent-to-agent calls through a JSON-RPC 2.0 dispatcher written entirely by hand - implementing exactly the slice of the A2A protocol it can do honestly. The final missing chapter, and the close of both series. Part 12 of FastEndpoints — The Missing Chapters.

By Kumar Chandrachooda 05 Jul 2026 5 min read
A well-known card, a JSON-RPC door, and only the promises it can keep

Last week's chapter ended with FastEndpoints teaching its endpoints to be MCP tools. Its sibling package answers a different protocol: A2A, the agent-to-agent standard, where your service publishes an agent card - a machine-readable manifest of skills at a well-known URL - and other agents call those skills over JSON-RPC. FastEndpoints.A2A is the last uncovered package in the repository, and it makes a fitting finale, because it is the clearest expression of a design ethic this series has been circling for twenty-nine parts: implement exactly what you can do honestly, and make the boundary loud.

The shape

bld.Services.AddFastEndpoints().AddA2A(o =>
{
    o.AgentName = "orders-agent";
    o.Description = "Order management skills for the storefront.";
    o.Version = "1.4.0";
});
var app = bld.Build();
app.UseFastEndpoints().UseA2A();   // /a2a + /.well-known/agent-card.json

Endpoints opt in exactly as they did for MCP - this.A2ASkill("order_status", tags: ["orders"]) in Configure(), or an [A2ASkill] class attribute - and the metadata rides the same public EndpointDefinition bag, the same zero-core-changes seam. Underneath, the identical compiled-in shared plumbing does the work: the EndpointInvoker running the real pipeline on a synthetic HttpContext, the argument builder, the FluentValidation schema enrichment, the deny-anonymous-by-default visibility filter evaluated per caller. One engine, two protocol faces.

The A2A face has two doors. GET /.well-known/agent-card.json returns the card - name, version, provider, and one skill entry per visible endpoint, addressed to a single JSON-RPC interface. Visibility is caller-specific: the card is built per request against SkillVisibilityFilter, so an anonymous crawler and an authenticated partner agent see different skill lists from the same URL. And POST /a2a takes the calls.

A JSON-RPC dispatcher, artisanal

Here is the surprise in the csproj: unlike the MCP package, which builds on Microsoft's official SDK, A2A has no protocol dependency at all. The JSON-RPC 2.0 front door is written by hand - parse the envelope, validate jsonrpc: "2.0", method, id kind - and reading it is a catalog of small correctness decisions most hand-rolled RPC endpoints skip:

  • Batch requests are rejected outright (HTTP 400, InvalidRequest) rather than half-supported.
  • Notifications - requests without an id - are honoured per spec: dispatched fire-and-forget, answered with 204 No Content, errors swallowed, because a notification's contract is precisely “no response.”
  • Version negotiation runs before execution. An A2A-Version header (or query param) other than 1.0 returns error -32009 without invoking anything - the integration tests assert the endpoint's execution count stays at zero.
  • The response envelope omits error on success and result on failure - never both, never empty siblings.
  • The error codes are a proper centralized catalog: -32700 parse, -32600 invalid request, -32601 method/skill not found, -32602 invalid params, -32603 internal, plus A2A-range codes for endpoint HTTP errors (-32000), unknown tasks (-32001), unsupported content types (-32005) and version mismatch (-32009).

Only one method exists: SendMessage. The message validator enforces the A2A grammar - messageId required, role must be the user role, at least one part, and each part exactly one of text/data/raw/url - then checks that the caller's accepted output modes intersect the skill's declared ones. A data part becomes the endpoint's arguments verbatim; a text part must parse as JSON. On the way out, a JSON response becomes a data part in an agent-role message (fresh messageId, contextId echoed or minted), anything else a text part. Endpoint failures map with the same discipline as MCP: HTTP errors carry status and body under -32000, validation failures list property/error/code under -32602, and unhandled exceptions log server-side but cross the wire as a generic internal error. Nothing leaks.

Two security behaviours deserve exact statement. A hidden skill invoked by id returns MethodNotFound - byte-identical to a skill that never existed, so probing reveals nothing. And if two visible skills collide on an id, dispatch rejects the call before executing either - a shadowed skill can never run by accident.

Advertised: false

My favourite property of this package is what it refuses to pretend. The A2A protocol includes streaming, push notifications, and long-running tasks. The agent card's capability flags for streaming and push are hardcoded false. Any message carrying a taskId gets -32001 before execution - tasks are not implemented, so the package says so at the protocol level rather than accepting and dropping them. The AgentCard doc comment states the policy in one line: only the fields FastEndpoints can synthesise from opt-in endpoints are populated. A 1.0.0-beta.3 package that implements 40% of a protocol and advertises exactly that 40% is rarer than it should be. Compare the alternative you have met in the wild: the endpoint that accepts every field and honours some.

Closing both arcs

Twenty-nine parts ago, the parent series opened with a bet: that one class per endpoint was the right unit of organisation, and that this library's source was worth reading end to end. Seventeen parts covered the spine - discovery, the IResult trick, binding, validation, security, messaging, jobs, testing, generators. Twelve more covered what the first pass missed - mappers, the response surface, antiforgery, the static config and resolver that power everything, health probes, OData, the native OpenAPI stack, client generation, command rules, and the two agent faces.

Read as one corpus, the repository tells a directional story. Reflection is migrating to Roslyn (generators, AOT-safe packages, build-time document export). NSwag is migrating to the platform's native OpenAPI. And the assumed caller is migrating from a developer with a generated client toward a machine that reads schemas - MCP tools, A2A skills, cards at well-known URLs. Through all of it, the library's character holds steady: startup crashes over silent misconfiguration, singletons and statics bought consciously for speed, seams left deliberately public for the next integration, and comments honest enough to admit uncertainty.

The verdict hasn't moved since the parent retrospective: FastEndpoints will not fit every project, and its sharp edges are real - they recurred in half these chapters. But if these twenty-nine parts have a single takeaway, it is not about this library at all. It is that reading a well-built codebase beats any course: every chapter here - range requests, CSRF, JSON-RPC, IStartupFilters, MSBuild targets, three-valued attribute logic - taught general ASP.NET Core and protocol engineering through one repository's concrete choices. Find the library your stack leans on hardest, and read it. The missing chapters are always in the source.