# Blazor Server app

> Configure MentorAgent in a Blazor Web App with Server interactivity: services, the options that matter, the widget and authentication.

- URL: https://docs.mentoragent.net/configure/blazor-server
- Verified with: MentorAgent 1.0.0
- Last updated: 2026-10-14
- Packages: Novior.MentorAgent

This page adds MentorAgent and its chat widget to a Blazor Web App with Server interactivity. The widget and the agent run in the same server process and talk through in-process events: you install one package and map no endpoint.

## When to choose it

- Your app is a Blazor Web App with Server interactivity, or you can make the layout that hosts the widget interactive on the server.
- You want the simplest setup. The model key stays on the server, and the browser only receives the rendered UI.

For the other hosts see [Blazor WebAssembly and Auto](https://docs.mentoragent.net/configure/blazor-webassembly), [.NET MAUI](https://docs.mentoragent.net/configure/maui) and [Any front end](https://docs.mentoragent.net/configure/any-front-end), for React, Angular, Vue or a native mobile app. [Channels](https://docs.mentoragent.net/concepts/channels#the-channels-at-a-glance) compares them all.

## Install the packages

**.NET CLI**

```shell
dotnet add package Novior.MentorAgent
```

**PackageReference**

```xml title="YourApp.csproj"
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent" Version="1.0.0" />
</ItemGroup>
```

**Central Package Management**

```xml title="Directory.Packages.props"
<ItemGroup>
  <PackageVersion Include="Novior.MentorAgent" Version="1.0.0" />
</ItemGroup>
```

```xml title="YourApp.csproj"
<ItemGroup>
  <PackageReference Include="Novior.MentorAgent" />
</ItemGroup>
```

**File-based app**

```csharp title="app.cs"
#:package Novior.MentorAgent@1.0.0
```

`Novior.MentorAgent` brings the widget's components with it, but it does not include a model provider package. The code on this page uses OpenAI, so add its package too:

```shell
dotnet add package Microsoft.Extensions.AI.OpenAI --version 10.6.0
```

For another provider, see [Model providers](https://docs.mentoragent.net/configure/model-providers#install-the-packages). You do not need `Novior.MentorAgent.Server` to run the widget on this host. Add it only if the same app serves MCP or A2A: see [MCP server](https://docs.mentoragent.net/configure/mcp-server) and [A2A server](https://docs.mentoragent.net/configure/a2a-server).

## Minimal configuration

1. Keep the model key out of the source code:

   ```shell
   dotnet user-secrets init
   dotnet user-secrets set OpenAI:Key <your-key>
   ```

2. Register MentorAgent in `Program.cs`, next to the template's `AddRazorComponents`:

   ```csharp title="Program.cs"
   using Novior.MentorAgent.Abstractions.Models;   // MentorLanguage, MentorTheme, ChatPosition, MentorshipLevel
   using Novior.MentorAgent.Extensions;            // AddMentorAgent
   using Microsoft.Extensions.AI;           // AsIChatClient
   using OpenAI;                            // OpenAIClient
   ```

   ```csharp title="Program.cs"
   builder.Services.AddMentorAgent(options =>
   {
       options.AppName = "Case management";
       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];
   });
   ```

   `AppName`, a model and `ScanAssemblies` are required. The `ChatClient` line connects OpenAI: for Azure OpenAI, Ollama or Foundry, take the line from [Model providers](https://docs.mentoragent.net/configure/model-providers). `ScanAssemblies` is where MentorAgent looks for your actions, agents and pages. Register the services that hold your actions as usual, for example with `AddScoped`.

3. Make the app interactive as a whole. In `Components/App.razor` these are the two lines the template writes when you choose *Interactivity location: Global* (`dotnet new blazor --interactivity Server --all-interactive`):

   ```razor title="Components/App.razor"
   <HeadOutlet @rendermode="InteractiveServer" />
   <Routes @rendermode="InteractiveServer" />
   ```

4. Add the widget to the layout:

   ```razor title="Components/Layout/MainLayout.razor"
   @using Novior.MentorAgent.Abstractions.Components
   <ChatWidget />
   ```

   The widget injects its own stylesheet and script. You do not change `App.razor` for them.

Run the app: the widget button appears in the bottom-right corner of every page that uses the layout.

Each circuit, the connection that Blazor Server keeps with each browser tab, gets its own [coordinator](https://docs.mentoragent.net/concepts/agent-levels), conversation and page context. Memory stores and the rate limiter are shared by the whole process.

## Complete configuration

A configuration for an application with real users, still in `Program.cs`:

```csharp title="Program.cs"
builder.Services.AddMentorAgent(options =>
{
    // Required
    options.AppName = "Case management";
    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];

    // What the assistant knows about the application
    options.AppDescription = "Support case management: case status, notes and closure";
    options.Language = MentorLanguage.English;
    options.MentorshipLevel = MentorshipLevel.Standard;

    // The widget
    options.BotName = "Case assistant";
    options.WelcomeMessage = "Ask me about your support cases.";
    options.Theme = MentorTheme.Minimal; // the light theme
    options.PrimaryColor = "#2563eb";
    options.Position = ChatPosition.BottomRight;

    // Limits and safety
    options.MaxMessageLength = 2000;
    options.RateLimitPerUser = 20; // messages per window, per user
    options.EnableSafetyCheck = true;

    // Exception details in replies: never in production
    options.IncludeWorkflowExceptionDetails = builder.Environment.IsDevelopment();
});
```

### Options that matter here

| Option | Why it matters on Blazor Server |
|---|---|
| [`AppDescription`](https://docs.mentoragent.net/reference/options#appdescription) | A real sentence about your application. The prompt and the safety check use it to tell ordinary questions from attempts to extract the prompt or secrets |
| [`Language`](https://docs.mentoragent.net/reference/options#language) | The language of the answers and of the widget's own texts |
| [`BotName`](https://docs.mentoragent.net/reference/options#botname), [`WelcomeMessage`](https://docs.mentoragent.net/reference/options#welcomemessage), [`Theme`](https://docs.mentoragent.net/reference/options#theme), [`PrimaryColor`](https://docs.mentoragent.net/reference/options#primarycolor), [`Position`](https://docs.mentoragent.net/reference/options#position) | The widget reads them from `MentorOptions` on this host. `Theme = Minimal` is the light theme. `PrimaryColor` takes one six-digit hex color and derives the other shades from it: pressed, glow, accent border and button shadow. A Content Security Policy without `style-src 'unsafe-inline'` blocks the colors that `Theme = Dark`, `Theme = Minimal` and `PrimaryColor` set: see [Content Security Policy](https://docs.mentoragent.net/configure/production#content-security-policy) |
| [`MentorshipLevel`](https://docs.mentoragent.net/reference/options#mentorshiplevel) | How much the agent explains and suggests: `Minimal`, `Standard` or `Proactive` |
| [`MaxMessageLength`](https://docs.mentoragent.net/reference/options#maxmessagelength) | Longer messages are refused before the model is called, so they cost nothing |
| [`RateLimitPerUser`](https://docs.mentoragent.net/reference/options#ratelimitperuser) | Counted per signed-in user. Without sign-in it is counted per circuit, so a reload starts a new count |
| [`EnableSafetyCheck`](https://docs.mentoragent.net/reference/options#enablesafetycheck) | One extra model call checks each message before the answer, for prompt injection, jailbreaks and attempts to extract the system prompt or credentials. Point `ClassifierChatClient` at a small model to keep it fast |
| [`IncludeWorkflowExceptionDetails`](https://docs.mentoragent.net/reference/options#includeworkflowexceptiondetails) | Puts specialist and team errors in the reply the user reads: development only |

Every option, with its type and default, is in [Configuration options](https://docs.mentoragent.net/reference/options); the widget's own are under [Widget](https://docs.mentoragent.net/reference/options#widget).

**Colors, sizes and fonts.** Beyond `Theme` and `PrimaryColor`, the widget reads CSS variables that start with `--bm-`. Override them in a stylesheet loaded after the widget's:

```css
:root {
    --bm-panel-w: 480px;
    --bm-font: 'Inter', system-ui, sans-serif;
}
```

The widget requests nothing from another origin. The package ships its two fonts, Sora and JetBrains Mono, under the SIL Open Font License 1.1. To use your own fonts, override `--bm-font` and `--bm-font-mono`: the browser then never downloads the packaged files. The policy that the widget needs is in [Content Security Policy](https://docs.mentoragent.net/configure/production#content-security-policy).

## Security and roles

- **Roles.** MentorAgent reads the circuit's signed-in user through `AuthenticationStateProvider` and checks `RequiredRoles` against the user's role claims. Set up authentication as in any Blazor Web App. Without it every user is anonymous, and every action with `RequiredRoles` is refused. The order of the checks is in [Two checks, in this order](https://docs.mentoragent.net/concepts/governance#two-checks-in-this-order).
- **Confirmation.** Before an action that asks for confirmation runs, the widget shows the confirmation box. An action asks when it declares `RequiresConfirmation = true`. A `[MentorAction]` that declares neither `ReadOnly = true` nor `RequiresConfirmation` asks too. Declare `ReadOnly = true` on an action that only reads. If the circuit closes while the box is open, the turn ends with it. `RequireConfirmation = false` turns off these boxes, but never the role checks. The only approvals that still appear are those asked by the MCP servers that the model provider calls directly ([`HostedMcpServers`](https://docs.mentoragent.net/reference/options#hostedmcpservers)).
- **Anonymous users.** With `AnonymousIdentity` at its default, `PerSession`, each circuit has its own memory and its own rate-limit count. Use `Shared` only for an application with a single user.
- **Other channels.** If the same app also serves MCP or A2A, with `Novior.MentorAgent.Server`, actions that ask for confirmation are [withheld](https://docs.mentoragent.net/reference/glossary#withheld) there. That includes a `[MentorAction]` that declares neither `ReadOnly = true` nor `RequiresConfirmation`. An A2A task runs as the user of its request, signed in only when the endpoint requires authentication: see [Where nobody can answer](https://docs.mentoragent.net/concepts/governance#where-nobody-can-answer).

## Common errors

- **The widget button appears, but nothing happens when you click it.** The layout is rendered statically, so the widget has no circuit. Make the app interactive (step 3 above), or give the widget its own render mode as in [option A of WebAssembly and Auto](https://docs.mentoragent.net/configure/blazor-webassembly#option-a-keep-the-widget-on-the-server). A static page also cannot hand the agent its page context or UI actions.
- **The app stops at startup with `InvalidOperationException`.** The message says what is missing: "Configure ChatClient or Agent", "AppName is required" or "ScanAssemblies cannot be empty". MentorAgent checks the options when the host starts, before the first request.
- **The key is not found outside your machine.** User secrets are loaded only in the `Development` environment. Elsewhere, read the key from an environment variable or a secret store.
- **Every action with `RequiredRoles` is refused.** Nobody is signed in, or the user's roles are not role claims. Check what `AuthenticationStateProvider` returns for that user.

---

- Previous: [Model providers](https://docs.mentoragent.net/configure/model-providers.md)
- Next: [Blazor WebAssembly and Auto](https://docs.mentoragent.net/configure/blazor-webassembly.md)
