# Blazor WebAssembly and Auto

> Keep the widget on the server, or run it in a WebAssembly or Auto client with Novior.MentorAgent.Blazor, backed by Novior.MentorAgent.Server.

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

This page is for a standalone Blazor WebAssembly app, and for a Blazor Web App with Auto or WebAssembly interactivity. In such an app the agent always runs on the server. The widget can stay on the server too (option A), or run in the browser and reach the agent over SignalR (option B).

## When to choose it

### Option A: keep the widget on the server

Choose it for a Blazor Web App with Auto or WebAssembly interactivity whose layout is rendered statically: *Interactivity location: Per page/component* in the template. Only the widget is pinned to Server interactivity, that is, it gets its own `InteractiveServer` render mode. The rest of the app keeps its render mode. You need neither `Novior.MentorAgent.Server` nor `Novior.MentorAgent.Blazor`. An app created with WebAssembly interactivity also needs Server interactivity turned on.

### Option B: run the widget in WebAssembly

Choose it for a standalone Blazor WebAssembly app, for a Blazor Web App with *Interactivity location: Global*, or when the widget must run in the browser. The server project gets `Novior.MentorAgent.Server`, which adds the SignalR hub; the client project gets `Novior.MentorAgent.Blazor`, which gives the same `<ChatWidget />` a hub connection. The model key never reaches the browser, and the agent's features (specialists, RAG, MCP, A2A, memory, confirmation) all run on the server.

For a desktop app see [.NET MAUI](https://docs.mentoragent.net/configure/maui); for a JavaScript or mobile client, [Any front end](https://docs.mentoragent.net/configure/any-front-end).

## Install the packages

**Option A.** The server project needs `Novior.MentorAgent` and the package of your model provider, as on [Blazor Server](https://docs.mentoragent.net/configure/blazor-server#install-the-packages).

**Option B.** In the server project, `Novior.MentorAgent.Server` brings `Novior.MentorAgent` with it:

**.NET CLI**

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

**PackageReference**

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

**Central Package Management**

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

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

**File-based app**

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

With either option, MentorAgent does not include a model provider package. The server code on this page uses OpenAI, so add its package to the server project:

```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).

In the client project:

**.NET CLI**

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

**PackageReference**

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

**Central Package Management**

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

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

**File-based app**

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

> **Warning:**
> Call `AddMentorAgent()` only in the server project. The client project registers `AddMentorAgentBlazor()` and nothing else from MentorAgent.

## Minimal configuration

**Option A.** Register MentorAgent in the server project with the same `Program.cs` lines as on [Blazor Server](https://docs.mentoragent.net/configure/blazor-server#minimal-configuration):

```csharp title="Program.cs (server)"
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 (server)"
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];
});
```

The Auto template already turns on Server interactivity. The WebAssembly template does not: in the same `Program.cs`, chain `.AddInteractiveServerComponents()` to `AddRazorComponents()`, and `.AddInteractiveServerRenderMode()` to `MapRazorComponents<App>()`. Without them every page answers HTTP 500.

Then pin the widget to Server interactivity in the server project's layout:

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

This works only because the layout is static. With *Interactivity location: Global* (`--all-interactive`), option A fails:

- the layout is in the client project, inside the interactive router, and a child component cannot switch to another interactive render mode;
- the client project does not know `ChatWidget` without a MentorAgent package, and the build stops. With the package added, the build, the startup and the prerendered page all succeed: the error appears only in the browser, when WebAssembly renders the layout;
- with Auto, the widget works while the app still runs on the server, and fails once it runs in WebAssembly.

With global interactivity, use option B.

**Option B.**

1. In the server project, register the services and map the hub:

   ```csharp title="Program.cs (server)"
   using Novior.MentorAgent.Extensions;          // AddMentorAgent
   using Novior.MentorAgent.Server.Extensions;   // AddMentorAgentServer, MapMentorAgentServer
   using Microsoft.Extensions.AI;         // AsIChatClient
   using OpenAI;                          // OpenAIClient
   ```

   ```csharp title="Program.cs (server)"
   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];
   });
   builder.Services.AddMentorAgentServer();
   // after var app = builder.Build();
   app.MapMentorAgentServer();   // /mentor-hub, /mentor/chat, /mentor/approve, /mentor/cancel ...
   ```

2. In the client project, register the widget's services:

   ```csharp title="Program.cs (client)"
   using Novior.MentorAgent.Abstractions.Models;   // MentorLanguage, MentorTheme
   using Novior.MentorAgent.Blazor.Extensions;     // AddMentorAgentBlazor
   ```

   ```csharp title="Program.cs (client)"
   // The widget posts Stop and confirmations through an HttpClient: the standalone template registers this line, a Blazor Web App client does not
   builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

   builder.Services.AddMentorAgentBlazor(options =>
   {
       // Same origin as the server: the default "/mentor-hub" is resolved against the base address of the page
       options.HubUrl = "/mentor-hub";
   });
   ```

   `HubUrl` defaults to `/mentor-hub`. A relative value is resolved against the base address of the page, as a browser does: `/mentor-hub` points to the root of the origin, and `mentor-hub` to the base path of the app. It works when the server also serves the client, as in a Blazor Web App or a hosted WebAssembly app. For a server on another origin, set an absolute `HubUrl`: see [Complete configuration](https://docs.mentoragent.net/configure/blazor-webassembly#complete-configuration).

3. Add the widget to a layout or page of the client project, as on Blazor Server. In a Blazor Web App the client project has a layout only with *Interactivity location: Global*:

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

4. In a standalone app, reference the widget's stylesheet and script in `wwwroot/index.html`. That page is served before .NET starts in the browser, so the widget cannot add them in time:

   ```html
   <link href="_content/Novior.MentorAgent.Abstractions/css/MentorAgent.css?v=1.0.0" rel="stylesheet" />
   <script src="_content/Novior.MentorAgent.Abstractions/js/MentorAgent.js?v=1.0.0"></script>
   ```

   Keep the `?v=` query string and change it on every upgrade: nothing else tells the browser that its cached copy is old.

   A Blazor Web App has no `index.html`. Its server prerenders the widget, and the widget adds both tags to the `<head>` of the page itself, as on [Blazor Server](https://docs.mentoragent.net/configure/blazor-server). Add the two lines to `Components/App.razor` only if you turn prerendering off.

## Complete configuration

A standalone client on another origin, with signed-in users. On the server, allow the client's origin and require authentication on the hub:

```csharp title="Program.cs (server)"
builder.Services.AddCors(options => options.AddPolicy("MentorClients", policy => policy
    .WithOrigins("https://localhost:7001")   // the WebAssembly app's origin
    .AllowAnyHeader()
    .AllowAnyMethod()
    .AllowCredentials()));                   // SignalR needs credentials

// Plus your authentication scheme (JWT bearer, cookies): builder.Services.AddAuthentication()...
app.UseCors("MentorClients");   // before authentication and authorization
app.UseAuthentication();
app.UseAuthorization();
app.MapMentorAgentServer(configureTransports: e => e.RequireAuthorization());
```

On the client, point to the server and send the user's token:

```csharp title="Program.cs (client)"
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

builder.Services.AddMentorAgentBlazor(options =>
{
    // Server on another origin: absolute URL, and CORS on the server
    options.HubUrl = "https://api.example.com/mentor-hub";
    // The signed-in user's token, for the hub, Stop and confirmations
    options.AccessTokenProvider = () => Task.FromResult(tokenStore.Token);

    // The widget: on WebAssembly these settings live here, not in MentorOptions
    options.BotName = "Case assistant";
    options.Language = MentorLanguage.English;
    options.Theme = MentorTheme.Minimal;
    options.EnableSuggestions = true;
});
```

`TokenStore` stands for whatever your application uses to keep the signed-in user's token.

### Options that matter here

| Option | Project | Why it matters |
|---|---|---|
| [`HubUrl`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionshuburl) | client | The default `/mentor-hub` is resolved against the base address of the page. Set an absolute URL for a server on another origin. The origin of an absolute URL is also where the widget posts Stop and confirmations |
| [`AccessTokenProvider`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsaccesstokenprovider) | client | The user's token, read on connect and on every reconnect, and sent with Stop and confirmations. Without it every user is anonymous to the server |
| [`ConfigureConnection`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsconfigureconnection) | client | Anything else the hub connection needs: a header, a transport, cookies |
| [`BotName`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsbotname), [`Theme`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionstheme), [`PrimaryColor`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsprimarycolor), [`Position`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsposition), [`WelcomeMessage`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionswelcomemessage), [`Language`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionslanguage) | client | On WebAssembly the widget reads its look and its own texts from the client options, not from `MentorOptions`. In a Blazor Web App the server prerenders it with `MentorOptions` first: give both the same values |
| [`EnableSuggestions`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptionsenablesuggestions) | client | Off by default on the client, while it is on by default in `MentorOptions` |
| [`EnableOnboardingTour`](https://docs.mentoragent.net/reference/options#enableonboardingtour) | client | Turn it on in the client options. The server answers `GET /mentor/tour` whatever its own value. In a Blazor Web App, give `MentorOptions` the same value, because the server prerenders the widget |
| [`EnableImageInput`](https://docs.mentoragent.net/reference/options#enableimageinput) | both | Turn it on in the client options and in `MentorOptions` |
| Everything about the agent | server | Model, agents, RAG, memory, MCP, A2A, costs and limits stay in `MentorOptions`, on the server. `Language` there sets the language of the answers |

Every server option is in [Configuration options](https://docs.mentoragent.net/reference/options), and every client option under [`MentorAgentBlazorOptions`](https://docs.mentoragent.net/reference/options#mentoragentblazoroptions).

## Security and roles

- **Who the user is.** `AddMentorAgentServer()` passes the user of the hub connection to MentorAgent. `RequiredRoles`, per-user memory and the rate limit then work as on Blazor Server. The client must send the token through `AccessTokenProvider`.
- **In a Blazor Web App the hub still knows the user.** `AddRazorComponents()` and `AddMentorAgentServer()` can come in either order. MentorAgent reads the user of the hub connection from the request, before it asks Blazor's own provider, which works only inside a [circuit](https://docs.mentoragent.net/reference/glossary#circuit). So [role-gated](https://docs.mentoragent.net/reference/glossary#gated) actions, per-user memory and the rate limit work with option B, if the client sends the token through `AccessTokenProvider`. To add claims or roles for the hub, see [Secure the endpoints](https://docs.mentoragent.net/configure/secure-endpoints#security-and-roles).
- **With Auto, the user comes from two places.** On the first visit the page runs on the server, and the widget runs in the circuit: the user is the one of the circuit. After the switch to WebAssembly the widget uses the hub: the user is the one of the token that `AccessTokenProvider` sends. Without a token, the widget is anonymous after the switch and role-gated actions are refused.
- **When the token is read.** The widget opens the connection with the first message and keeps it for the life of the page. A user who signs in after that stays anonymous until the page reloads.
- **JWT bearer.** In the browser, WebSocket and Server-Sent Events connections cannot carry an `Authorization` header, so SignalR sends the token in the `access_token` query string. The server must read it from there for `/mentor-hub`: see [Choose the authentication scheme](https://docs.mentoragent.net/configure/secure-endpoints#choose-the-authentication-scheme).
- **Confirmation.** The widget shows the confirmation box. The answer goes to `POST /mentor/approve` and Stop to `POST /mentor/cancel`, on the hub's origin and with the same token. While a signed-in user's connection is open, only that user can send them. `POST /mentor/approve` answers `404` when nothing waits for the answer any more: it was answered already, the turn was stopped, or the connection closed. The widget treats `404` as "nothing left to answer". When the connection closes, the turn waiting for a confirmation ends with it. See [Where nobody can answer](https://docs.mentoragent.net/concepts/governance#where-nobody-can-answer) for the other channels.
- **The hub is open until you protect it.** `configureTransports: e => e.RequireAuthorization()` covers the hub and both `/mentor/chat` endpoints.

## Common errors

- **The widget never connects from another origin.** The browser blocked the SignalR handshake. List the client's origin with `AllowCredentials()`, and use an absolute `HubUrl`. Call `UseCors` before `UseAuthentication()` and `UseAuthorization()`, and write those two calls in `Program.cs` yourself. Otherwise authorization can refuse the browser's preflight request.
- **The widget appears without styles for a moment.** The stylesheet and script are missing from the host page.
- **After an upgrade, the tour does not start and voice output waits for the end of the answer.** The browser still has an old `MentorAgent.js`: change the `?v=` value.
- **Every page answers HTTP 500 after you pin the widget: "the required endpoints are not mapped on the server".** The app was created with WebAssembly interactivity and has no Server interactivity: add `AddInteractiveServerComponents()` and `AddInteractiveServerRenderMode()`, as in [Minimal configuration](https://docs.mentoragent.net/configure/blazor-webassembly#minimal-configuration).
- **"Cannot create a component of type 'Novior.MentorAgent.Abstractions.Components.ChatWidget'… is not supported by WebAssembly rendering" in the browser console.** The widget is pinned to the server inside an interactive router (*Interactivity location: Global*). Option A needs a static layout: with global interactivity, use option B.
- **The WebAssembly runtime does not start, and the console says `CannotResolveService` for `HttpClient`.** The client project of a Blazor Web App registers no `HttpClient`, and the widget needs one for Stop and confirmations. Add the `AddScoped(sp => new HttpClient { … })` line of the client snippet in [Minimal configuration](https://docs.mentoragent.net/configure/blazor-webassembly#minimal-configuration).
- **Every action with `RequiredRoles` is refused, and memory starts over at every reconnect.** The server sees an anonymous caller: set `AccessTokenProvider`, and with JWT bearer read the token from the query string.
- **Stop or a confirmation seems ignored.** The client has no `HttpClient` registered, or the server's CORS policy does not allow the `POST` or the `Authorization` header. The widget only logs the failure, in the browser console.
- **`SerializeSessionAsync` throws `NotSupportedException`.** On WebAssembly the conversation lives on the server. Save and restore it over HTTP with `GET` and `POST /mentor/session`, as described in [Any front end](https://docs.mentoragent.net/configure/any-front-end#connect-the-client). The snapshot is opaque: store it as text and send it back unchanged.

---

- Previous: [Blazor Server app](https://docs.mentoragent.net/configure/blazor-server.md)
- Next: [.NET MAUI (Blazor Hybrid)](https://docs.mentoragent.net/configure/maui.md)
