# Attributes

> The parameters of MentorAction, MentorAgent, MentorTeam, TeamMember, MentorPage and MentorSkill, with their defaults and rules.

- URL: https://docs.mentoragent.net/reference/attributes
- Verified with: MentorAgent 1.0.0
- Last updated: 2026-10-13
- Packages: Novior.MentorAgent

Attributes on your own classes and methods tell MentorAgent what the agent may do. MentorAgent finds them in the assemblies listed in [`ScanAssemblies`](https://docs.mentoragent.net/reference/options#scanassemblies). Register every class whose methods become tools in dependency injection (DI). If the container cannot create a class, MentorAgent leaves its methods out and logs a warning.

## At a glance

| Attribute | Applies to | Namespace | What it declares |
|---|---|---|---|
| `[MentorAction]` | `Method` | `Novior.MentorAgent.Attributes` | A tool the agent can call |
| `[MentorAgent]` | `Class` | `Novior.MentorAgent.Attributes` | A specialist agent the coordinator hands requests to |
| `[MentorTeam]` | `Class` | `Novior.MentorAgent.Attributes` | A team of agents that discuss a request in a group chat |
| `[TeamMember]` | `Property` | global namespace | A member of a team |
| `[TeamTerminationCondition]` | `Method` | global namespace | The rule that ends a team's discussion |
| `[MentorPage]` | `Class` | `Novior.MentorAgent.Attributes` | A page the agent can open |
| `[MentorSkill]` | `Class` | `Novior.MentorAgent.Attributes` | A skill whose instructions the model loads when it needs them |

`[TeamMember]` and `[TeamTerminationCondition]` are in the global namespace, so they need no `using`. [Which level to use](https://docs.mentoragent.net/concepts/agent-levels#which-level-to-use) explains when to use actions, specialists or teams.

## `[MentorAction]`

Put it on a public method of a service you register in DI: the method becomes a tool the agent can call. A method with only `[Description]` from `System.ComponentModel` is an action too, with no rules: no roles and no confirmation. The default confirmation of [`RequireConfirmationForWrites`](https://docs.mentoragent.net/reference/options#requireconfirmationforwrites) covers `[MentorAction]` methods only.

```csharp title="CaseService.cs"
public class CaseService(ICaseStore store)
{
    // Only reads: it never asks for confirmation
    [MentorAction(Description = "Gets the status of a support case", ReadOnly = true)]
    public Task<string> GetStatusAsync(string caseNumber)
        => store.GetStatusAsync(caseNumber);

    [MentorAction(
        Description = "Closes a support case with a resolution",
        Category = "Cases",
        RequiresConfirmation = true,
        RequiredRoles = ["Supervisors", "Admins"],
        ProactiveHint = "Suggest checking the case status first",
        NavigateTo = "/cases")]
    public Task CloseCaseAsync(string caseNumber, string resolution)
        => store.CloseAsync(caseNumber, resolution);
}
```

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Description` | `string` | Required | The tool's description: what the model reads to decide when to call it |
| `Category` | `string?` | `null` | Used only on the A2A [agent card](https://docs.mentoragent.net/reference/glossary#agent-card), where an action is listed as an A2A skill: `Category` becomes that skill's tag. MentorAgent does not group actions by it |
| `RequiresConfirmation` | `bool` | `false` | Shows the confirmation box before the method runs. Write `true` on an action that changes data or cannot be undone, and `false` on a write that must run without asking. Left out, the action follows [`RequireConfirmationForWrites`](https://docs.mentoragent.net/reference/options#requireconfirmationforwrites): it asks unless it is `ReadOnly`. [Tool filtering](https://docs.mentoragent.net/reference/options#enabletoolfiltering) reads it: an action without confirmation can reach the model on any message, and one with confirmation never travels alone |
| `ReadOnly` | `bool` | `false` | Declares that the action only reads: it changes no data. MentorAgent takes your word for it and does not check it. The MCP server publishes the action with `readOnlyHint`: some MCP clients skip their own confirmation for such a tool, so declare it only on an action that really only reads. [`RequireLookupForRecordQuestions`](https://docs.mentoragent.net/reference/options#requirelookupforrecordquestions) can require the agent to call it. Ignored when the action also needs confirmation. For a method that has only `[Description]`, use [`IsReadOnlyAction`](https://docs.mentoragent.net/reference/options#isreadonlyaction) |
| `RequiredRoles` | `string[]` | empty | The roles allowed to run the action: any one of them is enough. The user must be signed in. On the hub, on `/mentor/chat` and over A2A, the user is the caller of the request. In a Blazor circuit, it is the user that the `AuthenticationStateProvider` of the host reports. Empty means no role check, for anonymous users too |
| `ProactiveHint` | `string?` | `null` | Guidance for the model, appended to the tool's description as `Guidance: <hint>` for the coordinator, the specialists and the team members. It is not added on the MCP server, and tool filtering ignores it |
| `NavigateTo` | `string?` | `null` | A URL the agent opens after the method succeeds |

- **Tool names.** The tool name comes from the method name: PascalCase becomes snake case (lowercase words joined by underscores), and a trailing `Async` is dropped first. `CloseCaseAsync` is `close_case`. Options that name tools, such as [`RequiresApproval`](https://docs.mentoragent.net/reference/options#requiresapproval), use this name.
- **Roles on a server.** On the SignalR hub, on `/mentor/chat` and over A2A, MentorAgent reads the user from the request. `AddMentorAgentServer()` (or `AddMentorAgentA2A()` for A2A alone) gives it the caller, whatever the order of `AddRazorComponents()` and `AddMentorAgentServer()` in a Blazor Web App. In a Blazor [circuit](https://docs.mentoragent.net/reference/glossary#circuit), it asks the `AuthenticationStateProvider` of the host. A claim that only your own provider adds is not seen on the hub, on `/mentor/chat` or over A2A: add it with an `IClaimsTransformation`, which ASP.NET Core applies to `HttpContext.User` and so to the user of the hub connection.
- **Which methods become tools.** MentorAgent finds the public instance methods declared on the class itself that have `[MentorAction]` or a `[Description]` with a text. It ignores inherited methods, static methods and an empty `[Description]`. The same rules apply to a specialist and to the `Tools` of a team member.
- **Every level.** The same checks apply when a specialist or a team member calls the method. See [Roles, confirmation and governance](https://docs.mentoragent.net/concepts/governance#two-checks-in-this-order).

## `[MentorAgent]`

Put it on a class to make it a specialist: its `[Description]` and `[MentorAction]` methods become the specialist's tools, and the coordinator hands it the requests it can handle. Register the class in DI. Specialists need [`ChatClient`](https://docs.mentoragent.net/reference/options#chatclient). Implementing `IMentorAgent` is optional: MentorAgent never reads it.

```csharp title="CaseAgent.cs"
[MentorAgent(
    Name = "CaseAgent",
    Description = "Handles support cases: status, history and closing",
    HandoffTo = ["BillingAgent"])]
public class CaseAgent(ICaseStore store) : IMentorAgent
{
    [Description("Gets the status of a support case")]
    public Task<string> GetCaseStatusAsync(string caseNumber)
        => store.GetStatusAsync(caseNumber);
}
```

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Name` | `string` | Required | The agent's name: the key in the handoff workflow and in the coordinator's prompt |
| `Description` | `string` | Required | What the agent can do. The coordinator reads it to decide when to delegate |
| `HandoffTo` | `string[]` | empty | The names of other `[MentorAgent]` agents it can hand off to, matched regardless of case. A name that matches no agent logs a warning, and that handoff is skipped |
| `Instructions` | `string?` | `null` | The agent's system prompt, used exactly as written. With `null`, MentorAgent writes one from `Name` and `Description`, including the grounding rule: state only facts a tool returned. A custom prompt replaces that rule, so include your own |

## `[MentorTeam]`

Put it on a class to define a group chat: the members discuss the request, then the coordinator answers with the result. The team is a tool of the coordinator, named after the team: `EscalationReview` becomes `escalation_review`. Teams need [`ChatClient`](https://docs.mentoragent.net/reference/options#chatclient). Implementing `IMentorTeam` is optional.

```csharp title="EscalationReviewTeam.cs"
[MentorTeam(
    Name = "EscalationReview",
    Description = "Reviews a request to escalate a support case",
    TriggerOn = ["escalation review"],
    MaxIterations = 6,
    HandoffTo = ["CaseAgent"])]
public class EscalationReviewTeam : IMentorTeam
{
    [TeamMember(
        Role = "Analyst",
        Instructions = "Summarize the case history and propose a decision.",
        Tools = [typeof(CaseReports)])]
    public object? Analyst { get; set; }

    [TeamMember(
        Role = "Approver",
        Instructions = "Review the proposal and approve or reject it, with reasons.")]
    public object? Approver { get; set; }

    [TeamTerminationCondition]
    public bool ShouldTerminate(string lastMessage, string lastSpeaker)
        => lastSpeaker == "Approver"
           && (lastMessage.Contains("[TEAM:APPROVED]", StringComparison.Ordinal)
               || lastMessage.Contains("[TEAM:REJECTED]", StringComparison.Ordinal));
}
```

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Name` | `string` | Required | The team's name, and the name of its tool |
| `Description` | `string` | Required | What the team is for. The coordinator reads it to decide when to use the team |
| `TriggerOn` | `string[]` | empty | Keywords added to the coordinator's prompt as an "always delegate to this team" directive. The model still decides: choose specific words, because a broad one sends ordinary requests through the group chat |
| `MaxIterations` | `int` | `10` | The maximum number of times the members speak before the discussion is stopped |
| `HandoffTo` | `string[]` | empty | Specialists that carry out the plan once the team approves it. The team's tool description names them, and when `[TEAM:APPROVED]` appears anywhere in the discussion the result asks the coordinator to carry out the approved actions. The coordinator's model makes that call |

## `[TeamMember]`

Put it on a property of a `[MentorTeam]` class, one property per member, as in the example above.

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Role` | `string` | Required | The member's role in the team, for example `"Analyst"` |
| `Instructions` | `string` | Required | The member's instructions. MentorAgent appends the language, the role, the grounding rule and the decision protocol described below |
| `Tools` | `Type[]` | empty | Classes whose tools the member can call during the discussion. They are the toolbox of this member only: the coordinator does not offer their methods, and the MCP server and the A2A card do not publish them. A `[MentorSkill]` class keeps its coordinator tools. Keep the tools read-only, declare them with `ReadOnly = true`, and register the classes in DI: a class the container cannot resolve is skipped with a warning, and the member runs without it |

Writes belong to the specialists named in the team's `HandoffTo`, which keep their own role and confirmation checks.

## `[TeamTerminationCondition]`

Put it on one method of the team class, with the signature `bool ShouldTerminate(string lastMessage, string lastSpeaker)`: the discussion ends when it returns `true`. Without it, the members speak in turn until `MaxIterations`. The attribute has no parameters.

MentorAgent instructs the members to mark their decision with `[TEAM:APPROVED]` or `[TEAM:REJECTED]`, and removes both before the text reaches the user. Your condition can end the discussion on any text it looks for, but only `[TEAM:APPROVED]`, anywhere in the discussion, makes the result ask the coordinator to hand off.

## `[MentorPage]`

Put it on a routable Blazor page to let the agent open it with `navigate_to`. The agent knows the page from the start, without visiting it first. In a `.razor` file, write it with `@attribute`. With a front end of your own, such as React, put it on an empty class, one per page, in a scanned assembly. Your client then receives the URL, in the `NavigationRequested` event or in the `navigation` frame over Server-Sent Events, and routes to it.

```razor title="Cases.razor"
@page "/cases"
@using Novior.MentorAgent.Attributes
@attribute [MentorPage(Url = "/cases", Name = "Cases",
    Description = "The list of support cases, with filters by status")]
```

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Url` | `string` | Required | The page's URL, for example `"/cases"` |
| `Name` | `string` | Required | The page's name in the system prompt |
| `Description` | `string?` | `null` | What the page offers, shown to the model. The classifiers of [`RefuseOutOfScope`](https://docs.mentoragent.net/reference/options#refuseoutofscope) and [`RequireActionForChangeRequests`](https://docs.mentoragent.net/reference/options#requireactionforchangerequests) see it too |
| `HasUIActions` | `bool` | `false` | After opening the page, `navigate_to` waits until the page calls `PageContext.SignalReady()`. It waits only when the turn runs inside the Blazor app that shows the page, as on Blazor Server or .NET MAUI: turns on the hub, over Server-Sent Events (SSE) and over A2A never wait |
| `ReadyTimeout` | `int` | `2000` | How long `navigate_to` waits for `SignalReady()`, in milliseconds |

## `[MentorSkill]`

Put it on a class to register a skill: instructions the model loads with `load_skill` only when it needs them, while the prompt carries just the name and the description. The class's `[Description]` and `[MentorAction]` methods become tools, so register it in DI when it has any. Skills need [`EnableSkills`](https://docs.mentoragent.net/reference/options#enableskills).

```csharp title="CaseEscalationSkill.cs"
[MentorSkill(
    Name = "case-escalation",
    Description = "Rules for escalating a support case to a supervisor",
    InstructionsFile = "Skills/case-escalation/SKILL.md")]
public class CaseEscalationSkill
{
}
```

| Parameter | Type | Default | Effect |
|---|---|---|---|
| `Name` | `string` | Required | The skill's unique name in kebab-case: the key for `load_skill` |
| `Description` | `string` | Required | One sentence shown in the skill catalog, the only text the model sees before it loads the skill |
| `InstructionsFile` | `string?` | `null` | A Markdown file with the instructions, absolute or relative to the content root. With `null`, MentorAgent reads `SKILL.md` in the folder named after the skill, inside [`SkillsFolder`](https://docs.mentoragent.net/reference/options#skillsfolder) |
| `Instructions` | `string?` | `null` | The instructions inline, in Markdown. They win over `InstructionsFile` |

When no file is found, the instructions are just the skill's name and description; a path you gave that does not exist also logs a warning.

---

- Previous: [Production checklist](https://docs.mentoragent.net/configure/production.md)
- Next: [Configuration options](https://docs.mentoragent.net/reference/options.md)
