Questa pagina non è ancora tradotta in italiano: qui la trovi in inglese.
A2A server
Let other AI agents call your .NET agent over the Agent-to-Agent (A2A) protocol: the agent card, authentication, and the actions that need confirmation.
On this page
Use this page to let other AI agents find your .NET agent and call it with the Agent-to-Agent (A2A) protocol. The A2A server publishes an agent card, a JSON document that describes your agent, at /.well-known/agent-card.json. Other agents send their requests, called tasks, to /a2a. MentorAgent runs each task as a full turn, with your model and your actions.
When to choose it
Section titled “When to choose it”Choose it when another program should send questions to your agent in natural language: another MentorAgent host, any other A2A agent, or an orchestrator (a program that splits a job among several agents).
The whole agent, not single methods. A task goes through your model, prompt, specialists, teams and documents, and costs a turn on your model. To publish single methods for a client whose own model chooses, see Model Context Protocol (MCP) server.
Answers come from tools. The caller is a program that cannot tell a looked-up figure from an invented one. So on a task:
- MentorAgent tells the model to call a tool first and to state only what a tool returned.
- If the reply uses no tool that returns data, MentorAgent discards it and asks the model again, this time requiring a tool call. A navigation, a UI action, a memory write and a MentorAgent skill do not count as such a tool.
- If the second reply returns no data either, the classifier model judges whether it states something about your records, such as a figure or that an entry does not exist. If it does, the caller gets
NOT GROUNDED: …instead.
A reply that states no data, such as a policy read from a MentorAgent skill or your documents, is returned as it is.
One reply per task. The caller gets the complete reply in one message when the turn ends, not a stream of tokens. A turn that produces no reply closes the task as failed.
No forwarding. A task that arrives over A2A is never passed on to the remote agents of this host. So two hosts can call each other without an endless loop. A chain of three MentorAgent hosts does not work: point the first host directly at the third one.
Any kind of host can serve tasks, such as a Blazor Server app or a backend. To call other A2A agents from your agent, see MCP and A2A clients.
Install the packages
Section titled “Install the packages”.NET CLI
dotnet add package Novior.MentorAgent.ServerPackageReference
<ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" Version="1.2.0" />
</ItemGroup>Central Package Management
<ItemGroup>
<PackageVersion Include="Novior.MentorAgent.Server" Version="1.2.0" />
</ItemGroup><ItemGroup>
<PackageReference Include="Novior.MentorAgent.Server" />
</ItemGroup>File-based app
#:package Novior.MentorAgent.Server@1.2.0Novior.MentorAgent.Server brings Novior.MentorAgent and the A2A SDK for ASP.NET Core (1.0.0-preview3) with it. Add it to a Blazor Server app too: you do not need the hub for A2A. Neither package includes a model provider package. The code on this page uses OpenAI, so add its package too:
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0For another provider, see Model providers.
Minimal configuration
Section titled “Minimal configuration”Keep the model key out of the source code:
dotnet user-secrets init dotnet user-secrets set OpenAI:Key <your-key>Write the actions as usual. These are the same three as on the MCP server page:
public class CaseTools(ICaseStore store, ICaseQueue queue) { // For every caller. ReadOnly: MCP clients are told that it only reads [MentorAction( Description = "Gets the status of a support case", ReadOnly = true)] public Task<string> GetCaseStatus(string caseNumber) => store.GetStatusAsync(caseNumber); // Needs a signed-in user with the role [MentorAction( Description = "Lists the open cases assigned to a team", RequiredRoles = ["Supervisors"], ReadOnly = true)] public Task<IReadOnlyList<string>> ListTeamCases(string team) => queue.ListOpenCasesAsync(team); // Needs a person to confirm it [MentorAction( Description = "Closes a support case with a resolution", RequiresConfirmation = true)] public Task CloseCase(string caseNumber, string resolution) => store.CloseAsync(caseNumber, resolution); }Turn the server on, register it and map the endpoints in
Program.cs:// AddMentorAgent, AddMentorAgentA2A, MapMentorAgentA2A using Novior.MentorAgent.Extensions; using Novior.MentorAgent.Models; // AgentCardInfo using Microsoft.Extensions.AI; // AsIChatClient using OpenAI; // OpenAIClientbuilder.Services.AddScoped<CaseTools>(); builder.Services.AddMentorAgent(options => { options.AppName = "Case management"; // the agent card's name options.AppDescription = "Support case management: case status, notes and closure"; options.ChatClient = new OpenAIClient( builder.Configuration["OpenAI:Key"] ?? throw new InvalidOperationException( "Set OpenAI:Key with dotnet user-secrets.")) .GetChatClient("gpt-4o-mini") .AsIChatClient(); options.ScanAssemblies = [typeof(Program).Assembly]; options.A2AServerEnabled = true; }); // Novior.MentorAgent.Server: registers the A2A agent // (AddMentorAgentServer() calls it too) builder.Services.AddMentorAgentA2A(); // after var app = builder.Build(); app.MapMentorAgentA2A(); // POST /a2a and GET /.well-known/agent-card.jsonAddMentorAgentA2A()registers the A2A agent whenA2AServerEnabledis on. Call it afterAddMentorAgent().AddMentorAgentServer()calls it for you. MentorAgent readsA2AServerEnabledwhenAddMentorAgentruns, so set it in that callback.
Run the app and open /.well-known/agent-card.json. The card lists A2A skills: entries that tell callers what your agent can do. An A2A skill is not a MentorAgent skill. The card has:
- your
AppNameandAppDescription; - one A2A skill for each tool name, here
get_case_status. The skills come from the actions of classes registered in dependency injection that have no role and do not ask for confirmation. If another action with the same name has a role or asks for confirmation, the name is left out; - the task endpoint, which uses JSON-RPC;
- text as input and output, and the streaming capability.
The card says that streaming is supported, but a reply still arrives as one message when the turn ends.
A caller that cancels a task, with tasks/cancel, stops its turn as Stop does: no action starts after it, and the task ends Canceled.
The card still shows only the path /a2a. An agent on another host needs the full URL: the complete configuration sets it.
Complete configuration
Section titled “Complete configuration”Set the public URL of the task endpoint in the configuration of each environment:
{
"A2A": {
"PublicUrl": "https://cases.example.com/a2a"
}
}Then describe the card, require authentication and limit each calling agent:
builder.Services.AddScoped<CaseTools>();
builder.Services.AddMentorAgent(options =>
{
options.AppName = "Case management";
options.AppDescription =
"Support case management: case status, notes and closure";
options.ChatClient = new OpenAIClient(
builder.Configuration["OpenAI:Key"]
?? throw new InvalidOperationException(
"Set OpenAI:Key with dotnet user-secrets."))
.GetChatClient("gpt-4o-mini")
.AsIChatClient();
options.ScanAssemblies = [typeof(Program).Assembly];
options.A2AServerEnabled = true;
// The public URL of the task endpoint, path included:
// the card publishes it as it is
options.A2AServerUrl = builder.Configuration["A2A:PublicUrl"];
options.AgentCard = new AgentCardInfo
{
Version = "1.2.0",
Provider = "Support operations",
DocumentationUrl = "https://cases.example.com/docs/agent",
};
// Every task is a full turn with the model: a limit per calling agent
options.RateLimitPerUser = 30;
});
// Registers the agent, and passes the authenticated caller
// of each task to MentorAgent,
// for RequiredRoles and RateLimitPerUser
builder.Services.AddMentorAgentA2A();
// Plus your authentication scheme for the calling agents:
// builder.Services.AddAuthentication()...
// after var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
// The task endpoint and the card
app.MapMentorAgentA2A(configure: e => e.RequireAuthorization());A2AServerUrlis the full public URL of the task endpoint, path included, and the card publishes it as it is. If you map the endpoint elsewhere, withMapMentorAgentA2A("/agent")orA2AServerPath, the URL must end with that path.AgentCardfills the card's version, provider, icon and documentation URL.Tagsis not published: the A2A card has no field for it.- The
configureparameter ofMapMentorAgentA2Aapplies to both endpoints: the task handler and the card. A MentorAgent caller sends its credentials in theHeadersof itsRemoteAgentsentry, with the card request and with every task. AddMentorAgentA2A()registers the agent, and passes the authenticated caller of/a2ato MentorAgent, in any host: a backend or a Blazor Server app.RequiredRolesandRateLimitPerUserthen work for the calling agent.AddMentorAgentServer()calls it for you.
Options for the A2A server
Section titled “Options for the A2A server”| Option | Why it matters for the A2A server |
|---|---|
A2AServerEnabled | Turns the agent on. AddMentorAgentA2A() then registers the task handler. Without the flag, MapMentorAgentA2A() stops the app at startup. MentorAgent reads it when AddMentorAgent runs |
AppName, AppDescription | The card's name and description: what an orchestrator chooses your agent by |
A2AServerUrl | The full public URL of the task endpoint. Without it the card has only the path, which clients on another host cannot follow |
A2AServerPath | Where the task endpoint is mapped when you pass no path. The card stays at /.well-known/agent-card.json |
AgentCard | Version, provider, icon and documentation URL on the card |
EnableImageInput | Adds image/png to the card's input modes |
RateLimitPerUser | Counted per calling agent only when the request is authenticated. The user id is the NameIdentifier claim or, if that claim is missing, sub, oid or the name. Otherwise the task is anonymous. With the default AnonymousIdentity, every task starts a new count. With Shared, all anonymous callers spend one allowance |
RequireConfirmationForWrites | On by default: a [MentorAction] that declares nothing asks for confirmation, so the card leaves it out and a task is refused. With false, only RequiresConfirmation = true asks |
ClassifierChatClient | The model for MentorAgent's own short decisions. On A2A it also judges whether a reply written without data states something about your records |
Every option is in Configuration options, and the A2A ones under A2A.
Security and roles
Section titled “Security and roles”A task comes from a program: nobody on the other side can answer a confirmation. The card and the task follow the same rules:
| Action | On the card | If a task reaches it |
|---|---|---|
A read with no roles, such as get_case_status (ReadOnly = true) | Listed as an A2A skill | Runs |
RequiredRoles, such as list_team_cases | Left out | Offered to the turn, and run, only for an authenticated caller who holds one of the roles. The endpoint must authenticate the request: otherwise the caller is anonymous, and the action is left out, and refused if called |
An action that asks for confirmation, such as close_case | Left out | Refused at once |
- Writes ask by default. Which declarations ask, and so stay off the card, is in Every declaration, on every channel. A write that declares
RequiresConfirmation = false, or a method with only[Description], is on the card and runs for every caller. Mark a method that changes data with[MentorAction]. - How MentorAgent knows the caller.
- The MCP server asks your
McpCallerPrincipal. - The A2A server reads the user of the request, in every host. A Blazor Server app is included. MentorAgent reads that user before it asks Blazor's own
AuthenticationStateProvider, which works only inside a circuit. - To add claims or roles for a task, see Secure the endpoints.
- A task whose caller is not authenticated is anonymous, and its role-gated actions are refused.
- The MCP server asks your
- The endpoints are open until you protect them.
/a2aruns a paid turn for whoever reaches it, and the card describes your application to anyone. Secure the endpoints protects them together with the others. - Access is decided here, not by the caller. A MentorAgent host that lists your agent in its
RemoteAgentsdoes not check roles before it sends you a request: any of its users can reach you. What a task may do depends on the authentication on/a2aand on the roles of your actions. - Each task starts with no history. A task runs in its own dependency injection scope, a set of services created for that task only. It has a new coordinator and a new conversation: nothing carries over from earlier tasks. It has no browser, no current page and no UI actions.
- Per-record checks are your code, in your methods or in an action policy, as on every channel: see What stays in your code.
How the other channels handle confirmations is in Where nobody can answer.
Common errors
Section titled “Common errors”- The app stops at startup with "Cannot map A2A endpoint: A2AServerEnabled is false". Set
options.A2AServerEnabled = trueinAddMentorAgent, or remove the call. - The app stops at startup with "Cannot map the A2A endpoint: the A2A server is not registered". Nothing registered the agent. Call
AddMentorAgentA2A()afterAddMentorAgent(), or callAddMentorAgentServer(). - The app stops at startup with "A2AServerEnabled is true, but AddMentorAgentA2A() ran before AddMentorAgent()". Call
AddMentorAgentA2A()afterAddMentorAgent(). - The app stops at startup with "A2AServerEnabled is true, but it was false when AddMentorAgent() read it". Set
A2AServerEnabledin theAddMentorAgentcallback. A laterConfigure<MentorOptions>or a configuration binding comes too late. See Troubleshooting. - The build does not find
AddMentorAgentA2AorMapMentorAgentA2A. Both methods are in theNovior.MentorAgent.Serverpackage. Add it to the project. - A calling agent fails before it sends anything, or sends tasks to the wrong address.
A2AServerUrlis not set, or it does not include the path of the endpoint. The card then carries only the path. A MentorAgent caller resolves the path against your host, but other clients, such as the A2A SDK, cannot. On Windows they fail withUriFormatException. On Linux and macOS they read the path as afile:URI, which an HTTP client refuses. - The card lists no A2A skills. There are four possible causes:
- Every action has roles or asks for confirmation. A
[MentorAction]that declares nothing asks by default: see Every declaration, on every channel. - The class has
[MentorAgent], or it is the toolbox of a team member. - The assembly of the class is not in
ScanAssemblies. - The class is not registered in dependency injection.
- Every action has roles or asks for confirmation. A
- The caller receives "NOT GROUNDED: …". Two attempts ended without a tool that returns data, and the reply stated something about your records. Give the agent an action that returns that data.
- The task fails with "this agent could not produce a reply to the request". The turn produced no text. The cause is in your log, just before the warning about the task.
- A role-gated action is refused for a caller that holds the role. MentorAgent sees an anonymous caller. Require authentication on the endpoint with
configure: e => e.RequireAuthorization(), callUseAuthentication()andUseAuthorization()before the map call, and check that the roles are in the claim that the identity names as itsRoleClaimType. - The caller gets
401on the card.configureprotects the card as well as the task endpoint: the caller must send its credentials for both.
Vedi anche
Sezione “Vedi anche”- Canali: come i client arrivano all'agente — Scegli come i client raggiungono il tuo agente: il widget della chat, SignalR o HTTP dal tuo front end, o un protocollo per altri agenti e strumenti AI.
- MCP server — Expose the methods of your .NET app as MCP tools for Claude Desktop, VS Code and other MCP clients, with authentication, role checks and writes withheld.
- Secure the endpoints — Protect the SignalR hub, Server-Sent Events, /mcp and /a2a endpoints with authentication and CORS before you expose them.
- Configuration options — Every MentorOptions property with its type, default and effect, grouped by area, plus the options of the WebAssembly client and the YAML loader.
